diff options
author | Luke Bennett <lbennett@gitlab.com> | 2018-09-24 14:39:00 +0100 |
---|---|---|
committer | Luke Bennett <lbennett@gitlab.com> | 2018-09-24 14:39:00 +0100 |
commit | 05afd11e16aecd43adfb869ae90aa6cae13916ec (patch) | |
tree | 63b31431981d48ac970853778dd01a5301bb9564 /app | |
parent | 086549d986a453e1b2dd0d09ffbd19d0487d9c51 (diff) | |
parent | 28086b203ae397e01d5e9870dfbddd66466450c2 (diff) | |
download | gitlab-ce-05afd11e16aecd43adfb869ae90aa6cae13916ec.tar.gz |
Merge remote-tracking branch 'origin/master' into ce-6983-promote-starting-a-gitlab-com-trial
Diffstat (limited to 'app')
1180 files changed, 8846 insertions, 4293 deletions
diff --git a/app/assets/images/auth_buttons/auth0_64.png b/app/assets/images/auth_buttons/auth0_64.png Binary files differnew file mode 100644 index 00000000000..5ad59659380 --- /dev/null +++ b/app/assets/images/auth_buttons/auth0_64.png diff --git a/app/assets/images/auth_buttons/azure_64.png b/app/assets/images/auth_buttons/azure_64.png Binary files differindex 85de7793440..168a9c81395 100644 --- a/app/assets/images/auth_buttons/azure_64.png +++ b/app/assets/images/auth_buttons/azure_64.png diff --git a/app/assets/images/auth_buttons/bitbucket_64.png b/app/assets/images/auth_buttons/bitbucket_64.png Binary files differindex b3d022a5a70..0edf7f52a11 100644 --- a/app/assets/images/auth_buttons/bitbucket_64.png +++ b/app/assets/images/auth_buttons/bitbucket_64.png diff --git a/app/assets/images/auth_buttons/google_64.png b/app/assets/images/auth_buttons/google_64.png Binary files differindex 720824230a5..389c1cd54ca 100644 --- a/app/assets/images/auth_buttons/google_64.png +++ b/app/assets/images/auth_buttons/google_64.png diff --git a/app/assets/images/auth_buttons/jwt_64.png b/app/assets/images/auth_buttons/jwt_64.png Binary files differnew file mode 100644 index 00000000000..ca97ae47002 --- /dev/null +++ b/app/assets/images/auth_buttons/jwt_64.png diff --git a/app/assets/images/auth_buttons/shibboleth_64.png b/app/assets/images/auth_buttons/shibboleth_64.png Binary files differnew file mode 100644 index 00000000000..d4c752f9400 --- /dev/null +++ b/app/assets/images/auth_buttons/shibboleth_64.png diff --git a/app/assets/images/cluster_app_logos/elasticsearch.png b/app/assets/images/cluster_app_logos/elasticsearch.png Binary files differnew file mode 100644 index 00000000000..96e9e0ff934 --- /dev/null +++ b/app/assets/images/cluster_app_logos/elasticsearch.png diff --git a/app/assets/images/cluster_app_logos/gitlab.png b/app/assets/images/cluster_app_logos/gitlab.png Binary files differnew file mode 100644 index 00000000000..cb2195fc6a2 --- /dev/null +++ b/app/assets/images/cluster_app_logos/gitlab.png diff --git a/app/assets/images/cluster_app_logos/helm.png b/app/assets/images/cluster_app_logos/helm.png Binary files differnew file mode 100644 index 00000000000..2989cae7b93 --- /dev/null +++ b/app/assets/images/cluster_app_logos/helm.png diff --git a/app/assets/images/cluster_app_logos/jeager.png b/app/assets/images/cluster_app_logos/jeager.png Binary files differnew file mode 100644 index 00000000000..be5bf2a0c9c --- /dev/null +++ b/app/assets/images/cluster_app_logos/jeager.png diff --git a/app/assets/images/cluster_app_logos/jupyterhub.png b/app/assets/images/cluster_app_logos/jupyterhub.png Binary files differnew file mode 100644 index 00000000000..80c7343067f --- /dev/null +++ b/app/assets/images/cluster_app_logos/jupyterhub.png diff --git a/app/assets/images/cluster_app_logos/kubernetes.png b/app/assets/images/cluster_app_logos/kubernetes.png Binary files differnew file mode 100644 index 00000000000..4d774909c10 --- /dev/null +++ b/app/assets/images/cluster_app_logos/kubernetes.png diff --git a/app/assets/images/cluster_app_logos/meltano.png b/app/assets/images/cluster_app_logos/meltano.png Binary files differnew file mode 100644 index 00000000000..7a2d82fbe27 --- /dev/null +++ b/app/assets/images/cluster_app_logos/meltano.png diff --git a/app/assets/images/cluster_app_logos/prometheus.png b/app/assets/images/cluster_app_logos/prometheus.png Binary files differnew file mode 100644 index 00000000000..a8663449b88 --- /dev/null +++ b/app/assets/images/cluster_app_logos/prometheus.png diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue index 155c348286c..97232d7f783 100644 --- a/app/assets/javascripts/badges/components/badge.vue +++ b/app/assets/javascripts/badges/components/badge.vue @@ -1,13 +1,11 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'Badge', components: { Icon, - LoadingIcon, Tooltip, }, directives: { @@ -80,7 +78,7 @@ export default { /> </a> - <loading-icon + <gl-loading-icon v-show="isLoading" :inline="true" /> @@ -105,8 +103,8 @@ export default { </div> <button - v-tooltip v-show="hasError" + v-tooltip :title="s__('Badges|Reload badge image')" class="btn btn-transparent btn-sm text-primary" type="button" diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue index b3f25da87ce..aff7c4180e3 100644 --- a/app/assets/javascripts/badges/components/badge_form.vue +++ b/app/assets/javascripts/badges/components/badge_form.vue @@ -4,7 +4,6 @@ import { mapActions, mapState } from 'vuex'; import createFlash from '~/flash'; import { s__, sprintf } from '~/locale'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import createEmptyBadge from '../empty_badge'; import Badge from './badge.vue'; @@ -15,7 +14,6 @@ export default { components: { Badge, LoadingButton, - LoadingIcon, }, props: { isEditing: { @@ -207,7 +205,7 @@ export default { :link-url="renderedLinkUrl" /> <p v-show="isRendering"> - <loading-icon + <gl-loading-icon :inline="true" /> </p> diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index d2ec0fbb2c0..359d3e10380 100644 --- a/app/assets/javascripts/badges/components/badge_list.vue +++ b/app/assets/javascripts/badges/components/badge_list.vue @@ -1,6 +1,5 @@ <script> import { mapState } from 'vuex'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import BadgeListRow from './badge_list_row.vue'; import { GROUP_BADGE } from '../constants'; @@ -8,7 +7,6 @@ export default { name: 'BadgeList', components: { BadgeListRow, - LoadingIcon, }, computed: { ...mapState(['badges', 'isLoading', 'kind']), @@ -31,10 +29,10 @@ export default { class="badge badge-pill" >{{ badges.length }}</span> </div> - <loading-icon + <gl-loading-icon v-show="isLoading" + :size="2" class="card-body" - size="2" /> <div v-if="hasNoBadges" diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue index 712d81d0430..5d16ba3ce6d 100644 --- a/app/assets/javascripts/badges/components/badge_list_row.vue +++ b/app/assets/javascripts/badges/components/badge_list_row.vue @@ -2,7 +2,6 @@ import { mapActions, mapState } from 'vuex'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import { PROJECT_BADGE } from '../constants'; import Badge from './badge.vue'; @@ -11,7 +10,6 @@ export default { components: { Badge, Icon, - LoadingIcon, }, props: { badge: { @@ -79,7 +77,7 @@ export default { name="remove" /> </button> - <loading-icon + <gl-loading-icon v-show="badge.isDeleting" :inline="true" /> diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js index 84fef4d8b4f..8c4eccc34a3 100644 --- a/app/assets/javascripts/behaviors/index.js +++ b/app/assets/javascripts/behaviors/index.js @@ -1,15 +1,19 @@ import './autosize'; import './bind_in_out'; import './markdown/render_gfm'; +import initGFMInput from './markdown/gfm_auto_complete'; import initCopyAsGFM from './markdown/copy_as_gfm'; import initCopyToClipboard from './copy_to_clipboard'; import './details_behavior'; import installGlEmojiElement from './gl_emoji'; import './quick_submit'; import './requires_input'; +import initPageShortcuts from './shortcuts'; import './toggler_behavior'; -import '../preview_markdown'; +import './preview_markdown'; installGlEmojiElement(); +initGFMInput(); initCopyAsGFM(); initCopyToClipboard(); +initPageShortcuts(); diff --git a/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js new file mode 100644 index 00000000000..a303e504cc7 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/gfm_auto_complete.js @@ -0,0 +1,19 @@ +import $ from 'jquery'; +import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; +import GfmAutoComplete from '~/gfm_auto_complete'; + +export default function initGFMInput() { + $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { + const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources); + const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete); + + gfm.setup($(el), { + emojis: true, + members: enableGFM, + issues: enableGFM, + milestones: enableGFM, + mergeRequests: enableGFM, + labels: enableGFM, + }); + }); +} diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index dbff2bd4b10..429455f97ec 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -3,7 +3,7 @@ import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; -// Render Gitlab flavoured Markdown +// Render GitLab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. // diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js index 0964baf8954..0964baf8954 100644 --- a/app/assets/javascripts/preview_markdown.js +++ b/app/assets/javascripts/behaviors/preview_markdown.js diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js new file mode 100644 index 00000000000..7987a533ae5 --- /dev/null +++ b/app/assets/javascripts/behaviors/shortcuts.js @@ -0,0 +1,35 @@ +import Shortcuts from './shortcuts/shortcuts'; + +export default function initPageShortcuts() { + const { page } = document.body.dataset; + const pagesWithCustomShortcuts = [ + 'projects:activity', + 'projects:artifacts:browse', + 'projects:artifacts:file', + 'projects:blame:show', + 'projects:blob:show', + 'projects:commit:show', + 'projects:commits:show', + 'projects:find_file:show', + 'projects:issues:edit', + 'projects:issues:index', + 'projects:issues:new', + 'projects:issues:show', + 'projects:merge_requests:creations:diffs', + 'projects:merge_requests:creations:new', + 'projects:merge_requests:edit', + 'projects:merge_requests:index', + 'projects:merge_requests:show', + 'projects:network:show', + 'projects:show', + 'projects:tree:show', + 'groups:show', + ]; + + // the pages above have their own shortcuts sub-classes instantiated elsewhere + // TODO: replace this whitelist with something more automated/maintainable + if (page && !pagesWithCustomShortcuts.includes(page)) { + return new Shortcuts(); + } + return false; +} diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js index 99c71d6524a..6719bfd6d22 100644 --- a/app/assets/javascripts/shortcuts.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js @@ -1,9 +1,9 @@ import $ from 'jquery'; import Cookies from 'js-cookie'; import Mousetrap from 'mousetrap'; -import axios from './lib/utils/axios_utils'; -import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility'; -import findAndFollowLink from './shortcuts_dashboard_navigation'; +import axios from '../../lib/utils/axios_utils'; +import { refreshCurrentPage, visitUrl } from '../../lib/utils/url_utility'; +import findAndFollowLink from '../../lib/utils/navigation_utility'; const defaultStopCallback = Mousetrap.stopCallback; Mousetrap.stopCallback = (e, element, combo) => { diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js index 908b9cab93d..052e33b4a2b 100644 --- a/app/assets/javascripts/shortcuts_blob.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js @@ -1,5 +1,5 @@ import Mousetrap from 'mousetrap'; -import { getLocationHash, visitUrl } from './lib/utils/url_utility'; +import { getLocationHash, visitUrl } from '../../lib/utils/url_utility'; import Shortcuts from './shortcuts'; const defaults = { diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js index 8658081c6c2..8658081c6c2 100644 --- a/app/assets/javascripts/shortcuts_find_file.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js index e9451be31fd..5e48bf5a35c 100644 --- a/app/assets/javascripts/shortcuts_issuable.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js @@ -1,9 +1,9 @@ import $ from 'jquery'; import Mousetrap from 'mousetrap'; import _ from 'underscore'; -import Sidebar from './right_sidebar'; +import Sidebar from '../../right_sidebar'; import Shortcuts from './shortcuts'; -import { CopyAsGFM } from './behaviors/markdown/copy_as_gfm'; +import { CopyAsGFM } from '../markdown/copy_as_gfm'; export default class ShortcutsIssuable extends Shortcuts { constructor(isMergeRequest) { diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js index 6b595764bc5..fa9b2c9f755 100644 --- a/app/assets/javascripts/shortcuts_navigation.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js @@ -1,5 +1,5 @@ import Mousetrap from 'mousetrap'; -import findAndFollowLink from './shortcuts_dashboard_navigation'; +import findAndFollowLink from '../../lib/utils/navigation_utility'; import Shortcuts from './shortcuts'; export default class ShortcutsNavigation extends Shortcuts { diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js index a88c280fa3b..a88c280fa3b 100644 --- a/app/assets/javascripts/shortcuts_network.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js diff --git a/app/assets/javascripts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js index 41865dcf4ba..8b7e6a56d25 100644 --- a/app/assets/javascripts/shortcuts_wiki.js +++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js @@ -1,6 +1,6 @@ import Mousetrap from 'mousetrap'; import ShortcutsNavigation from './shortcuts_navigation'; -import findAndFollowLink from './shortcuts_dashboard_navigation'; +import findAndFollowLink from '../../lib/utils/navigation_utility'; export default class ShortcutsWiki extends ShortcutsNavigation { constructor() { diff --git a/app/assets/javascripts/blob/3d_viewer/index.js b/app/assets/javascripts/blob/3d_viewer/index.js index 68d4ddad551..1bdf1aeb76c 100644 --- a/app/assets/javascripts/blob/3d_viewer/index.js +++ b/app/assets/javascripts/blob/3d_viewer/index.js @@ -29,12 +29,12 @@ export default class Renderer { this.scene.add(this.camera); - // Setup the viewer + // Set up the viewer this.setupRenderer(); this.setupGrid(); this.setupLight(); - // Setup OrbitControls + // Set up OrbitControls this.controls = new OrbitControls( this.camera, this.renderer.domElement, diff --git a/app/assets/javascripts/boards/components/board_blank_state.vue b/app/assets/javascripts/boards/components/board_blank_state.vue index 286529b4d13..cde22725a89 100644 --- a/app/assets/javascripts/boards/components/board_blank_state.vue +++ b/app/assets/javascripts/boards/components/board_blank_state.vue @@ -83,7 +83,7 @@ export default { right on the way to making the most of your board. </p> <button - class="btn btn-create btn-inverted btn-block" + class="btn btn-success btn-inverted btn-block" type="button" @click.stop="addDefaultLists"> Add default lists diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue index bfc8d9b03ad..7ddb22ad824 100644 --- a/app/assets/javascripts/boards/components/board_list.vue +++ b/app/assets/javascripts/boards/components/board_list.vue @@ -3,7 +3,6 @@ import Sortable from 'sortablejs'; import boardNewIssue from './board_new_issue.vue'; import boardCard from './board_card.vue'; import eventHub from '../eventhub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; const Store = gl.issueBoards.BoardsStore; @@ -12,7 +11,6 @@ export default { components: { boardCard, boardNewIssue, - loadingIcon, }, props: { groupId: { @@ -217,7 +215,7 @@ export default { v-if="loading" class="board-list-loading text-center" aria-label="Loading issues"> - <loading-icon /> + <gl-loading-icon /> </div> <board-new-issue v-if="list.type !== 'closed' && showIssueForm" @@ -233,19 +231,19 @@ export default { <board-card v-for="(issue, index) in issues" ref="issue" + :key="issue.id" :index="index" :list="list" :issue="issue" :issue-link-base="issueLinkBase" :group-id="groupId" :root-path="rootPath" - :disabled="disabled" - :key="issue.id" /> + :disabled="disabled" /> <li v-if="showCount" class="board-list-count text-center" data-issue-id="-1"> - <loading-icon + <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" /> diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue index 1e3cd43d1f0..f248f53fa51 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -110,9 +110,9 @@ export default { Title </label> <input + :id="list.id + '-title'" ref="input" v-model="title" - :id="list.id + '-title'" class="form-control" type="text" name="issue_title" diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index d50641dc3a9..f56d3fe000c 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -170,8 +170,8 @@ tooltip-placement="bottom" /> <span - v-tooltip v-if="shouldRenderCounter" + v-tooltip :title="assigneeCounterTooltip" class="avatar-counter" > @@ -184,10 +184,10 @@ class="board-card-footer" > <button - v-tooltip v-for="label in issue.labels" v-if="showLabel(label)" :key="label.id" + v-tooltip :style="labelStyle(label)" :title="label.description" class="badge color-label" diff --git a/app/assets/javascripts/boards/components/modal/index.vue b/app/assets/javascripts/boards/components/modal/index.vue index 33e72a6782e..0c4c709324d 100644 --- a/app/assets/javascripts/boards/components/modal/index.vue +++ b/app/assets/javascripts/boards/components/modal/index.vue @@ -1,7 +1,6 @@ <script> /* global ListIssue */ - import queryData from '~/boards/utils/query_data'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; + import { urlParamsToObject } from '~/lib/utils/common_utils'; import ModalHeader from './header.vue'; import ModalList from './list.vue'; import ModalFooter from './footer.vue'; @@ -14,7 +13,6 @@ ModalHeader, ModalList, ModalFooter, - loadingIcon, }, props: { newIssuePath: { @@ -109,13 +107,11 @@ loadIssues(clearIssues = false) { if (!this.showAddIssuesModal) return false; - return gl.boardService - .getBacklog( - queryData(this.filter.path, { - page: this.page, - per: this.perPage, - }), - ) + return gl.boardService.getBacklog({ + ...urlParamsToObject(this.filter.path), + page: this.page, + per: this.perPage, + }) .then(res => res.data) .then(data => { if (clearIssues) { @@ -169,7 +165,7 @@ class="add-issues-list text-center" > <div class="add-issues-list-loading"> - <loading-icon /> + <gl-loading-icon /> </div> </section> <modal-footer/> diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue index ef9844d5562..d4676914e02 100644 --- a/app/assets/javascripts/boards/components/project_select.vue +++ b/app/assets/javascripts/boards/components/project_select.vue @@ -2,14 +2,10 @@ import $ from 'jquery'; import _ from 'underscore'; import eventHub from '../eventhub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import Api from '../../api'; export default { name: 'BoardProjectSelect', - components: { - loadingIcon, - }, props: { groupId: { type: Number, @@ -119,7 +115,7 @@ export default { </div> <div class="dropdown-content"></div> <div class="dropdown-loading"> - <loading-icon /> + <gl-loading-icon /> </div> </div> </div> diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index bc263cbbfea..662363a6f26 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -9,7 +9,7 @@ import '~/vue_shared/models/assignee'; import FilteredSearchBoards from './filtered_search_boards'; import eventHub from './eventhub'; -import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first +import sidebarEventHub from '~/sidebar/event_hub'; import './models/issue'; import './models/list'; import './models/milestone'; @@ -24,7 +24,7 @@ import './components/board'; import './components/board_sidebar'; import './components/new_list_dropdown'; import BoardAddIssuesModal from './components/modal/index.vue'; -import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first +import '~/vue_shared/vue_resource_interceptor'; export default () => { const $boardApp = document.getElementById('board-app'); @@ -229,7 +229,7 @@ export default () => { template: ` <div class="board-extra-actions"> <button - class="btn btn-create prepend-left-10" + class="btn btn-success prepend-left-10" type="button" data-placement="bottom" ref="addIssuesButton" diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js index ad473404c29..d416b76f0f4 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 '~/vue_shared/models/label'; import ListAssignee from '~/vue_shared/models/assignee'; -import queryData from '../utils/query_data'; +import { urlParamsToObject } from '~/lib/utils/common_utils'; const PER_PAGE = 20; @@ -115,7 +115,10 @@ class List { } getIssues(emptyIssues = true) { - const data = queryData(gl.issueBoards.BoardsStore.filter.path, { page: this.page }); + const data = { + ...urlParamsToObject(gl.issueBoards.BoardsStore.filter.path), + page: this.page, + }; if (this.label && data.label_name) { data.label_name = data.label_name.filter(label => label !== this.label.title); diff --git a/app/assets/javascripts/boards/utils/query_data.js b/app/assets/javascripts/boards/utils/query_data.js deleted file mode 100644 index 65315979df7..00000000000 --- a/app/assets/javascripts/boards/utils/query_data.js +++ /dev/null @@ -1,21 +0,0 @@ -export default (path, extraData) => path.split('&').reduce((dataParam, filterParam) => { - if (filterParam === '') return dataParam; - - const data = dataParam; - const paramSplit = filterParam.split('='); - const paramKeyNormalized = paramSplit[0].replace('[]', ''); - const isArray = paramSplit[0].indexOf('[]'); - const value = decodeURIComponent(paramSplit[1].replace(/\+/g, ' ')); - - if (isArray !== -1) { - if (!data[paramKeyNormalized]) { - data[paramKeyNormalized] = []; - } - - data[paramKeyNormalized].push(value); - } else { - data[paramKeyNormalized] = value; - } - - return data; -}, extraData); diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 0fdf0c7a389..ebf76af5966 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -1,16 +1,12 @@ import Visibility from 'visibilityjs'; import Vue from 'vue'; +import initDismissableCallout from '~/dismissable_callout'; import { s__, sprintf } from '../locale'; import Flash from '../flash'; import Poll from '../lib/utils/poll'; import initSettingsPanels from '../settings_panels'; import eventHub from './event_hub'; -import { - APPLICATION_STATUS, - REQUEST_LOADING, - REQUEST_SUCCESS, - REQUEST_FAILURE, -} from './constants'; +import { APPLICATION_STATUS, REQUEST_LOADING, REQUEST_SUCCESS, REQUEST_FAILURE } from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; import applications from './components/applications.vue'; @@ -66,6 +62,7 @@ export default class Clusters { this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); + initDismissableCallout('.js-cluster-security-warning'); initSettingsPanels(); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); @@ -129,7 +126,8 @@ export default class Clusters { if (!Visibility.hidden()) { this.poll.makeRequest(); } else { - this.service.fetchData() + this.service + .fetchData() .then(data => this.handleSuccess(data)) .catch(() => Clusters.handleError()); } @@ -177,15 +175,21 @@ export default class Clusters { checkForNewInstalls(prevApplicationMap, newApplicationMap) { const appTitles = Object.keys(newApplicationMap) - .filter(appId => newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && - prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && - prevApplicationMap[appId].status !== null) + .filter( + appId => + newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED && + prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED && + prevApplicationMap[appId].status !== null, + ) .map(appId => newApplicationMap[appId].title); if (appTitles.length > 0) { - const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), { - appList: appTitles.join(', '), - }); + const text = sprintf( + s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), + { + appList: appTitles.join(', '), + }, + ); Flash(text, 'notice', this.successApplicationContainer); } } @@ -218,13 +222,18 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestReason', null); - this.service.installApplication(appId, data.params) + this.service + .installApplication(appId, data.params) .then(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); }) .catch(() => { this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); - this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); + this.store.updateAppProperty( + appId, + 'requestReason', + s__('ClusterIntegration|Request to begin installing failed'), + ); }); } diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js index 1e5c733d151..789c8360124 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,14 +1,14 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import setupToggleButtons from '~/toggle_buttons'; -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +import initDismissableCallout from '~/dismissable_callout'; import ClustersService from './services/clusters_service'; export default () => { const clusterList = document.querySelector('.js-clusters-list'); - gcpSignupOffer(); + initDismissableCallout('.gcp-signup-offer'); // The empty state won't have a clusterList if (clusterList) { diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue index 651f3b50236..0452729d3ff 100644 --- a/app/assets/javascripts/clusters/components/application_row.vue +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -2,6 +2,7 @@ /* eslint-disable vue/require-default-prop */ import { s__, sprintf } from '../../locale'; import eventHub from '../event_hub'; + import identicon from '../../vue_shared/components/identicon.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue'; import { APPLICATION_STATUS, @@ -13,6 +14,7 @@ export default { components: { loadingButton, + identicon, }, props: { id: { @@ -31,6 +33,16 @@ type: String, required: false, }, + logoUrl: { + type: String, + required: false, + default: null, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, status: { type: String, required: false, @@ -60,6 +72,18 @@ isKnownStatus() { return Object.values(APPLICATION_STATUS).includes(this.status); }, + isInstalled() { + return ( + this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED + ); + }, + hasLogo() { + return !!this.logoUrl; + }, + identiconId() { + // generate a deterministic integer id for the identicon background + return this.id.charCodeAt(0); + }, rowJsClass() { return `js-cluster-application-row-${this.id}`; }, @@ -128,37 +152,81 @@ <template> <div - :class="rowJsClass" - class="gl-responsive-table-row gl-responsive-table-row-col-span" + :class="[ + rowJsClass, + isInstalled && 'cluster-application-installed', + disabled && 'cluster-application-disabled' + ]" + class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span" > <div class="gl-responsive-table-row-layout" role="row" > - <a - v-if="titleLink" - :href="titleLink" - target="blank" - rel="noopener noreferrer" + <div + class="table-section append-right-8 section-align-top" role="gridcell" - class="table-section section-15 section-align-top js-cluster-application-title" > - {{ title }} - </a> - <span - v-else - class="table-section section-15 section-align-top js-cluster-application-title" - > - {{ title }} - </span> + <img + v-if="hasLogo" + :src="logoUrl" + :alt="`${title} logo`" + class="cluster-application-logo avatar s40" + /> + <identicon + v-else + :entity-id="identiconId" + :entity-name="title" + size-class="s40" + /> + </div> <div - class="table-section section-wrap" + class="table-section cluster-application-description section-wrap" role="gridcell" > + <strong> + <a + v-if="titleLink" + :href="titleLink" + target="blank" + rel="noopener noreferrer" + class="js-cluster-application-title" + > + {{ title }} + </a> + <span + v-else + class="js-cluster-application-title" + > + {{ title }} + </span> + </strong> <slot name="description"></slot> + <div + v-if="hasError || isUnknownStatus" + class="cluster-application-error text-danger prepend-top-10" + > + <p class="js-cluster-application-general-error-message append-bottom-0"> + {{ generalErrorDescription }} + </p> + <ul v-if="statusReason || requestReason"> + <li + v-if="statusReason" + class="js-cluster-application-status-error-message" + > + {{ statusReason }} + </li> + <li + v-if="requestReason" + class="js-cluster-application-request-error-message" + > + {{ requestReason }} + </li> + </ul> + </div> </div> <div - :class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" + :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }" class="table-section table-button-footer section-align-top" role="gridcell" > @@ -168,6 +236,7 @@ > <a :href="manageLink" + :class="{ disabled: disabled }" class="btn" > {{ manageButtonLabel }} @@ -176,7 +245,7 @@ <div class="btn-group table-action-buttons"> <loading-button :loading="installButtonLoading" - :disabled="installButtonDisabled" + :disabled="disabled || installButtonDisabled" :label="installButtonLabel" class="js-cluster-application-install-button" @click="installClicked" @@ -184,35 +253,5 @@ </div> </div> </div> - <div - v-if="hasError || isUnknownStatus" - class="gl-responsive-table-row-layout" - role="row" - > - <div - class="alert alert-danger alert-block append-bottom-0 clusters-error-alert" - role="gridcell" - > - <div> - <p class="js-cluster-application-general-error-message"> - {{ generalErrorDescription }} - </p> - <ul v-if="statusReason || requestReason"> - <li - v-if="statusReason" - class="js-cluster-application-status-error-message" - > - {{ statusReason }} - </li> - <li - v-if="requestReason" - class="js-cluster-application-request-error-message" - > - {{ requestReason }} - </li> - </ul> - </div> - </div> - </div> </div> </template> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index d708a9e595a..a1069985178 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -1,5 +1,14 @@ <script> import _ from 'underscore'; +import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg'; +import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png'; +import gitlabLogo from 'images/cluster_app_logos/gitlab.png'; +import helmLogo from 'images/cluster_app_logos/helm.png'; +import jeagerLogo from 'images/cluster_app_logos/jeager.png'; +import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png'; +import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png'; +import meltanoLogo from 'images/cluster_app_logos/meltano.png'; +import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; @@ -37,21 +46,21 @@ export default { default: '', }, }, + data: () => ({ + elasticsearchLogo, + gitlabLogo, + helmLogo, + jeagerLogo, + jupyterhubLogo, + kubernetesLogo, + meltanoLogo, + prometheusLogo, + }), computed: { - generalApplicationDescription() { - return sprintf( - _.escape( - s__( - `ClusterIntegration|Install applications on your Kubernetes cluster. - Read more about %{helpLink}`, - ), - ), - { - helpLink: `<a href="${this.helpPath}"> - ${_.escape(s__('ClusterIntegration|installing applications'))} - </a>`, - }, - false, + helmInstalled() { + return ( + this.applications.helm.status === APPLICATION_STATUS.INSTALLED || + this.applications.helm.status === APPLICATION_STATUS.UPDATED ); }, ingressId() { @@ -128,224 +137,240 @@ export default { return this.applications.jupyter.hostname; }, }, + created() { + this.helmInstallIllustration = helmInstallIllustration; + }, }; </script> <template> - <section - id="cluster-applications" - class="settings no-animate expanded" - > - <div class="settings-header"> - <h4> - {{ s__('ClusterIntegration|Applications') }} - </h4> - <p - class="append-bottom-0" - v-html="generalApplicationDescription" - > - </p> - </div> + <section id="cluster-applications"> + <h4> + {{ s__('ClusterIntegration|Applications') }} + </h4> + <p class="append-bottom-0"> + {{ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster. + Helm Tiller is required to install any of the following applications.`) }} + <a :href="helpPath"> + {{ __('More information') }} + </a> + </p> - <div class="settings-content"> - <div class="append-bottom-20"> - <application-row - id="helm" - :title="applications.helm.title" - :status="applications.helm.status" - :status-reason="applications.helm.statusReason" - :request-status="applications.helm.requestStatus" - :request-reason="applications.helm.requestReason" - title-link="https://docs.helm.sh/" - > - <div slot="description"> - {{ s__(`ClusterIntegration|Helm streamlines installing - and managing Kubernetes applications. - Tiller runs inside of your Kubernetes Cluster, - and manages releases of your charts.`) }} - </div> - </application-row> - <application-row - :id="ingressId" - :title="applications.ingress.title" - :status="applications.ingress.status" - :status-reason="applications.ingress.statusReason" - :request-status="applications.ingress.requestStatus" - :request-reason="applications.ingress.requestReason" - title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" + <div class="cluster-application-list prepend-top-10"> + <application-row + id="helm" + :logo-url="helmLogo" + :title="applications.helm.title" + :status="applications.helm.status" + :status-reason="applications.helm.statusReason" + :request-status="applications.helm.requestStatus" + :request-reason="applications.helm.requestReason" + class="rounded-top" + title-link="https://docs.helm.sh/" + > + <div slot="description"> + {{ s__(`ClusterIntegration|Helm streamlines installing + and managing Kubernetes applications. + Tiller runs inside of your Kubernetes Cluster, + and manages releases of your charts.`) }} + </div> + </application-row> + <div + v-show="!helmInstalled" + class="cluster-application-warning" + > + <div + class="svg-container" + v-html="helmInstallIllustration" > - <div slot="description"> - <p> - {{ s__(`ClusterIntegration|Ingress gives you a way to route - requests to services based on the request host or path, - centralizing a number of services into a single entrypoint.`) }} - </p> + </div> + {{ s__(`ClusterIntegration|You must first install Helm Tiller before + installing the applications below`) }} + </div> + <application-row + :id="ingressId" + :logo-url="kubernetesLogo" + :title="applications.ingress.title" + :status="applications.ingress.status" + :status-reason="applications.ingress.statusReason" + :request-status="applications.ingress.requestStatus" + :request-reason="applications.ingress.requestReason" + :disabled="!helmInstalled" + title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|Ingress gives you a way to route + requests to services based on the request host or path, + centralizing a number of services into a single entrypoint.`) }} + </p> - <template v-if="ingressInstalled"> - <div class="form-group"> - <label for="ingress-ip-address"> - {{ s__('ClusterIntegration|Ingress IP Address') }} - </label> - <div - v-if="ingressExternalIp" - class="input-group" - > - <input - id="ingress-ip-address" - :value="ingressExternalIp" - type="text" - class="form-control js-ip-address" - readonly - /> - <span class="input-group-append"> - <clipboard-button - :text="ingressExternalIp" - :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" - class="input-group-text js-clipboard-btn" - /> - </span> - </div> + <template v-if="ingressInstalled"> + <div class="form-group"> + <label for="ingress-ip-address"> + {{ s__('ClusterIntegration|Ingress IP Address') }} + </label> + <div + v-if="ingressExternalIp" + class="input-group" + > <input - v-else + id="ingress-ip-address" + :value="ingressExternalIp" type="text" class="form-control js-ip-address" readonly - value="?" /> + <span class="input-group-append"> + <clipboard-button + :text="ingressExternalIp" + :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')" + class="input-group-text js-clipboard-btn" + /> + </span> </div> + <input + v-else + type="text" + class="form-control js-ip-address" + readonly + value="?" + /> + </div> - <p - v-if="!ingressExternalIp" - class="settings-message js-no-ip-message" - > - {{ s__(`ClusterIntegration|The IP address is in - the process of being assigned. Please check your Kubernetes - cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} + <p + v-if="!ingressExternalIp" + class="settings-message js-no-ip-message" + > + {{ s__(`ClusterIntegration|The IP address is in + the process of being assigned. Please check your Kubernetes + cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} - <a - :href="ingressHelpPath" - target="_blank" - rel="noopener noreferrer" - > - {{ __('More information') }} - </a> - </p> + <a + :href="ingressHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> - <p> - {{ s__(`ClusterIntegration|Point a wildcard DNS to this - generated IP address in order to access - your application after it has been deployed.`) }} - <a - :href="ingressDnsHelpPath" - target="_blank" - rel="noopener noreferrer" - > - {{ __('More information') }} - </a> - </p> + <p> + {{ s__(`ClusterIntegration|Point a wildcard DNS to this + generated IP address in order to access + your application after it has been deployed.`) }} + <a + :href="ingressDnsHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> - </template> - <div - v-else - v-html="ingressDescription" - > - </div> - </div> - </application-row> - <application-row - id="prometheus" - :title="applications.prometheus.title" - :manage-link="managePrometheusPath" - :status="applications.prometheus.status" - :status-reason="applications.prometheus.statusReason" - :request-status="applications.prometheus.requestStatus" - :request-reason="applications.prometheus.requestReason" - title-link="https://prometheus.io/docs/introduction/overview/" - > + </template> <div - slot="description" - v-html="prometheusDescription" + v-html="ingressDescription" > </div> - </application-row> - <application-row - id="runner" - :title="applications.runner.title" - :status="applications.runner.status" - :status-reason="applications.runner.statusReason" - :request-status="applications.runner.requestStatus" - :request-reason="applications.runner.requestReason" - title-link="https://docs.gitlab.com/runner/" - > - <div slot="description"> - {{ s__(`ClusterIntegration|GitLab Runner connects to this - project's repository and executes CI/CD jobs, - pushing results back and deploying, - applications to production.`) }} - </div> - </application-row> - <application-row - id="jupyter" - :title="applications.jupyter.title" - :status="applications.jupyter.status" - :status-reason="applications.jupyter.statusReason" - :request-status="applications.jupyter.requestStatus" - :request-reason="applications.jupyter.requestReason" - :install-application-request-params="{ hostname: applications.jupyter.hostname }" - title-link="https://jupyterhub.readthedocs.io/en/stable/" + </div> + </application-row> + <application-row + id="prometheus" + :logo-url="prometheusLogo" + :title="applications.prometheus.title" + :manage-link="managePrometheusPath" + :status="applications.prometheus.status" + :status-reason="applications.prometheus.statusReason" + :request-status="applications.prometheus.requestStatus" + :request-reason="applications.prometheus.requestReason" + :disabled="!helmInstalled" + title-link="https://prometheus.io/docs/introduction/overview/" + > + <div + slot="description" + v-html="prometheusDescription" > - <div slot="description"> - <p> - {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, - manages, and proxies multiple instances of the single-user - Jupyter notebook server. JupyterHub can be used to serve - notebooks to a class of students, a corporate data science group, - or a scientific research group.`) }} - </p> + </div> + </application-row> + <application-row + id="runner" + :logo-url="gitlabLogo" + :title="applications.runner.title" + :status="applications.runner.status" + :status-reason="applications.runner.statusReason" + :request-status="applications.runner.requestStatus" + :request-reason="applications.runner.requestReason" + :disabled="!helmInstalled" + title-link="https://docs.gitlab.com/runner/" + > + <div slot="description"> + {{ s__(`ClusterIntegration|GitLab Runner connects to this + project's repository and executes CI/CD jobs, + pushing results back and deploying, + applications to production.`) }} + </div> + </application-row> + <application-row + id="jupyter" + :logo-url="jupyterhubLogo" + :title="applications.jupyter.title" + :status="applications.jupyter.status" + :status-reason="applications.jupyter.statusReason" + :request-status="applications.jupyter.requestStatus" + :request-reason="applications.jupyter.requestReason" + :install-application-request-params="{ hostname: applications.jupyter.hostname }" + :disabled="!helmInstalled" + class="hide-bottom-border rounded-bottom" + title-link="https://jupyterhub.readthedocs.io/en/stable/" + > + <div slot="description"> + <p> + {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, + manages, and proxies multiple instances of the single-user + Jupyter notebook server. JupyterHub can be used to serve + notebooks to a class of students, a corporate data science group, + or a scientific research group.`) }} + </p> - <template v-if="ingressExternalIp"> - <div class="form-group"> - <label for="jupyter-hostname"> - {{ s__('ClusterIntegration|Jupyter Hostname') }} - </label> + <template v-if="ingressExternalIp"> + <div class="form-group"> + <label for="jupyter-hostname"> + {{ s__('ClusterIntegration|Jupyter Hostname') }} + </label> - <div class="input-group"> - <input - v-model="applications.jupyter.hostname" - :readonly="jupyterInstalled" - type="text" - class="form-control js-hostname" + <div class="input-group"> + <input + v-model="applications.jupyter.hostname" + :readonly="jupyterInstalled" + type="text" + class="form-control js-hostname" + /> + <span + class="input-group-btn" + > + <clipboard-button + :text="jupyterHostname" + :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" + class="js-clipboard-btn" /> - <span - class="input-group-btn" - > - <clipboard-button - :text="jupyterHostname" - :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" - class="js-clipboard-btn" - /> - </span> - </div> + </span> </div> - <p v-if="ingressInstalled"> - {{ s__(`ClusterIntegration|Replace this with your own hostname if you want. - If you do so, point hostname to Ingress IP Address from above.`) }} - <a - :href="ingressDnsHelpPath" - target="_blank" - rel="noopener noreferrer" - > - {{ __('More information') }} - </a> - </p> - </template> - </div> - </application-row> - <!-- - NOTE: Don't forget to update `clusters.scss` - min-height for this block and uncomment `application_spec` tests - --> - </div> + </div> + <p v-if="ingressInstalled"> + {{ s__(`ClusterIntegration|Replace this with your own hostname if you want. + If you do so, point hostname to Ingress IP Address from above.`) }} + <a + :href="ingressDnsHelpPath" + target="_blank" + rel="noopener noreferrer" + > + {{ __('More information') }} + </a> + </p> + </template> + </div> + </application-row> </div> </section> </template> diff --git a/app/assets/javascripts/clusters/components/gcp_signup_offer.js b/app/assets/javascripts/clusters/components/gcp_signup_offer.js deleted file mode 100644 index 04b778c6be9..00000000000 --- a/app/assets/javascripts/clusters/components/gcp_signup_offer.js +++ /dev/null @@ -1,8 +0,0 @@ -import PersistentUserCallout from '../../persistent_user_callout'; - -export default function gcpSignupOffer() { - const alertEl = document.querySelector('.gcp-signup-offer'); - if (!alertEl) return; - - new PersistentUserCallout(alertEl); -} diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue index 95c4be64d35..4849b0fa3db 100644 --- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue +++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue @@ -76,10 +76,10 @@ <template> <div class="content-list pipelines"> - <loading-icon + <gl-loading-icon v-if="isLoading" :label="s__('Pipelines|Loading Pipelines')" - size="3" + :size="3" class="prepend-top-20" /> diff --git a/app/assets/javascripts/commons/gitlab_ui.js b/app/assets/javascripts/commons/gitlab_ui.js index 923c036f5a4..aed26adfa5c 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,4 +1,16 @@ import Vue from 'vue'; -import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; +import Pagination from '@gitlab-org/gitlab-ui/dist/components/base/pagination'; +import progressBar from '@gitlab-org/gitlab-ui/dist/components/base/progress_bar'; +import modal from '@gitlab-org/gitlab-ui/dist/components/base/modal'; +import loadingIcon from '@gitlab-org/gitlab-ui/dist/components/base/loading_icon'; +import dModal from '@gitlab-org/gitlab-ui/dist/directives/modal'; +import dTooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip'; + +Vue.component('gl-pagination', Pagination); Vue.component('gl-progress-bar', progressBar); +Vue.component('gl-ui-modal', modal); +Vue.component('gl-loading-icon', loadingIcon); + +Vue.directive('gl-modal', dModal); +Vue.directive('gl-tooltip', dTooltip); diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js index 742cf490ad2..539d0d29e0d 100644 --- a/app/assets/javascripts/commons/polyfills.js +++ b/app/assets/javascripts/commons/polyfills.js @@ -14,10 +14,10 @@ import 'core-js/es6/map'; import 'core-js/es6/weak-map'; // Browser polyfills -import 'classlist-polyfill'; import 'formdata-polyfill'; import './polyfills/custom_event'; import './polyfills/element'; import './polyfills/event'; import './polyfills/nodelist'; import './polyfills/request_idle_callback'; +import './polyfills/svg'; diff --git a/app/assets/javascripts/commons/polyfills/element.js b/app/assets/javascripts/commons/polyfills/element.js index b593bde6aa2..dde5e8f54f9 100644 --- a/app/assets/javascripts/commons/polyfills/element.js +++ b/app/assets/javascripts/commons/polyfills/element.js @@ -1,12 +1,17 @@ -Element.prototype.closest = Element.prototype.closest || +// polyfill Element.classList and DOMTokenList with classList.js +import 'classlist-polyfill'; + +Element.prototype.closest = + Element.prototype.closest || function closest(selector, selectedElement = this) { if (!selectedElement) return null; - return selectedElement.matches(selector) ? - selectedElement : - Element.prototype.closest(selector, selectedElement.parentElement); + return selectedElement.matches(selector) + ? selectedElement + : Element.prototype.closest(selector, selectedElement.parentElement); }; -Element.prototype.matches = Element.prototype.matches || +Element.prototype.matches = + Element.prototype.matches || Element.prototype.matchesSelector || Element.prototype.mozMatchesSelector || Element.prototype.msMatchesSelector || @@ -15,13 +20,15 @@ Element.prototype.matches = Element.prototype.matches || function matches(selector) { const elms = (this.document || this.ownerDocument).querySelectorAll(selector); let i = elms.length - 1; - while (i >= 0 && elms.item(i) !== this) { i -= 1; } + while (i >= 0 && elms.item(i) !== this) { + i -= 1; + } return i > -1; }; // From the polyfill on MDN, https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove#Polyfill -((arr) => { - arr.forEach((item) => { +(arr => { + arr.forEach(item => { if (Object.prototype.hasOwnProperty.call(item, 'remove')) { return; } diff --git a/app/assets/javascripts/commons/polyfills/svg.js b/app/assets/javascripts/commons/polyfills/svg.js new file mode 100644 index 00000000000..8648a568f6f --- /dev/null +++ b/app/assets/javascripts/commons/polyfills/svg.js @@ -0,0 +1,5 @@ +import svg4everybody from 'svg4everybody'; + +// polyfill support for external SVG file references via <use xlink:href> +// @see https://css-tricks.com/svg-use-external-source/ +svg4everybody(); diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index 7399fc97d45..10548da8ec5 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -1,11 +1,7 @@ <script> -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import eventHub from '../eventhub'; export default { - components: { - loadingIcon, - }, props: { deployKey: { type: Object, @@ -45,7 +41,7 @@ export default { class="btn" @click="doAction"> <slot></slot> - <loading-icon + <gl-loading-icon v-if="isLoading" :inline="true" /> diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index d91e4809126..aa52f120fe7 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,7 +1,6 @@ <script> import { s__ } from '~/locale'; import Flash from '~/flash'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; import eventHub from '../eventhub'; import DeployKeysService from '../service'; @@ -11,7 +10,6 @@ import KeysPanel from './keys_panel.vue'; export default { components: { KeysPanel, - LoadingIcon, NavigationTabs, }, props: { @@ -114,10 +112,10 @@ export default { <template> <div class="append-bottom-default deploy-keys"> - <loading-icon + <gl-loading-icon v-if="isLoading && !hasKeys" :label="s__('DeployKeys|Loading deploy keys')" - size="2" + :size="2" /> <template v-else-if="hasKeys"> <div class="top-area scrolling-tabs-container inner-page-scroll-tabs"> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index f66ca070445..c05b9b1de79 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -145,8 +145,8 @@ export default { <icon :name="firstProject.can_push ? 'lock-open' : 'lock'"/> </a> <a - v-tooltip v-if="isExpandable" + v-tooltip :title="restProjectsTooltip" class="label deploy-project-label" @click="toggleExpanded" @@ -154,10 +154,10 @@ export default { <span>{{ restProjectsLabel }}</span> </a> <a - v-tooltip v-for="deployKeysProject in restProjects" v-else-if="isExpanded" :key="deployKeysProject.project.full_path" + v-tooltip :href="deployKeysProject.project.full_path" :title="projectTooltipTitle(deployKeysProject)" class="label deploy-project-label" @@ -198,8 +198,8 @@ export default { {{ __('Enable') }} </action-btn> <a - v-tooltip v-if="deployKey.can_edit" + v-tooltip :href="editDeployKeyPath" :title="__('Edit')" class="btn btn-default text-secondary" @@ -208,8 +208,8 @@ export default { <icon name="pencil"/> </a> <action-btn - v-tooltip v-if="isRemovable" + v-tooltip :deploy-key="deployKey" :title="__('Remove')" btn-css-class="btn-danger" @@ -219,8 +219,8 @@ export default { <icon name="remove"/> </action-btn> <action-btn - v-tooltip v-else-if="isEnabled" + v-tooltip :deploy-key="deployKey" :title="__('Disable')" btn-css-class="btn-warning" diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js index 5ed13488788..6fcad187b35 100644 --- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js +++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js @@ -1,4 +1,4 @@ -/* eslint-disable object-shorthand, func-names, comma-dangle, no-else-return, quotes */ +/* eslint-disable object-shorthand, func-names, no-else-return */ /* global CommentsStore */ /* global ResolveService */ @@ -25,44 +25,44 @@ const ResolveDiscussionBtn = Vue.extend({ }; }, computed: { - showButton: function () { + showButton: function() { if (this.discussion) { return this.discussion.isResolvable(); } else { return false; } }, - isDiscussionResolved: function () { + isDiscussionResolved: function() { if (this.discussion) { return this.discussion.isResolved(); } else { return false; } }, - buttonText: function () { + buttonText: function() { if (this.isDiscussionResolved) { - return "Unresolve discussion"; + return 'Unresolve discussion'; } else { - return "Resolve discussion"; + return 'Resolve discussion'; } }, - loading: function () { + loading: function() { if (this.discussion) { return this.discussion.loading; } else { return false; } - } + }, }, - created: function () { + created: function() { CommentsStore.createDiscussion(this.discussionId, this.canResolve); this.discussion = CommentsStore.state[this.discussionId]; }, methods: { - resolve: function () { + resolve: function() { ResolveService.toggleResolveForDiscussion(this.mergeRequestId, this.discussionId); - } + }, }, }); diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js index 0b3568e432d..e69eaad4423 100644 --- a/app/assets/javascripts/diff_notes/services/resolve.js +++ b/app/assets/javascripts/diff_notes/services/resolve.js @@ -8,9 +8,7 @@ window.gl = window.gl || {}; class ResolveServiceClass { constructor(root) { - this.noteResource = Vue.resource( - `${root}/notes{/noteId}/resolve?html=true`, - ); + this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`); this.discussionResource = Vue.resource( `${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`, ); @@ -51,10 +49,7 @@ class ResolveServiceClass { discussion.updateHeadline(data); }) .catch( - () => - new Flash( - 'An error occurred when trying to resolve a discussion. Please try again.', - ), + () => new Flash('An error occurred when trying to resolve a discussion. Please try again.'), ); } diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue index b5b05df4d34..bfb992340bc 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -4,7 +4,6 @@ import Icon from '~/vue_shared/components/icon.vue'; import { __ } from '~/locale'; import createFlash from '~/flash'; import eventHub from '../../notes/event_hub'; -import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import CompareVersions from './compare_versions.vue'; import ChangedFiles from './changed_files.vue'; import DiffFile from './diff_file.vue'; @@ -15,7 +14,6 @@ export default { name: 'DiffsApp', components: { Icon, - LoadingIcon, CompareVersions, ChangedFiles, DiffFile, @@ -59,7 +57,7 @@ export default { emailPatchPath: state => state.diffs.emailPatchPath, }), ...mapGetters('diffs', ['isParallelView']), - ...mapGetters(['isNotesFetched']), + ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), targetBranch() { return { branchName: this.targetBranchName, @@ -112,13 +110,26 @@ export default { }, created() { this.adjustView(); + eventHub.$once('fetchedNotesData', this.setDiscussions); }, methods: { - ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']), + ...mapActions('diffs', [ + 'setBaseConfig', + 'fetchDiffFiles', + 'startRenderDiffsQueue', + 'assignDiscussionsToDiff', + ]), + fetchData() { this.fetchDiffFiles() .then(() => { - requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 }); + requestIdleCallback( + () => { + this.setDiscussions(); + this.startRenderDiffsQueue(); + }, + { timeout: 1000 }, + ); }) .catch(() => { createFlash(__('Something went wrong on our end. Please try again!')); @@ -128,6 +139,16 @@ export default { eventHub.$emit('fetchNotesData'); } }, + setDiscussions() { + if (this.isNotesFetched) { + requestIdleCallback( + () => { + this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); + }, + { timeout: 1000 }, + ); + } + }, adjustView() { if (this.shouldShow && this.isParallelView) { window.mrTabs.expandViewContainer(); @@ -145,7 +166,7 @@ export default { v-if="isLoading" class="loading" > - <loading-icon /> + <gl-loading-icon /> </div> <div v-else diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue index 045688a32bf..0ec6b8b7f21 100644 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue @@ -63,7 +63,7 @@ export default { v-else role="button" class="fa fa-times dropdown-input-search" - @click="clearSearch" + @click.stop.prevent="clearSearch" ></i> </div> <div class="dropdown-content"> diff --git a/app/assets/javascripts/diffs/components/diff_discussions.vue b/app/assets/javascripts/diffs/components/diff_discussions.vue index e64d5511d78..cddbe554fbd 100644 --- a/app/assets/javascripts/diffs/components/diff_discussions.vue +++ b/app/assets/javascripts/diffs/components/diff_discussions.vue @@ -1,4 +1,5 @@ <script> +import { mapActions } from 'vuex'; import noteableDiscussion from '../../notes/components/noteable_discussion.vue'; export default { @@ -11,6 +12,14 @@ export default { required: true, }, }, + methods: { + ...mapActions('diffs', ['removeDiscussionsFromDiff']), + deleteNoteHandler(discussion) { + if (discussion.notes.length <= 1) { + this.removeDiscussionsFromDiff(discussion); + } + }, + }, }; </script> @@ -31,6 +40,7 @@ export default { :render-diff-file="false" :always-expanded="true" :discussions-by-diff-order="true" + @noteDeleted="deleteNoteHandler" /> </ul> </div> diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 59e9ba08b8b..bcbe374a90c 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -1,9 +1,8 @@ <script> -import { mapActions } from 'vuex'; +import { mapActions, mapGetters } from 'vuex'; import _ from 'underscore'; import { __, sprintf } from '~/locale'; import createFlash from '~/flash'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import DiffFileHeader from './diff_file_header.vue'; import DiffContent from './diff_content.vue'; @@ -11,7 +10,6 @@ export default { components: { DiffFileHeader, DiffContent, - LoadingIcon, }, props: { file: { @@ -30,6 +28,7 @@ export default { }; }, computed: { + ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), isCollapsed() { return this.file.collapsed || false; }, @@ -44,23 +43,23 @@ export default { ); }, showExpandMessage() { - return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge; + return ( + this.isCollapsed || + !this.file.highlightedDiffLines && + !this.isLoadingCollapsedDiff && + !this.file.tooLarge && + this.file.text + ); }, showLoadingIcon() { return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed); }, }, methods: { - ...mapActions('diffs', ['loadCollapsedDiff']), + ...mapActions('diffs', ['loadCollapsedDiff', 'assignDiscussionsToDiff']), handleToggle() { - const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file; - - if ( - collapsed && - !highlightedDiffLines && - parallelDiffLines !== undefined && - !parallelDiffLines.length - ) { + const { highlightedDiffLines, parallelDiffLines } = this.file; + if (!highlightedDiffLines && parallelDiffLines !== undefined && !parallelDiffLines.length) { this.handleLoadCollapsedDiff(); } else { this.file.collapsed = !this.file.collapsed; @@ -76,6 +75,14 @@ export default { this.file.collapsed = false; this.file.renderIt = true; }) + .then(() => { + requestIdleCallback( + () => { + this.assignDiscussionsToDiff(this.discussionsStructuredByLineCode); + }, + { timeout: 1000 }, + ); + }) .catch(() => { this.isLoadingCollapsedDiff = false; createFlash(__('Something went wrong on our end. Please try again!')); @@ -135,12 +142,12 @@ export default { :class="{ hidden: isCollapsed || file.tooLarge }" :diff-file="file" /> - <loading-icon - v-else-if="showLoadingIcon" + <gl-loading-icon + v-if="showLoadingIcon" class="diff-content loading" /> <div - v-if="showExpandMessage" + v-else-if="showExpandMessage" class="nothing-here-block diff-collapsed" > {{ __('This diff is collapsed.') }} diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index d3ffbe0415a..517fbf400e8 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -181,8 +181,8 @@ export default { </span> <strong - v-tooltip v-else + v-tooltip :title="filePath" class="file-title-name" data-container="body" @@ -255,8 +255,8 @@ export default { </a> <a - v-tooltip v-if="diffFile.externalUrl" + v-tooltip :href="diffFile.externalUrl" :title="`View on ${diffFile.formattedExternalUrl}`" target="_blank" diff --git a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue index 7e50a0aed84..1b59777f901 100644 --- a/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue +++ b/app/assets/javascripts/diffs/components/diff_gutter_avatars.vue @@ -1,15 +1,11 @@ <script> import { mapActions } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; -import tooltip from '~/vue_shared/directives/tooltip'; import { pluralize, truncate } from '~/lib/utils/text_utility'; import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; import { COUNT_OF_AVATARS_IN_GUTTER, LENGTH_OF_AVATAR_TOOLTIP } from '../constants'; export default { - directives: { - tooltip, - }, components: { Icon, UserAvatarImage, @@ -91,10 +87,10 @@ export default { @click.native="toggleDiscussions" /> <span - v-tooltip v-if="moreText" + v-gl-tooltip :title="moreText" - class="diff-comments-more-count has-tooltip js-diff-comment-avatar js-diff-comment-plus" + class="diff-comments-more-count js-diff-comment-avatar js-diff-comment-plus" data-container="body" data-placement="top" role="button" diff --git a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue index 8ad1ea34245..6eff3013dcd 100644 --- a/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue +++ b/app/assets/javascripts/diffs/components/diff_line_gutter_content.vue @@ -13,6 +13,10 @@ export default { Icon, }, props: { + line: { + type: Object, + required: true, + }, fileHash: { type: String, required: true, @@ -21,31 +25,16 @@ export default { type: String, required: true, }, - lineType: { - type: String, - required: false, - default: '', - }, lineNumber: { type: Number, required: false, default: 0, }, - lineCode: { - type: String, - required: false, - default: '', - }, linePosition: { type: String, required: false, default: '', }, - metaData: { - type: Object, - required: false, - default: () => ({}), - }, showCommentButton: { type: Boolean, required: false, @@ -76,11 +65,6 @@ export default { required: false, default: false, }, - discussions: { - type: Array, - required: false, - default: () => [], - }, }, computed: { ...mapState({ @@ -89,7 +73,7 @@ export default { }), ...mapGetters(['isLoggedIn']), lineHref() { - return this.lineCode ? `#${this.lineCode}` : '#'; + return `#${this.line.lineCode || ''}`; }, shouldShowCommentButton() { return ( @@ -103,20 +87,19 @@ export default { ); }, hasDiscussions() { - return this.discussions.length > 0; + return this.line.discussions && this.line.discussions.length > 0; }, shouldShowAvatarsOnGutter() { - if (!this.lineType && this.linePosition === LINE_POSITION_RIGHT) { + if (!this.line.type && this.linePosition === LINE_POSITION_RIGHT) { return false; } - return this.showCommentButton && this.hasDiscussions; }, }, methods: { ...mapActions('diffs', ['loadMoreLines', 'showCommentForm']), handleCommentButton() { - this.showCommentForm({ lineCode: this.lineCode }); + this.showCommentForm({ lineCode: this.line.lineCode }); }, handleLoadMoreLines() { if (this.isRequesting) { @@ -125,8 +108,8 @@ export default { this.isRequesting = true; const endpoint = this.contextLinesPath; - const oldLineNumber = this.metaData.oldPos || 0; - const newLineNumber = this.metaData.newPos || 0; + const oldLineNumber = this.line.metaData.oldPos || 0; + const newLineNumber = this.line.metaData.newPos || 0; const offset = newLineNumber - oldLineNumber; const bottom = this.isBottom; const { fileHash } = this; @@ -201,7 +184,7 @@ export default { </a> <diff-gutter-avatars v-if="shouldShowAvatarsOnGutter" - :discussions="discussions" + :discussions="line.discussions" /> </template> </div> diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue index cbe4551d06b..bb9bb821de3 100644 --- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue +++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue @@ -1,9 +1,7 @@ <script> import { mapState, mapGetters, mapActions } from 'vuex'; -import createFlash from '~/flash'; import { s__ } from '~/locale'; import noteForm from '../../notes/components/note_form.vue'; -import { getNoteFormData } from '../store/utils'; import autosave from '../../notes/mixins/autosave'; import { DIFF_NOTE_TYPE } from '../constants'; @@ -21,7 +19,7 @@ export default { type: Object, required: true, }, - position: { + linePosition: { type: String, required: false, default: '', @@ -38,6 +36,16 @@ export default { }), ...mapGetters('diffs', ['getDiffFileByHash']), ...mapGetters(['isLoggedIn', 'noteableType', 'getNoteableData', 'getNotesDataByProp']), + formData() { + return { + noteableData: this.noteableData, + noteableType: this.noteableType, + noteTargetLine: this.noteTargetLine, + diffViewType: this.diffViewType, + diffFile: this.getDiffFileByHash(this.diffFileHash), + linePosition: this.linePosition, + }; + }, }, mounted() { if (this.isLoggedIn) { @@ -52,8 +60,7 @@ export default { } }, methods: { - ...mapActions('diffs', ['cancelCommentForm']), - ...mapActions(['saveNote', 'refetchDiscussionById']), + ...mapActions('diffs', ['cancelCommentForm', 'assignDiscussionsToDiff', 'saveDiffDiscussion']), handleCancelCommentForm(shouldConfirm, isDirty) { if (shouldConfirm && isDirty) { const msg = s__('Notes|Are you sure you want to cancel creating this comment?'); @@ -72,32 +79,9 @@ export default { }); }, handleSaveNote(note) { - const selectedDiffFile = this.getDiffFileByHash(this.diffFileHash); - const postData = getNoteFormData({ - note, - noteableData: this.noteableData, - noteableType: this.noteableType, - noteTargetLine: this.noteTargetLine, - diffViewType: this.diffViewType, - diffFile: selectedDiffFile, - linePosition: this.position, - }); - - this.saveNote(postData) - .then(result => { - const endpoint = this.getNotesDataByProp('discussionsPath'); - - this.refetchDiscussionById({ path: endpoint, discussionId: result.discussion_id }) - .then(() => { - this.handleCancelCommentForm(); - }) - .catch(() => { - createFlash(s__('MergeRequests|Updating discussions failed')); - }); - }) - .catch(() => { - createFlash(s__('MergeRequests|Saving the comment failed')); - }); + return this.saveDiffDiscussion({ note, formData: this.formData }).then(() => + this.handleCancelCommentForm(), + ); }, }, }; diff --git a/app/assets/javascripts/diffs/components/diff_table_cell.vue b/app/assets/javascripts/diffs/components/diff_table_cell.vue index 33bc8d9971e..5d9a0b123fe 100644 --- a/app/assets/javascripts/diffs/components/diff_table_cell.vue +++ b/app/assets/javascripts/diffs/components/diff_table_cell.vue @@ -11,8 +11,6 @@ import { LINE_HOVER_CLASS_NAME, LINE_UNFOLD_CLASS_NAME, INLINE_DIFF_VIEW_TYPE, - LINE_POSITION_LEFT, - LINE_POSITION_RIGHT, } from '../constants'; export default { @@ -67,42 +65,24 @@ export default { required: false, default: false, }, - discussions: { - type: Array, - required: false, - default: () => [], - }, }, computed: { ...mapGetters(['isLoggedIn']), - normalizedLine() { - let normalizedLine; - - if (this.diffViewType === INLINE_DIFF_VIEW_TYPE) { - normalizedLine = this.line; - } else if (this.linePosition === LINE_POSITION_LEFT) { - normalizedLine = this.line.left; - } else if (this.linePosition === LINE_POSITION_RIGHT) { - normalizedLine = this.line.right; - } - - return normalizedLine; - }, isMatchLine() { - return this.normalizedLine.type === MATCH_LINE_TYPE; + return this.line.type === MATCH_LINE_TYPE; }, isContextLine() { - return this.normalizedLine.type === CONTEXT_LINE_TYPE; + return this.line.type === CONTEXT_LINE_TYPE; }, isMetaLine() { - const { type } = this.normalizedLine; + const { type } = this.line; return ( type === OLD_NO_NEW_LINE_TYPE || type === NEW_NO_NEW_LINE_TYPE || type === EMPTY_CELL_TYPE ); }, classNameMap() { - const { type } = this.normalizedLine; + const { type } = this.line; return { [type]: type, @@ -116,9 +96,9 @@ export default { }; }, lineNumber() { - const { lineType, normalizedLine } = this; + const { lineType } = this; - return lineType === OLD_LINE_TYPE ? normalizedLine.oldLine : normalizedLine.newLine; + return lineType === OLD_LINE_TYPE ? this.line.oldLine : this.line.newLine; }, }, }; @@ -129,20 +109,17 @@ export default { :class="classNameMap" > <diff-line-gutter-content + :line="line" :file-hash="fileHash" :context-lines-path="contextLinesPath" - :line-type="normalizedLine.type" - :line-code="normalizedLine.lineCode" :line-position="linePosition" :line-number="lineNumber" - :meta-data="normalizedLine.metaData" :show-comment-button="showCommentButton" :is-hover="isHover" :is-bottom="isBottom" :is-match-line="isMatchLine" :is-context-line="isContentLine" :is-meta-line="isMetaLine" - :discussions="discussions" /> </td> </template> 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 caf84dc9573..46a51859da5 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_comment_row.vue @@ -21,18 +21,13 @@ export default { type: Number, required: true, }, - discussions: { - type: Array, - required: false, - default: () => [], - }, }, computed: { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), className() { - return this.discussions.length ? '' : 'js-temp-notes-holder'; + return this.line.discussions.length ? '' : 'js-temp-notes-holder'; }, }, }; @@ -44,14 +39,13 @@ export default { class="notes_holder" > <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> + class="notes_content" + colspan="3" + > <div class="content"> <diff-discussions - v-if="discussions.length" - :discussions="discussions" + v-if="line.discussions.length" + :discussions="line.discussions" /> <diff-line-note-form v-if="diffLineCommentForms[line.lineCode]" diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue index 32d65ff994f..62fa34e835a 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapActions } from 'vuex'; import DiffTableCell from './diff_table_cell.vue'; import { NEW_LINE_TYPE, @@ -33,11 +33,6 @@ export default { required: false, default: false, }, - discussions: { - type: Array, - required: false, - default: () => [], - }, }, data() { return { @@ -68,7 +63,11 @@ export default { this.linePositionLeft = LINE_POSITION_LEFT; this.linePositionRight = LINE_POSITION_RIGHT; }, + mounted() { + this.scrollToLineIfNeededInline(this.line); + }, methods: { + ...mapActions('diffs', ['scrollToLineIfNeededInline']), handleMouseMove(e) { // To show the comment icon on the gutter we need to know if we hover the line. // Current table structure doesn't allow us to do this with CSS in both of the diff view types @@ -94,7 +93,6 @@ export default { :is-bottom="isBottom" :is-hover="isHover" :show-comment-button="true" - :discussions="discussions" class="diff-line-num old_line" /> <diff-table-cell @@ -104,7 +102,6 @@ export default { :line-type="newLineType" :is-bottom="isBottom" :is-hover="isHover" - :discussions="discussions" class="diff-line-num new_line" /> <td diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue index e7d789734c3..fbf9e77ac07 100644 --- a/app/assets/javascripts/diffs/components/inline_diff_view.vue +++ b/app/assets/javascripts/diffs/components/inline_diff_view.vue @@ -2,7 +2,6 @@ import { mapGetters, mapState } from 'vuex'; import inlineDiffTableRow from './inline_diff_table_row.vue'; import inlineDiffCommentRow from './inline_diff_comment_row.vue'; -import { trimFirstCharOfLineContent } from '../store/utils'; export default { components: { @@ -20,29 +19,17 @@ export default { }, }, computed: { - ...mapGetters('diffs', [ - 'commitId', - 'shouldRenderInlineCommentRow', - 'singleDiscussionByLineCode', - ]), + ...mapGetters('diffs', ['commitId', 'shouldRenderInlineCommentRow']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - normalizedDiffLines() { - return this.diffLines.map(line => (line.richText ? trimFirstCharOfLineContent(line) : line)); - }, diffLinesLength() { - return this.normalizedDiffLines.length; + return this.diffLines.length; }, userColorScheme() { return window.gon.user_color_scheme; }, }, - methods: { - discussionsList(line) { - return line.lineCode !== undefined ? this.singleDiscussionByLineCode(line.lineCode) : []; - }, - }, }; </script> @@ -53,23 +40,21 @@ export default { class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"> <tbody> <template - v-for="(line, index) in normalizedDiffLines" + v-for="(line, index) in diffLines" > <inline-diff-table-row + :key="line.lineCode" :file-hash="diffFile.fileHash" :context-lines-path="diffFile.contextLinesPath" :line="line" :is-bottom="index + 1 === diffLinesLength" - :key="line.lineCode" - :discussions="discussionsList(line)" /> <inline-diff-comment-row v-if="shouldRenderInlineCommentRow(line)" + :key="index" :diff-file-hash="diffFile.fileHash" :line="line" :line-index="index" - :key="index" - :discussions="discussionsList(line)" /> </template> </tbody> diff --git a/app/assets/javascripts/diffs/components/no_changes.vue b/app/assets/javascripts/diffs/components/no_changes.vue index d817157fbcd..6905630ad8c 100644 --- a/app/assets/javascripts/diffs/components/no_changes.vue +++ b/app/assets/javascripts/diffs/components/no_changes.vue @@ -38,7 +38,7 @@ export default { <div class="text-center"> <a :href="newBlobPath" - class="btn btn-save" + class="btn btn-success" > {{ __('Create commit') }} </a> 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 48b8feeb0b4..3339c56cbb6 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_comment_row.vue @@ -21,51 +21,49 @@ export default { type: Number, required: true, }, - leftDiscussions: { - type: Array, - required: false, - default: () => [], - }, - rightDiscussions: { - type: Array, - required: false, - default: () => [], - }, }, computed: { ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), leftLineCode() { - return this.line.left.lineCode; + return this.line.left && this.line.left.lineCode; }, rightLineCode() { - return this.line.right.lineCode; + return this.line.right && this.line.right.lineCode; }, hasExpandedDiscussionOnLeft() { - const discussions = this.leftDiscussions; - - return discussions ? discussions.every(discussion => discussion.expanded) : false; + return this.line.left && this.line.left.discussions + ? this.line.left.discussions.every(discussion => discussion.expanded) + : false; }, hasExpandedDiscussionOnRight() { - const discussions = this.rightDiscussions; - - return discussions ? discussions.every(discussion => discussion.expanded) : false; + return this.line.right && this.line.right.discussions + ? this.line.right.discussions.every(discussion => discussion.expanded) + : false; }, hasAnyExpandedDiscussion() { return this.hasExpandedDiscussionOnLeft || this.hasExpandedDiscussionOnRight; }, shouldRenderDiscussionsOnLeft() { - return this.leftDiscussions && this.hasExpandedDiscussionOnLeft; + return this.line.left && this.line.left.discussions && this.hasExpandedDiscussionOnLeft; }, shouldRenderDiscussionsOnRight() { - return this.rightDiscussions && this.hasExpandedDiscussionOnRight && this.line.right.type; + return ( + this.line.right && + this.line.right.discussions && + this.hasExpandedDiscussionOnRight && + this.line.right.type + ); }, showRightSideCommentForm() { - return this.line.right.type && this.diffLineCommentForms[this.rightLineCode]; + return ( + this.line.right && this.line.right.type && this.diffLineCommentForms[this.rightLineCode] + ); }, className() { - return this.leftDiscussions.length > 0 || this.rightDiscussions.length > 0 + return (this.left && this.line.left.discussions.length > 0) || + (this.right && this.line.right.discussions.length > 0) ? '' : 'js-temp-notes-holder'; }, @@ -85,8 +83,8 @@ export default { class="content" > <diff-discussions - v-if="leftDiscussions.length" - :discussions="leftDiscussions" + v-if="line.left.discussions.length" + :discussions="line.left.discussions" /> </div> <diff-line-note-form @@ -94,7 +92,7 @@ export default { :diff-file-hash="diffFileHash" :line="line.left" :note-target-line="line.left" - position="left" + line-position="left" /> </td> <td class="notes_line new"></td> @@ -104,8 +102,8 @@ export default { class="content" > <diff-discussions - v-if="rightDiscussions.length" - :discussions="rightDiscussions" + v-if="line.right.discussions.length" + :discussions="line.right.discussions" /> </div> <diff-line-note-form @@ -113,7 +111,7 @@ export default { :diff-file-hash="diffFileHash" :line="line.right" :note-target-line="line.right" - position="right" + line-position="right" /> </td> </tr> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index d4e54c2bd00..fcc3b3e9117 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -1,6 +1,6 @@ <script> +import { mapActions } from 'vuex'; import $ from 'jquery'; -import { mapGetters } from 'vuex'; import DiffTableCell from './diff_table_cell.vue'; import { NEW_LINE_TYPE, @@ -10,8 +10,7 @@ import { OLD_NO_NEW_LINE_TYPE, PARALLEL_DIFF_VIEW_TYPE, NEW_NO_NEW_LINE_TYPE, - LINE_POSITION_LEFT, - LINE_POSITION_RIGHT, + EMPTY_CELL_TYPE, } from '../constants'; export default { @@ -36,16 +35,6 @@ export default { required: false, default: false, }, - leftDiscussions: { - type: Array, - required: false, - default: () => [], - }, - rightDiscussions: { - type: Array, - required: false, - default: () => [], - }, }, data() { return { @@ -54,32 +43,33 @@ export default { }; }, computed: { - ...mapGetters('diffs', ['isParallelView']), isContextLine() { - return this.line.left.type === CONTEXT_LINE_TYPE; + return this.line.left && this.line.left.type === CONTEXT_LINE_TYPE; }, classNameMap() { return { [CONTEXT_LINE_CLASS_NAME]: this.isContextLine, - [PARALLEL_DIFF_VIEW_TYPE]: this.isParallelView, + [PARALLEL_DIFF_VIEW_TYPE]: true, }; }, parallelViewLeftLineType() { - if (this.line.right.type === NEW_NO_NEW_LINE_TYPE) { + if (this.line.right && this.line.right.type === NEW_NO_NEW_LINE_TYPE) { return OLD_NO_NEW_LINE_TYPE; } - return this.line.left.type; + return this.line.left ? this.line.left.type : EMPTY_CELL_TYPE; }, }, created() { this.newLineType = NEW_LINE_TYPE; this.oldLineType = OLD_LINE_TYPE; - this.linePositionLeft = LINE_POSITION_LEFT; - this.linePositionRight = LINE_POSITION_RIGHT; this.parallelDiffViewType = PARALLEL_DIFF_VIEW_TYPE; }, + mounted() { + this.scrollToLineIfNeededParallel(this.line); + }, methods: { + ...mapActions('diffs', ['scrollToLineIfNeededParallel']), handleMouseMove(e) { const isHover = e.type === 'mouseover'; const hoveringCell = e.target.closest('td'); @@ -116,47 +106,57 @@ export default { @mouseover="handleMouseMove" @mouseout="handleMouseMove" > - <diff-table-cell - :file-hash="fileHash" - :context-lines-path="contextLinesPath" - :line="line" - :line-type="oldLineType" - :line-position="linePositionLeft" - :is-bottom="isBottom" - :is-hover="isLeftHover" - :show-comment-button="true" - :diff-view-type="parallelDiffViewType" - :discussions="leftDiscussions" - class="diff-line-num old_line" - /> - <td - :id="line.left.lineCode" - :class="parallelViewLeftLineType" - class="line_content parallel left-side" - @mousedown.native="handleParallelLineMouseDown" - v-html="line.left.richText" - > - </td> - <diff-table-cell - :file-hash="fileHash" - :context-lines-path="contextLinesPath" - :line="line" - :line-type="newLineType" - :line-position="linePositionRight" - :is-bottom="isBottom" - :is-hover="isRightHover" - :show-comment-button="true" - :diff-view-type="parallelDiffViewType" - :discussions="rightDiscussions" - class="diff-line-num new_line" - /> - <td - :id="line.right.lineCode" - :class="line.right.type" - class="line_content parallel right-side" - @mousedown.native="handleParallelLineMouseDown" - v-html="line.right.richText" - > - </td> + <template v-if="line.left"> + <diff-table-cell + :file-hash="fileHash" + :context-lines-path="contextLinesPath" + :line="line.left" + :line-type="oldLineType" + :is-bottom="isBottom" + :is-hover="isLeftHover" + :show-comment-button="true" + :diff-view-type="parallelDiffViewType" + line-position="left" + class="diff-line-num old_line" + /> + <td + :id="line.left.lineCode" + :class="parallelViewLeftLineType" + class="line_content parallel left-side" + @mousedown.native="handleParallelLineMouseDown" + v-html="line.left.richText" + > + </td> + </template> + <template v-else> + <td class="diff-line-num old_line empty-cell"></td> + <td class="line_content parallel left-side empty-cell"></td> + </template> + <template v-if="line.right"> + <diff-table-cell + :file-hash="fileHash" + :context-lines-path="contextLinesPath" + :line="line.right" + :line-type="newLineType" + :is-bottom="isBottom" + :is-hover="isRightHover" + :show-comment-button="true" + :diff-view-type="parallelDiffViewType" + line-position="right" + class="diff-line-num new_line" + /> + <td + :id="line.right.lineCode" + :class="line.right.type" + class="line_content parallel right-side" + @mousedown.native="handleParallelLineMouseDown" + v-html="line.right.richText" + > + </td> + </template> + <template v-else> + <td class="diff-line-num old_line empty-cell"></td> + <td class="line_content parallel right-side empty-cell"></td> + </template> </tr> </template> diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue index 24ceb52a04a..3452f0d2b00 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_view.vue @@ -2,8 +2,6 @@ import { mapState, mapGetters } from 'vuex'; import parallelDiffTableRow from './parallel_diff_table_row.vue'; import parallelDiffCommentRow from './parallel_diff_comment_row.vue'; -import { EMPTY_CELL_TYPE } from '../constants'; -import { trimFirstCharOfLineContent } from '../store/utils'; export default { components: { @@ -21,46 +19,17 @@ export default { }, }, computed: { - ...mapGetters('diffs', [ - 'commitId', - 'singleDiscussionByLineCode', - 'shouldRenderParallelCommentRow', - ]), + ...mapGetters('diffs', ['commitId', 'shouldRenderParallelCommentRow']), ...mapState({ diffLineCommentForms: state => state.diffs.diffLineCommentForms, }), - parallelDiffLines() { - return this.diffLines.map(line => { - const parallelLine = Object.assign({}, line); - - if (line.left) { - parallelLine.left = trimFirstCharOfLineContent(line.left); - } else { - parallelLine.left = { type: EMPTY_CELL_TYPE }; - } - - if (line.right) { - parallelLine.right = trimFirstCharOfLineContent(line.right); - } else { - parallelLine.right = { type: EMPTY_CELL_TYPE }; - } - - return parallelLine; - }); - }, diffLinesLength() { - return this.parallelDiffLines.length; + return this.diffLines.length; }, userColorScheme() { return window.gon.user_color_scheme; }, }, - methods: { - discussionsByLine(line, leftOrRight) { - return line[leftOrRight] && line[leftOrRight].lineCode !== undefined ? - this.singleDiscussionByLineCode(line[leftOrRight].lineCode) : []; - }, - }, }; </script> @@ -73,16 +42,14 @@ export default { <table> <tbody> <template - v-for="(line, index) in parallelDiffLines" + v-for="(line, index) in diffLines" > <parallel-diff-table-row + :key="index" :file-hash="diffFile.fileHash" :context-lines-path="diffFile.contextLinesPath" :line="line" :is-bottom="index + 1 === diffLinesLength" - :key="index" - :left-discussions="discussionsByLine(line, 'left')" - :right-discussions="discussionsByLine(line, 'right')" /> <parallel-diff-comment-row v-if="shouldRenderParallelCommentRow(line)" @@ -90,8 +57,6 @@ export default { :line="line" :diff-file-hash="diffFile.fileHash" :line-index="index" - :left-discussions="discussionsByLine(line, 'left')" - :right-discussions="discussionsByLine(line, 'right')" /> </template> </tbody> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index f68afa44837..2795dddfc48 100644 --- a/app/assets/javascripts/diffs/constants.js +++ b/app/assets/javascripts/diffs/constants.js @@ -7,6 +7,7 @@ export const CONTEXT_LINE_TYPE = 'context'; export const EMPTY_CELL_TYPE = 'empty-cell'; export const COMMENT_FORM_TYPE = 'commentForm'; export const DIFF_NOTE_TYPE = 'DiffNote'; +export const LEGACY_DIFF_NOTE_TYPE = 'LegacyDiffNote'; export const NOTE_TYPE = 'Note'; export const NEW_LINE_TYPE = 'new'; export const OLD_LINE_TYPE = 'old'; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 4ab6ceb249a..98d8d5943f9 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -1,8 +1,12 @@ import Vue from 'vue'; import axios from '~/lib/utils/axios_utils'; import Cookies from 'js-cookie'; +import createFlash from '~/flash'; +import { s__ } from '~/locale'; import { handleLocationHash, historyPushState } from '~/lib/utils/common_utils'; -import { mergeUrlParams } from '~/lib/utils/url_utility'; +import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility'; +import { reduceDiscussionsToLineCodes } from '../../notes/stores/utils'; +import { getDiffPositionByLineCode, getNoteFormData } from './utils'; import * as types from './mutation_types'; import { PARALLEL_DIFF_VIEW_TYPE, @@ -29,25 +33,53 @@ export const fetchDiffFiles = ({ state, commit }) => { .then(handleLocationHash); }; -export const startRenderDiffsQueue = ({ state, commit }) => { - const checkItem = () => { - const nextFile = state.diffFiles.find( - file => !file.renderIt && (!file.collapsed || !file.text), - ); - if (nextFile) { - requestAnimationFrame(() => { - commit(types.RENDER_FILE, nextFile); +// This is adding line discussions to the actual lines in the diff tree +// once for parallel and once for inline mode +export const assignDiscussionsToDiff = ({ state, commit }, allLineDiscussions) => { + const diffPositionByLineCode = getDiffPositionByLineCode(state.diffFiles); + + Object.values(allLineDiscussions).forEach(discussions => { + if (discussions.length > 0) { + const { fileHash } = discussions[0]; + commit(types.SET_LINE_DISCUSSIONS_FOR_FILE, { + fileHash, + discussions, + diffPositionByLineCode, }); - requestIdleCallback( - () => { - checkItem(); - }, - { timeout: 1000 }, - ); } - }; + }); +}; + +export const removeDiscussionsFromDiff = ({ commit }, removeDiscussion) => { + const { fileHash, line_code } = removeDiscussion; + commit(types.REMOVE_LINE_DISCUSSIONS_FOR_FILE, { fileHash, lineCode: line_code }); +}; + +export const startRenderDiffsQueue = ({ state, commit }) => { + const checkItem = () => + new Promise(resolve => { + const nextFile = state.diffFiles.find( + file => !file.renderIt && (!file.collapsed || !file.text), + ); + + if (nextFile) { + requestAnimationFrame(() => { + commit(types.RENDER_FILE, nextFile); + }); + requestIdleCallback( + () => { + checkItem() + .then(resolve) + .catch(() => {}); + }, + { timeout: 1000 }, + ); + } else { + resolve(); + } + }); - checkItem(); + return checkItem(); }; export const setInlineDiffViewType = ({ commit }) => { @@ -91,6 +123,25 @@ export const loadMoreLines = ({ commit }, options) => { }); }; +export const scrollToLineIfNeededInline = (_, line) => { + const hash = getLocationHash(); + + if (hash && line.lineCode === hash) { + handleLocationHash(); + } +}; + +export const scrollToLineIfNeededParallel = (_, line) => { + const hash = getLocationHash(); + + if ( + hash && + ((line.left && line.left.lineCode === hash) || (line.right && line.right.lineCode === hash)) + ) { + handleLocationHash(); + } +}; + export const loadCollapsedDiff = ({ commit }, file) => axios.get(file.loadCollapsedDiffUrl).then(res => { commit(types.ADD_COLLAPSED_DIFFS, { @@ -130,5 +181,19 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { }); }; +export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { + const postData = getNoteFormData({ + note, + ...formData, + }); + + return dispatch('saveNote', postData, { root: true }) + .then(result => dispatch('updateDiscussion', result.discussion, { root: true })) + .then(discussion => + dispatch('assignDiscussionsToDiff', reduceDiscussionsToLineCodes([discussion])), + ) + .catch(() => createFlash(s__('MergeRequests|Saving the comment failed'))); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js index 4a47646d7fa..968ba3c5e13 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -17,7 +17,10 @@ export const commitId = state => (state.commit && state.commit.id ? state.commit export const diffHasAllExpandedDiscussions = (state, getters) => diff => { const discussions = getters.getDiffFileDiscussions(diff); - return (discussions.length && discussions.every(discussion => discussion.expanded)) || false; + return ( + (discussions && discussions.length && discussions.every(discussion => discussion.expanded)) || + false + ); }; /** @@ -28,7 +31,10 @@ export const diffHasAllExpandedDiscussions = (state, getters) => diff => { export const diffHasAllCollpasedDiscussions = (state, getters) => diff => { const discussions = getters.getDiffFileDiscussions(diff); - return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false; + return ( + (discussions && discussions.length && discussions.every(discussion => !discussion.expanded)) || + false + ); }; /** @@ -40,7 +46,9 @@ export const diffHasExpandedDiscussions = (state, getters) => diff => { const discussions = getters.getDiffFileDiscussions(diff); return ( - (discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) || + (discussions && + discussions.length && + discussions.find(discussion => discussion.expanded) !== undefined) || false ); }; @@ -64,45 +72,38 @@ export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) = discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), ) || []; -export const singleDiscussionByLineCode = (state, getters, rootState, rootGetters) => lineCode => { - if (!lineCode || lineCode === undefined) return []; - const discussions = rootGetters.discussionsByLineCode; - return discussions[lineCode] || []; -}; - -export const shouldRenderParallelCommentRow = (state, getters) => line => { - const leftLineCode = line.left.lineCode; - const rightLineCode = line.right.lineCode; - const leftDiscussions = getters.singleDiscussionByLineCode(leftLineCode); - const rightDiscussions = getters.singleDiscussionByLineCode(rightLineCode); - const hasDiscussion = leftDiscussions.length || rightDiscussions.length; +export const shouldRenderParallelCommentRow = state => line => { + const hasDiscussion = + (line.left && line.left.discussions && line.left.discussions.length) || + (line.right && line.right.discussions && line.right.discussions.length); - const hasExpandedDiscussionOnLeft = leftDiscussions.length - ? leftDiscussions.every(discussion => discussion.expanded) - : false; - const hasExpandedDiscussionOnRight = rightDiscussions.length - ? rightDiscussions.every(discussion => discussion.expanded) - : false; + const hasExpandedDiscussionOnLeft = + line.left && line.left.discussions && line.left.discussions.length + ? line.left.discussions.every(discussion => discussion.expanded) + : false; + const hasExpandedDiscussionOnRight = + line.right && line.right.discussions && line.right.discussions.length + ? line.right.discussions.every(discussion => discussion.expanded) + : false; if (hasDiscussion && (hasExpandedDiscussionOnLeft || hasExpandedDiscussionOnRight)) { return true; } - const hasCommentFormOnLeft = state.diffLineCommentForms[leftLineCode]; - const hasCommentFormOnRight = state.diffLineCommentForms[rightLineCode]; + const hasCommentFormOnLeft = line.left && state.diffLineCommentForms[line.left.lineCode]; + const hasCommentFormOnRight = line.right && state.diffLineCommentForms[line.right.lineCode]; return hasCommentFormOnLeft || hasCommentFormOnRight; }; -export const shouldRenderInlineCommentRow = (state, getters) => line => { +export const shouldRenderInlineCommentRow = state => line => { if (state.diffLineCommentForms[line.lineCode]) return true; - const lineDiscussions = getters.singleDiscussionByLineCode(line.lineCode); - if (lineDiscussions.length === 0) { + if (!line.discussions || line.discussions.length === 0) { return false; } - return lineDiscussions.every(discussion => discussion.expanded); + return line.discussions.every(discussion => discussion.expanded); }; // prevent babel-plugin-rewire from generating an invalid default during karma∂ tests diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 39d90a64aab..eb596b251c1 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -11,8 +11,10 @@ export default () => ({ endpoint: '', basePath: '', commit: null, + startVersion: null, diffFiles: [], mergeRequestDiffs: [], + mergeRequestDiff: null, diffLineCommentForms: {}, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, }); diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 20d1ebbe049..6860e24db6b 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -3,10 +3,10 @@ import * as getters from '../getters'; import mutations from '../mutations'; import createState from './diff_state'; -export default { +export default () => ({ namespaced: true, state: createState(), getters, actions, mutations, -}; +}); diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js index c999d637d50..f61efbe6e1e 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -9,3 +9,5 @@ export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES'; export const RENDER_FILE = 'RENDER_FILE'; +export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE'; +export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0522e32c410..59a2c09e54f 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,8 +1,13 @@ import Vue from 'vue'; -import _ from 'underscore'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils'; -import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants'; +import { + findDiffFile, + addLineReferences, + removeMatchLine, + addContextLines, + prepareDiffData, + isDiscussionApplicableToLine, +} from './utils'; import * as types from './mutation_types'; export default { @@ -17,38 +22,7 @@ export default { [types.SET_DIFF_DATA](state, data) { const diffData = convertObjectPropsToCamelCase(data, { deep: true }); - let showingLines = 0; - const filesLength = diffData.diffFiles.length; - let i; - for (i = 0; i < filesLength; i += 1) { - const file = diffData.diffFiles[i]; - if (file.parallelDiffLines) { - const linesLength = file.parallelDiffLines.length; - let u = 0; - for (u = 0; u < linesLength; u += 1) { - const line = file.parallelDiffLines[u]; - if (line.left) delete line.left.text; - if (line.right) delete line.right.text; - } - } - - if (file.highlightedDiffLines) { - const linesLength = file.highlightedDiffLines.length; - let u; - for (u = 0; u < linesLength; u += 1) { - const line = file.highlightedDiffLines[u]; - delete line.text; - } - } - - if (file.highlightedDiffLines) { - showingLines += file.parallelDiffLines.length; - } - Object.assign(file, { - renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, - collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, - }); - } + prepareDiffData(diffData); Object.assign(state, { ...diffData, @@ -98,19 +72,95 @@ export default { [types.ADD_COLLAPSED_DIFFS](state, { file, data }) { const normalizedData = convertObjectPropsToCamelCase(data, { deep: true }); + prepareDiffData(normalizedData); const [newFileData] = normalizedData.diffFiles.filter(f => f.fileHash === file.fileHash); - - if (newFileData) { - const index = _.findIndex(state.diffFiles, f => f.fileHash === file.fileHash); - state.diffFiles.splice(index, 1, newFileData); - } + const selectedFile = state.diffFiles.find(f => f.fileHash === file.fileHash); + Object.assign(selectedFile, { ...newFileData }); }, [types.EXPAND_ALL_FILES](state) { - // eslint-disable-next-line no-param-reassign state.diffFiles = state.diffFiles.map(file => ({ ...file, collapsed: false, })); }, + + [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, discussions, diffPositionByLineCode }) { + const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); + const firstDiscussion = discussions[0]; + const isDiffDiscussion = firstDiscussion.diff_discussion; + const hasLineCode = firstDiscussion.line_code; + const diffPosition = diffPositionByLineCode[firstDiscussion.line_code]; + + if ( + selectedFile && + isDiffDiscussion && + hasLineCode && + diffPosition && + isDiscussionApplicableToLine({ + discussion: firstDiscussion, + diffPosition, + latestDiff: state.latestDiff, + }) + ) { + const targetLine = selectedFile.parallelDiffLines.find( + line => + (line.left && line.left.lineCode === firstDiscussion.line_code) || + (line.right && line.right.lineCode === firstDiscussion.line_code), + ); + if (targetLine) { + if (targetLine.left && targetLine.left.lineCode === firstDiscussion.line_code) { + Object.assign(targetLine.left, { + discussions, + }); + } else { + Object.assign(targetLine.right, { + discussions, + }); + } + } + + if (selectedFile.highlightedDiffLines) { + const targetInlineLine = selectedFile.highlightedDiffLines.find( + line => line.lineCode === firstDiscussion.line_code, + ); + + if (targetInlineLine) { + Object.assign(targetInlineLine, { + discussions, + }); + } + } + } + }, + + [types.REMOVE_LINE_DISCUSSIONS_FOR_FILE](state, { fileHash, lineCode }) { + const selectedFile = state.diffFiles.find(f => f.fileHash === fileHash); + if (selectedFile) { + const targetLine = selectedFile.parallelDiffLines.find( + line => + (line.left && line.left.lineCode === lineCode) || + (line.right && line.right.lineCode === lineCode), + ); + if (targetLine) { + const side = targetLine.left && targetLine.left.lineCode === lineCode ? 'left' : 'right'; + + Object.assign(targetLine[side], { + discussions: [], + }); + } + + if (selectedFile.highlightedDiffLines) { + const targetInlineLine = selectedFile.highlightedDiffLines.find( + line => line.lineCode === lineCode, + ); + + if (targetInlineLine) { + Object.assign(targetInlineLine, { + discussions: [], + }); + } + } + } + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 82082ac508a..631e3de311e 100644 --- a/app/assets/javascripts/diffs/store/utils.js +++ b/app/assets/javascripts/diffs/store/utils.js @@ -4,10 +4,13 @@ import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, TEXT_DIFF_POSITION_TYPE, + LEGACY_DIFF_NOTE_TYPE, DIFF_NOTE_TYPE, NEW_LINE_TYPE, OLD_LINE_TYPE, MATCH_LINE_TYPE, + LINES_TO_BE_RENDERED_DIRECTLY, + MAX_LINES_TO_BE_RENDERED, } from '../constants'; export function findDiffFile(files, hash) { @@ -52,13 +55,17 @@ export function getNoteFormData(params) { note_project_id: '', target_type: noteableData.targetType, target_id: noteableData.id, + return_discussion: true, note: { note, position, noteable_type: noteableType, noteable_id: noteableData.id, commit_id: '', - type: DIFF_NOTE_TYPE, + type: + diffFile.diffRefs.startSha && diffFile.diffRefs.headSha + ? DIFF_NOTE_TYPE + : LEGACY_DIFF_NOTE_TYPE, line_code: noteTargetLine.lineCode, }, }; @@ -161,6 +168,11 @@ export function addContextLines(options) { * @returns {Object} */ export function trimFirstCharOfLineContent(line = {}) { + // eslint-disable-next-line no-param-reassign + delete line.text; + // eslint-disable-next-line no-param-reassign + line.discussions = []; + const parsedLine = Object.assign({}, line); if (line.richText) { @@ -174,7 +186,44 @@ export function trimFirstCharOfLineContent(line = {}) { return parsedLine; } -export function getDiffRefsByLineCode(diffFiles) { +// This prepares and optimizes the incoming diff data from the server +// by setting up incremental rendering and removing unneeded data +export function prepareDiffData(diffData) { + const filesLength = diffData.diffFiles.length; + let showingLines = 0; + for (let i = 0; i < filesLength; i += 1) { + const file = diffData.diffFiles[i]; + + if (file.parallelDiffLines) { + const linesLength = file.parallelDiffLines.length; + for (let u = 0; u < linesLength; u += 1) { + const line = file.parallelDiffLines[u]; + if (line.left) { + line.left = trimFirstCharOfLineContent(line.left); + } + if (line.right) { + line.right = trimFirstCharOfLineContent(line.right); + } + } + } + + if (file.highlightedDiffLines) { + const linesLength = file.highlightedDiffLines.length; + for (let u = 0; u < linesLength; u += 1) { + const line = file.highlightedDiffLines[u]; + Object.assign(line, { ...trimFirstCharOfLineContent(line) }); + } + showingLines += file.parallelDiffLines.length; + } + + Object.assign(file, { + renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY, + collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED, + }); + } +} + +export function getDiffPositionByLineCode(diffFiles) { return diffFiles.reduce((acc, diffFile) => { const { baseSha, headSha, startSha } = diffFile.diffRefs; const { newPath, oldPath } = diffFile; @@ -186,7 +235,16 @@ export function getDiffRefsByLineCode(diffFiles) { const { lineCode, oldLine, newLine } = line; if (lineCode) { - acc[lineCode] = { baseSha, headSha, startSha, newPath, oldPath, oldLine, newLine }; + acc[lineCode] = { + baseSha, + headSha, + startSha, + newPath, + oldPath, + oldLine, + newLine, + lineCode, + }; } }); } @@ -194,3 +252,18 @@ export function getDiffRefsByLineCode(diffFiles) { return acc; }, {}); } + +// This method will check whether the discussion is still applicable +// to the diff line in question regarding different versions of the MR +export function isDiscussionApplicableToLine({ discussion, diffPosition, latestDiff }) { + const { lineCode, ...diffPositionCopy } = diffPosition; + + if (discussion.original_position && discussion.position) { + const originalRefs = convertObjectPropsToCamelCase(discussion.original_position.formatter); + const refs = convertObjectPropsToCamelCase(discussion.position.formatter); + + return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); + } + + return latestDiff && discussion.active && lineCode === discussion.line_code; +} diff --git a/app/assets/javascripts/dismissable_callout.js b/app/assets/javascripts/dismissable_callout.js index 94f456bb3fc..27a3742f667 100644 --- a/app/assets/javascripts/dismissable_callout.js +++ b/app/assets/javascripts/dismissable_callout.js @@ -1,4 +1,4 @@ -import PersistentUserCallout from './persistent_user_callout'; +import PersistentUserCallout from '../../persistent_user_callout'; export default function initDismissableCallout(alertSelector) { const alertEl = document.querySelector(alertSelector); @@ -6,5 +6,5 @@ export default function initDismissableCallout(alertSelector) { return; } - new PersistentUserCallout(alertEl); // eslint-disable-line no-new + new PersistentUserCallout(alertEl); } diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js deleted file mode 100644 index a5af37e80b6..00000000000 --- a/app/assets/javascripts/dispatcher.js +++ /dev/null @@ -1,89 +0,0 @@ -/* eslint-disable consistent-return, no-new */ - -import $ from 'jquery'; -import GfmAutoComplete from './gfm_auto_complete'; -import { convertPermissionToBoolean } from './lib/utils/common_utils'; -import GlFieldErrors from './gl_field_errors'; -import Shortcuts from './shortcuts'; -import SearchAutocomplete from './search_autocomplete'; -import performanceBar from './performance_bar'; - -function initSearch() { - // Only when search form is present - if ($('.search').length) { - return new SearchAutocomplete(); - } -} - -function initFieldErrors() { - $('.gl-show-field-errors').each((i, form) => { - new GlFieldErrors(form); - }); -} - -function initPageShortcuts(page) { - const pagesWithCustomShortcuts = [ - 'projects:activity', - 'projects:artifacts:browse', - 'projects:artifacts:file', - 'projects:blame:show', - 'projects:blob:show', - 'projects:commit:show', - 'projects:commits:show', - 'projects:find_file:show', - 'projects:issues:edit', - 'projects:issues:index', - 'projects:issues:new', - 'projects:issues:show', - 'projects:merge_requests:creations:diffs', - 'projects:merge_requests:creations:new', - 'projects:merge_requests:edit', - 'projects:merge_requests:index', - 'projects:merge_requests:show', - 'projects:network:show', - 'projects:show', - 'projects:tree:show', - 'groups:show', - ]; - - if (pagesWithCustomShortcuts.indexOf(page) === -1) { - new Shortcuts(); - } -} - -function initGFMInput() { - $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => { - const gfm = new GfmAutoComplete( - gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources, - ); - const enableGFM = convertPermissionToBoolean( - el.dataset.supportsAutocomplete, - ); - gfm.setup($(el), { - emojis: true, - members: enableGFM, - issues: enableGFM, - milestones: enableGFM, - mergeRequests: enableGFM, - labels: enableGFM, - }); - }); -} - -function initPerformanceBar() { - if (document.querySelector('#js-peek')) { - performanceBar({ container: '#js-peek' }); - } -} - -export default () => { - initSearch(); - initFieldErrors(); - - const page = $('body').attr('data-page'); - if (page) { - initPageShortcuts(page); - initGFMInput(); - initPerformanceBar(); - } -}; diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js index 5528ad9f38d..d2778bcdf1c 100644 --- a/app/assets/javascripts/dropzone_input.js +++ b/app/assets/javascripts/dropzone_input.js @@ -1,12 +1,25 @@ import $ from 'jquery'; import Dropzone from 'dropzone'; import _ from 'underscore'; -import './preview_markdown'; +import './behaviors/preview_markdown'; import csrf from './lib/utils/csrf'; import axios from './lib/utils/axios_utils'; Dropzone.autoDiscover = false; +/** + * Return the error message string from the given response. + * + * @param {String|Object} res + */ +function getErrorMessage(res) { + if (!res || _.isString(res)) { + return res; + } + + return res.message; +} + export default function dropzoneInput(form) { const divHover = '<div class="div-dropzone-hover"></div>'; const iconPaperclip = '<i class="fa fa-paperclip div-dropzone-icon"></i>'; @@ -18,7 +31,7 @@ export default function dropzoneInput(form) { const $uploadingErrorContainer = form.find('.uploading-error-container'); const $uploadingErrorMessage = form.find('.uploading-error-message'); const $uploadingProgressContainer = form.find('.uploading-progress-container'); - const uploadsPath = window.uploads_path || null; + const uploadsPath = form.data('uploads-path') || window.uploads_path || null; const maxFileSize = gon.max_file_size || 10; const formTextarea = form.find('.js-gfm-input'); let handlePaste; @@ -42,7 +55,7 @@ export default function dropzoneInput(form) { if (!uploadsPath) { $formDropzone.addClass('js-invalid-dropzone'); - return; + return null; } const dropzone = $formDropzone.dropzone({ @@ -84,9 +97,7 @@ export default function dropzoneInput(form) { // xhr object (xhr.responseText is error message). // On error we hide the 'Attach' and 'Cancel' buttons // and show an error. - - // If there's xhr error message, let's show it instead of dropzone's one. - const message = xhr ? xhr.responseText : errorMessage; + const message = getErrorMessage(errorMessage || xhr.responseText); $uploadingErrorContainer.removeClass('hide'); $uploadingErrorMessage.html(message); @@ -274,4 +285,6 @@ export default function dropzoneInput(form) { $(this).closest('.gfm-form').find('.div-dropzone').click(); formTextarea.focus(); }); + + return Dropzone.forElement($formDropzone.get(0)); } diff --git a/app/assets/javascripts/environments/components/container.vue b/app/assets/javascripts/environments/components/container.vue index 9aa224fa407..9de851c9409 100644 --- a/app/assets/javascripts/environments/components/container.vue +++ b/app/assets/javascripts/environments/components/container.vue @@ -1,12 +1,10 @@ <script> - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import environmentTable from '../components/environments_table.vue'; export default { components: { environmentTable, - loadingIcon, tablePagination, }, props: { @@ -42,11 +40,11 @@ <template> <div class="environments-container"> - <loading-icon + <gl-loading-icon v-if="isLoading" + :size="3" class="prepend-top-default" label="Loading environments" - size="3" /> <slot name="emptyState"></slot> diff --git a/app/assets/javascripts/environments/components/empty_state.vue b/app/assets/javascripts/environments/components/empty_state.vue index 00e63c3467a..cf78f89981e 100644 --- a/app/assets/javascripts/environments/components/empty_state.vue +++ b/app/assets/javascripts/environments/components/empty_state.vue @@ -35,7 +35,7 @@ code gets deployed, such as staging or production.`) }} <a v-if="canCreateEnvironment" :href="newPath" - class="btn btn-create js-new-environment-button" + class="btn btn-success js-new-environment-button" > {{ s__("Environments|New environment") }} </a> diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index 63d83e307ee..e1f9248bc4c 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,7 +1,6 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { @@ -9,7 +8,6 @@ export default { tooltip, }, components: { - loadingIcon, Icon, }, props: { @@ -67,7 +65,7 @@ export default { aria-hidden="true" > </i> - <loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" /> </span> </button> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 4deeef4beb9..efbf88d0f11 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -9,12 +9,10 @@ import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import eventHub from '../event_hub'; -import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { components: { Icon, - LoadingIcon, }, directives: { @@ -70,6 +68,6 @@ export default { v-else name="redo"/> - <loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" /> </button> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index 8efdfb8abe0..e2ecf426e64 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -107,7 +107,7 @@ > <a :href="newEnvironmentPath" - class="btn btn-create" + class="btn btn-success" > {{ s__("Environments|New environment") }} </a> diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue index 016e9f7c7b3..16abafebbc0 100644 --- a/app/assets/javascripts/environments/components/environments_table.vue +++ b/app/assets/javascripts/environments/components/environments_table.vue @@ -2,13 +2,11 @@ /** * Render environments table. */ -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import environmentItem from './environment_item.vue'; export default { components: { environmentItem, - loadingIcon, }, props: { @@ -85,10 +83,10 @@ export default { :model="model"> <div is="environment-item" + :key="`environment-item-${i}`" :model="model" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - :key="`environment-item-${i}`" /> <template @@ -97,17 +95,17 @@ export default { <div v-if="model.isLoadingFolderContent" :key="`loading-item-${i}`"> - <loading-icon size="2" /> + <gl-loading-icon :size="2" /> </div> <template v-else> <div is="environment-item" v-for="(children, index) in model.children" + :key="`env-item-${i}-${index}`" :model="children" :can-create-deployment="canCreateDeployment" :can-read-environment="canReadEnvironment" - :key="`env-item-${i}-${index}`" /> <div :key="`sub-div-${i}`"> diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index d88624f7f8d..d71964612c5 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -13,7 +13,6 @@ import eventHub from '../event_hub'; import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsService from '../services/environments_service'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue'; import environmentTable from '../components/environments_table.vue'; import tabs from '../../vue_shared/components/navigation_tabs.vue'; @@ -24,7 +23,6 @@ export default { components: { environmentTable, container, - loadingIcon, tabs, tablePagination, }, diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index 2f27c9351bc..03dfa942d69 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -16,7 +16,7 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { const hideOnScroll = togglePopover.bind($selector, false); $selector - // Setup popover + // Set up popover .data('content', $popoverContent.prop('outerHTML')) .popover({ html: true, diff --git a/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js new file mode 100644 index 00000000000..b4588cc1318 --- /dev/null +++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js @@ -0,0 +1,14 @@ +import FilteredSearchTokenKeys from './filtered_search_token_keys'; + +const tokenKeys = [{ + key: 'status', + type: 'string', + param: 'status', + symbol: '', + icon: 'messages', + tag: 'status', +}]; + +const AdminRunnersFilteredSearchTokenKeys = new FilteredSearchTokenKeys(tokenKeys); + +export default AdminRunnersFilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue index a8eb8d94be3..21b5ccdb613 100644 --- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue +++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue @@ -72,8 +72,8 @@ export default { @click="onItemActivated(item.text)"> <span> <span - v-for="(token, index) in item.tokens" - :key="`dropdown-token-${index}`" + v-for="(token, tokenIndex) in item.tokens" + :key="`dropdown-token-${tokenIndex}`" class="filtered-search-history-dropdown-token" > <span class="name">{{ token.prefix }}</span> diff --git a/app/assets/javascripts/filtered_search/dropdown_hint.js b/app/assets/javascripts/filtered_search/dropdown_hint.js index 184b34b7b5e..8aecf9725e6 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -62,7 +62,7 @@ export default class DropdownHint extends FilteredSearchDropdown { renderContent() { const dropdownData = this.tokenKeys.get() .map(tokenKey => ({ - icon: `fa-${tokenKey.icon}`, + icon: `${gon.sprite_icons}#${tokenKey.icon}`, hint: tokenKey.key, tag: `:${tokenKey.tag}`, type: tokenKey.type, diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js index 296571606d6..a750647f8be 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js @@ -7,6 +7,7 @@ import DropdownHint from './dropdown_hint'; import DropdownEmoji from './dropdown_emoji'; import DropdownNonUser from './dropdown_non_user'; import DropdownUser from './dropdown_user'; +import NullDropdown from './null_dropdown'; import FilteredSearchVisualTokens from './filtered_search_visual_tokens'; export default class FilteredSearchDropdownManager { @@ -90,6 +91,11 @@ export default class FilteredSearchDropdownManager { gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, + status: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-status'), + }, }; supportedTokens.forEach((type) => { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 81286c54c4c..d25f6f95b22 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_manager.js +++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js @@ -3,10 +3,10 @@ import { getParameterByName, getUrlParamsArray, } from '~/lib/utils/common_utils'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { visitUrl } from '../lib/utils/url_utility'; import Flash from '../flash'; import FilteredSearchContainer from './container'; -import FilteredSearchTokenKeys from './filtered_search_token_keys'; import RecentSearchesRoot from './recent_searches_root'; import RecentSearchesStore from './stores/recent_searches_store'; import RecentSearchesService from './services/recent_searches_service'; @@ -23,7 +23,7 @@ export default class FilteredSearchManager { isGroup = false, isGroupAncestor = true, isGroupDecendent = false, - filteredSearchTokenKeys = FilteredSearchTokenKeys, + filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys, stateFiltersSelector = '.issues-state-filters', }) { this.isGroup = isGroup; diff --git a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js index 087ef5cd6f2..5d131b396a0 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_token_keys.js +++ b/app/assets/javascripts/filtered_search/filtered_search_token_keys.js @@ -1,103 +1,38 @@ -const tokenKeys = [{ - key: 'author', - type: 'string', - param: 'username', - symbol: '@', - icon: 'pencil', - tag: '@author', -}, { - key: 'assignee', - type: 'string', - param: 'username', - symbol: '@', - icon: 'user', - tag: '@assignee', -}, { - key: 'milestone', - type: 'string', - param: 'title', - symbol: '%', - icon: 'clock-o', - tag: '%milestone', -}, { - key: 'label', - type: 'array', - param: 'name[]', - symbol: '~', - icon: 'tag', - tag: '~label', -}]; - -if (gon.current_user_id) { - // Appending tokenkeys only logged-in - tokenKeys.push({ - key: 'my-reaction', - type: 'string', - param: 'emoji', - symbol: '', - icon: 'thumbs-up', - tag: 'emoji', - }); -} - -const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', -}]; - -const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); +export default class FilteredSearchTokenKeys { + constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) { + this.tokenKeys = tokenKeys; + this.alternativeTokenKeys = alternativeTokenKeys; + this.conditions = conditions; -const conditions = [{ - url: 'assignee_id=0', - tokenKey: 'assignee', - value: 'none', -}, { - url: 'milestone_title=No+Milestone', - tokenKey: 'milestone', - value: 'none', -}, { - url: 'milestone_title=%23upcoming', - tokenKey: 'milestone', - value: 'upcoming', -}, { - url: 'milestone_title=%23started', - tokenKey: 'milestone', - value: 'started', -}, { - url: 'label_name[]=No+Label', - tokenKey: 'label', - value: 'none', -}]; + this.tokenKeysWithAlternative = this.tokenKeys.concat(this.alternativeTokenKeys); + } -export default class FilteredSearchTokenKeys { - static get() { - return tokenKeys; + get() { + return this.tokenKeys; } - static getKeys() { - return tokenKeys.map(i => i.key); + getKeys() { + return this.tokenKeys.map(i => i.key); } - static getAlternatives() { - return alternativeTokenKeys; + getAlternatives() { + return this.alternativeTokenKeys; } - static getConditions() { - return conditions; + getConditions() { + return this.conditions; } - static searchByKey(key) { - return tokenKeys.find(tokenKey => tokenKey.key === key) || null; + searchByKey(key) { + return this.tokenKeys.find(tokenKey => tokenKey.key === key) || null; } - static searchBySymbol(symbol) { - return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; + searchBySymbol(symbol) { + return this.tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null; } - static searchByKeyParam(keyParam) { - return tokenKeysWithAlternative.find((tokenKey) => { + searchByKeyParam(keyParam) { + return this.tokenKeysWithAlternative.find((tokenKey) => { let tokenKeyParam = tokenKey.key; // Replace hyphen with underscore to compare keyParam with tokenKeyParam @@ -112,12 +47,12 @@ export default class FilteredSearchTokenKeys { }) || null; } - static searchByConditionUrl(url) { - return conditions.find(condition => condition.url === url) || null; + searchByConditionUrl(url) { + return this.conditions.find(condition => condition.url === url) || null; } - static searchByConditionKeyValue(key, value) { - return conditions + searchByConditionKeyValue(key, value) { + return this.conditions .find(condition => condition.tokenKey === key && condition.value === value) || null; } } diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js new file mode 100644 index 00000000000..cc7291c9f59 --- /dev/null +++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js @@ -0,0 +1,77 @@ +import FilteredSearchTokenKeys from './filtered_search_token_keys'; + +export const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + icon: 'pencil', + tag: '@author', +}, { + key: 'assignee', + type: 'string', + param: 'username', + symbol: '@', + icon: 'user', + tag: '@assignee', +}, { + key: 'milestone', + type: 'string', + param: 'title', + symbol: '%', + icon: 'clock', + tag: '%milestone', +}, { + key: 'label', + type: 'array', + param: 'name[]', + symbol: '~', + icon: 'labels', + tag: '~label', +}]; + +if (gon.current_user_id) { + // Appending tokenkeys only logged-in + tokenKeys.push({ + key: 'my-reaction', + type: 'string', + param: 'emoji', + symbol: '', + icon: 'thumb-up', + tag: 'emoji', + }); +} + +export const alternativeTokenKeys = [{ + key: 'label', + type: 'string', + param: 'name', + symbol: '~', +}]; + +export const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', +}, { + url: 'milestone_title=No+Milestone', + tokenKey: 'milestone', + value: 'none', +}, { + url: 'milestone_title=%23upcoming', + tokenKey: 'milestone', + value: 'upcoming', +}, { + url: 'milestone_title=%23started', + tokenKey: 'milestone', + value: 'started', +}, { + url: 'label_name[]=No+Label', + tokenKey: 'label', + value: 'none', +}]; + +const IssuableFilteredSearchTokenKeys = + new FilteredSearchTokenKeys(tokenKeys, alternativeTokenKeys, conditions); + +export default IssuableFilteredSearchTokenKeys; diff --git a/app/assets/javascripts/filtered_search/null_dropdown.js b/app/assets/javascripts/filtered_search/null_dropdown.js new file mode 100644 index 00000000000..4cfce2a5beb --- /dev/null +++ b/app/assets/javascripts/filtered_search/null_dropdown.js @@ -0,0 +1,9 @@ +import FilteredSearchDropdown from './filtered_search_dropdown'; + +export default class NullDropdown extends FilteredSearchDropdown { + renderContent(forceShowList = false) { + this.droplab.changeHookList(this.hookId, this.dropdown, [], this.config); + + super.renderContent(forceShowList); + } +} diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js index 8b4f3b05ee7..f820f0dc3f0 100644 --- a/app/assets/javascripts/fly_out_nav.js +++ b/app/assets/javascripts/fly_out_nav.js @@ -65,8 +65,8 @@ export const hideMenu = (el) => { const parentEl = el.parentNode; - el.style.display = ''; // eslint-disable-line no-param-reassign - el.style.transform = ''; // eslint-disable-line no-param-reassign + el.style.display = ''; + el.style.transform = ''; el.classList.remove(IS_ABOVE_CLASS); parentEl.classList.remove(IS_OVER_CLASS); parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS); diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue index 2f030de8967..70a8838b772 100644 --- a/app/assets/javascripts/frequent_items/components/app.vue +++ b/app/assets/javascripts/frequent_items/components/app.vue @@ -1,6 +1,5 @@ <script> import { mapState, mapActions, mapGetters } from 'vuex'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import AccessorUtilities from '~/lib/utils/accessor'; import eventHub from '../event_hub'; import store from '../store/'; @@ -13,7 +12,6 @@ import frequentItemsMixin from './frequent_items_mixin'; export default { store, components: { - LoadingIcon, FrequentItemsSearchInput, FrequentItemsList, }, @@ -98,11 +96,11 @@ export default { <frequent-items-search-input :namespace="namespace" /> - <loading-icon + <gl-loading-icon v-if="isLoadingItems" :label="translations.loadingMessage" + :size="2" class="loading-animation prepend-top-20" - size="2" /> <div v-if="!isLoadingItems && !hasSearchQuery" diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue index 1f1665ff7fe..2399ee15332 100644 --- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue +++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue @@ -1,5 +1,5 @@ <script> -/* eslint-disable vue/require-default-prop, vue/require-prop-types */ +/* eslint-disable vue/require-default-prop */ import Identicon from '../../vue_shared/components/identicon.vue'; export default { diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js index c74de7ac34d..e672284a2d0 100644 --- a/app/assets/javascripts/gl_form.js +++ b/app/assets/javascripts/gl_form.js @@ -18,7 +18,7 @@ export default class GLForm { }); // Before we start, we should clean up any previous data for this form this.destroy(); - // Setup the form + // Set up the form this.setupForm(); this.form.data('glForm', this); } diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index b0765747a36..a032f291546 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -2,23 +2,32 @@ /* global Flash */ import $ from 'jquery'; -import { s__ } from '~/locale'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; +import { s__, sprintf } from '~/locale'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { HIDDEN_CLASS } from '~/lib/utils/constants'; import { getParameterByName } from '~/lib/utils/common_utils'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import eventHub from '../event_hub'; -import { COMMON_STR } from '../constants'; +import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants'; import groupsComponent from './groups.vue'; export default { components: { - loadingIcon, DeprecatedModal, groupsComponent, }, props: { + action: { + type: String, + required: false, + default: '', + }, + containerId: { + type: String, + required: false, + default: '', + }, store: { type: Object, required: true, @@ -56,31 +65,28 @@ export default { ? COMMON_STR.GROUP_SEARCH_EMPTY : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - eventHub.$on('fetchPage', this.fetchPage); - eventHub.$on('toggleChildren', this.toggleChildren); - eventHub.$on('showLeaveGroupModal', this.showLeaveGroupModal); - eventHub.$on('updatePagination', this.updatePagination); - eventHub.$on('updateGroups', this.updateGroups); + eventHub.$on(`${this.action}fetchPage`, this.fetchPage); + eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren); + eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal); + eventHub.$on(`${this.action}updatePagination`, this.updatePagination); + eventHub.$on(`${this.action}updateGroups`, this.updateGroups); }, mounted() { this.fetchAllGroups(); + + if (this.containerId) { + this.containerEl = document.getElementById(this.containerId); + } }, beforeDestroy() { - eventHub.$off('fetchPage', this.fetchPage); - eventHub.$off('toggleChildren', this.toggleChildren); - eventHub.$off('showLeaveGroupModal', this.showLeaveGroupModal); - eventHub.$off('updatePagination', this.updatePagination); - eventHub.$off('updateGroups', this.updateGroups); + eventHub.$off(`${this.action}fetchPage`, this.fetchPage); + eventHub.$off(`${this.action}toggleChildren`, this.toggleChildren); + eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal); + eventHub.$off(`${this.action}updatePagination`, this.updatePagination); + eventHub.$off(`${this.action}updateGroups`, this.updateGroups); }, methods: { - fetchGroups({ - parentId, - page, - filterGroupsBy, - sortBy, - archived, - updatePagination, - }) { + fetchGroups({ parentId, page, filterGroupsBy, sortBy, archived, updatePagination }) { return this.service .getGroups(parentId, page, filterGroupsBy, sortBy, archived) .then(res => { @@ -165,13 +171,13 @@ export default { } }, showLeaveGroupModal(group, parentGroup) { + const { fullName } = group; this.targetGroup = group; this.targetParentGroup = parentGroup; this.showModal = true; - this.groupLeaveConfirmationMessage = s__( - `GroupsTree|Are you sure you want to leave the "${ - group.fullName - }" group?`, + this.groupLeaveConfirmationMessage = sprintf( + s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'), + { fullName }, ); }, hideLeaveGroupModal() { @@ -197,16 +203,35 @@ export default { this.targetGroup.isBeingRemoved = false; }); }, + showEmptyState() { + const { containerEl } = this; + const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS); + const emptyStateEl = containerEl.querySelector('.empty-state'); + + if (contentListEl) { + contentListEl.remove(); + } + + if (emptyStateEl) { + emptyStateEl.classList.remove(HIDDEN_CLASS); + } + }, updatePagination(headers) { this.store.setPaginationInfo(headers); }, updateGroups(groups, fromSearch) { - this.isSearchEmpty = groups ? groups.length === 0 : false; + const hasGroups = groups && groups.length > 0; + this.isSearchEmpty = !hasGroups; + if (fromSearch) { this.store.setSearchedGroups(groups); } else { this.store.setGroups(groups); } + + if (this.action && !hasGroups && !fromSearch) { + this.showEmptyState(); + } }, }, }; @@ -214,11 +239,11 @@ export default { <template> <div> - <loading-icon + <gl-loading-icon v-if="isLoading" :label="s__('GroupsTree|Loading groups')" + :size="2" class="loading-animation prepend-top-20" - size="2" /> <groups-component v-if="!isLoading" @@ -226,6 +251,7 @@ export default { :search-empty="isSearchEmpty" :search-empty-message="searchEmptyMessage" :page-info="pageInfo" + :action="action" /> <deprecated-modal v-show="showModal" diff --git a/app/assets/javascripts/groups/components/group_folder.vue b/app/assets/javascripts/groups/components/group_folder.vue index 647c9d0046d..bcc7a638346 100644 --- a/app/assets/javascripts/groups/components/group_folder.vue +++ b/app/assets/javascripts/groups/components/group_folder.vue @@ -11,8 +11,12 @@ export default { }, groups: { type: Array, + required: true, + }, + action: { + type: String, required: false, - default: () => ([]), + default: '', }, }, computed: { @@ -37,6 +41,7 @@ export default { :key="index" :group="group" :parent-group="parentGroup" + :action="action" /> <li v-if="hasMoreChildren" diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue index 2b9e2a929fc..44d6fa26914 100644 --- a/app/assets/javascripts/groups/components/group_item.vue +++ b/app/assets/javascripts/groups/components/group_item.vue @@ -30,6 +30,11 @@ export default { type: Object, required: true, }, + action: { + type: String, + required: false, + default: '', + }, }, computed: { groupDomId() { @@ -56,10 +61,12 @@ export default { methods: { onClickRowGroup(e) { const NO_EXPAND_CLS = 'no-expand'; - if (!(e.target.classList.contains(NO_EXPAND_CLS) || - e.target.parentElement.classList.contains(NO_EXPAND_CLS))) { + const targetClasses = e.target.classList; + const parentElClasses = e.target.parentElement.classList; + + if (!(targetClasses.contains(NO_EXPAND_CLS) || parentElClasses.contains(NO_EXPAND_CLS))) { if (this.hasChildren) { - eventHub.$emit('toggleChildren', this.group); + eventHub.$emit(`${this.action}toggleChildren`, this.group); } else { visitUrl(this.group.relativePath); } @@ -93,7 +100,7 @@ export default { </div> <div :class="{ 'content-loading': group.isChildrenLoading }" - class="avatar-container s24 d-none d-sm-block" + class="avatar-container s24 d-none d-sm-flex" > <a :href="group.relativePath" @@ -158,6 +165,7 @@ export default { v-if="group.isOpen && hasChildren" :parent-group="group" :groups="group.children" + :action="action" /> </li> </template> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 73ae928b0d9..81b2e5ea37b 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,39 +1,44 @@ <script> - import tablePagination from '~/vue_shared/components/table_pagination.vue'; - import eventHub from '../event_hub'; - import { getParameterByName } from '../../lib/utils/common_utils'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; +import eventHub from '../event_hub'; +import { getParameterByName } from '../../lib/utils/common_utils'; - export default { - components: { - tablePagination, +export default { + components: { + PaginationLinks, + }, + props: { + groups: { + type: Array, + required: true, }, - props: { - groups: { - type: Array, - required: true, - }, - pageInfo: { - type: Object, - required: true, - }, - searchEmpty: { - type: Boolean, - required: true, - }, - searchEmptyMessage: { - type: String, - required: true, - }, + pageInfo: { + type: Object, + required: true, }, - methods: { - change(page) { - const filterGroupsParam = getParameterByName('filter_groups'); - const sortParam = getParameterByName('sort'); - const archivedParam = getParameterByName('archived'); - eventHub.$emit('fetchPage', page, filterGroupsParam, sortParam, archivedParam); - }, + searchEmpty: { + type: Boolean, + required: true, }, - }; + searchEmptyMessage: { + type: String, + required: true, + }, + action: { + type: String, + required: false, + default: '', + }, + }, + methods: { + change(page) { + const filterGroupsParam = getParameterByName('filter_groups'); + const sortParam = getParameterByName('sort'); + const archivedParam = getParameterByName('archived'); + eventHub.$emit(`${this.action}fetchPage`, page, filterGroupsParam, sortParam, archivedParam); + }, + }, +}; </script> <template> @@ -44,14 +49,18 @@ > {{ searchEmptyMessage }} </div> - <group-folder - v-if="!searchEmpty" - :groups="groups" - /> - <table-pagination - v-if="!searchEmpty" - :change="change" - :page-info="pageInfo" - /> + <template + v-else + > + <group-folder + :groups="groups" + :action="action" + /> + <pagination-links + :change="change" + :page-info="pageInfo" + class="d-flex justify-content-center prepend-top-default" + /> + </template> </div> </template> diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue index 24eec4901ec..c1783d5ce25 100644 --- a/app/assets/javascripts/groups/components/item_actions.vue +++ b/app/assets/javascripts/groups/components/item_actions.vue @@ -21,6 +21,11 @@ export default { type: Object, required: true, }, + action: { + type: String, + required: false, + default: '', + }, }, computed: { leaveBtnTitle() { @@ -32,7 +37,7 @@ export default { }, methods: { onLeaveGroup() { - eventHub.$emit('showLeaveGroupModal', this.group, this.parentGroup); + eventHub.$emit(`${this.action}showLeaveGroupModal`, this.group, this.parentGroup); }, }, }; @@ -41,8 +46,8 @@ export default { <template> <div class="controls"> <a - v-tooltip v-if="group.canEdit" + v-tooltip :href="group.editPath" :title="editBtnTitle" :aria-label="editBtnTitle" @@ -52,8 +57,8 @@ export default { <icon name="settings"/> </a> <a - v-tooltip v-if="group.canLeave" + v-tooltip :href="group.leavePath" :title="leaveBtnTitle" :aria-label="leaveBtnTitle" diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index b8baed682f5..9c246cf3ba6 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -2,13 +2,23 @@ import { __, s__ } from '../locale'; export const MAX_CHILDREN_COUNT = 20; +export const ACTIVE_TAB_SUBGROUPS_AND_PROJECTS = 'subgroups_and_projects'; +export const ACTIVE_TAB_SHARED = 'shared'; +export const ACTIVE_TAB_ARCHIVED = 'archived'; + +export const GROUPS_LIST_HOLDER_CLASS = '.js-groups-list-holder'; +export const GROUPS_FILTER_FORM_CLASS = '.js-group-filter-form'; +export const CONTENT_LIST_CLASS = '.content-list'; + export const COMMON_STR = { FAILURE: __('An error occurred. Please try again.'), - LEAVE_FORBIDDEN: s__('GroupsTree|Failed to leave the group. Please make sure you are not the only owner.'), + LEAVE_FORBIDDEN: s__( + 'GroupsTree|Failed to leave the group. Please make sure you are not the only owner.', + ), LEAVE_BTN_TITLE: s__('GroupsTree|Leave this group'), EDIT_BTN_TITLE: s__('GroupsTree|Edit group'), - GROUP_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups matched your search'), - GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|Sorry, no groups or projects matched your search'), + GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'), + GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'), }; export const ITEM_TYPE = { @@ -17,8 +27,12 @@ export const ITEM_TYPE = { }; export const GROUP_VISIBILITY_TYPE = { - public: __('Public - The group and any public projects can be viewed without any authentication.'), - internal: __('Internal - The group and any internal projects can be viewed by any logged in user.'), + public: __( + 'Public - The group and any public projects can be viewed without any authentication.', + ), + internal: __( + 'Internal - The group and any internal projects can be viewed by any logged in user.', + ), private: __('Private - The group and its projects can only be viewed by members.'), }; diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js index e6db1746487..693519729ac 100644 --- a/app/assets/javascripts/groups/groups_filterable_list.js +++ b/app/assets/javascripts/groups/groups_filterable_list.js @@ -4,13 +4,23 @@ import eventHub from './event_hub'; import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils'; export default class GroupFilterableList extends FilterableList { - constructor({ form, filter, holder, filterEndpoint, pagePath, dropdownSel, filterInputField }) { + constructor({ + form, + filter, + holder, + filterEndpoint, + pagePath, + dropdownSel, + filterInputField, + action, + }) { super(form, filter, holder, filterInputField); this.form = form; this.filterEndpoint = filterEndpoint; this.pagePath = pagePath; this.filterInputField = filterInputField; this.$dropdown = $(dropdownSel); + this.action = action; } getFilterEndpoint() { @@ -20,15 +30,16 @@ export default class GroupFilterableList extends FilterableList { getPagePath(queryData) { const params = queryData ? $.param(queryData) : ''; const queryString = params ? `?${params}` : ''; - return `${this.pagePath}${queryString}`; + const path = this.pagePath || window.location.pathname; + return `${path}${queryString}`; } bindEvents() { super.bindEvents(); - this.onFilterOptionClikWrapper = this.onOptionClick.bind(this); + this.onFilterOptionClickWrapper = this.onOptionClick.bind(this); - this.$dropdown.on('click', 'a', this.onFilterOptionClikWrapper); + this.$dropdown.on('click', 'a', this.onFilterOptionClickWrapper); } onFilterInput() { @@ -53,7 +64,12 @@ export default class GroupFilterableList extends FilterableList { } setDefaultFilterOption() { - const defaultOption = $.trim(this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').first().text()); + const defaultOption = $.trim( + this.$dropdown + .find('.dropdown-menu li.js-filter-sort-order a') + .first() + .text(), + ); this.$dropdown.find('.dropdown-label').text(defaultOption); } @@ -65,11 +81,19 @@ export default class GroupFilterableList extends FilterableList { // Get type of option selected from dropdown const currentTargetClassList = e.currentTarget.parentElement.classList; const isOptionFilterBySort = currentTargetClassList.contains('js-filter-sort-order'); - const isOptionFilterByArchivedProjects = currentTargetClassList.contains('js-filter-archived-projects'); + const isOptionFilterByArchivedProjects = currentTargetClassList.contains( + 'js-filter-archived-projects', + ); // Get option query param, also preserve currently applied query param - const sortParam = getParameterByName('sort', isOptionFilterBySort ? e.currentTarget.href : window.location.href); - const archivedParam = getParameterByName('archived', isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href); + const sortParam = getParameterByName( + 'sort', + isOptionFilterBySort ? e.currentTarget.href : window.location.href, + ); + const archivedParam = getParameterByName( + 'archived', + isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href, + ); if (sortParam) { queryData.sort = sortParam; @@ -86,7 +110,9 @@ export default class GroupFilterableList extends FilterableList { this.$dropdown.find('.dropdown-label').text($.trim(e.currentTarget.text)); this.$dropdown.find('.dropdown-menu li.js-filter-sort-order a').removeClass('is-active'); } else if (isOptionFilterByArchivedProjects) { - this.$dropdown.find('.dropdown-menu li.js-filter-archived-projects a').removeClass('is-active'); + this.$dropdown + .find('.dropdown-menu li.js-filter-archived-projects a') + .removeClass('is-active'); } $(e.target).addClass('is-active'); @@ -98,11 +124,19 @@ export default class GroupFilterableList extends FilterableList { onFilterSuccess(res, queryData) { const currentPath = this.getPagePath(queryData); - window.history.replaceState({ - page: currentPath, - }, document.title, currentPath); - - eventHub.$emit('updateGroups', res.data, Object.prototype.hasOwnProperty.call(queryData, this.filterInputField)); - eventHub.$emit('updatePagination', normalizeHeaders(res.headers)); + window.history.replaceState( + { + page: currentPath, + }, + document.title, + currentPath, + ); + + eventHub.$emit( + `${this.action}updateGroups`, + res.data, + Object.prototype.hasOwnProperty.call(queryData, this.filterInputField), + ); + eventHub.$emit(`${this.action}updatePagination`, normalizeHeaders(res.headers)); } } diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js index 83a9008a94b..0f68f05b523 100644 --- a/app/assets/javascripts/groups/index.js +++ b/app/assets/javascripts/groups/index.js @@ -7,18 +7,26 @@ import GroupsService from './service/groups_service'; import groupsApp from './components/app.vue'; import groupFolderComponent from './components/group_folder.vue'; import groupItemComponent from './components/group_item.vue'; +import { GROUPS_LIST_HOLDER_CLASS, CONTENT_LIST_CLASS } from './constants'; Vue.use(Translate); -export default () => { - const el = document.getElementById('js-groups-tree'); +export default (containerId = 'js-groups-tree', endpoint, action = '') => { + const containerEl = document.getElementById(containerId); + let dataEl; // Don't do anything if element doesn't exist (No groups) // This is for when the user enters directly to the page via URL - if (!el) { + if (!containerEl) { return; } + const el = action ? containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS) : containerEl; + + if (action) { + dataEl = containerEl.querySelector(CONTENT_LIST_CLASS); + } + Vue.component('group-folder', groupFolderComponent); Vue.component('group-item', groupItemComponent); @@ -29,20 +37,26 @@ export default () => { groupsApp, }, data() { - const { dataset } = this.$options.el; + const { dataset } = dataEl || this.$options.el; const hideProjects = dataset.hideProjects === 'true'; + const service = new GroupsService(endpoint || dataset.endpoint); const store = new GroupsStore(hideProjects); - const service = new GroupsService(dataset.endpoint); return { + action, store, service, hideProjects, loading: true, + containerId, }; }, beforeMount() { - const { dataset } = this.$options.el; + if (this.action) { + return; + } + + const { dataset } = dataEl || this.$options.el; let groupFilterList = null; const form = document.querySelector(dataset.formSel); const filter = document.querySelector(dataset.filterSel); @@ -52,10 +66,11 @@ export default () => { form, filter, holder, - filterEndpoint: dataset.endpoint, + filterEndpoint: endpoint || dataset.endpoint, pagePath: dataset.path, dropdownSel: dataset.dropdownSel, filterInputField: 'filter', + action: this.action, }; groupFilterList = new GroupFilterableList(opts); @@ -64,9 +79,11 @@ export default () => { render(createElement) { return createElement('groups-app', { props: { + action: this.action, store: this.store, service: this.service, hideProjects: this.hideProjects, + containerId: this.containerId, }, }); }, diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue index 6db7b9d6b0e..52ccc537c9d 100644 --- a/app/assets/javascripts/ide/components/branches/search_list.vue +++ b/app/assets/javascripts/ide/components/branches/search_list.vue @@ -1,13 +1,11 @@ <script> import { mapActions, mapState } from 'vuex'; import _ from 'underscore'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; import Item from './item.vue'; export default { components: { - LoadingIcon, Item, Icon, }, @@ -62,8 +60,8 @@ export default { <div class="position-relative"> <input ref="searchInput" - :placeholder="__('Search branches')" v-model="search" + :placeholder="__('Search branches')" type="search" class="form-control dropdown-input-field" @input="searchBranches" @@ -76,10 +74,10 @@ export default { </div> </div> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> - <loading-icon + <gl-loading-icon v-if="isLoading" + :size="2" class="mt-3 mb-3 align-self-center ml-auto mr-auto" - size="2" /> <ul v-else diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue new file mode 100644 index 00000000000..c3ca147e850 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -0,0 +1,78 @@ +<script> +import $ from 'jquery'; +import { mapActions } from 'vuex'; +import { __ } from '~/locale'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '../changed_file_icon.vue'; + +export default { + components: { + FileIcon, + ChangedFileIcon, + }, + props: { + activeFile: { + type: Object, + required: true, + }, + }, + computed: { + activeButtonText() { + return this.activeFile.staged ? __('Unstage') : __('Stage'); + }, + isStaged() { + return !this.activeFile.changed && this.activeFile.staged; + }, + }, + methods: { + ...mapActions(['stageChange', 'unstageChange']), + actionButtonClicked() { + if (this.activeFile.staged) { + this.unstageChange(this.activeFile.path); + } else { + this.stageChange(this.activeFile.path); + } + }, + showDiscardModal() { + $(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show'); + }, + }, +}; +</script> + +<template> + <div class="d-flex ide-commit-editor-header align-items-center"> + <file-icon + :file-name="activeFile.name" + :size="16" + class="mr-2" + /> + <strong class="mr-2"> + {{ activeFile.path }} + </strong> + <changed-file-icon + :file="activeFile" + /> + <div class="ml-auto"> + <button + v-if="!isStaged" + type="button" + class="btn btn-remove btn-inverted append-right-8" + @click="showDiscardModal" + > + {{ __('Discard') }} + </button> + <button + :class="{ + 'btn-success': !isStaged, + 'btn-warning': isStaged + }" + type="button" + class="btn btn-inverted" + @click="actionButtonClicked" + > + {{ activeButtonText }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue index d0fb0e3d99e..3e3539e364b 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue @@ -1,7 +1,9 @@ <script> +import $ from 'jquery'; import { mapActions } from 'vuex'; import { __, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import ListItem from './list_item.vue'; @@ -9,6 +11,7 @@ export default { components: { Icon, ListItem, + GlModal, }, directives: { tooltip, @@ -56,6 +59,11 @@ export default { type: String, required: true, }, + emptyStateText: { + type: String, + required: false, + default: __('No changes'), + }, }, computed: { titleText() { @@ -68,11 +76,19 @@ export default { }, }, methods: { - ...mapActions(['stageAllChanges', 'unstageAllChanges']), + ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']), actionBtnClicked() { this[this.action](); + + $(this.$refs.actionBtn).tooltip('hide'); + }, + openDiscardModal() { + $('#discard-all-changes').modal('show'); }, }, + discardModalText: __( + "You will loose all the unstaged changes you've made in this project. This action cannot be undone.", + ), }; </script> @@ -81,27 +97,32 @@ export default { class="ide-commit-list-container" > <header - class="multi-file-commit-panel-header" + class="multi-file-commit-panel-header d-flex mb-0" > <div - class="multi-file-commit-panel-header-title" + class="d-flex align-items-center flex-fill" > <icon v-once :name="iconName" :size="18" + class="append-right-8" /> - {{ titleText }} + <strong> + {{ titleText }} + </strong> <div class="d-flex ml-auto"> <button + ref="actionBtn" v-tooltip - v-show="filesLength" + :title="actionBtnText" + :aria-label="actionBtnText" + :disabled="!filesLength" :class="{ - 'd-flex': filesLength + 'disabled-content': !filesLength }" - :title="actionBtnText" type="button" - class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center" + class="d-flex ide-staged-action-btn p-0 border-0 align-items-center" data-placement="bottom" data-container="body" data-boundary="viewport" @@ -109,18 +130,32 @@ export default { > <icon :name="actionBtnIcon" - :size="12" + :size="16" class="ml-auto mr-auto" /> </button> - <span + <button + v-if="!stagedList" + v-tooltip + :title="__('Discard all changes')" + :aria-label="__('Discard all changes')" + :disabled="!filesLength" :class="{ - 'rounded-right': !filesLength + 'disabled-content': !filesLength }" - class="ide-commit-file-count order-0 rounded-left text-center" + type="button" + class="d-flex ide-staged-action-btn p-0 border-0 align-items-center" + data-placement="bottom" + data-container="body" + data-boundary="viewport" + @click="openDiscardModal" > - {{ filesLength }} - </span> + <icon + :size="16" + name="remove-all" + class="ml-auto mr-auto" + /> + </button> </div> </div> </header> @@ -143,9 +178,19 @@ export default { </ul> <p v-else - class="multi-file-commit-list form-text text-muted" + class="multi-file-commit-list form-text text-muted text-center" > - {{ __('No changes') }} + {{ emptyStateText }} </p> + <gl-modal + v-if="!stagedList" + id="discard-all-changes" + :footer-primary-button-text="__('Discard all changes')" + :header-title-text="__('Discard all unstaged changes?')" + footer-primary-button-variant="danger" + @submit="discardAllChanges" + > + {{ $options.discardModalText }} + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue index 391004dcd3c..10c78a80302 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue @@ -2,6 +2,7 @@ import { mapActions } from 'vuex'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; import StageButton from './stage_button.vue'; import UnstageButton from './unstage_button.vue'; import { viewerTypes } from '../../constants'; @@ -12,6 +13,7 @@ export default { Icon, StageButton, UnstageButton, + FileIcon, }, directives: { tooltip, @@ -48,7 +50,7 @@ export default { return `${getCommitIconMap(this.file).icon}${suffix}`; }, iconClass() { - return `${getCommitIconMap(this.file).class} append-right-8`; + return `${getCommitIconMap(this.file).class} ml-auto mr-auto`; }, fullKey() { return `${this.keyPrefix}-${this.file.key}`; @@ -105,17 +107,24 @@ export default { @click="openFileInEditor" > <span class="multi-file-commit-list-file-path d-flex align-items-center"> - <icon - :name="iconName" - :size="16" - :css-classes="iconClass" + <file-icon + :file-name="file.name" + class="append-right-8" />{{ file.name }} </span> + <div class="ml-auto d-flex align-items-center"> + <div class="d-flex align-items-center ide-commit-list-changed-icon"> + <icon + :name="iconName" + :size="16" + :css-classes="iconClass" + /> + </div> + <component + :is="actionComponent" + :path="file.path" + /> + </div> </div> - <component - :is="actionComponent" - :path="file.path" - class="d-flex position-absolute" - /> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue index e6044401c9f..8a1836a5c92 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue @@ -1,11 +1,15 @@ <script> +import $ from 'jquery'; import { mapActions } from 'vuex'; +import { sprintf, __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; +import GlModal from '~/vue_shared/components/gl_modal.vue'; export default { components: { Icon, + GlModal, }, directives: { tooltip, @@ -16,8 +20,22 @@ export default { required: true, }, }, + computed: { + modalId() { + return `discard-file-${this.path}`; + }, + modalTitle() { + return sprintf( + __('Discard changes to %{path}?'), + { path: this.path }, + ); + }, + }, methods: { ...mapActions(['stageChange', 'discardFileChanges']), + showDiscardModal() { + $(document.getElementById(this.modalId)).modal('show'); + }, }, }; </script> @@ -25,51 +43,50 @@ export default { <template> <div v-once - class="multi-file-discard-btn dropdown" + class="multi-file-discard-btn d-flex" > <button v-tooltip :aria-label="__('Stage changes')" :title="__('Stage changes')" type="button" - class="btn btn-blank append-right-5 d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - @click.stop="stageChange(path)" + @click.stop.prevent="stageChange(path)" > <icon - :size="12" + :size="16" name="mobile-issue-close" + class="ml-auto mr-auto" /> </button> <button v-tooltip - :title="__('More actions')" + :aria-label="__('Discard changes')" + :title="__('Discard changes')" type="button" - class="btn btn-blank d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - data-toggle="dropdown" - data-display="static" + @click.stop.prevent="showDiscardModal" > <icon - :size="12" - name="ellipsis_h" + :size="16" + name="remove" + class="ml-auto mr-auto" /> </button> - <div class="dropdown-menu dropdown-menu-right"> - <ul> - <li> - <button - type="button" - @click.stop="discardFileChanges(path)" - > - {{ __('Discard changes') }} - </button> - </li> - </ul> - </div> + <gl-modal + :id="modalId" + :header-title-text="modalTitle" + :footer-primary-button-text="__('Discard changes')" + footer-primary-button-variant="danger" + @submit="discardFileChanges(path)" + > + {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }} + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue index 9cec73ec00e..86c40602074 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue @@ -25,22 +25,23 @@ export default { <template> <div v-once - class="multi-file-discard-btn" + class="multi-file-discard-btn d-flex" > <button v-tooltip :aria-label="__('Unstage changes')" :title="__('Unstage changes')" type="button" - class="btn btn-blank d-flex align-items-center" + class="btn btn-blank align-items-center" data-container="body" data-boundary="viewport" data-placement="bottom" - @click="unstageChange(path)" + @click.stop.prevent="unstageChange(path)" > <icon - :size="12" - name="history" + :size="16" + name="redo" + class="ml-auto mr-auto" /> </button> </div> diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue index acbc98b7a7b..a20dc0a7006 100644 --- a/app/assets/javascripts/ide/components/error_message.vue +++ b/app/assets/javascripts/ide/components/error_message.vue @@ -1,11 +1,7 @@ <script> import { mapActions } from 'vuex'; -import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { - components: { - LoadingIcon, - }, props: { message: { type: Object, @@ -59,7 +55,7 @@ export default { @click.stop.prevent="clickAction" > {{ message.actionText }} - <loading-icon + <gl-loading-icon v-show="isLoading" inline /> diff --git a/app/assets/javascripts/ide/components/file_finder/index.vue b/app/assets/javascripts/ide/components/file_finder/index.vue index 0ba33053717..760ed8654ee 100644 --- a/app/assets/javascripts/ide/components/file_finder/index.vue +++ b/app/assets/javascripts/ide/components/file_finder/index.vue @@ -174,8 +174,8 @@ export default { <div class="dropdown-input"> <input ref="searchInput" - :placeholder="__('Search files')" v-model="searchText" + :placeholder="__('Search files')" type="search" class="dropdown-input-field" autocomplete="off" diff --git a/app/assets/javascripts/ide/components/file_finder/item.vue b/app/assets/javascripts/ide/components/file_finder/item.vue index f5252ce7706..a612739d641 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -78,10 +78,10 @@ export default { class="diff-changed-file-name" > <span - v-for="(char, index) in file.name.split('')" - :key="index + char" + v-for="(char, charIndex) in file.name.split('')" + :key="charIndex + char" :class="{ - highlighted: nameSearchTextOccurences.indexOf(index) >= 0, + highlighted: nameSearchTextOccurences.indexOf(charIndex) >= 0, }" v-text="char" > @@ -91,10 +91,10 @@ export default { class="diff-changed-file-path prepend-top-5" > <span - v-for="(char, index) in pathWithEllipsis.split('')" - :key="index + char" + v-for="(char, charIndex) in pathWithEllipsis.split('')" + :key="charIndex + char" :class="{ - highlighted: pathSearchTextOccurences.indexOf(index) >= 0, + highlighted: pathSearchTextOccurences.indexOf(charIndex) >= 0, }" v-text="char" > diff --git a/app/assets/javascripts/ide/components/file_row_extra.vue b/app/assets/javascripts/ide/components/file_row_extra.vue new file mode 100644 index 00000000000..44a360ab909 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_row_extra.vue @@ -0,0 +1,104 @@ +<script> +import { mapGetters } from 'vuex'; +import { n__, __, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import Icon from '~/vue_shared/components/icon.vue'; +import NewDropdown from './new_dropdown/index.vue'; +import ChangedFileIcon from './changed_file_icon.vue'; +import MrFileIcon from './mr_file_icon.vue'; + +export default { + name: 'FileRowExtra', + directives: { + tooltip, + }, + components: { + Icon, + NewDropdown, + ChangedFileIcon, + MrFileIcon, + }, + props: { + file: { + type: Object, + required: true, + }, + mouseOver: { + type: Boolean, + required: true, + }, + }, + computed: { + ...mapGetters([ + 'getChangesInFolder', + 'getUnstagedFilesCountForPath', + 'getStagedFilesCountForPath', + ]), + folderUnstagedCount() { + return this.getUnstagedFilesCountForPath(this.file.path); + }, + folderStagedCount() { + return this.getStagedFilesCountForPath(this.file.path); + }, + changesCount() { + return this.getChangesInFolder(this.file.path); + }, + folderChangesTooltip() { + if (this.changesCount === 0) return undefined; + + if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) { + return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount); + } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) { + return n__('%d staged change', '%d staged changes', this.folderStagedCount); + } + + return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), { + unstaged: this.folderUnstagedCount, + staged: this.folderStagedCount, + }); + }, + showTreeChangesCount() { + return this.file.type === 'tree' && this.changesCount > 0 && !this.file.opened; + }, + showChangedFileIcon() { + return this.file.changed || this.file.tempFile || this.file.staged; + }, + }, +}; +</script> + +<template> + <div class="float-right ide-file-icon-holder"> + <mr-file-icon + v-if="file.mrChange" + /> + <span + v-if="showTreeChangesCount" + class="ide-tree-changes" + > + {{ changesCount }} + <icon + v-tooltip + :title="folderChangesTooltip" + :size="12" + data-container="body" + data-placement="right" + name="file-modified" + css-classes="prepend-left-5 ide-file-modified" + /> + </span> + <changed-file-icon + v-else-if="showChangedFileIcon" + :file="file" + :show-tooltip="true" + :show-staged-icon="true" + :force-modified-icon="true" + /> + <new-dropdown + :type="file.type" + :path="file.path" + :mouse-over="mouseOver" + class="prepend-left-8" + /> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue new file mode 100644 index 00000000000..23be5f45f16 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_templates/bar.vue @@ -0,0 +1,80 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Dropdown from './dropdown.vue'; + +export default { + components: { + Dropdown, + }, + computed: { + ...mapGetters(['activeFile']), + ...mapGetters('fileTemplates', ['templateTypes']), + ...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']), + showTemplatesDropdown() { + return Object.keys(this.selectedTemplateType).length > 0; + }, + }, + watch: { + activeFile: 'setInitialType', + }, + mounted() { + this.setInitialType(); + }, + methods: { + ...mapActions('fileTemplates', [ + 'setSelectedTemplateType', + 'fetchTemplate', + 'undoFileTemplate', + ]), + setInitialType() { + const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name); + + if (initialTemplateType) { + this.setSelectedTemplateType(initialTemplateType); + } + }, + selectTemplateType(templateType) { + this.setSelectedTemplateType(templateType); + }, + selectTemplate(template) { + this.fetchTemplate(template); + }, + undo() { + this.undoFileTemplate(); + }, + }, +}; +</script> + +<template> + <div class="d-flex align-items-center ide-file-templates"> + <strong class="append-right-default"> + {{ __('File templates') }} + </strong> + <dropdown + :data="templateTypes" + :label="selectedTemplateType.name || __('Choose a type...')" + class="mr-2" + @click="selectTemplateType" + /> + <dropdown + v-if="showTemplatesDropdown" + :label="__('Choose a template...')" + :is-async-data="true" + :searchable="true" + :title="__('File templates')" + class="mr-2" + @click="selectTemplate" + /> + <transition name="fade"> + <button + v-show="updateSuccess" + type="button" + class="btn btn-default" + @click="undo" + > + {{ __('Undo') }} + </button> + </transition> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue new file mode 100644 index 00000000000..ef1f6de3a86 --- /dev/null +++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue @@ -0,0 +1,123 @@ +<script> +import $ from 'jquery'; +import { mapActions, mapState } from 'vuex'; +import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; + +export default { + components: { + DropdownButton, + }, + props: { + data: { + type: Array, + required: false, + default: () => [], + }, + label: { + type: String, + required: true, + }, + title: { + type: String, + required: false, + default: null, + }, + isAsyncData: { + type: Boolean, + required: false, + default: false, + }, + searchable: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapState('fileTemplates', ['templates', 'isLoading']), + outputData() { + return (this.isAsyncData ? this.templates : this.data).filter(t => { + if (!this.searchable) return true; + + return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0; + }); + }, + showLoading() { + return this.isAsyncData ? this.isLoading : false; + }, + }, + mounted() { + $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync); + }, + beforeDestroy() { + $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync); + }, + methods: { + ...mapActions('fileTemplates', ['fetchTemplateTypes']), + fetchTemplatesIfAsync() { + if (this.isAsyncData) { + this.fetchTemplateTypes(); + } + }, + clickItem(item) { + this.$emit('click', item); + }, + }, +}; +</script> + +<template> + <div class="dropdown"> + <dropdown-button + :toggle-text="label" + data-display="static" + /> + <div class="dropdown-menu pb-0"> + <div + v-if="title" + class="dropdown-title ml-0 mr-0" + > + {{ title }} + </div> + <div + v-if="!showLoading && searchable" + class="dropdown-input" + > + <input + v-model="search" + :placeholder="__('Filter...')" + type="search" + class="dropdown-input-field" + /> + <i + aria-hidden="true" + class="fa fa-search dropdown-input-search" + ></i> + </div> + <div class="dropdown-content"> + <gl-loading-icon + v-if="showLoading" + :size="2" + /> + <ul v-else> + <li + v-for="(item, index) in outputData" + :key="index" + > + <button + type="button" + @click="clickItem(item)" + > + {{ item.name }} + </button> + </li> + </ul> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue index 6a5ab35a16a..a3add3b778f 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue'; import FindFile from './file_finder/index.vue'; import RightPane from './panes/right.vue'; import ErrorMessage from './error_message.vue'; +import CommitEditorHeader from './commit_sidebar/editor_header.vue'; const originalStopCallback = Mousetrap.stopCallback; @@ -23,6 +24,7 @@ export default { FindFile, RightPane, ErrorMessage, + CommitEditorHeader, }, computed: { ...mapState([ @@ -34,7 +36,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -96,7 +98,12 @@ export default { <template v-if="activeFile" > + <commit-editor-header + v-if="isCommitModeActive" + :active-file="activeFile" + /> <repo-tabs + v-else :active-file="activeFile" :files="openFiles" :viewer="viewer" diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 00ae5ea2c15..e658d1bf956 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -2,15 +2,16 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import Icon from '~/vue_shared/components/icon.vue'; import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import RepoFile from './repo_file.vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; import NavDropdown from './nav_dropdown.vue'; +import FileRowExtra from './file_row_extra.vue'; export default { components: { Icon, - RepoFile, SkeletonLoadingContainer, NavDropdown, + FileRow, }, props: { viewerType: { @@ -34,8 +35,9 @@ export default { this.updateViewer(this.viewerType); }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'toggleTreeOpen']), }, + FileRowExtra, }; </script> @@ -63,11 +65,13 @@ export default { <div class="ide-tree-body h-100" > - <repo-file + <file-row v-for="file in currentTree.tree" :key="file.key" :file="file" :level="0" + :extra-component="$options.FileRowExtra" + @toggleTreeOpen="toggleTreeOpen" /> </div> </template> diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue index 3b16b860ecd..acd37605d16 100644 --- a/app/assets/javascripts/ide/components/jobs/list.vue +++ b/app/assets/javascripts/ide/components/jobs/list.vue @@ -1,11 +1,9 @@ <script> import { mapActions } from 'vuex'; -import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Stage from './stage.vue'; export default { components: { - LoadingIcon, Stage, }, props: { @@ -26,10 +24,10 @@ export default { <template> <div> - <loading-icon + <gl-loading-icon v-if="loading && !stages.length" + :size="2" class="prepend-top-default" - size="2" /> <template v-else> <stage diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue index 15e881b7bc8..ec168d36b9e 100644 --- a/app/assets/javascripts/ide/components/jobs/stage.vue +++ b/app/assets/javascripts/ide/components/jobs/stage.vue @@ -2,7 +2,6 @@ import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue'; -import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Item from './item.vue'; export default { @@ -12,7 +11,6 @@ export default { components: { Icon, CiIcon, - LoadingIcon, Item, }, props: { @@ -71,8 +69,8 @@ export default { :size="24" /> <strong - v-tooltip="showTooltip" ref="stageTitle" + v-tooltip="showTooltip" :title="showTooltip ? stage.name : null" data-container="body" class="prepend-left-8 ide-stage-title" @@ -96,7 +94,7 @@ export default { v-show="!stage.isCollapsed" class="card-body" > - <loading-icon + <gl-loading-icon v-if="showLoadingIcon" /> <template v-else> diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue index fc612956688..c8343e77860 100644 --- a/app/assets/javascripts/ide/components/merge_requests/list.vue +++ b/app/assets/javascripts/ide/components/merge_requests/list.vue @@ -3,7 +3,6 @@ import { mapActions, mapState } from 'vuex'; import _ from 'underscore'; import { __ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Item from './item.vue'; import TokenedInput from '../shared/tokened_input.vue'; @@ -14,7 +13,6 @@ const SEARCH_TYPES = [ export default { components: { - LoadingIcon, TokenedInput, Item, Icon, @@ -98,10 +96,10 @@ export default { </div> </div> <div class="dropdown-content ide-merge-requests-dropdown-content d-flex"> - <loading-icon + <gl-loading-icon v-if="isLoading" + :size="2" class="mt-3 mb-3 align-self-center ml-auto mr-auto" - size="2" /> <template v-else> <ul diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index e500ef0e1b5..bcd53ac1ba2 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -1,6 +1,7 @@ <script> +import $ from 'jquery'; import { __ } from '~/locale'; -import { mapActions, mapState } from 'vuex'; +import { mapActions, mapState, mapGetters } from 'vuex'; import GlModal from '~/vue_shared/components/gl_modal.vue'; import { modalTypes } from '../../constants'; @@ -15,6 +16,7 @@ export default { }, computed: { ...mapState(['entryModal']), + ...mapGetters('fileTemplates', ['templateTypes']), entryName: { get() { if (this.entryModal.type === modalTypes.rename) { @@ -31,7 +33,9 @@ export default { if (this.entryModal.type === modalTypes.tree) { return __('Create new directory'); } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); + return this.entryModal.entry.type === modalTypes.tree + ? __('Rename folder') + : __('Rename file'); } return __('Create new file'); @@ -40,11 +44,16 @@ export default { if (this.entryModal.type === modalTypes.tree) { return __('Create directory'); } else if (this.entryModal.type === modalTypes.rename) { - return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file'); + return this.entryModal.entry.type === modalTypes.tree + ? __('Rename folder') + : __('Rename file'); } return __('Create file'); }, + isCreatingNew() { + return this.entryModal.type !== modalTypes.rename; + }, }, methods: { ...mapActions(['createTempEntry', 'renameEntry']), @@ -61,6 +70,14 @@ export default { }); } }, + createFromTemplate(template) { + this.createTempEntry({ + name: template.name, + type: this.entryModal.type, + }); + + $('#ide-new-entry').modal('toggle'); + }, focusInput() { this.$refs.fieldName.focus(); }, @@ -77,6 +94,7 @@ export default { :header-title-text="modalTitle" :footer-primary-button-text="buttonLabel" footer-primary-button-variant="success" + modal-size="lg" @submit="submitForm" @open="focusInput" @closed="closedModal" @@ -84,16 +102,35 @@ export default { <div class="form-group row" > - <label class="label-bold col-form-label col-sm-3"> + <label class="label-bold col-form-label col-sm-2"> {{ __('Name') }} </label> - <div class="col-sm-9"> + <div class="col-sm-10"> <input ref="fieldName" v-model="entryName" type="text" class="form-control" + placeholder="/dir/file_name" /> + <ul + v-if="isCreatingNew" + class="prepend-top-default list-inline" + > + <li + v-for="(template, index) in templateTypes" + :key="index" + class="list-inline-item" + > + <button + type="button" + class="btn btn-missing p-1 pr-2 pl-2" + @click="createFromTemplate(template)" + > + {{ template.name }} + </button> + </li> + </ul> </div> </div> </gl-modal> diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue index 5757dfdc925..0a2681b7a1e 100644 --- a/app/assets/javascripts/ide/components/pipelines/list.vue +++ b/app/assets/javascripts/ide/components/pipelines/list.vue @@ -2,7 +2,6 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { sprintf, __ } from '../../../locale'; -import LoadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Icon from '../../../vue_shared/components/icon.vue'; import CiIcon from '../../../vue_shared/components/ci_icon.vue'; import Tabs from '../../../vue_shared/components/tabs/tabs'; @@ -12,7 +11,6 @@ import JobsList from '../jobs/list.vue'; export default { components: { - LoadingIcon, Icon, CiIcon, Tabs, @@ -50,10 +48,10 @@ export default { <template> <div class="ide-pipeline"> - <loading-icon + <gl-loading-icon v-if="showLoadingIcon" + :size="2" class="prepend-top-default" - size="2" /> <template v-else-if="latestPipeline !== null"> <header diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue index 39a1bd1f61b..37a8ad36507 100644 --- a/app/assets/javascripts/ide/components/preview/clientside.vue +++ b/app/assets/javascripts/ide/components/preview/clientside.vue @@ -3,14 +3,12 @@ import { mapActions, mapGetters, mapState } from 'vuex'; import _ from 'underscore'; import { Manager } from 'smooshpack'; import { listen } from 'codesandbox-api'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Navigator from './navigator.vue'; import { packageJsonPath } from '../../constants'; import { createPathWithExt } from '../../utils'; export default { components: { - LoadingIcon, Navigator, }, data() { @@ -177,9 +175,9 @@ export default { {{ s__('IDE|Get started with Live Preview') }} </a> </div> - <loading-icon + <gl-loading-icon v-else - size="2" + :size="2" class="align-self-center mt-auto mb-auto" /> </div> diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue index 4bf346946b6..42f23801692 100644 --- a/app/assets/javascripts/ide/components/preview/navigator.vue +++ b/app/assets/javascripts/ide/components/preview/navigator.vue @@ -1,12 +1,10 @@ <script> import { listen } from 'codesandbox-api'; import Icon from '~/vue_shared/components/icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; export default { components: { Icon, - LoadingIcon, }, props: { manager: { @@ -138,7 +136,7 @@ export default { class="ide-navigator-location form-control bg-white" readonly /> - <loading-icon + <gl-loading-icon v-if="loading" class="position-absolute ide-preview-loading-icon" /> diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue index 6f1a941fbc4..d3b24c5b793 100644 --- a/app/assets/javascripts/ide/components/repo_commit_section.vue +++ b/app/assets/javascripts/ide/components/repo_commit_section.vue @@ -95,8 +95,9 @@ export default { :file-list="changedFiles" :action-btn-text="__('Stage all changes')" :active-file-key="activeFileKey" + :empty-state-text="__('There are no unstaged changes')" action="stageAllChanges" - action-btn-icon="mobile-issue-close" + action-btn-icon="stage-all" item-action-component="stage-button" class="is-first" icon-name="unstaged" @@ -108,8 +109,9 @@ export default { :action-btn-text="__('Unstage all changes')" :staged-list="true" :active-file-key="activeFileKey" + :empty-state-text="__('There are no staged changes')" action="unstageAllChanges" - action-btn-icon="history" + action-btn-icon="unstage-all" item-action-component="unstage-button" icon-name="staged" /> diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue index f55aa843444..d3a73e84cc7 100644 --- a/app/assets/javascripts/ide/components/repo_editor.vue +++ b/app/assets/javascripts/ide/components/repo_editor.vue @@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue'; import { activityBarViews, viewerTypes } from '../constants'; import Editor from '../lib/editor'; import ExternalLink from './external_link.vue'; +import FileTemplatesBar from './file_templates/bar.vue'; export default { components: { ContentViewer, DiffViewer, ExternalLink, + FileTemplatesBar, }, props: { file: { @@ -34,6 +36,7 @@ export default { 'isCommitModeActive', 'isReviewModeActive', ]), + ...mapGetters('fileTemplates', ['showFileTemplatesBar']), shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, @@ -216,7 +219,7 @@ export default { id="ide" class="blob-viewer-container blob-editor-container" > - <div class="ide-mode-tabs clearfix" > + <div class="ide-mode-tabs clearfix"> <ul v-if="!shouldHideEditor && isEditModeActive" class="nav-links float-left" @@ -249,6 +252,9 @@ export default { :file="file" /> </div> + <file-templates-bar + v-if="showFileTemplatesBar(file.name)" + /> <div v-show="!shouldHideEditor && file.viewMode ==='editor'" ref="editor" diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue deleted file mode 100644 index 110eda83bb4..00000000000 --- a/app/assets/javascripts/ide/components/repo_file.vue +++ /dev/null @@ -1,227 +0,0 @@ -<script> -import { mapActions, mapGetters } from 'vuex'; -import { n__, __, sprintf } from '~/locale'; -import tooltip from '~/vue_shared/directives/tooltip'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; -import Icon from '~/vue_shared/components/icon.vue'; -import FileIcon from '~/vue_shared/components/file_icon.vue'; -import router from '../ide_router'; -import NewDropdown from './new_dropdown/index.vue'; -import FileStatusIcon from './repo_file_status_icon.vue'; -import ChangedFileIcon from './changed_file_icon.vue'; -import MrFileIcon from './mr_file_icon.vue'; - -export default { - name: 'RepoFile', - directives: { - tooltip, - }, - components: { - SkeletonLoadingContainer, - NewDropdown, - FileStatusIcon, - FileIcon, - ChangedFileIcon, - MrFileIcon, - Icon, - }, - props: { - file: { - type: Object, - required: true, - }, - level: { - type: Number, - required: true, - }, - }, - data() { - return { - mouseOver: false, - }; - }, - computed: { - ...mapGetters([ - 'getChangesInFolder', - 'getUnstagedFilesCountForPath', - 'getStagedFilesCountForPath', - ]), - folderUnstagedCount() { - return this.getUnstagedFilesCountForPath(this.file.path); - }, - folderStagedCount() { - return this.getStagedFilesCountForPath(this.file.path); - }, - changesCount() { - return this.getChangesInFolder(this.file.path); - }, - folderChangesTooltip() { - if (this.changesCount === 0) return undefined; - - if (this.folderUnstagedCount > 0 && this.folderStagedCount === 0) { - return n__('%d unstaged change', '%d unstaged changes', this.folderUnstagedCount); - } else if (this.folderUnstagedCount === 0 && this.folderStagedCount > 0) { - return n__('%d staged change', '%d staged changes', this.folderStagedCount); - } - - return sprintf(__('%{unstaged} unstaged and %{staged} staged changes'), { - unstaged: this.folderUnstagedCount, - staged: this.folderStagedCount, - }); - }, - isTree() { - return this.file.type === 'tree'; - }, - isBlob() { - return this.file.type === 'blob'; - }, - levelIndentation() { - return { - marginLeft: `${this.level * 16}px`, - }; - }, - fileClass() { - return { - 'file-open': this.isBlob && this.file.opened, - 'file-active': this.isBlob && this.file.active, - folder: this.isTree, - 'is-open': this.file.opened, - }; - }, - showTreeChangesCount() { - return this.isTree && this.changesCount > 0 && !this.file.opened; - }, - showChangedFileIcon() { - return this.file.changed || this.file.tempFile || this.file.staged; - }, - }, - watch: { - 'file.active': function fileActiveWatch(active) { - if (this.file.type === 'blob' && active) { - this.scrollIntoView(); - } - }, - }, - mounted() { - if (this.hasPathAtCurrentRoute()) { - this.scrollIntoView(true); - } - }, - methods: { - ...mapActions(['toggleTreeOpen']), - clickFile() { - // Manual Action if a tree is selected/opened - if (this.isTree && this.hasUrlAtCurrentRoute()) { - this.toggleTreeOpen(this.file.path); - } - - router.push(`/project${this.file.url}`); - }, - scrollIntoView(isInit = false) { - const block = isInit && this.isTree ? 'center' : 'nearest'; - - this.$el.scrollIntoView({ - behavior: 'smooth', - block, - }); - }, - hasPathAtCurrentRoute() { - if (!this.$router || !this.$router.currentRoute) { - return false; - } - - // - strip route up to "/-/" and ending "/" - const routePath = this.$router.currentRoute.path - .replace(/^.*?[/]-[/]/g, '') - .replace(/[/]$/g, ''); - - // - strip ending "/" - const filePath = this.file.path.replace(/[/]$/g, ''); - - return filePath === routePath; - }, - hasUrlAtCurrentRoute() { - return this.$router.currentRoute.path === `/project${this.file.url}`; - }, - toggleHover(over) { - this.mouseOver = over; - }, - }, -}; -</script> - -<template> - <div> - <div - :class="fileClass" - class="file" - role="button" - @click="clickFile" - @mouseover="toggleHover(true)" - @mouseout="toggleHover(false)" - > - <div - class="file-name" - > - <span - :style="levelIndentation" - class="ide-file-name str-truncated" - > - <file-icon - :file-name="file.name" - :loading="file.loading" - :folder="isTree" - :opened="file.opened" - :size="16" - /> - {{ file.name }} - <file-status-icon - :file="file" - /> - </span> - <span class="float-right ide-file-icon-holder"> - <mr-file-icon - v-if="file.mrChange" - /> - <span - v-if="showTreeChangesCount" - class="ide-tree-changes" - > - {{ changesCount }} - <icon - v-tooltip - :title="folderChangesTooltip" - :size="12" - data-container="body" - data-placement="right" - name="file-modified" - css-classes="prepend-left-5 ide-file-modified" - /> - </span> - <changed-file-icon - v-else-if="showChangedFileIcon" - :file="file" - :show-tooltip="true" - :show-staged-icon="true" - :force-modified-icon="true" - class="float-right" - /> - </span> - <new-dropdown - :type="file.type" - :path="file.path" - :mouse-over="mouseOver" - class="float-right prepend-left-8" - /> - </div> - </div> - <template v-if="file.opened"> - <repo-file - v-for="childFile in file.tree" - :key="childFile.key" - :file="childFile" - :level="level + 1" - /> - </template> - </div> -</template> diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue index 76a3333be50..97589e116c5 100644 --- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue +++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue @@ -26,8 +26,8 @@ export default { <template> <span - v-tooltip v-if="file.file_lock" + v-tooltip :title="lockTooltip" data-container="body" > diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index aa02dfbddc4..b8b64aead30 100644 --- a/app/assets/javascripts/ide/stores/actions.js +++ b/app/assets/javascripts/ide/stores/actions.js @@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; import flash from '~/flash'; import * as types from './mutation_types'; import FilesDecoratorWorker from './workers/files_decorator_worker'; +import { stageKeys } from '../constants'; export const redirectToUrl = (_, url) => visitUrl(url); @@ -122,14 +123,28 @@ export const scrollToTab = () => { }); }; -export const stageAllChanges = ({ state, commit }) => { +export const stageAllChanges = ({ state, commit, dispatch }) => { + const openFile = state.openFiles[0]; + commit(types.SET_LAST_COMMIT_MSG, ''); state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path)); + + dispatch('openPendingTab', { + file: state.stagedFiles.find(f => f.path === openFile.path), + keyPrefix: stageKeys.staged, + }); }; -export const unstageAllChanges = ({ state, commit }) => { +export const unstageAllChanges = ({ state, commit, dispatch }) => { + const openFile = state.openFiles[0]; + state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path)); + + dispatch('openPendingTab', { + file: state.changedFiles.find(f => f.path === openFile.path), + keyPrefix: stageKeys.unstaged, + }); }; export const updateViewer = ({ commit }, viewer) => { @@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES); export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => { const entry = state.entries[entryPath || path]; + commit(types.RENAME_ENTRY, { path, name, entryPath }); if (entry.type === 'tree') { @@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath ); } - if (!entryPath) { + if (!entryPath && !entry.tempFile) { dispatch('deleteEntry', path); } }; diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js index 28b9d0df201..30dcf7ef4df 100644 --- a/app/assets/javascripts/ide/stores/actions/file.js +++ b/app/assets/javascripts/ide/stores/actions/file.js @@ -5,7 +5,7 @@ import service from '../../services'; import * as types from '../mutation_types'; import router from '../../ide_router'; import { setPageTitle } from '../utils'; -import { viewerTypes } from '../../constants'; +import { viewerTypes, stageKeys } from '../../constants'; export const closeFile = ({ commit, state, dispatch }, file) => { const { path } = file; @@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) = eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content); }; -export const stageChange = ({ commit, state }, path) => { +export const stageChange = ({ commit, state, dispatch }, path) => { const stagedFile = state.stagedFiles.find(f => f.path === path); + const openFile = state.openFiles.find(f => f.path === path); commit(types.STAGE_CHANGE, path); commit(types.SET_LAST_COMMIT_MSG, ''); @@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => { if (stagedFile) { eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content); } + + if (openFile && openFile.active) { + const file = state.stagedFiles.find(f => f.path === path); + + dispatch('openPendingTab', { + file, + keyPrefix: stageKeys.staged, + }); + } }; -export const unstageChange = ({ commit }, path) => { +export const unstageChange = ({ commit, dispatch, state }, path) => { + const openFile = state.openFiles.find(f => f.path === path); + commit(types.UNSTAGE_CHANGE, path); + + if (openFile && openFile.active) { + const file = state.changedFiles.find(f => f.path === path); + + dispatch('openPendingTab', { + file, + keyPrefix: stageKeys.unstaged, + }); + } }; -export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => { +export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => { if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false; state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`)); commit(types.ADD_PENDING_TAB, { file, keyPrefix }); - dispatch('scrollToTab'); - router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`); return true; diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js index a601dc8f5a0..877d88bb060 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -8,6 +8,7 @@ import commitModule from './modules/commit'; import pipelines from './modules/pipelines'; import mergeRequests from './modules/merge_requests'; import branches from './modules/branches'; +import fileTemplates from './modules/file_templates'; Vue.use(Vuex); @@ -22,6 +23,7 @@ export const createStore = () => pipelines, mergeRequests, branches, + fileTemplates: fileTemplates(), }, }); diff --git a/app/assets/javascripts/ide/stores/modules/branches/mutations.js b/app/assets/javascripts/ide/stores/modules/branches/mutations.js index 081ec2d4c28..0a455f4500f 100644 --- a/app/assets/javascripts/ide/stores/modules/branches/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/branches/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js index 43237a29466..dd53213ed18 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js @@ -1,6 +1,7 @@ import Api from '~/api'; import { __ } from '~/locale'; import * as types from './mutation_types'; +import eventHub from '../../../eventhub'; export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); export const receiveTemplateTypesError = ({ commit, dispatch }) => { @@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => { .catch(() => dispatch('receiveTemplateTypesError')); }; -export const setSelectedTemplateType = ({ commit }, type) => +export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => { commit(types.SET_SELECTED_TEMPLATE_TYPE, type); + if (rootGetters.activeFile.prevPath === type.name) { + dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true }); + } else if (rootGetters.activeFile.name !== type.name) { + dispatch( + 'renameEntry', + { + path: rootGetters.activeFile.path, + name: type.name, + }, + { root: true }, + ); + } +}; + export const receiveTemplateError = ({ dispatch }, template) => { dispatch( 'setErrorMessage', @@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) => { root: true }, ); commit(types.SET_UPDATE_SUCCESS, true); + eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content); }; export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { @@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => { dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true }); commit(types.SET_UPDATE_SUCCESS, false); + + eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw); + + if (file.prevPath) { + dispatch('discardFileChanges', file.path, { root: true }); + } }; // prevent babel-plugin-rewire from generating an invalid default during karma tests diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js index 38318fd49bf..628babe6a01 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js @@ -1,3 +1,5 @@ +import { activityBarViews } from '../../../constants'; + export const templateTypes = () => [ { name: '.gitlab-ci.yml', @@ -17,7 +19,8 @@ export const templateTypes = () => [ }, ]; -export const showFileTemplatesBar = (_, getters) => name => - getters.templateTypes.find(t => t.name === name); +export const showFileTemplatesBar = (_, getters, rootState) => name => + getters.templateTypes.find(t => t.name === name) && + rootState.currentActivityView === activityBarViews.edit; export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js index dfa5ef54413..383ff5db392 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js @@ -3,10 +3,10 @@ import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; -export default { +export default () => ({ namespaced: true, actions, state: createState(), getters, mutations, -}; +}); diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js index e413e61eaaa..674782a28ca 100644 --- a/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/file_templates/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js index 98102a68e08..0eba9c39817 100644 --- a/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/merge_requests/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js index 5a2213bbe89..b4be100cb07 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; import { normalizeJob } from './utils'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 0347f803757..2c8535bda59 100644 --- a/app/assets/javascripts/ide/stores/mutations.js +++ b/app/assets/javascripts/ide/stores/mutations.js @@ -1,4 +1,4 @@ -/* eslint-disable no-param-reassign */ +import Vue from 'vue'; import * as types from './mutation_types'; import projectMutations from './mutations/project'; import mergeRequestMutation from './mutations/merge_request'; @@ -227,7 +227,7 @@ export default { path: newPath, name: entryPath ? oldEntry.name : name, tempFile: true, - prevPath: oldEntry.path, + prevPath: oldEntry.tempFile ? null : oldEntry.path, url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath), tree: [], parentPath, @@ -246,6 +246,20 @@ export default { if (newEntry.type === 'blob') { state.changedFiles = state.changedFiles.concat(newEntry); } + + if (state.entries[newPath].opened) { + state.openFiles.push(state.entries[newPath]); + } + + if (oldEntry.tempFile) { + const filterMethod = f => f.path !== oldEntry.path; + + state.openFiles = state.openFiles.filter(filterMethod); + state.changedFiles = state.changedFiles.filter(filterMethod); + parent.tree = parent.tree.filter(filterMethod); + + Vue.delete(state.entries, oldEntry.path); + } }, ...projectMutations, ...mergeRequestMutation, diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js index a937fb157f8..6ca246c1d63 100644 --- a/app/assets/javascripts/ide/stores/mutations/file.js +++ b/app/assets/javascripts/ide/stores/mutations/file.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from '../mutation_types'; import { sortTree } from '../utils'; import { diffModes } from '../../constants'; @@ -56,7 +55,7 @@ export default { f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath), ); - if (file.tempFile) { + if (file.tempFile && file.content === '') { Object.assign(state.entries[file.path], { content: raw, }); diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js index 35eaf21a836..9e848699163 100644 --- a/app/assets/javascripts/issuable_bulk_update_actions.js +++ b/app/assets/javascripts/issuable_bulk_update_actions.js @@ -36,7 +36,7 @@ export default { }, getSelectedIssues() { - return this.issues.has('.selected_issue:checked'); + return this.issues.has('.selected-issuable:checked'); }, getLabelsFromSelection() { @@ -110,7 +110,7 @@ export default { getOriginalCommonIds() { const labelIds = []; - this.getElement('.selected_issue:checked').each((i, el) => { + this.getElement('.selected-issuable:checked').each((i, el) => { labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); return _.intersection.apply(this, labelIds); @@ -119,7 +119,7 @@ export default { // From issuable's initial bulk selection getOriginalMarkedIds() { const labelIds = []; - this.getElement('.selected_issue:checked').each((i, el) => { + this.getElement('.selected-issuable:checked').each((i, el) => { labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels')); }); return _.intersection.apply(this, labelIds); @@ -132,7 +132,7 @@ export default { let issuableLabels = []; // Collect unique label IDs for all checked issues - this.getElement('.selected_issue:checked').each((i, el) => { + this.getElement('.selected-issuable:checked').each((i, el) => { issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'); issuableLabels.forEach((labelId) => { // Store unique IDs diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar.js index 2307c8e0d85..74150ce3a8b 100644 --- a/app/assets/javascripts/issuable_bulk_update_sidebar.js +++ b/app/assets/javascripts/issuable_bulk_update_sidebar.js @@ -30,7 +30,7 @@ export default class IssuableBulkUpdateSidebar { this.$otherFilters = $('.issues-other-filters'); this.$checkAllContainer = $('.check-all-holder'); this.$issueChecks = $('.issue-check'); - this.$issuesList = $('.selected_issue'); + this.$issuesList = $('.selected-issuable'); this.$issuableIdsInput = $('#update_issuable_ids'); } @@ -55,7 +55,7 @@ export default class IssuableBulkUpdateSidebar { } updateFormState() { - const noCheckedIssues = !$('.selected_issue:checked').length; + const noCheckedIssues = !$('.selected-issuable:checked').length; this.toggleSubmitButtonDisabled(noCheckedIssues); this.updateSelectedIssuableIds(); @@ -123,7 +123,7 @@ export default class IssuableBulkUpdateSidebar { } static getCheckedIssueIds() { - const $checkedIssues = $('.selected_issue:checked'); + const $checkedIssues = $('.selected-issuable:checked'); if ($checkedIssues.length > 0) { return $.map($checkedIssues, value => $(value).data('id')); diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 597c6d69a81..7fd3ea61aa7 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -53,7 +53,7 @@ <button :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" :disabled="formState.updateLoading || !isSubmitEnabled" - class="btn btn-save float-left" + class="btn btn-success float-left" type="submit" @click.prevent="updateIssuable"> Save changes diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index b5e8e0ea44b..cf99e9a9cd8 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -76,8 +76,8 @@ export default { > </h2> <button - v-tooltip v-if="showInlineEditButton && canUpdate" + v-tooltip type="button" class="btn btn-default btn-edit btn-svg js-issuable-edit" title="Edit title and description" diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue index 1e7f4b2c3f7..63324e68d68 100644 --- a/app/assets/javascripts/jobs/components/header.vue +++ b/app/assets/javascripts/jobs/components/header.vue @@ -1,13 +1,11 @@ <script> import ciHeader from '../../vue_shared/components/header_ci_component.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import callout from '../../vue_shared/components/callout.vue'; export default { name: 'JobHeaderSection', components: { ciHeader, - loadingIcon, callout, }, props: { @@ -59,7 +57,7 @@ export default { actions.push({ label: 'New issue', path: this.job.new_issue_path, - cssClass: 'js-new-issue btn btn-new btn-inverted d-none d-md-block d-lg-block d-xl-block', + cssClass: 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', type: 'link', }); } @@ -82,9 +80,9 @@ export default { :should-render-triggered-label="jobStarted" item-name="Job" /> - <loading-icon + <gl-loading-icon v-if="isLoading" - size="2" + :size="2" class="prepend-top-default append-bottom-default" /> </div> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index 513851e376f..2cbf0f85266 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -78,8 +78,8 @@ <div class="controllers float-right"> <!-- links --> <a - v-tooltip v-if="rawTracePath" + v-tooltip :title="s__('Job|Show complete raw')" :href="rawTracePath" class="js-raw-link-controller controllers-buttons" @@ -89,8 +89,8 @@ </a> <button - v-tooltip v-if="canEraseJob" + v-tooltip :title="s__('Job|Erase job log')" type="button" class="js-erase-link controllers-buttons" diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index b81109bdd06..93e2292ff84 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -25,9 +25,9 @@ class="build-job" > <a - v-tooltip v-for="job in jobs" :key="job.id" + v-tooltip :href="job.path" :title="job.tooltip" :class="{ active: job.active, retried: job.retried }" diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue index 36d4a3e2bc9..80c2a5fb48b 100644 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue @@ -1,5 +1,4 @@ <script> -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import Icon from '~/vue_shared/components/icon.vue'; @@ -9,7 +8,6 @@ export default { name: 'SidebarDetailsBlock', components: { DetailRow, - LoadingIcon, Icon, }, mixins: [timeagoMixin], @@ -132,7 +130,7 @@ export default { <a v-if="job.new_issue_path" :href="job.new_issue_path" - class="js-new-issue btn btn-new btn-inverted" + class="js-new-issue btn btn-success btn-inverted" > {{ __('New issue') }} </a> @@ -232,10 +230,10 @@ export default { </div> </div> </template> - <loading-icon + <gl-loading-icon v-if="isLoading" + :size="2" class="prepend-top-10" - size="2" /> </div> </template> diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js index 2a451ef0cd1..cd12ef87d40 100644 --- a/app/assets/javascripts/jobs/store/mutations.js +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -1,5 +1,3 @@ -/* eslint-disable no-param-reassign */ - import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 6499b919787..1c7bca78df3 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -449,11 +449,11 @@ export default class LabelsSelect { } bindEvents() { - return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue); + return $('body').on('change', '.selected-issuable', this.onSelectCheckboxIssue); } // eslint-disable-next-line class-methods-use-this onSelectCheckboxIssue() { - if ($('.selected_issue:checked').length) { + if ($('.selected-issuable:checked').length) { return; } return $('.issues-bulk-update .labels-filter .dropdown-toggle-text').text('Label'); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 3e208764b3e..30925940807 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js +++ b/app/assets/javascripts/lib/utils/common_utils.js @@ -56,7 +56,8 @@ export const rstrip = val => { return val; }; -export const updateTooltipTitle = ($tooltipEl, newTitle) => $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); +export const updateTooltipTitle = ($tooltipEl, newTitle) => + $tooltipEl.attr('title', newTitle).tooltip('_fixTitle'); export const disableButtonIfEmptyField = (fieldSelector, buttonSelector, eventName = 'input') => { const field = $(fieldSelector); @@ -86,6 +87,7 @@ export const handleLocationHash = () => { const fixedTabs = document.querySelector('.js-tabs-affix'); const fixedDiffStats = document.querySelector('.js-diff-files-changed'); const fixedNav = document.querySelector('.navbar-gitlab'); + const performanceBar = document.querySelector('#js-peek'); let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -102,6 +104,10 @@ export const handleLocationHash = () => { adjustment -= fixedDiffStats.offsetHeight; } + if (performanceBar) { + adjustment -= performanceBar.offsetHeight; + } + window.scrollBy(0, adjustment); }; @@ -131,17 +137,43 @@ export const parseUrlPathname = url => { return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`; }; -// We can trust that each param has one & since values containing & will be encoded -// Remove the first character of search as it is always ? -export const getUrlParamsArray = () => - window.location.search - .slice(1) - .split('&') +const splitPath = (path = '') => path.replace(/^\?/, '').split('&'); + +export const urlParamsToArray = (path = '') => + splitPath(path) + .filter(param => param.length > 0) .map(param => { const split = param.split('='); return [decodeURI(split[0]), split[1]].join('='); }); +export const getUrlParamsArray = () => urlParamsToArray(window.location.search); + +export const urlParamsToObject = (path = '') => + splitPath(path).reduce((dataParam, filterParam) => { + if (filterParam === '') { + return dataParam; + } + + const data = dataParam; + let [key, value] = filterParam.split('='); + const isArray = key.includes('[]'); + key = key.replace('[]', ''); + value = decodeURIComponent(value.replace(/\+/g, ' ')); + + if (isArray) { + if (!data[key]) { + data[key] = []; + } + + data[key].push(value); + } else { + data[key] = value; + } + + return data; + }, {}); + export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; // Identify following special clicks @@ -189,7 +221,7 @@ export const getParameterByName = (name, urlToParse) => { return decodeURIComponent(results[2].replace(/\+/g, ' ')); }; -const handleSelectedRange = (range) => { +const handleSelectedRange = range => { const container = range.commonAncestorContainer; // add context to fragment if needed if (container.tagName === 'OL') { @@ -426,7 +458,7 @@ export const backOff = (fn, timeout = 60000) => { export const createOverlayIcon = (iconPath, overlayPath) => { const faviconImage = document.createElement('img'); - return new Promise((resolve) => { + return new Promise(resolve => { faviconImage.onload = () => { const size = 32; @@ -437,13 +469,29 @@ export const createOverlayIcon = (iconPath, overlayPath) => { const context = canvas.getContext('2d'); context.clearRect(0, 0, size, size); context.drawImage( - faviconImage, 0, 0, faviconImage.width, faviconImage.height, 0, 0, size, size, + faviconImage, + 0, + 0, + faviconImage.width, + faviconImage.height, + 0, + 0, + size, + size, ); const overlayImage = document.createElement('img'); overlayImage.onload = () => { context.drawImage( - overlayImage, 0, 0, overlayImage.width, overlayImage.height, 0, 0, size, size, + overlayImage, + 0, + 0, + overlayImage.width, + overlayImage.height, + 0, + 0, + size, + size, ); const faviconWithOverlayUrl = canvas.toDataURL(); @@ -456,17 +504,21 @@ export const createOverlayIcon = (iconPath, overlayPath) => { }); }; -export const setFaviconOverlay = (overlayPath) => { +export const setFaviconOverlay = overlayPath => { const faviconEl = document.getElementById('favicon'); - if (!faviconEl) { return null; } + if (!faviconEl) { + return null; + } const iconPath = faviconEl.getAttribute('data-original-href'); - return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => faviconEl.setAttribute('href', faviconWithOverlayUrl)); + return createOverlayIcon(iconPath, overlayPath).then(faviconWithOverlayUrl => + faviconEl.setAttribute('href', faviconWithOverlayUrl), + ); }; -export const setFavicon = (faviconPath) => { +export const setFavicon = faviconPath => { const faviconEl = document.getElementById('favicon'); if (faviconEl && faviconPath) { faviconEl.setAttribute('href', faviconPath); @@ -491,7 +543,7 @@ export const setCiStatusFavicon = pageUrl => } return resetFavicon(); }) - .catch((error) => { + .catch(error => { resetFavicon(); throw error; }); diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/lib/utils/navigation_utility.js index 9f69f110d06..1579b225e44 100644 --- a/app/assets/javascripts/shortcuts_dashboard_navigation.js +++ b/app/assets/javascripts/lib/utils/navigation_utility.js @@ -1,4 +1,4 @@ -import { visitUrl } from './lib/utils/url_utility'; +import { visitUrl } from './url_utility'; /** * Helper function that finds the href of the fiven selector and updates the location. diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js index 2be3c97bd95..879f94a26ec 100644 --- a/app/assets/javascripts/lib/utils/text_utility.js +++ b/app/assets/javascripts/lib/utils/text_utility.js @@ -49,6 +49,16 @@ export const dasherize = str => str.replace(/[_\s]+/g, '-'); export const slugify = str => str.trim().toLowerCase(); /** + * Replaces whitespaces with hyphens and converts to lower case + * @param {String} str + * @returns {String} + */ +export const slugifyWithHyphens = str => { + const regex = new RegExp(/\s+/, 'g'); + return str.toLowerCase().replace(regex, '-'); +}; + +/** * Truncates given text * * @param {String} string diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js index 72b72f4247d..a282c2df441 100644 --- a/app/assets/javascripts/lib/utils/url_utility.js +++ b/app/assets/javascripts/lib/utils/url_utility.js @@ -47,9 +47,9 @@ export function removeParamQueryString(url, param) { return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&'); } -export function removeParams(params) { +export function removeParams(params, source = window.location.href) { const url = document.createElement('a'); - url.href = window.location.href; + url.href = source; params.forEach(param => { url.search = removeParamQueryString(url.search, param); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 2718f73a830..e8aac51a299 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -2,7 +2,6 @@ import jQuery from 'jquery'; import Cookies from 'js-cookie'; -import svg4everybody from 'svg4everybody'; // bootstrap webpack, common libs, polyfills, and behaviors import './webpack'; @@ -25,10 +24,12 @@ import initLayoutNav from './layout_nav'; import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import initLogoAnimation from './logo'; -import './milestone_select'; import './frequent_items'; import initBreadcrumbs from './breadcrumb'; -import initDispatcher from './dispatcher'; +import initUsagePingConsent from './usage_ping_consent'; +import initPerformanceBar from './performance_bar'; +import initSearchAutocomplete from './search_autocomplete'; +import GlFieldErrors from './gl_field_errors'; // expose jQuery as global (TODO: remove these) window.jQuery = jQuery; @@ -40,8 +41,6 @@ if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) { import(/* webpackMode: "eager" */ './test_utils/'); } -svg4everybody(); - document.addEventListener('beforeunload', () => { // Unbind scroll events $(document).off('scroll'); @@ -78,6 +77,10 @@ document.addEventListener('DOMContentLoaded', () => { initImporterStatus(); initTodoToggle(); initLogoAnimation(); + initUsagePingConsent(); + + if (document.querySelector('.search')) initSearchAutocomplete(); + if (document.querySelector('#js-peek')) initPerformanceBar({ container: '#js-peek' }); // Set the default path for all cookies to GitLab's root directory Cookies.defaults.path = gon.relative_url_root || '/'; @@ -268,5 +271,6 @@ document.addEventListener('DOMContentLoaded', () => { }); } - initDispatcher(); + // initialize field errors + $('.gl-show-field-errors').each((i, form) => new GlFieldErrors(form)); }); diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js index 53d7504de35..763429d7242 100644 --- a/app/assets/javascripts/merge_request_tabs.js +++ b/app/assets/javascripts/merge_request_tabs.js @@ -115,8 +115,9 @@ export default class MergeRequestTabs { this.mergeRequestTabs && this.mergeRequestTabs.querySelector(`a[data-action='${action}']`) && this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click - ) + ) { this.mergeRequestTabs.querySelector(`a[data-action='${action}']`).click(); + } this.initAffix(); } diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ae96ac3b80c..a07a0ecfc76 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -214,8 +214,8 @@ export default { :show-panels="showPanels" > <graph - v-for="(graphData, index) in groupData.metrics" - :key="index" + v-for="(graphData, graphIndex) in groupData.metrics" + :key="graphIndex" :graph-data="graphData" :hover-data="hoverData" :update-aspect-ratio="updateAspectRatio" diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index e5680a0499f..a13f30e6079 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -82,11 +82,12 @@ export default { value: 0, }, currentXCoordinate: 0, - currentCoordinates: [], + currentCoordinates: {}, showFlag: false, showFlagContent: false, timeSeries: [], realPixelRatio: 1, + seriesUnderMouse: [], }; }, computed: { @@ -126,6 +127,9 @@ export default { this.draw(); }, methods: { + showDot(path) { + return this.showFlagContent && this.seriesUnderMouse.includes(path); + }, draw() { const breakpointSize = bp.getBreakpointSize(); const query = this.graphData.queries[0]; @@ -155,7 +159,24 @@ export default { point.y = e.clientY; point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point.x += 7; - const firstTimeSeries = this.timeSeries[0]; + + this.seriesUnderMouse = this.timeSeries.filter((series) => { + const mouseX = series.timeSeriesScaleX.invert(point.x); + let minDistance = Infinity; + + const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => { + const distance = Math.abs(Number(new Date(x)) - Number(mouseX)); + if (distance < minDistance) { + minDistance = distance; + return x; + } + return closest; + }); + + return series.values.find(v => v.time.toString() === closestTickMark); + }); + + const firstTimeSeries = this.seriesUnderMouse[0]; const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x); const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1); const d0 = firstTimeSeries.values[overlayIndex - 1]; @@ -190,6 +211,17 @@ export default { axisXScale.domain(d3.extent(allValues, d => d.time)); axisYScale.domain([0, d3.max(allValues.map(d => d.value))]); + this.allXAxisValues = this.timeSeries.reduce((obj, series) => { + const seriesKeys = {}; + series.values.forEach(v => { + seriesKeys[v.time] = true; + }); + return { + ...obj, + ...seriesKeys, + }; + }, {}); + const xAxis = d3 .axisBottom() .scale(axisXScale) @@ -277,9 +309,8 @@ export default { :line-style="path.lineStyle" :line-color="path.lineColor" :area-color="path.areaColor" - :current-coordinates="currentCoordinates[index]" - :current-time-series-index="index" - :show-dot="showFlagContent" + :current-coordinates="currentCoordinates[path.metricTag]" + :show-dot="showDot(path)" /> <graph-deployment :deployment-data="reducedDeploymentData" @@ -303,7 +334,7 @@ export default { :graph-height="graphHeight" :graph-height-offset="graphHeightOffset" :show-flag-content="showFlagContent" - :time-series="timeSeries" + :time-series="seriesUnderMouse" :unit-of-display="unitOfDisplay" :legend-title="legendTitle" :deployment-flag-data="deploymentFlagData" diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue index 1e6803abf3a..5f00d20ca3f 100644 --- a/app/assets/javascripts/monitoring/components/graph/flag.vue +++ b/app/assets/javascripts/monitoring/components/graph/flag.vue @@ -52,7 +52,7 @@ export default { required: true, }, currentCoordinates: { - type: Array, + type: Object, required: true, }, }, @@ -91,8 +91,8 @@ export default { }, methods: { seriesMetricValue(seriesIndex, series) { - const indexFromCoordinates = this.currentCoordinates[seriesIndex] - ? this.currentCoordinates[seriesIndex].currentDataIndex : 0; + const indexFromCoordinates = this.currentCoordinates[series.metricTag] + ? this.currentCoordinates[series.metricTag].currentDataIndex : 0; const index = this.deploymentFlagData ? this.deploymentFlagData.seriesIndex : indexFromCoordinates; diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue index 3276f3a1ceb..ef18ae5c2c8 100644 --- a/app/assets/javascripts/monitoring/components/graph/legend.vue +++ b/app/assets/javascripts/monitoring/components/graph/legend.vue @@ -58,8 +58,8 @@ export default { </td> <template v-for="(track, trackIndex) in series.tracksLegend"> <track-line - :track="track" - :key="`track-line-${trackIndex}`"/> + :key="`track-line-${trackIndex}`" + :track="track"/> <td :key="`track-info-${trackIndex}`"> <track-info :track="track" diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js index 4f23814ff3e..007451d5c7a 100644 --- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js +++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js @@ -50,19 +50,24 @@ const mixins = { }, positionFlag() { - const timeSeries = this.timeSeries[0]; - const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1); + const timeSeries = this.seriesUnderMouse[0]; + if (!timeSeries) { + return; + } + const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate); this.currentData = timeSeries.values[hoveredDataIndex]; this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time)); - this.currentCoordinates = this.timeSeries.map((series) => { - const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1); + this.currentCoordinates = {}; + + this.seriesUnderMouse.forEach((series) => { + const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate); const currentData = series.values[currentDataIndex]; const currentX = Math.floor(series.timeSeriesScaleX(currentData.time)); const currentY = Math.floor(series.timeSeriesScaleY(currentData.value)); - return { + this.currentCoordinates[series.metricTag] = { currentX, currentY, currentDataIndex, diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js index cee39fd0559..eff0d7325cd 100644 --- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js +++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js @@ -2,7 +2,7 @@ import _ from 'underscore'; import { scaleLinear, scaleTime } from 'd3-scale'; import { line, area, curveLinear } from 'd3-shape'; import { extent, max, sum } from 'd3-array'; -import { timeMinute } from 'd3-time'; +import { timeMinute, timeSecond } from 'd3-time'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; const d3 = { @@ -14,6 +14,7 @@ const d3 = { extent, max, timeMinute, + timeSecond, sum, }; @@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom return defaultColorPalette[pick]; } + function findByDate(series, time) { + const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60); + if (val) { + return val.value; + } + return NaN; + } + + // The timeseries data may have gaps in it + // but we need a regularly-spaced set of time/value pairs + // this gives us a complete range of one minute intervals + // offset the same amount as the original data + const [minX, maxX] = xDom; + const offset = d3.timeMinute(minX) - Number(minX); + const datesWithoutGaps = d3.timeSecond.every(60) + .range(d3.timeMinute.offset(minX, -1), maxX) + .map(d => d - offset); + query.result.forEach((timeSeries, timeSeriesNumber) => { let metricTag = ''; let lineColor = ''; @@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom }); } + const values = datesWithoutGaps.map(time => ({ + time, + value: findByDate(timeSeries.values, time), + })); + timeSeriesParsed.push({ - linePath: lineFunction(timeSeries.values), - areaPath: areaFunction(timeSeries.values), + linePath: lineFunction(values), + areaPath: areaFunction(values), timeSeriesScaleX, timeSeriesScaleY, values: timeSeries.values, diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js index dd2019001db..446eb477efc 100644 --- a/app/assets/javascripts/mr_notes/stores/index.js +++ b/app/assets/javascripts/mr_notes/stores/index.js @@ -9,7 +9,7 @@ Vue.use(Vuex); export default new Vuex.Store({ modules: { page: mrPageModule, - notes: notesModule, - diffs: diffsModule, + notes: notesModule(), + diffs: diffsModule(), }, }); diff --git a/app/assets/javascripts/notebook/index.vue b/app/assets/javascripts/notebook/index.vue index e2e3b08c77f..f241df9620d 100644 --- a/app/assets/javascripts/notebook/index.vue +++ b/app/assets/javascripts/notebook/index.vue @@ -51,10 +51,10 @@ <template> <div v-if="hasNotebook"> <component - v-for="(cell, index) in cells" :is="cellType(cell.cell_type)" - :cell="cell" + v-for="(cell, index) in cells" :key="index" + :cell="cell" :code-css-class="codeCssClass" /> </div> </template> diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js index 8124ae6201f..0c966e0808a 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -154,7 +154,11 @@ export default class Notes { this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList); this.$wrapperEl.on('click', '.js-toggle-lazy-diff', this.loadLazyDiff); - this.$wrapperEl.on('click', '.js-toggle-lazy-diff-retry-button', this.onClickRetryLazyLoad.bind(this)); + this.$wrapperEl.on( + 'click', + '.js-toggle-lazy-diff-retry-button', + this.onClickRetryLazyLoad.bind(this), + ); // fetch notes when tab becomes visible this.$wrapperEl.on('visibilitychange', this.visibilityChange); @@ -252,9 +256,7 @@ export default class Notes { discussionNoteForm = $textarea.closest('.js-discussion-note-form'); if (discussionNoteForm.length) { if ($textarea.val() !== '') { - if ( - !window.confirm('Are you sure you want to cancel creating this comment?') - ) { + if (!window.confirm('Are you sure you want to cancel creating this comment?')) { return; } } @@ -266,9 +268,7 @@ export default class Notes { originalText = $textarea.closest('form').data('originalNote'); newText = $textarea.val(); if (originalText !== newText) { - if ( - !window.confirm('Are you sure you want to cancel editing this comment?') - ) { + if (!window.confirm('Are you sure you want to cancel editing this comment?')) { return; } } @@ -631,7 +631,7 @@ export default class Notes { * * deactivates the submit button when text is empty * hides the preview button when text is empty - * setup GFM auto complete + * set up GFM auto complete * show the form */ setupNoteForm(form, enableGFM = defaultAutocompleteConfig) { @@ -954,7 +954,7 @@ export default class Notes { * Note: dataHolder must have the "discussionId" and "lineCode" data attributes set. */ setupDiscussionNoteForm(dataHolder, form) { - // setup note target + // set up note target let diffFileData = dataHolder.closest('.text-file'); if (diffFileData.length === 0) { @@ -1036,7 +1036,7 @@ export default class Notes { $diffFile[0].dispatchEvent(clickEvent); - // Setup comment form + // Set up comment form let newForm; const $noteContainer = $link.closest('.diff-viewer').find('.note-container'); const $form = $noteContainer.find('> .discussion-form'); @@ -1074,7 +1074,7 @@ export default class Notes { addForm = false; let lineTypeSelector = ''; rowCssToAdd = - '<tr class="notes_holder js-temp-notes-holder"><td class="notes_line" colspan="2"></td><td class="notes_content"><div class="content"></div></td></tr>'; + '<tr class="notes_holder js-temp-notes-holder"><td class="notes_content" colspan="3"><div class="content"></div></td></tr>'; // In parallel view, look inside the correct left/right pane if (this.isParallelView()) { lineTypeSelector = `.${lineType}`; @@ -1316,8 +1316,7 @@ export default class Notes { $retryButton.prop('disabled', true); - return this.loadLazyDiff(e) - .then(() => { + return this.loadLazyDiff(e).then(() => { $retryButton.prop('disabled', false); }); } @@ -1343,18 +1342,18 @@ export default class Notes { */ if (url) { return axios - .get(url) - .then(({ data }) => { - // Reset state in case last request returned error - $successContainer.removeClass('hidden'); - $errorContainer.addClass('hidden'); - - Notes.renderDiffContent($container, data); - }) - .catch(() => { - $successContainer.addClass('hidden'); - $errorContainer.removeClass('hidden'); - }); + .get(url) + .then(({ data }) => { + // Reset state in case last request returned error + $successContainer.removeClass('hidden'); + $errorContainer.addClass('hidden'); + + Notes.renderDiffContent($container, data); + }) + .catch(() => { + $successContainer.addClass('hidden'); + $errorContainer.removeClass('hidden'); + }); } return Promise.resolve(); } @@ -1545,12 +1544,8 @@ export default class Notes { <div class="note-header"> <div class="note-header-info"> <a href="/${_.escape(currentUsername)}"> - <span class="d-none d-sm-inline-block">${_.escape( - currentUsername, - )}</span> - <span class="note-headline-light">${_.escape( - currentUsername, - )}</span> + <span class="d-none d-sm-inline-block">${_.escape(currentUsername)}</span> + <span class="note-headline-light">${_.escape(currentUsername)}</span> </a> </div> </div> @@ -1565,9 +1560,7 @@ export default class Notes { ); $tempNote.find('.d-none.d-sm-inline-block').text(_.escape(currentUserFullname)); - $tempNote - .find('.note-headline-light') - .text(`@${_.escape(currentUsername)}`); + $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`); return $tempNote; } diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue index 6612bc44e0b..7735133c470 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -374,7 +374,7 @@ js-gfm-input js-autosize markdown-area js-vue-textarea" append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"> <button :disabled="isSubmitButtonDisabled" - class="btn btn-create comment-btn js-comment-button js-comment-submit-button" + class="btn btn-success comment-btn js-comment-button js-comment-submit-button" type="submit" @click.prevent="handleSave()"> {{ __(commentButtonTitle) }} diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue index fc7b52be241..4fd93304a03 100644 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ b/app/assets/javascripts/notes/components/diff_file_header.vue @@ -41,8 +41,8 @@ export default { </div> <template v-else> <component - ref="titleWrapper" :is="titleTag" + ref="titleWrapper" :href="diffFile.discussionPath" > <span v-html="diffFile.blobIcon"></span> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 27ff7dea909..802be022ba6 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -148,10 +148,9 @@ export default { </tr> <tr class="notes_holder"> <td - class="notes_line" - colspan="2" - ></td> - <td class="notes_content"> + class="notes_content" + colspan="3" + > <slot></slot> </td> </tr> diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue index cdbbb342331..beb53da0e6d 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -7,7 +7,6 @@ import editSvg from 'icons/_icon_pencil.svg'; import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg'; import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg'; import ellipsisSvg from 'icons/_ellipsis_v.svg'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; export default { @@ -15,21 +14,19 @@ export default { directives: { tooltip, }, - components: { - loadingIcon, - }, props: { authorId: { type: Number, required: true, }, noteId: { - type: Number, + type: String, required: true, }, noteUrl: { type: String, - required: true, + required: false, + default: '', }, accessLevel: { type: String, @@ -152,9 +149,9 @@ export default { v-else v-html="resolveDiscussionSvg"></div> </template> - <loading-icon + <gl-loading-icon v-else - :inline="true" + inline /> </button> </div> @@ -171,7 +168,7 @@ export default { href="#" title="Add reaction" > - <loading-icon :inline="true" /> + <gl-loading-icon inline/> <span class="link-highlight award-control-icon-neutral" v-html="emojiSmiling"> @@ -225,11 +222,11 @@ export default { Report as abuse </a> </li> - <li> + <li v-if="noteUrl"> <button :data-clipboard-text="noteUrl" type="button" - css-class="btn-default btn-transparent" + class="btn-default btn-transparent js-btn-copy-note-link" > Copy link </button> diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue index e111d3b9ac2..c68860d98ae 100644 --- a/app/assets/javascripts/notes/components/note_awards_list.vue +++ b/app/assets/javascripts/notes/components/note_awards_list.vue @@ -25,7 +25,7 @@ export default { required: true, }, noteId: { - type: Number, + type: String, required: true, }, canAwardEmoji: { @@ -182,9 +182,9 @@ export default { <div class="note-awards"> <div class="awards js-awards-block"> <button - v-tooltip v-for="(awardList, awardName, index) in groupedAwards" :key="index" + v-tooltip :class="getAwardClassBindings(awardList)" :title="awardTitle(awardList)" class="btn award-control" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index abcd4422d7c..2d47d55f33c 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -20,9 +20,9 @@ export default { default: '', }, noteId: { - type: Number, + type: String, required: false, - default: 0, + default: '', }, markdownVersion: { type: Number, @@ -67,7 +67,10 @@ export default { 'getUserDataByProp', ]), noteHash() { - return `#note_${this.noteId}`; + if (this.noteId) { + return `#note_${this.noteId}`; + } + return '#'; }, markdownPreviewPath() { return this.getNoteableDataByProp('preview_note_path'); @@ -168,8 +171,8 @@ export default { id="note_note" ref="textarea" slot="textarea" - :data-supports-quick-actions="!isEditing" v-model="updatedNoteBody" + :data-supports-quick-actions="!isEditing" name="note[note]" class="note-textarea js-gfm-input js-note-text js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" @@ -185,7 +188,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" <button :disabled="isDisabled" type="button" - class="js-vue-issue-save btn btn-save js-comment-button " + class="js-vue-issue-save btn btn-success js-comment-button " @click="handleUpdate()"> {{ saveButtonTitle }} </button> diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue index a621418cf72..d669d12a39b 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -9,7 +9,8 @@ export default { props: { author: { type: Object, - required: true, + required: false, + default: () => ({}), }, createdAt: { type: String, @@ -21,7 +22,7 @@ export default { default: '', }, noteId: { - type: Number, + type: String, required: true, }, includeToggle: { @@ -72,7 +73,10 @@ export default { {{ __('Toggle discussion') }} </button> </div> - <a :href="author.path"> + <a + v-if="Object.keys(author).length" + :href="author.path" + > <span class="note-header-author-name">{{ author.name }}</span> <span v-if="author.status_tooltip_html" @@ -81,6 +85,9 @@ export default { @{{ author.username }} </span> </a> + <span v-else> + {{ __('A deleted user') }} + </span> <span class="note-headline-light"> <span class="note-headline-meta"> <template v-if="actionText"> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 0fe1c16854a..6ede7562edf 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -137,8 +137,10 @@ export default { return this.unresolvedDiscussions.length > 1; }, showJumpToNextDiscussion() { - return this.hasMultipleUnresolvedDiscussions && - !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder); + return ( + this.hasMultipleUnresolvedDiscussions && + !this.isLastUnresolvedDiscussion(this.discussion.id, this.discussionsByDiffOrder) + ); }, shouldRenderDiffs() { const { diffDiscussion, diffFile } = this.transformedDiscussion; @@ -256,11 +258,16 @@ Please check your network connection and try again.`; }); }, jumpToNextDiscussion() { - const nextId = - this.nextUnresolvedDiscussionId(this.discussion.id, this.discussionsByDiffOrder); + const nextId = this.nextUnresolvedDiscussionId( + this.discussion.id, + this.discussionsByDiffOrder, + ); this.jumpToDiscussion(nextId); }, + deleteNoteHandler(note) { + this.$emit('noteDeleted', this.discussion, note); + }, }, }; </script> @@ -270,6 +277,7 @@ Please check your network connection and try again.`; <div class="timeline-entry-inner"> <div class="timeline-icon"> <user-avatar-link + v-if="author" :link-href="author.path" :img-src="author.avatar_url" :img-alt="author.name" @@ -340,10 +348,11 @@ Please check your network connection and try again.`; <div class="discussion-notes"> <ul class="notes"> <component - v-for="note in discussion.notes" :is="componentName(note)" - :note="componentData(note)" + v-for="note in discussion.notes" :key="note.id" + :note="componentData(note)" + @handleDeleteNote="deleteNoteHandler" /> </ul> <div diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 4ebeb5599f2..7579fc852c6 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -86,6 +86,7 @@ export default { // eslint-disable-next-line no-alert if (window.confirm('Are you sure you want to delete this comment?')) { this.isDeleting = true; + this.$emit('handleDeleteNote', this.note); this.deleteNote(this.note) .then(() => { diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 9b8713b40fb..d8e8efb982a 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -10,7 +10,6 @@ import systemNote from '../../vue_shared/components/notes/system_note.vue'; import commentForm from './comment_form.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue'; export default { @@ -20,7 +19,6 @@ export default { noteableDiscussion, systemNote, commentForm, - loadingIcon, placeholderNote, placeholderSystemNote, }, @@ -138,6 +136,7 @@ export default { .then(() => { this.isLoading = false; this.setNotesFetchedState(true); + eventHub.$emit('fetchedNotesData'); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) @@ -188,10 +187,10 @@ export default { class="notes main-notes-list timeline" > <component - v-for="discussion in allDiscussions" :is="getComponentName(discussion)" - v-bind="getComponentData(discussion)" + v-for="discussion in allDiscussions" :key="discussion.id" + v-bind="getComponentData(discussion)" /> </ul> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 3eefbe11c37..320dfa47d5a 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -10,6 +10,7 @@ import service from '../services/notes_service'; import loadAwardsHandler from '../../awards_handler'; import sidebarTimeTrackingEventHub from '../../sidebar/event_hub'; import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; +import mrWidgetEventHub from '../../vue_merge_request_widget/event_hub'; let eTagPoll; @@ -43,18 +44,17 @@ export const fetchDiscussions = ({ commit }, path) => commit(types.SET_INITIAL_DISCUSSIONS, discussions); }); -export const refetchDiscussionById = ({ commit }, { path, discussionId }) => - service - .fetchDiscussions(path) - .then(res => res.json()) - .then(discussions => { - const selectedDiscussion = discussions.find(discussion => discussion.id === discussionId); - if (selectedDiscussion) commit(types.UPDATE_DISCUSSION, selectedDiscussion); - }); +export const updateDiscussion = ({ commit, state }, discussion) => { + commit(types.UPDATE_DISCUSSION, discussion); -export const deleteNote = ({ commit }, note) => + return utils.findNoteObjectById(state.discussions, discussion.id); +}; + +export const deleteNote = ({ commit, dispatch }, note) => service.deleteNote(note.path).then(() => { commit(types.DELETE_NOTE, note); + + dispatch('updateMergeRequestWidget'); }); export const updateNote = ({ commit }, { endpoint, note }) => @@ -75,20 +75,22 @@ export const replyToDiscussion = ({ commit }, { endpoint, data }) => return res; }); -export const createNewNote = ({ commit }, { endpoint, data }) => +export const createNewNote = ({ commit, dispatch }, { endpoint, data }) => service .createNewNote(endpoint, data) .then(res => res.json()) .then(res => { if (!res.errors) { commit(types.ADD_NEW_NOTE, res); + + dispatch('updateMergeRequestWidget'); } return res; }); export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES); -export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => +export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) => service .toggleResolveNote(endpoint, isResolved) .then(res => res.json()) @@ -96,6 +98,8 @@ export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE; commit(mutationType, res); + + dispatch('updateMergeRequestWidget'); }); export const closeIssue = ({ commit, dispatch, state }) => { @@ -152,26 +156,28 @@ export const saveNote = ({ commit, dispatch }, noteData) => { const replyId = noteData.data.in_reply_to_discussion_id; const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; - commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders $('.notes-form .flash-container').hide(); // hide previous flash notification + commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders - if (hasQuickActions) { - placeholderText = utils.stripQuickActions(placeholderText); - } + if (replyId) { + if (hasQuickActions) { + placeholderText = utils.stripQuickActions(placeholderText); + } - if (placeholderText.length) { - commit(types.SHOW_PLACEHOLDER_NOTE, { - noteBody: placeholderText, - replyId, - }); - } + if (placeholderText.length) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + noteBody: placeholderText, + replyId, + }); + } - if (hasQuickActions) { - commit(types.SHOW_PLACEHOLDER_NOTE, { - isSystemNote: true, - noteBody: utils.getQuickActionText(note), - replyId, - }); + if (hasQuickActions) { + commit(types.SHOW_PLACEHOLDER_NOTE, { + isSystemNote: true, + noteBody: utils.getQuickActionText(note), + replyId, + }); + } } return dispatch(methodToDispatch, noteData).then(res => { @@ -211,7 +217,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => { if (errors && errors.commands_only) { Flash(errors.commands_only, 'notice', noteData.flashContainer); } - commit(types.REMOVE_PLACEHOLDER_NOTES); + if (replyId) { + commit(types.REMOVE_PLACEHOLDER_NOTES); + } return res; }); @@ -320,5 +328,9 @@ export const fetchDiscussionDiffLines = ({ commit }, discussion) => }); }); +export const updateMergeRequestWidget = () => { + mrWidgetEventHub.$emit('mr.discussion.updated'); +}; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js index 5b3b9f8776f..d4babf1fab2 100644 --- a/app/assets/javascripts/notes/stores/getters.js +++ b/app/assets/javascripts/notes/stores/getters.js @@ -1,5 +1,6 @@ import _ from 'underscore'; import * as constants from '../constants'; +import { reduceDiscussionsToLineCodes } from './utils'; import { collapseSystemNotes } from './collapse_utils'; export const discussions = state => collapseSystemNotes(state.discussions); @@ -28,17 +29,8 @@ export const notesById = state => return acc; }, {}); -export const discussionsByLineCode = state => - state.discussions.reduce((acc, note) => { - if (note.diff_discussion && note.line_code && note.resolvable) { - // For context about line notes: there might be multiple notes with the same line code - const items = acc[note.line_code] || []; - items.push(note); - - Object.assign(acc, { [note.line_code]: items }); - } - return acc; - }, {}); +export const discussionsStructuredByLineCode = state => + reduceDiscussionsToLineCodes(state.discussions); export const noteableType = state => { const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants; diff --git a/app/assets/javascripts/notes/stores/index.js b/app/assets/javascripts/notes/stores/index.js index 0f48b8880f4..f105b7d0d11 100644 --- a/app/assets/javascripts/notes/stores/index.js +++ b/app/assets/javascripts/notes/stores/index.js @@ -1,16 +1,8 @@ import Vue from 'vue'; import Vuex from 'vuex'; -import * as actions from './actions'; -import * as getters from './getters'; -import mutations from './mutations'; -import module from './modules'; +import notesModule from './modules'; Vue.use(Vuex); export default () => - new Vuex.Store({ - state: module.state, - actions, - getters, - mutations, - }); + new Vuex.Store(notesModule()); diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js index b4cb9267e0f..61dbb075586 100644 --- a/app/assets/javascripts/notes/stores/modules/index.js +++ b/app/assets/javascripts/notes/stores/modules/index.js @@ -2,7 +2,7 @@ import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; -export default { +export default () => ({ state: { discussions: [], targetNoteHash: null, @@ -24,4 +24,4 @@ export default { actions, getters, mutations, -}; +}); diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index ab6a95e2601..73e55705f39 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -4,7 +4,8 @@ import * as constants from '../constants'; import { isInMRPage } from '../../lib/utils/common_utils'; export default { - [types.ADD_NEW_NOTE](state, note) { + [types.ADD_NEW_NOTE](state, data) { + const note = data.discussion ? data.discussion.notes[0] : data; const { discussion_id, type } = note; const [exists] = state.discussions.filter(n => n.id === note.discussion_id); const isDiscussion = type === constants.DISCUSSION_NOTE || type === constants.DIFF_NOTE; @@ -54,13 +55,12 @@ export default { [types.EXPAND_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - - discussion.expanded = true; + Object.assign(discussion, { expanded: true }); }, [types.COLLAPSE_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - discussion.expanded = false; + Object.assign(discussion, { expanded: false }); }, [types.REMOVE_PLACEHOLDER_NOTES](state) { @@ -95,10 +95,18 @@ export default { [types.SET_USER_DATA](state, data) { Object.assign(state, { userData: data }); }, + [types.SET_INITIAL_DISCUSSIONS](state, discussionsData) { const discussions = []; discussionsData.forEach(discussion => { + if (discussion.diff_file) { + Object.assign(discussion, { + fileHash: discussion.diff_file.file_hash, + truncated_diff_lines: discussion.truncated_diff_lines || [], + }); + } + // To support legacy notes, should be very rare case. if (discussion.individual_note && discussion.notes.length > 1) { discussion.notes.forEach(n => { @@ -168,8 +176,7 @@ export default { [types.TOGGLE_DISCUSSION](state, { discussionId }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - - discussion.expanded = !discussion.expanded; + Object.assign(discussion, { expanded: !discussion.expanded }); }, [types.UPDATE_NOTE](state, note) { @@ -185,16 +192,12 @@ export default { [types.UPDATE_DISCUSSION](state, noteData) { const note = noteData; - let index = 0; - - state.discussions.forEach((n, i) => { - if (n.id === note.id) { - index = i; - } - }); - + const selectedDiscussion = state.discussions.find(disc => disc.id === note.id); note.expanded = true; // override expand flag to prevent collapse - state.discussions.splice(index, 1, note); + if (note.diff_file) { + Object.assign(note, { fileHash: note.diff_file.file_hash }); + } + Object.assign(selectedDiscussion, { ...note }); }, [types.CLOSE_ISSUE](state) { @@ -215,12 +218,7 @@ export default { [types.SET_DISCUSSION_DIFF_LINES](state, { discussionId, diffLines }) { const discussion = utils.findNoteObjectById(state.discussions, discussionId); - const index = state.discussions.indexOf(discussion); - - const discussionWithDiffLines = Object.assign({}, discussion, { - truncated_diff_lines: diffLines, - }); - state.discussions.splice(index, 1, discussionWithDiffLines); + discussion.truncated_diff_lines = diffLines; }, }; diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js index a0e096ebfaf..0e41ff03d67 100644 --- a/app/assets/javascripts/notes/stores/utils.js +++ b/app/assets/javascripts/notes/stores/utils.js @@ -2,13 +2,11 @@ import AjaxCache from '~/lib/utils/ajax_cache'; const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm; -export const findNoteObjectById = (notes, id) => - notes.filter(n => n.id === id)[0]; +export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0]; export const getQuickActionText = note => { let text = 'Applying command'; - const quickActions = - AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; + const quickActions = AjaxCache.get(gl.GfmAutoComplete.dataSources.commands) || []; const executedCommands = quickActions.filter(command => { const commandRegex = new RegExp(`/${command.name}`); @@ -27,7 +25,18 @@ export const getQuickActionText = note => { return text; }; +export const reduceDiscussionsToLineCodes = selectedDiscussions => + selectedDiscussions.reduce((acc, note) => { + if (note.diff_discussion && note.line_code) { + // For context about line notes: there might be multiple notes with the same line code + const items = acc[note.line_code] || []; + items.push(note); + + Object.assign(acc, { [note.line_code]: items }); + } + return acc; + }, {}); + export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note); -export const stripQuickActions = note => - note.replace(REGEX_QUICK_ACTIONS, '').trim(); +export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim(); diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js new file mode 100644 index 00000000000..c40503603be --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js @@ -0,0 +1,8 @@ +import UsagePingPayload from './../usage_ping_payload'; + +document.addEventListener('DOMContentLoaded', () => { + new UsagePingPayload( + document.querySelector('.js-usage-ping-payload-trigger'), + document.querySelector('.js-usage-ping-payload'), + ).init(); +}); diff --git a/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js new file mode 100644 index 00000000000..9a1bc46bf4a --- /dev/null +++ b/app/assets/javascripts/pages/admin/application_settings/usage_ping_payload.js @@ -0,0 +1,62 @@ +import axios from '../../../lib/utils/axios_utils'; +import { __ } from '../../../locale'; +import flash from '../../../flash'; + +export default class UsagePingPayload { + constructor(trigger, container) { + this.trigger = trigger; + this.container = container; + this.isVisible = false; + this.isInserted = false; + } + + init() { + this.spinner = this.trigger.querySelector('.js-spinner'); + this.text = this.trigger.querySelector('.js-text'); + + this.trigger.addEventListener('click', event => { + event.preventDefault(); + + if (this.isVisible) return this.hidePayload(); + + return this.requestPayload(); + }); + } + + requestPayload() { + if (this.isInserted) return this.showPayload(); + + this.spinner.classList.add('d-inline'); + + return axios + .get(this.container.dataset.endpoint, { + responseType: 'text', + }) + .then(({ data }) => { + this.spinner.classList.remove('d-inline'); + this.insertPayload(data); + }) + .catch(() => { + this.spinner.classList.remove('d-inline'); + flash(__('Error fetching usage ping data.')); + }); + } + + hidePayload() { + this.isVisible = false; + this.container.classList.add('d-none'); + this.text.textContent = __('Preview payload'); + } + + showPayload() { + this.isVisible = true; + this.container.classList.remove('d-none'); + this.text.textContent = __('Hide payload'); + } + + insertPayload(data) { + this.isInserted = true; + this.container.innerHTML = data; + this.showPayload(); + } +} diff --git a/app/assets/javascripts/pages/admin/runners/index.js b/app/assets/javascripts/pages/admin/runners/index.js new file mode 100644 index 00000000000..ce8fd18b6a2 --- /dev/null +++ b/app/assets/javascripts/pages/admin/runners/index.js @@ -0,0 +1,10 @@ +import initFilteredSearch from '~/pages/search/init_filtered_search'; +import AdminRunnersFilteredSearchTokenKeys from '~/filtered_search/admin_runners_filtered_search_token_keys'; +import { FILTERED_SEARCH } from '~/pages/constants'; + +document.addEventListener('DOMContentLoaded', () => { + initFilteredSearch({ + page: FILTERED_SEARCH.ADMIN_RUNNERS, + filteredSearchTokenKeys: AdminRunnersFilteredSearchTokenKeys, + }); +}); diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index d6aa4bb95d2..8d5efcdcd96 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -155,10 +155,7 @@ /> </form> </template> - <template - slot="secondary-button" - slot-scope="props" - > + <template slot="secondary-button"> <button :disabled="!canSubmit" type="button" diff --git a/app/assets/javascripts/pages/constants.js b/app/assets/javascripts/pages/constants.js index 328b6541636..5e119454ce1 100644 --- a/app/assets/javascripts/pages/constants.js +++ b/app/assets/javascripts/pages/constants.js @@ -3,4 +3,5 @@ export const FILTERED_SEARCH = { MERGE_REQUESTS: 'merge_requests', ISSUES: 'issues', + ADMIN_RUNNERS: 'admin/runners', }; diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js index 79987642796..b9277106a71 100644 --- a/app/assets/javascripts/pages/dashboard/groups/index/index.js +++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js @@ -1,3 +1,5 @@ import initGroupsList from '~/groups'; -document.addEventListener('DOMContentLoaded', initGroupsList); +document.addEventListener('DOMContentLoaded', () => { + initGroupsList(); +}); diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js index 5cfe8723204..79c3be771d0 100644 --- a/app/assets/javascripts/pages/groups/boards/index.js +++ b/app/assets/javascripts/pages/groups/boards/index.js @@ -1,5 +1,5 @@ import UsersSelect from '~/users_select'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js index 914f804fdd3..736c6a62610 100644 --- a/app/assets/javascripts/pages/groups/issues/index.js +++ b/app/assets/javascripts/pages/groups/issues/index.js @@ -1,11 +1,13 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; import { FILTERED_SEARCH } from '~/pages/constants'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, isGroupDecendent: true, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); projectSelect(); }); diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js index 1600faa3611..b798a254459 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,11 +1,13 @@ import projectSelect from '~/project_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, isGroupDecendent: true, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); projectSelect(); }); diff --git a/app/assets/javascripts/pages/groups/show/group_tabs.js b/app/assets/javascripts/pages/groups/show/group_tabs.js new file mode 100644 index 00000000000..c6fe61d2bd9 --- /dev/null +++ b/app/assets/javascripts/pages/groups/show/group_tabs.js @@ -0,0 +1,136 @@ +import $ from 'jquery'; +import { removeParams } from '~/lib/utils/url_utility'; +import createGroupTree from '~/groups'; +import { + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, + CONTENT_LIST_CLASS, + GROUPS_LIST_HOLDER_CLASS, + GROUPS_FILTER_FORM_CLASS, +} from '~/groups/constants'; +import UserTabs from '~/pages/users/user_tabs'; +import GroupFilterableList from '~/groups/groups_filterable_list'; + +export default class GroupTabs extends UserTabs { + constructor({ defaultAction = 'subgroups_and_projects', action, parentEl }) { + super({ defaultAction, action, parentEl }); + } + + bindEvents() { + this.$parentEl + .off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') + .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); + } + + tabShown(event) { + const $target = $(event.target); + const action = $target.data('action') || $target.data('targetSection'); + const source = $target.attr('href') || $target.data('targetPath'); + + document.querySelector(GROUPS_FILTER_FORM_CLASS).action = source; + + this.setTab(action); + return this.setCurrentAction(source); + } + + setTab(action) { + const loadableActions = [ + ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + ACTIVE_TAB_SHARED, + ACTIVE_TAB_ARCHIVED, + ]; + this.enableSearchBar(action); + this.action = action; + + if (this.loaded[action]) { + return; + } + + if (loadableActions.includes(action)) { + this.cleanFilterState(); + this.loadTab(action); + } + } + + loadTab(action) { + const elId = `js-groups-${action}-tree`; + const endpoint = this.getEndpoint(action); + + this.toggleLoading(true); + + createGroupTree(elId, endpoint, action); + this.loaded[action] = true; + + this.toggleLoading(false); + } + + getEndpoint(action) { + const { endpointsDefault, endpointsShared } = this.$parentEl.data(); + let endpoint; + + switch (action) { + case ACTIVE_TAB_ARCHIVED: + endpoint = `${endpointsDefault}?archived=only`; + break; + case ACTIVE_TAB_SHARED: + endpoint = endpointsShared; + break; + default: + // ACTIVE_TAB_SUBGROUPS_AND_PROJECTS + endpoint = endpointsDefault; + break; + } + + return endpoint; + } + + enableSearchBar(action) { + const containerEl = document.getElementById(action); + const form = document.querySelector(GROUPS_FILTER_FORM_CLASS); + const filter = form.querySelector('.js-groups-list-filter'); + const holder = containerEl.querySelector(GROUPS_LIST_HOLDER_CLASS); + const dataEl = containerEl.querySelector(CONTENT_LIST_CLASS); + const endpoint = this.getEndpoint(action); + + if (!dataEl) { + return; + } + + const { dataset } = dataEl; + const opts = { + form, + filter, + holder, + filterEndpoint: endpoint || dataset.endpoint, + pagePath: null, + dropdownSel: '.js-group-filter-dropdown-wrap', + filterInputField: 'filter', + action, + }; + + if (!this.loaded[action]) { + const filterableList = new GroupFilterableList(opts); + filterableList.initSearch(); + } + } + + cleanFilterState() { + const values = Object.values(this.loaded); + const loadedTabs = values.filter(e => e === true); + + if (!loadedTabs.length) { + return; + } + + const newState = removeParams(['page'], window.location.search); + + window.history.replaceState( + { + url: newState, + }, + document.title, + newState, + ); + } +} diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js index d7b35d2b26b..3a45fd70d02 100644 --- a/app/assets/javascripts/pages/groups/show/index.js +++ b/app/assets/javascripts/pages/groups/show/index.js @@ -1,14 +1,22 @@ /* eslint-disable no-new */ +import { getPagePath } from '~/lib/utils/common_utils'; +import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants'; import NewGroupChild from '~/groups/new_group_child'; import notificationsDropdown from '~/notifications_dropdown'; import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; -import ShortcutsNavigation from '~/shortcuts_navigation'; -import initGroupsList from '~/groups'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; +import GroupTabs from './group_tabs'; document.addEventListener('DOMContentLoaded', () => { const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); + const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED]; + const paths = window.location.pathname.split('/'); + const subpath = paths[paths.length - 1]; + const action = loadableActions.includes(subpath) ? subpath : getPagePath(1); + + new GroupTabs({ parentEl: '.groups-listing', action }); new ShortcutsNavigation(); new NotificationsForm(); notificationsDropdown(); @@ -17,6 +25,4 @@ document.addEventListener('DOMContentLoaded', () => { if (newGroupChildWrapper) { new NewGroupChild(newGroupChildWrapper); } - - initGroupsList(); }); diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js deleted file mode 100644 index 2d5020dbef4..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import initUsagePing from './usage_ping'; - -document.addEventListener('DOMContentLoaded', initUsagePing); diff --git a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js deleted file mode 100644 index 914a9661c27..00000000000 --- a/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js +++ /dev/null @@ -1,13 +0,0 @@ -import axios from '../../../lib/utils/axios_utils'; -import { __ } from '../../../locale'; -import flash from '../../../flash'; - -export default function UsagePing() { - const el = document.querySelector('.usage-data'); - - axios.get(el.dataset.endpoint, { - responseType: 'text', - }).then(({ data }) => { - el.innerHTML = data; - }).catch(() => flash(__('Error fetching usage ping data.'))); -} diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js index 5543ad82428..d39ea3d10bf 100644 --- a/app/assets/javascripts/pages/projects/activity/index.js +++ b/app/assets/javascripts/pages/projects/activity/index.js @@ -1,5 +1,5 @@ import Activities from '~/activities'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { new Activities(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js index ea7458fe9b8..26dc90a56d7 100644 --- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js @@ -1,5 +1,5 @@ import BuildArtifacts from '~/build_artifacts'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js index 8484e5e9848..249900d6cb7 100644 --- a/app/assets/javascripts/pages/projects/artifacts/file/index.js +++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js @@ -1,5 +1,5 @@ import BlobViewer from '~/blob/viewer/index'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js index 5cfe8723204..79c3be771d0 100644 --- a/app/assets/javascripts/pages/projects/boards/index.js +++ b/app/assets/javascripts/pages/projects/boards/index.js @@ -1,5 +1,5 @@ import UsersSelect from '~/users_select'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import initBoards from '~/boards'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js index 2e23cce11ce..f477424811d 100644 --- a/app/assets/javascripts/pages/projects/commit/show/index.js +++ b/app/assets/javascripts/pages/projects/commit/show/index.js @@ -3,7 +3,7 @@ import $ from 'jquery'; import Diff from '~/diff'; import ZenMode from '~/zen_mode'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown'; import initNotes from '~/init_notes'; import initChangesDropdown from '~/init_changes_dropdown'; diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js index 3682020579b..ad671ce9351 100644 --- a/app/assets/javascripts/pages/projects/commits/show/index.js +++ b/app/assets/javascripts/pages/projects/commits/show/index.js @@ -1,6 +1,6 @@ import CommitsList from '~/commits'; import GpgBadges from '~/gpg_badges'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js index 24630c2aa05..388d7d7bdda 100644 --- a/app/assets/javascripts/pages/projects/find_file/show/index.js +++ b/app/assets/javascripts/pages/projects/find_file/show/index.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import ProjectFindFile from '~/project_find_file'; -import ShortcutsFindFile from '~/shortcuts_find_file'; +import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file'; document.addEventListener('DOMContentLoaded', () => { const findElement = document.querySelector('.js-file-finder'); diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js index cc0e6553e83..5659e13981a 100644 --- a/app/assets/javascripts/pages/projects/index.js +++ b/app/assets/javascripts/pages/projects/index.js @@ -1,7 +1,7 @@ -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +import initDismissableCallout from '~/dismissable_callout'; import initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; import Project from './project'; -import ShortcutsNavigation from '../../shortcuts_navigation'; +import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation'; document.addEventListener('DOMContentLoaded', () => { const { page } = document.body.dataset; @@ -12,7 +12,7 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - gcpSignupOffer(); + initDismissableCallout('.gcp-signup-offer'); initGkeDropdowns(); } diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js index 56ab3fcdfcb..bc08ccf3584 100644 --- a/app/assets/javascripts/pages/projects/init_blob.js +++ b/app/assets/javascripts/pages/projects/init_blob.js @@ -1,7 +1,7 @@ import LineHighlighter from '~/line_highlighter'; import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater'; -import ShortcutsNavigation from '~/shortcuts_navigation'; -import ShortcutsBlob from '~/shortcuts_blob'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; +import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob'; import BlobForkSuggestion from '~/blob/blob_fork_suggestion'; import initBlobBundle from '~/blob_edit/blob_bundle'; diff --git a/app/assets/javascripts/pages/projects/issues/form.js b/app/assets/javascripts/pages/projects/issues/form.js index b2b8e5d2300..197bfa8a394 100644 --- a/app/assets/javascripts/pages/projects/issues/form.js +++ b/app/assets/javascripts/pages/projects/issues/form.js @@ -5,7 +5,7 @@ import GLForm from '~/gl_form'; import IssuableForm from '~/issuable_form'; import LabelsSelect from '~/labels_select'; import MilestoneSelect from '~/milestone_select'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import IssuableTemplateSelectors from '~/templates/issuable_template_selectors'; export default () => { diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js index 70fdb0ef40d..a56c0bb6be8 100644 --- a/app/assets/javascripts/pages/projects/issues/index/index.js +++ b/app/assets/javascripts/pages/projects/issues/index/index.js @@ -1,15 +1,17 @@ /* eslint-disable no-new */ import IssuableIndex from '~/issuable_index'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.ISSUES, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); new IssuableIndex(ISSUABLE_INDEX.ISSUE); diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js index 500fbd27340..74b3a515e84 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,6 +1,6 @@ import initIssuableSidebar from '~/init_issuable_sidebar'; import Issue from '~/issue'; -import ShortcutsIssuable from '~/shortcuts_issuable'; +import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import ZenMode from '~/zen_mode'; import '~/notes/index'; import '~/issue_show/index'; diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index a7aa616319f..3647048a872 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,13 +1,15 @@ import IssuableIndex from '~/issuable_index'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import UsersSelect from '~/users_select'; import initFilteredSearch from '~/pages/search/init_filtered_search'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; document.addEventListener('DOMContentLoaded', () => { initFilteredSearch({ page: FILTERED_SEARCH.MERGE_REQUESTS, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, }); new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js index 3a3c21f2202..e3971618da5 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import Diff from '~/diff'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import GLForm from '~/gl_form'; import IssuableForm from '~/issuable_form'; import LabelsSelect from '~/labels_select'; diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js index 26ead75cec4..7bfb83a2204 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js +++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js @@ -1,6 +1,6 @@ import ZenMode from '~/zen_mode'; import initIssuableSidebar from '~/init_issuable_sidebar'; -import ShortcutsIssuable from '~/shortcuts_issuable'; +import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; import { handleLocationHash } from '~/lib/utils/common_utils'; import howToMerge from '~/how_to_merge'; import initPipelines from '~/commit/pipelines/pipelines_bundle'; diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js index a0b14fed10f..9f05f63b742 100644 --- a/app/assets/javascripts/pages/projects/network/show/index.js +++ b/app/assets/javascripts/pages/projects/network/show/index.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import ShortcutsNetwork from '../../../../shortcuts_network'; +import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network'; import Network from '../network'; document.addEventListener('DOMContentLoaded', () => { diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index 0d05668b285..ef53d67e7cb 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -147,8 +147,8 @@ <div class="cron-interval-input-wrapper"> <input id="schedule_cron" - :placeholder="__('Define a custom pattern with cron syntax')" v-model="cronInterval" + :placeholder="__('Define a custom pattern with cron syntax')" :name="inputNameAttribute" :disabled="!isEditable" class="form-control inline cron-interval-input" diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js index a853624e944..34a13eb3251 100644 --- a/app/assets/javascripts/pages/projects/project.js +++ b/app/assets/javascripts/pages/projects/project.js @@ -13,40 +13,59 @@ export default class Project { constructor() { const $cloneOptions = $('ul.clone-options-dropdown'); const $projectCloneField = $('#project_clone'); - const $cloneBtnText = $('a.clone-dropdown-btn span'); + const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); - const selectedCloneOption = $cloneBtnText.text().trim(); + const selectedCloneOption = $cloneBtnLabel.text().trim(); if (selectedCloneOption.length > 0) { $(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active'); } - $('a', $cloneOptions).on('click', (e) => { + $('a', $cloneOptions).on('click', e => { + e.preventDefault(); const $this = $(e.currentTarget); const url = $this.attr('href'); - const activeText = $this.find('.dropdown-menu-inner-title').text(); + const cloneType = $this.data('cloneType'); - e.preventDefault(); + $('.is-active', $cloneOptions).removeClass('is-active'); + $(`a[data-clone-type="${cloneType}"]`).each(function() { + const $el = $(this); + const activeText = $el.find('.dropdown-menu-inner-title').text(); + const $container = $el.closest('.project-clone-holder'); + const $label = $container.find('.js-clone-dropdown-label'); - $('.is-active', $cloneOptions).not($this).removeClass('is-active'); - $this.toggleClass('is-active'); - $projectCloneField.val(url); - $cloneBtnText.text(activeText); + $el.toggleClass('is-active'); + $label.text(activeText); + }); - return $('.clone').text(url); + $projectCloneField.val(url); + $('.js-git-empty .js-clone').text(url); }); // Ref switcher Project.initRefSwitcher(); $('.project-refs-select').on('change', function() { - return $(this).parents('form').submit(); + return $(this) + .parents('form') + .submit(); }); $('.hide-no-ssh-message').on('click', function(e) { Cookies.set('hide_no_ssh_message', 'false'); - $(this).parents('.no-ssh-key-message').remove(); + $(this) + .parents('.no-ssh-key-message') + .remove(); return e.preventDefault(); }); $('.hide-no-password-message').on('click', function(e) { Cookies.set('hide_no_password_message', 'false'); - $(this).parents('.no-password-message').remove(); + $(this) + .parents('.no-password-message') + .remove(); + return e.preventDefault(); + }); + $('.hide-auto-devops-implicitly-enabled-banner').on('click', function(e) { + const projectId = $(this).data('project-id'); + const cookieKey = `hide_auto_devops_implicitly_enabled_banner_${projectId}`; + Cookies.set(cookieKey, 'false'); + $(this).parents('.auto-devops-implicitly-enabled-banner').remove(); return e.preventDefault(); }); Project.projectSelectDropdown(); @@ -58,7 +77,7 @@ export default class Project { } static changeProject(url) { - return window.location = url; + return (window.location = url); } static initRefSwitcher() { @@ -73,14 +92,15 @@ export default class Project { selected = $dropdown.data('selected'); return $dropdown.glDropdown({ data(term, callback) { - axios.get($dropdown.data('refsUrl'), { - params: { - ref: $dropdown.data('ref'), - search: term, - }, - }) - .then(({ data }) => callback(data)) - .catch(() => flash(__('An error occurred while getting projects'))); + axios + .get($dropdown.data('refsUrl'), { + params: { + ref: $dropdown.data('ref'), + search: term, + }, + }) + .then(({ data }) => callback(data)) + .catch(() => flash(__('An error occurred while getting projects'))); }, selectable: true, filterable: true, diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue index ae88b765abf..875f6928bed 100644 --- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue +++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue @@ -240,8 +240,8 @@ help-text="Lightweight issue tracking system for this project" > <project-feature-setting - :options="featureAccessLevelOptions" v-model="issuesAccessLevel" + :options="featureAccessLevelOptions" name="project[project_feature_attributes][issues_access_level]" /> </project-setting-row> @@ -250,8 +250,8 @@ help-text="View and edit files in this project" > <project-feature-setting - :options="featureAccessLevelOptions" v-model="repositoryAccessLevel" + :options="featureAccessLevelOptions" name="project[project_feature_attributes][repository_access_level]" /> </project-setting-row> @@ -261,8 +261,8 @@ help-text="Submit changes to be merged upstream" > <project-feature-setting - :options="repoFeatureAccessLevelOptions" v-model="mergeRequestsAccessLevel" + :options="repoFeatureAccessLevelOptions" :disabled-input="!repositoryEnabled" name="project[project_feature_attributes][merge_requests_access_level]" /> @@ -272,8 +272,8 @@ help-text="Build, test, and deploy your changes" > <project-feature-setting - :options="repoFeatureAccessLevelOptions" v-model="buildsAccessLevel" + :options="repoFeatureAccessLevelOptions" :disabled-input="!repositoryEnabled" name="project[project_feature_attributes][builds_access_level]" /> @@ -308,8 +308,8 @@ help-text="Pages for project documentation" > <project-feature-setting - :options="featureAccessLevelOptions" v-model="wikiAccessLevel" + :options="featureAccessLevelOptions" name="project[project_feature_attributes][wiki_access_level]" /> </project-setting-row> @@ -318,8 +318,8 @@ help-text="Share code pastes with others out of Git repository" > <project-feature-setting - :options="featureAccessLevelOptions" v-model="snippetsAccessLevel" + :options="featureAccessLevelOptions" name="project[project_feature_attributes][snippets_access_level]" /> </project-setting-row> diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js index b76f2f76449..7302c1ab202 100644 --- a/app/assets/javascripts/pages/projects/show/index.js +++ b/app/assets/javascripts/pages/projects/show/index.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import initBlob from '~/blob_edit/blob_bundle'; -import ShortcutsNavigation from '~/shortcuts_navigation'; +import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import NotificationsForm from '~/notifications_form'; import UserCallout from '~/user_callout'; import TreeView from '~/tree'; @@ -8,15 +8,18 @@ import BlobViewer from '~/blob/viewer/index'; import Activities from '~/activities'; import { ajaxGet } from '~/lib/utils/common_utils'; import GpgBadges from '~/gpg_badges'; +import initReadMore from '~/read_more'; import Star from '../../../star'; import notificationsDropdown from '../../../notifications_dropdown'; document.addEventListener('DOMContentLoaded', () => { + initReadMore(); new Star(); // eslint-disable-line no-new notificationsDropdown(); new ShortcutsNavigation(); // eslint-disable-line no-new new NotificationsForm(); // eslint-disable-line no-new - new UserCallout({ // eslint-disable-line no-new + // eslint-disable-next-line no-new + new UserCallout({ setCalloutPerProject: false, className: 'js-autodevops-banner', }); diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js index 33d69d891d8..400aed35e32 100644 --- a/app/assets/javascripts/pages/projects/tree/show/index.js +++ b/app/assets/javascripts/pages/projects/tree/show/index.js @@ -4,7 +4,7 @@ import initBlob from '~/blob_edit/blob_bundle'; import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; import GpgBadges from '~/gpg_badges'; import TreeView from '../../../../tree'; -import ShortcutsNavigation from '../../../../shortcuts_navigation'; +import ShortcutsNavigation from '../../../../behaviors/shortcuts/shortcuts_navigation'; import BlobViewer from '../../../../blob/viewer'; import NewCommitForm from '../../../../new_commit_form'; import { ajaxGet } from '../../../../lib/utils/common_utils'; diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 0289209ff1e..75cb6374ad5 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -1,12 +1,8 @@ <script> import _ from 'underscore'; -import GlModal from '~/vue_shared/components/gl_modal.vue'; import { s__, sprintf } from '~/locale'; export default { - components: { - GlModal, - }, props: { deleteWikiUrl: { type: String, @@ -25,6 +21,9 @@ export default { }, }, computed: { + modalId() { + return 'delete-wiki-modal'; + }, message() { return s__('WikiPageConfirmDelete|Are you sure you want to delete this page?'); }, @@ -47,31 +46,41 @@ export default { </script> <template> - <gl-modal - id="delete-wiki-modal" - :header-title-text="title" - :footer-primary-button-text="s__('WikiPageConfirmDelete|Delete page')" - footer-primary-button-variant="danger" - @submit="onSubmit" - > - {{ message }} - <form - ref="form" - :action="deleteWikiUrl" - method="post" - class="js-requires-input" + <div class="d-inline-block"> + <button + v-gl-modal="modalId" + type="button" + class="btn btn-danger" + > + {{ __('Delete') }} + </button> + <gl-ui-modal + :title="title" + :ok-title="s__('WikiPageConfirmDelete|Delete page')" + :modal-id="modalId" + title-tag="h4" + ok-variant="danger" + @ok="onSubmit" > - <input - ref="method" - type="hidden" - name="_method" - value="delete" - /> - <input - :value="csrfToken" - type="hidden" - name="authenticity_token" - /> - </form> - </gl-modal> + {{ message }} + <form + ref="form" + :action="deleteWikiUrl" + method="post" + class="js-requires-input" + > + <input + ref="method" + type="hidden" + name="_method" + value="delete" + /> + <input + :value="csrfToken" + type="hidden" + name="authenticity_token" + /> + </form> + </gl-ui-modal> + </div> </template> diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js index 0a0fe3fc137..c2629090f01 100644 --- a/app/assets/javascripts/pages/projects/wikis/index.js +++ b/app/assets/javascripts/pages/projects/wikis/index.js @@ -2,8 +2,8 @@ import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; import csrf from '~/lib/utils/csrf'; +import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki'; import Wikis from './wikis'; -import ShortcutsWiki from '../../../shortcuts_wiki'; import ZenMode from '../../../zen_mode'; import GLForm from '../../../gl_form'; import deleteWikiModal from './components/delete_wiki_modal.vue'; @@ -14,15 +14,15 @@ document.addEventListener('DOMContentLoaded', () => { new ZenMode(); // eslint-disable-line no-new new GLForm($('.wiki-form')); // eslint-disable-line no-new - const deleteWikiButton = document.getElementById('delete-wiki-button'); + const deleteWikiModalWrapperEl = document.getElementById('delete-wiki-modal-wrapper'); - if (deleteWikiButton) { + if (deleteWikiModalWrapperEl) { Vue.use(Translate); - const { deleteWikiUrl, pageTitle } = deleteWikiButton.dataset; - const deleteWikiModalEl = document.getElementById('delete-wiki-modal'); - const deleteModal = new Vue({ // eslint-disable-line - el: deleteWikiModalEl, + const { deleteWikiUrl, pageTitle } = deleteWikiModalWrapperEl.dataset; + + new Vue({ // eslint-disable-line no-new + el: deleteWikiModalWrapperEl, data: { deleteWikiUrl: '', }, diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue index 0fdb0a080cf..7836d4f3b09 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -130,8 +130,8 @@ export default { </div> <simple-metric v-for="metric in $options.simpleMetrics" - :current-request="currentRequest" :key="metric" + :current-request="currentRequest" :metric="metric" /> <div diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue index 1952dd453f4..9b4ba0c1a9a 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue @@ -1,12 +1,10 @@ <script> import _ from 'underscore'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import StageColumnComponent from './stage_column_component.vue'; export default { components: { StageColumnComponent, - LoadingIcon, }, props: { isLoading: { @@ -59,9 +57,9 @@ export default { <div class="build-content middle-block js-pipeline-graph"> <div class="pipeline-visualization pipeline-graph pipeline-tab-content"> <div class="text-center"> - <loading-icon + <gl-loading-icon v-if="isLoading" - size="3" + :size="3" /> </div> @@ -70,9 +68,9 @@ export default { class="stage-column-list"> <stage-column-component v-for="(stage, index) in graph" + :key="stage.name" :title="capitalizeStageName(stage.name)" :jobs="stage.groups" - :key="stage.name" :stage-connector-class="stageConnectorClass(index, stage)" :is-first-column="isFirstColumn(index)" @refreshPipelineGraph="refreshPipelineGraph" diff --git a/app/assets/javascripts/pipelines/components/graph/job_component.vue b/app/assets/javascripts/pipelines/components/graph/job_component.vue index 9ac16b7e541..a1504592bbc 100644 --- a/app/assets/javascripts/pipelines/components/graph/job_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/job_component.vue @@ -98,8 +98,8 @@ export default { <template> <div class="ci-job-component"> <a - v-tooltip v-if="status.has_details" + v-tooltip :href="status.details_path" :title="tooltipText" :class="cssClassJobName" @@ -115,8 +115,8 @@ export default { </a> <div - v-tooltip v-else + v-tooltip :title="tooltipText" :class="cssClassJobName" class="js-job-component-tooltip non-details-job-component" diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue index e7b2de52f76..567ea119343 100644 --- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue +++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue @@ -62,9 +62,9 @@ export default { <ul> <li v-for="(job, index) in jobs" + :id="jobId(job)" :key="job.id" :class="buildConnnectorClass(index)" - :id="jobId(job)" class="build" > diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 001eaeaa065..1f9187c3d65 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,13 +1,11 @@ <script> import ciHeader from '../../vue_shared/components/header_ci_component.vue'; import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { name: 'PipelineHeaderSection', components: { ciHeader, - loadingIcon, }, props: { pipeline: { @@ -89,9 +87,9 @@ export default { item-name="Pipeline" @actionClicked="postAction" /> - <loading-icon + <gl-loading-icon v-if="isLoading" - size="2" + :size="2" class="prepend-top-default append-bottom-default" /> </div> diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue index 9501afb7493..efb80d3a3c0 100644 --- a/app/assets/javascripts/pipelines/components/nav_controls.vue +++ b/app/assets/javascripts/pipelines/components/nav_controls.vue @@ -43,7 +43,7 @@ export default { <a v-if="newPipelinePath" :href="newPipelinePath" - class="btn btn-create js-run-pipeline" + class="btn btn-success js-run-pipeline" > {{ s__('Pipelines|Run Pipeline') }} </a> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue index 75db1e9ae7c..40df07650c9 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue @@ -67,29 +67,29 @@ export default { </span> <div class="label-container"> <span - v-tooltip v-if="pipeline.flags.latest" + v-tooltip class="js-pipeline-url-latest badge badge-success" title="Latest pipeline for this branch"> latest </span> <span - v-tooltip v-if="pipeline.flags.yaml_errors" + v-tooltip :title="pipeline.yaml_errors" class="js-pipeline-url-yaml badge badge-danger"> yaml invalid </span> <span - v-tooltip v-if="pipeline.flags.failure_reason" + v-tooltip :title="pipeline.failure_reason" class="js-pipeline-url-failure badge badge-danger"> error </span> <a - v-popover="popoverOptions" v-if="pipeline.flags.auto_devops" + v-popover="popoverOptions" tabindex="0" class="js-pipeline-url-autodevops badge badge-info autodevops-badge" role="button"> diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue index c9d2dc3a3c5..ea526cf1309 100644 --- a/app/assets/javascripts/pipelines/components/pipelines.vue +++ b/app/assets/javascripts/pipelines/components/pipelines.vue @@ -319,10 +319,10 @@ export default { <div class="content-list pipelines"> - <loading-icon + <gl-loading-icon v-if="stateToRender === $options.stateMap.loading" :label="s__('Pipelines|Loading Pipelines')" - size="3" + :size="3" class="prepend-top-20" /> diff --git a/app/assets/javascripts/pipelines/components/pipelines_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_actions.vue index 1c8d7303c52..017dd560621 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,6 +1,5 @@ <script> import eventHub from '../event_hub'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import icon from '../../vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; @@ -9,7 +8,6 @@ export default { tooltip, }, components: { - loadingIcon, icon, }, props: { @@ -60,7 +58,7 @@ export default { class="fa fa-caret-down" aria-hidden="true"> </i> - <loading-icon v-if="isLoading" /> + <gl-loading-icon v-if="isLoading" /> </button> <ul class="dropdown-menu dropdown-menu-right"> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 29b347824de..a39cc265601 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -132,10 +132,8 @@ export default { if (this.pipeline.ref) { return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => { if (prop === 'path') { - // eslint-disable-next-line no-param-reassign accumulator.ref_url = this.pipeline.ref[prop]; } else { - // eslint-disable-next-line no-param-reassign accumulator[prop] = this.pipeline.ref[prop]; } return accumulator; diff --git a/app/assets/javascripts/pipelines/components/stage.vue b/app/assets/javascripts/pipelines/components/stage.vue index c7df69c69ed..47c15b1a9c4 100644 --- a/app/assets/javascripts/pipelines/components/stage.vue +++ b/app/assets/javascripts/pipelines/components/stage.vue @@ -18,14 +18,12 @@ import Flash from '../../flash'; import axios from '../../lib/utils/axios_utils'; import eventHub from '../event_hub'; import Icon from '../../vue_shared/components/icon.vue'; -import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import JobComponent from './graph/job_component.vue'; import tooltip from '../../vue_shared/directives/tooltip'; import { PIPELINES_TABLE } from '../constants'; export default { components: { - LoadingIcon, Icon, JobComponent, }, @@ -157,9 +155,9 @@ export default { <template> <div class="dropdown"> <button - v-tooltip id="stageDropdown" ref="dropdown" + v-tooltip :class="triggerButtonClass" :title="stage.title" class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button" @@ -191,7 +189,7 @@ export default { class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container" aria-labelledby="stageDropdown" > - <loading-icon v-if="isLoading"/> + <gl-loading-icon v-if="isLoading"/> <ul v-else class="js-builds-dropdown-list scrollable-menu" diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js index 2cb558b0dec..8929b397f6c 100644 --- a/app/assets/javascripts/pipelines/mixins/pipelines.js +++ b/app/assets/javascripts/pipelines/mixins/pipelines.js @@ -4,7 +4,6 @@ import Flash from '../../flash'; import Poll from '../../lib/utils/poll'; import EmptyState from '../components/empty_state.vue'; import SvgBlankState from '../components/blank_state.vue'; -import LoadingIcon from '../../vue_shared/components/loading_icon.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue'; import eventHub from '../event_hub'; import { CANCEL_REQUEST } from '../constants'; @@ -14,7 +13,6 @@ export default { PipelinesTableComponent, SvgBlankState, EmptyState, - LoadingIcon, }, data() { return { diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js index c15d8ba49e1..d5266544307 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js @@ -1,5 +1,4 @@ import _ from 'underscore'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue'; import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue'; @@ -9,7 +8,6 @@ import store from '../store'; export default { store, components: { - LoadingIcon, DropdownButton, DropdownSearchInput, DropdownHiddenInput, diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue index d4497924ad8..2c02f436b69 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue @@ -126,7 +126,7 @@ export default { </ul> </div> <div class="dropdown-loading"> - <loading-icon /> + <gl-loading-icon /> </div> </div> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue index 08d0a122579..fc17e2fab49 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue @@ -187,7 +187,7 @@ export default { </ul> </div> <div class="dropdown-loading"> - <loading-icon /> + <gl-loading-icon /> </div> </div> </div> diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue index b5476684c6a..ca7c79f75f0 100644 --- a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue +++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue @@ -100,7 +100,7 @@ export default { </ul> </div> <div class="dropdown-loading"> - <loading-icon /> + <gl-loading-icon /> </div> </div> </div> diff --git a/app/assets/javascripts/projects/project_import_gitlab_project.js b/app/assets/javascripts/projects/project_import_gitlab_project.js index 4e20fce1460..fbef3a0b059 100644 --- a/app/assets/javascripts/projects/project_import_gitlab_project.js +++ b/app/assets/javascripts/projects/project_import_gitlab_project.js @@ -1,9 +1,19 @@ import $ from 'jquery'; import { getParameterValues } from '../lib/utils/url_utility'; +import projectNew from './project_new'; export default () => { - const path = getParameterValues('path')[0]; + const pathParam = getParameterValues('path')[0]; + const nameParam = getParameterValues('name')[0]; + const $projectPath = $('.js-path-name'); + const $projectName = $('.js-project-name'); - // get the path url and append it in the inputS - $('.js-path-name').val(path); + // get the path url and append it in the input + $projectPath.val(pathParam); + + // get the project name from the URL and set it as input value + $projectName.val(nameParam); + + // generate slug when project name changes + $projectName.keyup(() => projectNew.onProjectNameChange($projectName, $projectPath)); }; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 04badad0f34..8a079b4b38a 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; +import { slugifyWithHyphens } from '../lib/utils/text_utility'; let hasUserDefinedProjectPath = false; @@ -29,18 +30,23 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => { } }; +const onProjectNameChange = ($projectNameInput, $projectPathInput) => { + const slug = slugifyWithHyphens($projectNameInput.val()); + $projectPathInput.val(slug); +}; + const bindEvents = () => { const $newProjectForm = $('#new_project'); const $projectImportUrl = $('#project_import_url'); - const $projectPath = $('#project_path'); + const $projectPath = $('.tab-pane.active #project_path'); const $useTemplateBtn = $('.template-button > input'); const $projectFieldsForm = $('.project-fields-form'); const $selectedTemplateText = $('.selected-template'); const $changeTemplateBtn = $('.change-template'); const $selectedIcon = $('.selected-icon'); - const $templateProjectNameInput = $('#template-project-name #project_path'); const $pushNewProjectTipTrigger = $('.push-new-project-tip'); const $projectTemplateButtons = $('.project-templates-buttons'); + const $projectName = $('.tab-pane.active #project_name'); if ($newProjectForm.length !== 1) { return; @@ -57,7 +63,8 @@ const bindEvents = () => { $('.btn_import_gitlab_project').on('click', () => { const importHref = $('a.btn_import_gitlab_project').attr('href'); - $('.btn_import_gitlab_project').attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&path=${$projectPath.val()}`); + $('.btn_import_gitlab_project') + .attr('href', `${importHref}?namespace_id=${$('#project_namespace_id').val()}&name=${$projectName.val()}&path=${$projectPath.val()}`); }); if ($pushNewProjectTipTrigger) { @@ -111,7 +118,15 @@ const bindEvents = () => { const selectedTemplate = templates[value]; $selectedTemplateText.text(selectedTemplate.text); $(selectedTemplate.icon).clone().addClass('d-block').appendTo($selectedIcon); - $templateProjectNameInput.focus(); + + const $activeTabProjectName = $('.tab-pane.active #project_name'); + const $activeTabProjectPath = $('.tab-pane.active #project_path'); + $activeTabProjectName.focus(); + $activeTabProjectName + .keyup(() => { + onProjectNameChange($activeTabProjectName, $activeTabProjectPath); + hasUserDefinedProjectPath = $activeTabProjectPath.val().trim().length > 0; + }); } $useTemplateBtn.on('change', chooseTemplate); @@ -131,9 +146,15 @@ const bindEvents = () => { }); $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl)); + + $projectName.keyup(() => { + onProjectNameChange($projectName, $projectPath); + hasUserDefinedProjectPath = $projectPath.val().trim().length > 0; + }); }; export default { bindEvents, deriveProjectPathFromUrl, + onProjectNameChange, }; diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue index 1c1e17563a1..120b4fc2f2b 100644 --- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue +++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue @@ -1,7 +1,6 @@ <script> import Visibility from 'visibilityjs'; import ciIcon from '~/vue_shared/components/ci_icon.vue'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import Poll from '~/lib/utils/poll'; import Flash from '~/flash'; import { s__, sprintf } from '~/locale'; @@ -14,7 +13,6 @@ export default { }, components: { ciIcon, - loadingIcon, }, props: { endpoint: { @@ -100,10 +98,10 @@ export default { </script> <template> <div class="ci-status-link"> - <loading-icon + <gl-loading-icon v-if="isLoading" + :size="3" label="Loading pipeline status" - size="3" /> <a v-else diff --git a/app/assets/javascripts/read_more.js b/app/assets/javascripts/read_more.js new file mode 100644 index 00000000000..d2d1ac8c76a --- /dev/null +++ b/app/assets/javascripts/read_more.js @@ -0,0 +1,41 @@ +/** + * ReadMore + * + * Adds "read more" functionality to elements. + * + * Specifically, it looks for a trigger, by default ".js-read-more-trigger", and adds the class + * "is-expanded" to the previous element in order to provide a click to expand functionality. + * + * This is useful for long text elements that you would like to truncate, especially for mobile. + * + * Example Markup + * <div class="read-more-container"> + * <p>Some text that should be long enough to have to truncate within a specified container.</p> + * <p>This text will not appear in the container, as only the first line can be truncated.</p> + * <p>This should also not appear, if everything is working correctly!</p> + * </div> + * <button class="js-read-more-trigger">Read more</button> + * + */ +export default function initReadMore(triggerSelector = '.js-read-more-trigger') { + const triggerEls = document.querySelectorAll(triggerSelector); + + if (!triggerEls) return; + + triggerEls.forEach(triggerEl => { + const targetEl = triggerEl.previousElementSibling; + + if (!targetEl) { + return; + } + + triggerEl.addEventListener( + 'click', + e => { + targetEl.classList.add('is-expanded'); + e.target.remove(); + }, + { once: true }, + ); + }); +} diff --git a/app/assets/javascripts/registry/components/app.vue b/app/assets/javascripts/registry/components/app.vue index 31f88675912..7e2287ac4db 100644 --- a/app/assets/javascripts/registry/components/app.vue +++ b/app/assets/javascripts/registry/components/app.vue @@ -1,7 +1,6 @@ <script> import { mapGetters, mapActions } from 'vuex'; import Flash from '../../flash'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import store from '../stores'; import collapsibleContainer from './collapsible_container.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; @@ -10,7 +9,6 @@ name: 'RegistryListApp', components: { collapsibleContainer, - loadingIcon, }, props: { endpoint: { @@ -42,9 +40,9 @@ </script> <template> <div> - <loading-icon + <gl-loading-icon v-if="isLoading" - size="3" + :size="3" /> <collapsible-container diff --git a/app/assets/javascripts/registry/components/collapsible_container.vue b/app/assets/javascripts/registry/components/collapsible_container.vue index 4116c4a0489..d9bf41924d1 100644 --- a/app/assets/javascripts/registry/components/collapsible_container.vue +++ b/app/assets/javascripts/registry/components/collapsible_container.vue @@ -2,16 +2,15 @@ import { mapActions } from 'vuex'; import Flash from '../../flash'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; import tableRegistry from './table_registry.vue'; import { errorMessages, errorMessagesTypes } from '../constants'; + import { __ } from '../../locale'; export default { name: 'CollapsibeContainerRegisty', components: { clipboardButton, - loadingIcon, tableRegistry, }, directives: { @@ -46,7 +45,10 @@ handleDeleteRepository() { this.deleteRepo(this.repo) - .then(() => this.fetchRepos()) + .then(() => { + Flash(__('This container registry has been scheduled for deletion.'), 'notice'); + this.fetchRepos(); + }) .catch(() => this.showError(errorMessagesTypes.DELETE_REPO)); }, @@ -86,8 +88,8 @@ <div class="controls d-none d-sm-block float-right"> <button - v-tooltip v-if="repo.canDelete" + v-tooltip :title="s__('ContainerRegistry|Remove repository')" :aria-label="s__('ContainerRegistry|Remove repository')" type="button" @@ -103,10 +105,10 @@ </div> </div> - <loading-icon + <gl-loading-icon v-if="repo.isLoading" + :size="2" class="append-bottom-20" - size="2" /> <div diff --git a/app/assets/javascripts/registry/components/table_registry.vue b/app/assets/javascripts/registry/components/table_registry.vue index 9f4973c3490..fafb35bd69a 100644 --- a/app/assets/javascripts/registry/components/table_registry.vue +++ b/app/assets/javascripts/registry/components/table_registry.vue @@ -118,8 +118,8 @@ <td class="content"> <button - v-tooltip v-if="item.canDelete" + v-tooltip :title="s__('ContainerRegistry|Remove tag')" :aria-label="s__('ContainerRegistry|Remove tag')" type="button" diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue index 7b37f4e9a97..fb8c6402d02 100644 --- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -92,16 +92,16 @@ v-for="(report, i) in reports" > <summary-row + :key="`summary-row-${i}`" :summary="reportText(report)" :status-icon="getReportIcon(report)" - :key="`summary-row-${i}`" /> <issues-list v-if="shouldRenderIssuesList(report)" + :key="`issues-list-${i}`" :unresolved-issues="report.existing_failures" :new-issues="report.new_failures" :resolved-issues="report.resolved_failures" - :key="`issues-list-${i}`" :component="$options.componentNames.TestIssueBody" class="report-block-group-list" /> diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_issues.vue index c553a374f66..a2a03945ae3 100644 --- a/app/assets/javascripts/reports/components/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_issues.vue @@ -37,8 +37,8 @@ export default { <ul class="report-block-list"> <li v-for="(issue, index) in issues" - :class="{ 'is-dismissed': issue.isDismissed }" :key="index" + :class="{ 'is-dismissed': issue.isDismissed }" class="report-block-list-issue" > <issue-status-icon @@ -47,8 +47,8 @@ export default { /> <component - v-if="component" :is="component" + v-if="component" :issue="issue" :status="issue.status || status" :is-new="isNew" diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/reports/components/summary_row.vue index 4456d84c968..51188981bed 100644 --- a/app/assets/javascripts/reports/components/summary_row.vue +++ b/app/assets/javascripts/reports/components/summary_row.vue @@ -1,6 +1,5 @@ <script> import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; import Popover from '~/vue_shared/components/help_popover.vue'; /** @@ -15,7 +14,6 @@ export default { name: 'ReportSummaryRow', components: { CiIcon, - LoadingIcon, Popover, }, props: { @@ -46,7 +44,7 @@ export default { <template> <div class="report-block-list-issue report-block-list-issue-parent"> <div class="report-block-list-icon append-right-10 prepend-left-5"> - <loading-icon + <gl-loading-icon v-if="statusIcon === 'loading'" css-class="report-block-list-loading-icon" /> diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js index 1983a8c9e56..b88bff97075 100644 --- a/app/assets/javascripts/reports/store/mutations.js +++ b/app/assets/javascripts/reports/store/mutations.js @@ -1,4 +1,3 @@ -/* eslint-disable no-param-reassign */ import * as types from './mutation_types'; export default { diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js index aec09b8bc0a..50dd3c12382 100644 --- a/app/assets/javascripts/search_autocomplete.js +++ b/app/assets/javascripts/search_autocomplete.js @@ -68,7 +68,7 @@ function setSearchOptions() { } } -export default class SearchAutocomplete { +export class SearchAutocomplete { constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) { setSearchOptions(); this.bindEventContext(); @@ -499,3 +499,7 @@ export default class SearchAutocomplete { this.dropdownMenu.toggleClass('fade-out', !this.isScrolledUp()); } } + +export default function initSearchAutocomplete(opts) { + return new SearchAutocomplete(opts); +} diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue index 56d57f6aac8..286a16f7bbf 100644 --- a/app/assets/javascripts/sidebar/components/participants/participants.vue +++ b/app/assets/javascripts/sidebar/components/participants/participants.vue @@ -1,7 +1,6 @@ <script> import { __, n__, sprintf } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; export default { @@ -9,7 +8,6 @@ tooltip, }, components: { - loadingIcon, userAvatarImage, }, props: { @@ -93,7 +91,7 @@ aria-hidden="true" > </i> - <loading-icon + <gl-loading-icon v-if="loading" class="js-participants-collapsed-loading-icon" /> @@ -105,7 +103,7 @@ </span> </div> <div class="title hide-collapsed"> - <loading-icon + <gl-loading-icon v-if="loading" :inline="true" class="js-participants-expanded-loading-icon" diff --git a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue index ca3b9338c29..2ee3e1f322e 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -19,19 +19,23 @@ export default { TimeTrackingHelpState, }, props: { + // eslint-disable-next-line vue/prop-name-casing time_estimate: { type: Number, required: true, }, + // eslint-disable-next-line vue/prop-name-casing time_spent: { type: Number, required: true, }, + // eslint-disable-next-line vue/prop-name-casing human_time_estimate: { type: String, required: false, default: '', }, + // eslint-disable-next-line vue/prop-name-casing human_time_spent: { type: String, required: false, diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue index ffaed9c7193..a6b3a674952 100644 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue @@ -3,7 +3,6 @@ import { __ } from '~/locale'; import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; const MARK_TEXT = __('Mark todo as done'); const TODO_TEXT = __('Add todo'); @@ -14,7 +13,6 @@ export default { }, components: { Icon, - LoadingIcon, }, props: { issuableId: { @@ -90,7 +88,7 @@ export default { > {{ buttonLabel }} </span> - <loading-icon + <gl-loading-icon v-show="isActionActive" :inline="true" /> diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/usage_ping_consent.js new file mode 100644 index 00000000000..ae3fde190e3 --- /dev/null +++ b/app/assets/javascripts/usage_ping_consent.js @@ -0,0 +1,30 @@ +import $ from 'jquery'; +import axios from './lib/utils/axios_utils'; +import Flash, { hideFlash } from './flash'; +import { convertPermissionToBoolean } from './lib/utils/common_utils'; + +export default () => { + $('body').on('click', '.js-usage-consent-action', (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); // overwrite rails listener + + const { url, checkEnabled, pingEnabled } = e.target.dataset; + const data = { + application_setting: { + version_check_enabled: convertPermissionToBoolean(checkEnabled), + usage_ping_enabled: convertPermissionToBoolean(pingEnabled), + }, + }; + + const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message')); + + axios.put(url, data) + .then(() => { + hideConsentMessage(); + }) + .catch(() => { + hideConsentMessage(); + Flash('Something went wrong. Try again later.'); + }); + }); +}; 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 d530ab2767b..70518ad73e8 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -106,8 +106,8 @@ export default { </tooltip-on-truncate> </template> <span - v-tooltip v-if="hasDeploymentTime" + v-tooltip :title="deployment.deployed_at_formatted" class="js-deploy-time" > diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index 9aff95dcfec..035ae791a1d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -1,11 +1,9 @@ <script> import ciIcon from '../../vue_shared/components/ci_icon.vue'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; export default { components: { ciIcon, - loadingIcon, }, props: { status: { @@ -37,7 +35,7 @@ v-if="isLoading" class="mr-widget-icon" > - <loading-icon /> + <gl-loading-icon /> </div> <ci-icon diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue index 2133124347c..01294d5b40c 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue @@ -1,5 +1,4 @@ <script> - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; @@ -7,7 +6,6 @@ name: 'MRWidgetAutoMergeFailed', components: { statusIcon, - loadingIcon, }, props: { mr: { @@ -44,7 +42,7 @@ class="btn btn-sm btn-default" @click="refreshWidget" > - <loading-icon + <gl-loading-icon v-if="isRefreshing" :inline="true" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue index 1a444c04a1d..8184ef33022 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue @@ -1,7 +1,6 @@ <script> import Flash from '~/flash'; import tooltip from '~/vue_shared/directives/tooltip'; - import loadingIcon from '~/vue_shared/components/loading_icon.vue'; import { s__, __ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import MrWidgetAuthorTime from '../../components/mr_widget_author_time.vue'; @@ -15,7 +14,6 @@ }, components: { MrWidgetAuthorTime, - loadingIcon, statusIcon, ClipboardButton, }, @@ -116,8 +114,8 @@ :date-readable="mr.metrics.readableMergedAt" /> <a - v-tooltip v-if="mr.canRevertInCurrentMR" + v-tooltip :title="revertTitle" class="btn btn-close btn-sm" href="#modal-revert-commit" @@ -127,8 +125,8 @@ {{ revertLabel }} </a> <a - v-tooltip v-else-if="mr.revertInForkPath" + v-tooltip :href="mr.revertInForkPath" :title="revertTitle" class="btn btn-close btn-sm" @@ -137,8 +135,8 @@ {{ revertLabel }} </a> <a - v-tooltip v-if="mr.canCherryPickInCurrentMR" + v-tooltip :title="cherryPickTitle" class="btn btn-default btn-sm" href="#modal-cherry-pick-commit" @@ -148,8 +146,8 @@ {{ cherryPickLabel }} </a> <a - v-tooltip v-else-if="mr.cherryPickInForkPath" + v-tooltip :href="mr.cherryPickInForkPath" :title="cherryPickTitle" class="btn btn-default btn-sm" @@ -195,7 +193,7 @@ </button> </p> <p v-if="shouldShowSourceBranchRemoving"> - <loading-icon :inline="true" /> + <gl-loading-icon :inline="true" /> <span> {{ s__("mrWidget|The source branch is being removed") }} </span> 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 2d8c3d6be87..f31c7a3edb8 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 @@ -2,14 +2,12 @@ import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; import statusIcon from '../mr_widget_status_icon.vue'; - import loadingIcon from '../../../vue_shared/components/loading_icon.vue'; import Flash from '../../../flash'; export default { name: 'MRWidgetRebase', components: { statusIcon, - loadingIcon, }, props: { mr: { @@ -115,7 +113,7 @@ js-toggle-container accept-action media space-children" class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" @click="rebase" > - <loading-icon v-if="isMakingRequest" /> + <gl-loading-icon v-if="isMakingRequest" /> Rebase </button> <span diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue index 25c1044fe2b..25ad329e196 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue @@ -37,8 +37,8 @@ export default { <div class="accept-control inline"> <label class="merge-param-checkbox"> <input - :disabled="isMergeButtonDisabled" v-model="squashBeforeMerge" + :disabled="isMergeButtonDisabled" type="checkbox" name="squash" class="qa-squash-checkbox" 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 086dbabe77e..e73b7e410d5 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 @@ -37,7 +37,7 @@ export default { <a v-if="mr.newBlobPath" :href="mr.newBlobPath" - class="btn btn-inverted btn-save"> + class="btn btn-inverted btn-success"> Create file </a> </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 a5ca7b719a1..23c3284cd21 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 @@ -255,7 +255,7 @@ export default { data-toggle="dropdown" aria-label="Select merge moment"> <i - class="fa fa-chevron-down" + class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true" ></i> </button> @@ -265,7 +265,7 @@ export default { role="menu"> <li> <a - class="merge_when_pipeline_succeeds" + class="merge_when_pipeline_succeeds qa-merge-when-pipeline-succeeds-option" href="#" @click.prevent="handleMergeButtonClick(true)"> <span class="media"> @@ -279,7 +279,7 @@ export default { </li> <li> <a - class="accept-merge-request" + class="accept-merge-request qa-merge-immediately-option" href="#" @click.prevent="handleMergeButtonClick(false, true)"> <span class="media"> diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js index 69a9132a2da..cc6e620f365 100644 --- a/app/assets/javascripts/vue_merge_request_widget/index.js +++ b/app/assets/javascripts/vue_merge_request_widget/index.js @@ -1,7 +1,4 @@ -import { - Vue, - mrWidgetOptions, -} from './dependencies'; +import { Vue, mrWidgetOptions } from './dependencies'; import Translate from '../vue_shared/translate'; Vue.use(Translate); 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 dc6be025f11..b5eaaf054e7 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 @@ -107,10 +107,14 @@ export default { created() { this.initPolling(); this.bindEventHubListeners(); + eventHub.$on('mr.discussion.updated', this.checkStatus); }, mounted() { this.handleMounted(); }, + beforeDestroy() { + eventHub.$off('mr.discussion.updated', this.checkStatus); + }, methods: { createService(store) { const endpoints = { diff --git a/app/assets/javascripts/vue_shared/components/bar_chart.vue b/app/assets/javascripts/vue_shared/components/bar_chart.vue index 3ced4eb691a..33af7a7f1df 100644 --- a/app/assets/javascripts/vue_shared/components/bar_chart.vue +++ b/app/assets/javascripts/vue_shared/components/bar_chart.vue @@ -291,8 +291,8 @@ export default { <template v-for="(data, index) in graphData"> <rect - v-tooltip :key="index" + v-tooltip :width="xScale.bandwidth()" :x="xScale(data.name)" :y="yScale(data.value)" diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue index d3cbe3c7e74..cfc5343217c 100644 --- a/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/diff_viewer/diff_viewer.vue @@ -46,7 +46,7 @@ export default { } }, basePath() { - // We might get the project path from rails with the relative url already setup + // We might get the project path from rails with the relative url already set up return this.projectPath.indexOf('/') === 0 ? '' : `${gon.relative_url_root}/`; }, fullOldPath() { diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue index af5ebcdc40a..31087017968 100644 --- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue +++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue @@ -1,11 +1,7 @@ <script> import { __ } from '~/locale'; -import LoadingIcon from '~/vue_shared/components/loading_icon.vue'; export default { - components: { - LoadingIcon, - }, props: { isDisabled: { type: Boolean, @@ -34,7 +30,7 @@ export default { data-toggle="dropdown" aria-expanded="false" > - <loading-icon + <gl-loading-icon v-show="isLoading" :inline="true" /> diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue index 878c805ada5..408f7d7965f 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/file_icon.vue @@ -1,6 +1,5 @@ <script> import getIconForFile from './file_icon/file_icon_map'; -import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import icon from '../../vue_shared/components/icon.vue'; /* This is a re-usable vue component for rendering a svg sprite @@ -17,7 +16,6 @@ import icon from '../../vue_shared/components/icon.vue'; */ export default { components: { - loadingIcon, icon, }, props: { @@ -84,7 +82,7 @@ export default { :size="size" css-classes="folder-icon" /> - <loading-icon + <gl-loading-icon v-if="loading" :inline="true" /> diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue new file mode 100644 index 00000000000..c797ad62a5d --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -0,0 +1,210 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; + +export default { + name: 'FileRow', + components: { + FileIcon, + Icon, + }, + props: { + file: { + type: Object, + required: true, + }, + level: { + type: Number, + required: true, + }, + extraComponent: { + type: Object, + required: false, + default: null, + }, + }, + data() { + return { + mouseOver: false, + }; + }, + computed: { + isTree() { + return this.file.type === 'tree'; + }, + isBlob() { + return this.file.type === 'blob'; + }, + levelIndentation() { + return { + marginLeft: `${this.level * 16}px`, + }; + }, + fileClass() { + return { + 'file-open': this.isBlob && this.file.opened, + 'is-active': this.isBlob && this.file.active, + folder: this.isTree, + 'is-open': this.file.opened, + }; + }, + }, + watch: { + 'file.active': function fileActiveWatch(active) { + if (this.file.type === 'blob' && active) { + this.scrollIntoView(); + } + }, + }, + mounted() { + if (this.hasPathAtCurrentRoute()) { + this.scrollIntoView(true); + } + }, + methods: { + toggleTreeOpen(path) { + this.$emit('toggleTreeOpen', path); + }, + clickFile() { + // Manual Action if a tree is selected/opened + if (this.isTree && this.hasUrlAtCurrentRoute()) { + this.toggleTreeOpen(this.file.path); + } + + if (this.$router) this.$router.push(`/project${this.file.url}`); + }, + scrollIntoView(isInit = false) { + const block = isInit && this.isTree ? 'center' : 'nearest'; + + this.$el.scrollIntoView({ + behavior: 'smooth', + block, + }); + }, + hasPathAtCurrentRoute() { + if (!this.$router || !this.$router.currentRoute) { + return false; + } + + // - strip route up to "/-/" and ending "/" + const routePath = this.$router.currentRoute.path + .replace(/^.*?[/]-[/]/g, '') + .replace(/[/]$/g, ''); + + // - strip ending "/" + const filePath = this.file.path.replace(/[/]$/g, ''); + + return filePath === routePath; + }, + hasUrlAtCurrentRoute() { + if (!this.$router || !this.$router.currentRoute) return true; + + return this.$router.currentRoute.path === `/project${this.file.url}`; + }, + toggleHover(over) { + this.mouseOver = over; + }, + }, +}; +</script> + +<template> + <div> + <div + :class="fileClass" + class="file-row" + role="button" + @click="clickFile" + @mouseover="toggleHover(true)" + @mouseout="toggleHover(false)" + > + <div + class="file-row-name-container" + > + <span + :style="levelIndentation" + class="file-row-name str-truncated" + > + <file-icon + :file-name="file.name" + :loading="file.loading" + :folder="isTree" + :opened="file.opened" + :size="16" + /> + {{ file.name }} + </span> + <component + :is="extraComponent" + v-if="extraComponent" + :file="file" + :mouse-over="mouseOver" + /> + </div> + </div> + <template v-if="file.opened"> + <file-row + v-for="childFile in file.tree" + :key="childFile.key" + :file="childFile" + :level="level + 1" + :extra-component="extraComponent" + @toggleTreeOpen="toggleTreeOpen" + /> + </template> + </div> +</template> + +<style> +.file-row { + display: flex; + align-items: center; + height: 32px; + padding: 4px 8px; + margin-left: -8px; + margin-right: -8px; + border-radius: 3px; + text-align: left; + cursor: pointer; +} + +.file-row:hover, +.file-row:focus { + background: #f2f2f2; +} + +.file-row:active { + background: #dfdfdf; +} + +.file-row.is-active { + background: #f2f2f2; +} + +.file-row-name-container { + display: flex; + width: 100%; + align-items: center; + overflow: visible; +} + +.file-row-name { + display: inline-block; + flex: 1; + max-width: inherit; + height: 18px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.file-row-name svg { + margin-right: 2px; + vertical-align: middle; +} + +.file-row-name .loading-container { + display: inline-block; + margin-right: 4px; +} +</style> 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 49fbce75110..b371b6adf7e 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,6 +1,5 @@ <script> import CiIconBadge from './ci_badge_link.vue'; -import LoadingIcon from './loading_icon.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; import tooltip from '../directives/tooltip'; import UserAvatarImage from './user_avatar/user_avatar_image.vue'; @@ -15,7 +14,6 @@ import UserAvatarImage from './user_avatar/user_avatar_image.vue'; export default { components: { CiIconBadge, - LoadingIcon, TimeagoTooltip, UserAvatarImage, }, @@ -128,18 +126,18 @@ export default { > <a v-if="action.type === 'link'" + :key="i" :href="action.path" :class="action.cssClass" - :key="i" > {{ action.label }} </a> <a v-else-if="action.type === 'ujs-link'" + :key="i" :href="action.path" :class="action.cssClass" - :key="i" data-method="post" rel="nofollow" > @@ -148,9 +146,9 @@ export default { <button v-else-if="action.type === 'button'" + :key="i" :disabled="action.isLoading" :class="action.cssClass" - :key="i" type="button" @click="onClickAction(action)" > diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 2ff0c056b9c..4cbd3e6429d 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -17,12 +17,7 @@ */ - import loadingIcon from './loading_icon.vue'; - export default { - components: { - loadingIcon, - }, props: { loading: { type: Boolean, @@ -60,7 +55,7 @@ @click="onClick" > <transition name="fade"> - <loading-icon + <gl-loading-icon v-if="loading" :inline="true" :class="{ diff --git a/app/assets/javascripts/vue_shared/components/loading_icon.vue b/app/assets/javascripts/vue_shared/components/loading_icon.vue deleted file mode 100644 index db22c5f02cd..00000000000 --- a/app/assets/javascripts/vue_shared/components/loading_icon.vue +++ /dev/null @@ -1,45 +0,0 @@ -<script> - export default { - props: { - label: { - type: String, - required: false, - default: 'Loading', - }, - - size: { - type: String, - required: false, - default: '1', - }, - - inline: { - type: Boolean, - required: false, - default: false, - }, - }, - - computed: { - rootElementType() { - return this.inline ? 'span' : 'div'; - }, - cssClass() { - return `fa-${this.size}x`; - }, - }, - }; -</script> -<template> - <component - :is="rootElementType" - class="loading-container text-center"> - <i - :class="cssClass" - :aria-label="label" - class="fa fa-spin fa-spinner" - aria-hidden="true" - > - </i> - </component> -</template> diff --git a/app/assets/javascripts/vue_shared/components/pagination_links.vue b/app/assets/javascripts/vue_shared/components/pagination_links.vue new file mode 100644 index 00000000000..1f2a679c145 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/pagination_links.vue @@ -0,0 +1,34 @@ +<script> +import { s__ } from '../../locale'; + +export default { + props: { + change: { + type: Function, + required: true, + }, + pageInfo: { + type: Object, + required: true, + }, + }, + firstText: s__('Pagination|« First'), + prevText: s__('Pagination|Prev'), + nextText: s__('Pagination|Next'), + lastText: s__('Pagination|Last »'), +}; +</script> + +<template> + <gl-pagination + v-bind="$attrs" + :change="change" + :page="pageInfo.page" + :per-page="pageInfo.perPage" + :total-items="pageInfo.total" + :first-text="$options.firstText" + :prev-text="$options.prevText" + :next-text="$options.nextText" + :last-text="$options.lastText" + /> +</template> 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 74998a4787d..9d757b27edc 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue @@ -1,6 +1,5 @@ <script> import datePicker from '../pikaday.vue'; - import loadingIcon from '../loading_icon.vue'; import toggleSidebar from './toggle_sidebar.vue'; import collapsedCalendarIcon from './collapsed_calendar_icon.vue'; import { dateInWords } from '../../../lib/utils/datetime_utility'; @@ -10,7 +9,6 @@ components: { datePicker, toggleSidebar, - loadingIcon, collapsedCalendarIcon, }, props: { @@ -112,7 +110,7 @@ /> <div class="title"> {{ label }} - <loading-icon + <gl-loading-icon v-if="isLoading" :inline="true" /> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue index a3fc358130f..3df286de129 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue @@ -3,7 +3,6 @@ import $ from 'jquery'; import { __ } from '~/locale'; import LabelsSelect from '~/labels_select'; import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue'; -import LoadingIcon from '../../loading_icon.vue'; import DropdownTitle from './dropdown_title.vue'; import DropdownValue from './dropdown_value.vue'; @@ -16,7 +15,6 @@ import DropdownCreateLabel from './dropdown_create_label.vue'; export default { components: { - LoadingIcon, DropdownTitle, DropdownValue, DropdownValueCollapsed, @@ -164,7 +162,7 @@ dropdown-menu-labels dropdown-menu-selectable" <dropdown-search-input/> <div class="dropdown-content"></div> <div class="dropdown-loading"> - <loading-icon /> + <gl-loading-icon /> </div> <dropdown-footer v-if="showCreate" diff --git a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue index 78fde463507..cd3ee544344 100644 --- a/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue +++ b/app/assets/javascripts/vue_shared/components/stacked_progress_bar.vue @@ -99,8 +99,8 @@ export default { {{ __("Not available") }} </span> <span - v-tooltip v-if="successPercent" + v-tooltip :title="successTooltip" :style="successBarStyle" class="status-green" @@ -109,8 +109,8 @@ export default { {{ successPercent }}% </span> <span - v-tooltip v-if="neutralPercent" + v-tooltip :title="neutralTooltip" :style="neutralBarStyle" class="status-neutral" @@ -119,8 +119,8 @@ export default { {{ neutralPercent }}% </span> <span - v-tooltip v-if="failurePercent" + v-tooltip :title="failureTooltip" :style="failureBarStyle" class="status-red" diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue index a897300b62b..5b9c51786d6 100644 --- a/app/assets/javascripts/vue_shared/components/toggle_button.vue +++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue @@ -1,7 +1,6 @@ <script> import { s__ } from '../../locale'; import icon from './icon.vue'; - import loadingIcon from './loading_icon.vue'; const ICON_ON = 'status_success_borderless'; const ICON_OFF = 'status_failed_borderless'; @@ -11,7 +10,6 @@ export default { components: { icon, - loadingIcon, }, model: { @@ -78,7 +76,7 @@ class="project-feature-toggle" @click="toggleFeature" > - <loadingIcon class="loading-icon" /> + <gl-loading-icon class="loading-icon" /> <span class="toggle-icon"> <icon :name="toggleIcon" diff --git a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue index 125826da6c3..d5b58574123 100644 --- a/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue +++ b/app/assets/javascripts/vue_shared/components/tooltip_on_truncate.vue @@ -51,8 +51,8 @@ export default { <template> <span - v-tooltip v-if="showTooltip" + v-tooltip :title="title" :data-placement="placement" class="js-show-tooltip" diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue index 01c36fec41a..08e102e57c3 100644 --- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -94,8 +94,8 @@ export default { :tooltip-text="avatarTooltipText" :tooltip-placement="tooltipPlacement" /><span - v-tooltip v-if="shouldShowUsername" + v-tooltip :title="tooltipText" :tooltip-placement="tooltipPlacement" >{{ username }}</span> diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js index 73b9131e5ba..b9693892f45 100644 --- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js +++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js @@ -28,7 +28,7 @@ Vue.http.interceptors.push((request, next) => { response.headers.forEach((value, key) => { headers[key] = value; }); - + // eslint-disable-next-line no-param-reassign response.headers = headers; }); }); diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss index c91f5e279ea..af73954bd2e 100644 --- a/app/assets/stylesheets/bootstrap_migration.scss +++ b/app/assets/stylesheets/bootstrap_migration.scss @@ -93,7 +93,6 @@ hr { } .form-group.row .col-form-label { - padding-top: 0; // Bootstrap 4 aligns labels to the left // for horizontal forms @include media-breakpoint-up(md) { diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index b1a20c06910..4ffb3e9ab42 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -27,7 +27,6 @@ @import 'framework/header'; @import 'framework/highlight'; @import 'framework/issue_box'; -@import 'framework/jquery'; @import 'framework/lists'; @import 'framework/logo'; @import 'framework/markdown_area'; @@ -64,3 +63,4 @@ @import 'framework/ci_variable_list'; @import 'framework/feature_highlight'; @import 'framework/terms'; +@import 'framework/read_more'; diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 9dd0384a228..fcf282a7d7c 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -69,7 +69,7 @@ .identicon { text-align: center; vertical-align: top; - color: $identicon-fg-color; + color: $gl-gray-700; background-color: $gray-darker; // Sizes @@ -104,6 +104,7 @@ a { width: 100%; + height: 100%; display: flex; } diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss index a265e4206f1..702276780e9 100644 --- a/app/assets/stylesheets/framework/awards.scss +++ b/app/assets/stylesheets/framework/awards.scss @@ -229,8 +229,8 @@ svg { margin-bottom: 1px; - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; border-radius: 50%; path { diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 72b4ed0ac33..686ce0c63a4 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -147,17 +147,12 @@ } &.btn-success, - &.btn-new, - &.btn-create, - &.btn-save { + &.btn-register { @include btn-green; } &.btn-inverted { - &.btn-success, - &.btn-new, - &.btn-create, - &.btn-save { + &.btn-success { @include btn-outline($white-light, $green-600, $green-500, $green-500, $white-light, $green-600, $green-600, $green-700); } @@ -165,6 +160,10 @@ @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700); } + &.btn-warning { + @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700); + } + &.btn-primary, &.btn-info { @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700); @@ -172,8 +171,7 @@ } &.btn-info, - &.btn-primary, - &.btn-register { + &.btn-primary { @include btn-blue; } @@ -248,7 +246,7 @@ .btn-terminal { svg { height: 14px; - width: 18px; + width: $default-icon-size; } } @@ -365,7 +363,7 @@ } .clone-dropdown-btn a { - color: $dropdown-link-color; + color: $gl-gray-700; &:hover { text-decoration: none; diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 72e27f9ad16..28dda65091d 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -43,7 +43,7 @@ color: $brand-info; } -.hint { font-style: italic; color: $hint-color; } +.hint { font-style: italic; color: $gl-gray-400; } .light { color: $gl-text-color; } .slead { @@ -70,13 +70,6 @@ pre { padding: 0; } - &.card.card-body-pre { - border: 1px solid $gray-darker; - background: $gray-light; - border-radius: 0; - color: $well-pre-color; - } - &.wrap { word-break: break-word; white-space: pre-wrap; @@ -121,49 +114,24 @@ hr { text-decoration: none; } -.back-link { - font-size: 14px; -} - table { a code { position: relative; top: -2px; margin-right: 3px; } - - td.permission-x { - background: $table-permission-x-bg !important; - text-align: center; - } } .loading { margin: 20px auto; height: 40px; - color: $loading-color; + color: $gl-gray-700; font-size: 32px; text-align: center; } -span.update-author { - display: block; - color: $update-author-color; - font-weight: $gl-font-weight-normal; - font-style: italic; - - strong { - font-weight: $gl-font-weight-bold; - font-style: normal; - } -} - -.field_with_errors { - display: inline; -} - p.time { - color: $time-color; + color: $gl-gray-400; font-size: 90%; margin: 30px 3px 3px 2px; } @@ -197,40 +165,11 @@ li.note { background-color: inherit; } -.project_member_show { - td:first-child { - color: $project-member-show-color; - } -} - -.rss-icon { - img { - width: 24px; - vertical-align: top; - } - - strong { - line-height: 24px; - } -} - .show-suppressed-diff, .show-all-commits { cursor: pointer; } -.git_error_tips { - @extend .col-lg-6; - text-align: left; - margin-top: 40px; - - pre { - background: $white-light; - border: 0; - font-size: 12px; - } -} - .error-message { padding: 10px; background: $red-400; @@ -258,7 +197,7 @@ li.note { .gitlab-promo { a { - color: $gl-promo-color; + color: $gl-gray-350; margin-right: 30px; } } @@ -271,19 +210,6 @@ li.note { } } -.control-group { - .controls { - span { - &.descr { - position: relative; - top: 2px; - left: 5px; - color: $control-group-descr-color; - } - } - } -} - img.emoji { height: 20px; vertical-align: top; @@ -302,12 +228,6 @@ img.emoji { margin-bottom: 10px; } -.side-filters { - fieldset { - margin-bottom: 15px; - } -} - .footer-links { margin-bottom: 20px; @@ -329,25 +249,6 @@ img.emoji { text-align: center; } -.header-with-avatar { - h3 { - margin: 0; - font-weight: $gl-font-weight-bold; - } - - .username { - font-size: 18px; - color: $username-color; - margin-top: 8px; - } - - .description { - font-size: $gl-font-size; - color: $description-color; - margin-top: 8px; - } -} - .dropzone .dz-preview .dz-progress { border-color: $border-color !important; @@ -386,16 +287,6 @@ img.emoji { } } -.content-separator { - margin-left: -$gl-padding; - margin-right: -$gl-padding; - border-top: 1px solid $border-color; -} - -.hide-bottom-border { - border-bottom: 0 !important; -} - .gl-accessibility { &:focus { display: flex; diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 8a224dc517e..8603714f709 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -607,25 +607,25 @@ width: 100%; min-height: 30px; padding: 0 7px; - color: $dropdown-input-color; + color: $gl-gray-700; line-height: 30px; border: 1px solid $dropdown-divider-color; border-radius: 2px; outline: 0; &:focus { - color: $dropdown-link-color; + color: $gl-gray-700; border-color: $blue-300; box-shadow: 0 0 4px $dropdown-input-focus-shadow; ~ .fa { - color: $dropdown-link-color; + color: $gl-gray-700; } } &:hover { ~ .fa { - color: $dropdown-link-color; + color: $gl-gray-700; } } } @@ -890,7 +890,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { position: absolute; top: 13px; right: 25px; - color: $md-area-border; + color: $gray-100; } } @@ -929,7 +929,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu { &:hover { .frequent-items-item-avatar-container .avatar { - border-color: $md-area-border; + border-color: $gray-100; } } diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss index 6c50ea719d3..be85e03430e 100644 --- a/app/assets/stylesheets/framework/emojis.scss +++ b/app/assets/stylesheets/framework/emojis.scss @@ -6,3 +6,13 @@ gl-emoji { font-size: 1.4em; line-height: 1em; } + +.user-status-emoji { + margin-right: $gl-padding-4; + + gl-emoji { + font-size: 1em; + line-height: 16px; + vertical-align: baseline; + } +} diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss index 1d3512bbb4c..53f198b47c6 100644 --- a/app/assets/stylesheets/framework/files.scss +++ b/app/assets/stylesheets/framework/files.scss @@ -184,7 +184,7 @@ &.line-numbers { float: none; - border-left: 1px solid $blame-line-numbers-border; + border-left: 1px solid $gl-gray-100; i { float: none; diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss index abfe350677e..d5693a5d1a1 100644 --- a/app/assets/stylesheets/framework/filters.scss +++ b/app/assets/stylesheets/framework/filters.scss @@ -92,8 +92,8 @@ display: -webkit-flex; display: flex; flex-shrink: 0; - margin-top: 5px; - margin-bottom: 5px; + margin-top: 4px; + margin-bottom: 4px; .selectable { display: -webkit-flex; @@ -216,8 +216,8 @@ vertical-align: inherit; img { - height: 18px; - width: 18px; + height: $default-icon-size; + width: $default-icon-size; } } @@ -389,9 +389,8 @@ .btn { text-overflow: ellipsis; - .fa { - width: 15px; - line-height: $line-height-base; + svg { + margin-right: $gl-padding-8; } .dropdown-label-box { diff --git a/app/assets/stylesheets/framework/jquery.scss b/app/assets/stylesheets/framework/jquery.scss deleted file mode 100644 index d1360a0c0eb..00000000000 --- a/app/assets/stylesheets/framework/jquery.scss +++ /dev/null @@ -1,15 +0,0 @@ -.ui-widget { - font-family: $regular-font; - font-size: $font-size-base; - - .ui-state-default { - border: 1px solid $white-light; - background: $white-light; - color: $jq-ui-default-color; - } - - .ui-state-highlight { - border: 0; - background: transparent; - } -} diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss index d4bae4cb137..9218df9b40f 100644 --- a/app/assets/stylesheets/framework/layout.scss +++ b/app/assets/stylesheets/framework/layout.scss @@ -69,10 +69,14 @@ body { float: right; } - /* Center alert text and alert action links on smaller screens */ - @include media-breakpoint-down(sm) { - .alert { - text-align: center; + .flex-alert { + @include media-breakpoint-up(lg) { + display: flex; + + .alert-message { + flex: 1; + padding-right: 40px; + } } .alert-link-group { @@ -80,6 +84,13 @@ body { } } + @include media-breakpoint-down(sm) { + .alert-link-group { + float: none; + margin-top: $gl-padding-8; + } + } + /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ .alert-warning { transition: background-color 0.15s, border-color 0.15s; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index fdc0454d837..d9d4a210f5f 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -111,6 +111,7 @@ ul.content-list { border-color: $white-normal; font-size: $gl-font-size; color: $gl-text-color; + word-break: break-word; &.no-description { .title { diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index d8391b59a8c..554e2b6720a 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -122,7 +122,7 @@ .markdown-area { border-radius: 0; background: $white-light; - border: 1px solid $md-area-border; + border: 1px solid $gray-100; min-height: 140px; max-height: 500px; padding: 5px; diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 7edb89ce6f3..7f37dd3de91 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -20,7 +20,7 @@ display: inline-block; overflow-x: auto; border: 0; - border-color: $md-area-border; + border-color: $gray-100; @supports (width: fit-content) { display: block; @@ -29,11 +29,11 @@ tr { th { - border-bottom: solid 2px $md-area-border; + border-bottom: solid 2px $gray-100; } td { - border-color: $md-area-border; + border-color: $gray-100; } } } diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 033e5e57177..6d20c46b99d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -44,12 +44,8 @@ .project-repo-buttons { display: block; - .count-buttons .btn { - margin: 0 10px; - } - - .count-buttons .count-with-arrow { - display: none; + .count-buttons .count-badge { + margin-top: $gl-padding-8; } } } diff --git a/app/assets/stylesheets/framework/read_more.scss b/app/assets/stylesheets/framework/read_more.scss new file mode 100644 index 00000000000..b84b6e0b256 --- /dev/null +++ b/app/assets/stylesheets/framework/read_more.scss @@ -0,0 +1,13 @@ +.read-more-container { + @include media-breakpoint-down(md) { + &:not(.is-expanded) { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + > * { + display: inline; + } + } + } +} diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss index 764bebd82c6..fc185ccfaad 100644 --- a/app/assets/stylesheets/framework/responsive_tables.scss +++ b/app/assets/stylesheets/framework/responsive_tables.scss @@ -39,7 +39,7 @@ .table-section { white-space: nowrap; - $section-widths: 10 15 20 25 30 40 50 100; + $section-widths: 5 10 15 20 25 30 40 50 100; @each $width in $section-widths { &.section-#{$width} { flex: 0 0 #{$width + '%'}; diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index 3ae2c7078d6..381c0290d32 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -237,7 +237,7 @@ } .group-path { - color: $group-path-color; + color: $gl-gray-400; } } @@ -257,7 +257,7 @@ .namespace-result { .namespace-kind { - color: $namespace-kind-color; + color: $gl-gray-350; font-weight: $gl-font-weight-normal; } diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss index 7152ef9bcfd..36ab38f1c9d 100644 --- a/app/assets/stylesheets/framework/snippets.scss +++ b/app/assets/stylesheets/framework/snippets.scss @@ -45,7 +45,7 @@ } } -.snippet-scope-menu .btn-new { +.snippet-scope-menu .btn-success { margin-top: 15px; } diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss index 20394cc1e52..8258da07e4d 100644 --- a/app/assets/stylesheets/framework/toggle.scss +++ b/app/assets/stylesheets/framework/toggle.scss @@ -31,7 +31,7 @@ height: 24px; cursor: pointer; user-select: none; - background: $feature-toggle-color-disabled; + background: $gl-gray-400; border-radius: 12px; padding: 3px; transition: all .4s ease; @@ -56,12 +56,12 @@ &, .toggle-icon-svg { - width: 18px; - height: 18px; + width: $default-icon-size; + height: $default-icon-size; } .toggle-icon-svg { - fill: $feature-toggle-color-disabled; + fill: $gl-gray-400; } .toggle-status-checked { diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 9929f1bdebf..0c1b8b92de3 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -61,12 +61,12 @@ padding: 3px 5px; font-size: 11px; line-height: 10px; - color: $kdb-color; + color: $gl-gray-700; vertical-align: middle; background-color: $kdb-bg; border-width: 1px; border-style: solid; - border-color: $kdb-border $kdb-border $kdb-border-bottom; + border-color: $gl-gray-200 $gl-gray-200 $kdb-border-bottom; border-image: none; border-radius: 3px; box-shadow: 0 -1px 0 $kdb-shadow inset; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d76f5cbd9ff..f66782ab882 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -31,6 +31,14 @@ $gray-dark: darken($gray-light, $darken-dark-factor); $gray-darker: #eee; $gray-darkest: #c4c4c4; +$gl-gray-100: #dddddd; +$gl-gray-200: #cccccc; +$gl-gray-350: #aaaaaa; +$gl-gray-400: #999999; +$gl-gray-500: #777777; +$gl-gray-600: #666666; +$gl-gray-700: #555555; + $green-50: #f1fdf6; $green-100: #dcf5e7; $green-200: #b3e6c8; @@ -207,11 +215,6 @@ $list-border: rgba(0, 0, 0, 0.05); $list-text-height: 42px; /* - * Markdown - */ -$md-area-border: #ddd; - -/* * Code */ $code-font-size: 90%; @@ -241,7 +244,6 @@ $input-horizontal-padding: 12px; /* * Misc */ -$progress-color: #c0392b; $header-height: 40px; $ide-statusbar-height: 25px; $fixed-layout-width: 1280px; @@ -250,20 +252,13 @@ $container-text-max-width: 540px; $gl-avatar-size: 40px; $border-radius-default: 4px; $border-radius-small: 2px; -$settings-icon-size: 18px; +$default-icon-size: 18px; $layout-link-gray: #7e7c7c; $btn-side-margin: 10px; $btn-sm-side-margin: 7px; $btn-margin-5: 5px; $sidebar-block-hover-color: #ebebeb; -$group-path-color: #999; -$namespace-kind-color: #aaa; -$panel-heading-link-color: #777; -$graph-author-email-color: #777; $count-arrow-border: #dce0e5; -$save-project-loader-color: #555; -$divergence-graph-bar-bg: #ccc; -$divergence-graph-separator-bg: #ccc; $general-hover-transition-duration: 100ms; $general-hover-transition-curve: linear; $highlight-changes-color: rgb(235, 255, 232); @@ -271,24 +266,13 @@ $performance-bar-height: 35px; $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; +$project-title-row-height: 24px; /* * Common component specific colors */ -$hint-color: #999; -$well-pre-color: #555; -$loading-color: #555; -$update-author-color: #999; $user-mention-bg: rgba($blue-500, 0.044); $user-mention-bg-hover: rgba($blue-500, 0.15); -$time-color: #999; -$project-member-show-color: #aaa; -$gl-promo-color: #aaa; -$control-group-descr-color: #666; -$table-permission-x-bg: #d9edf7; -$username-color: #666; -$description-color: #666; -$profiler-border: #eee; /* tanuki logo colors */ $tanuki-red: #e24329; @@ -319,9 +303,7 @@ $line-select-yellow: #fcf8e7; $line-select-yellow-dark: #f0e2bd; $dark-diff-match-bg: rgba(255, 255, 255, 0.3); $dark-diff-match-color: rgba(255, 255, 255, 0.1); -$file-mode-changed: #777; $diff-image-info-color: gray; -$diff-swipe-border: #999; $diff-view-modes-color: gray; $diff-view-modes-border: #c1c1c1; $diff-jagged-border-gradient-color: darken($white-normal, 8%); @@ -341,12 +323,10 @@ $dropdown-width: 300px; $dropdown-min-height: 40px; $dropdown-max-height: 312px; $dropdown-vertical-offset: 4px; -$dropdown-link-color: #555; $dropdown-empty-row-bg: rgba(#000, 0.04); $dropdown-shadow-color: rgba(#000, 0.1); $dropdown-divider-color: rgba(#000, 0.1); $dropdown-title-btn-color: #bfbfbf; -$dropdown-input-color: #555; $dropdown-input-fa-color: #c7c7c7; $dropdown-input-focus-shadow: rgba($blue-300, 0.4); $dropdown-loading-bg: rgba(#fff, 0.6); @@ -419,15 +399,9 @@ $location-icon-color: #e7e9ed; $note-disabled-comment-color: #b2b2b2; $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; -$note-line2-border: #ddd; $note-icon-gutter-width: 55px; /* -* Zen -*/ -$zen-control-color: #555; - -/* * Identicon */ $identicon-red: #ffebee; @@ -436,7 +410,6 @@ $identicon-indigo: #e8eaf6; $identicon-blue: #e3f2fd; $identicon-teal: #e0f2f1; $identicon-orange: #fbe9e7; -$identicon-fg-color: #555555; /* * Calendar @@ -505,16 +478,8 @@ $common-gray-light: #bbb; $common-gray-dark: #444; /* -* Events -*/ -$events-pre-color: #777; -$events-note-icon-color: #777; -$events-body-border: #ddd; - -/* * Files */ -$blame-line-numbers-border: #ddd; $logs-li-color: #888; $logs-p-color: #333; @@ -533,8 +498,6 @@ $input-short-md-width: 280px; * Help */ $document-index-color: #888; -$help-shortcut-color: #999; -$help-shortcut-mapping-color: #555; $help-shortcut-header-color: #333; /* @@ -545,12 +508,6 @@ $issues-today-border: #e1e8d5; $compare-display-color: #888; /* -* jQuery UI -*/ -$jq-ui-border: #ddd; -$jq-ui-default-color: #777; - -/* * Label */ $label-font-size: 12px; @@ -574,34 +531,19 @@ $fade-mask-transition-curve: ease-in-out; $login-brand-holder-color: #888; /* -* Nav -*/ -$nav-link-gray: #959494; -$nav-toggle-gray: #666; - -/* -* Notify -*/ -$notify-details: #777; -$notify-footer: #777; - -/* * Projects */ $project-option-descr-color: #54565b; -$project-breadcrumb-color: #999; $project-network-controls-color: #888; $feature-toggle-color: #fff; $feature-toggle-text-color: #fff; -$feature-toggle-color-disabled: #999; $feature-toggle-color-enabled: #4a8bee; /* Stat Graph */ $stat-graph-common-bg: #f3f3f3; -$stat-graph-axis-fill: #aaa; $stat-graph-selection-fill: #333; $stat-graph-selection-stroke: #333; @@ -612,17 +554,9 @@ $select2-drop-shadow1: rgba(76, 86, 103, 0.247059); $select2-drop-shadow2: rgba(31, 37, 50, 0.317647); /* -* Todo -*/ -$todo-body-pre-color: #777; -$todo-body-border: #ddd; - -/* * Typography */ $kdb-bg: #fcfcfc; -$kdb-color: #555; -$kdb-border: #ccc; $kdb-border-bottom: #bbb; $kdb-shadow: #bbb; $body-text-shadow: rgba(255, 255, 255, 0.01); @@ -631,7 +565,6 @@ $body-text-shadow: rgba(255, 255, 255, 0.01); * UI Dev Kit */ $ui-dev-kit-example-color: #bbb; -$ui-dev-kit-example-border: #ddd; /* Pipeline Graph @@ -665,12 +598,10 @@ $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); /* Performance Bar */ -$perf-bar-text: #999; $perf-bar-production: #222; $perf-bar-staging: #291430; $perf-bar-development: #4c1210; $perf-bar-bucket-bg: #111; -$perf-bar-bucket-color: #ccc; $perf-bar-bucket-box-shadow-from: rgba($white-light, 0.2); $perf-bar-bucket-box-shadow-to: rgba($black, 0.25); diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss index f2d296fb875..a4fbd9c073f 100644 --- a/app/assets/stylesheets/framework/zen.scss +++ b/app/assets/stylesheets/framework/zen.scss @@ -35,7 +35,7 @@ .zen-control { padding: 0; - color: $zen-control-color; + color: $gl-gray-700; background: none; border: 0; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index a81e5eb5ebf..f24c80bd81c 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -7,12 +7,12 @@ img { p.details { font-style: italic; - color: $notify-details; + color: $gl-gray-500; } .footer > p { font-size: small; - color: $notify-footer; + color: $gl-gray-500; } pre.commit-message { diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss index 5ff4e487d04..65f0a0d18e2 100644 --- a/app/assets/stylesheets/page_bundles/ide.scss +++ b/app/assets/stylesheets/page_bundles/ide.scss @@ -7,6 +7,8 @@ $ide-context-header-padding: 10px; $ide-project-avatar-end: $ide-context-header-padding + 48px; $ide-tree-padding: $gl-padding; $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; +$ide-commit-row-height: 32px; +$ide-commit-header-height: 48px; .project-refs-form, .project-refs-target-form { @@ -51,83 +53,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; flex: 1; min-height: 0; // firefox fix - .file { - height: 32px; - cursor: pointer; - - &.file-active { - background: $theme-gray-100; - } - - .ide-file-name { - flex: 1; - white-space: nowrap; - text-overflow: ellipsis; - max-width: inherit; - line-height: 16px; - display: inline-block; - height: 18px; - - svg { - vertical-align: middle; - margin-right: 2px; - } - - .loading-container { - margin-right: 4px; - display: inline-block; - } - } - - .ide-file-icon-holder { - display: flex; - align-items: center; - color: $theme-gray-700; - } - - .ide-file-changed-icon { - margin-left: auto; - - > svg { - display: block; - } - } - - .ide-new-btn { - display: none; - - .btn { - padding: 2px 5px; - } - } - - &:hover, - &:focus { - .ide-new-btn { - display: block; - } - } - - .folder-icon { - fill: $gl-text-color-secondary; - } - } - a { color: $gl-text-color; } - - th { - position: sticky; - top: 0; - } -} - -.file-name { - display: flex; - overflow: visible; - align-items: center; - width: 100%; } .multi-file-loading-container { @@ -567,24 +495,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .multi-file-commit-panel-header { - display: flex; - align-items: center; - margin-bottom: 0; + height: $ide-commit-header-height; border-bottom: 1px solid $white-dark; padding: 12px 0; } -.multi-file-commit-panel-header-title { - display: flex; - flex: 1; - align-items: center; - - svg { - margin-right: $gl-btn-padding; - color: $theme-gray-700; - } -} - .multi-file-commit-panel-collapse-btn { border-left: 1px solid $white-dark; margin-left: auto; @@ -594,8 +509,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; flex: 1; overflow: auto; padding: $grid-size 0; - margin-left: -$grid-size; - margin-right: -$grid-size; min-height: 60px; &.form-text.text-muted { @@ -638,8 +551,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } } -.multi-file-commit-list-path, -.ide-file-list .file { +.multi-file-commit-list-path { display: flex; align-items: center; margin-left: -$grid-size; @@ -647,29 +559,31 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; padding: $grid-size / 2 $grid-size; border-radius: $border-radius-default; text-align: left; + cursor: pointer; + height: $ide-commit-row-height; + padding-right: 0; &:hover, &:focus { background: $theme-gray-100; + + outline: 0; + + .multi-file-discard-btn { + > .btn { + display: flex; + } + } } &:active { background: $theme-gray-200; } -} - -.multi-file-commit-list-path { - cursor: pointer; &.is-active { background-color: $white-normal; } - &:hover, - &:focus { - outline: 0; - } - svg { min-width: 16px; vertical-align: middle; @@ -679,6 +593,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; .multi-file-commit-list-file-path { @include str-truncated(calc(100% - 30px)); + user-select: none; &:active { text-decoration: none; @@ -686,9 +601,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .multi-file-discard-btn { - top: 4px; - right: 8px; - bottom: 4px; + > .btn { + display: none; + width: $ide-commit-row-height; + height: $ide-commit-row-height; + } svg { top: 0; @@ -807,10 +724,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } .ide-staged-action-btn { - width: 22px; - margin-left: -1px; - border-top-left-radius: 0; - border-bottom-left-radius: 0; + width: $ide-commit-row-height; + height: $ide-commit-row-height; + color: inherit; > svg { top: 0; @@ -1401,9 +1317,17 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } } -.ide-new-btn .dropdown.show .ide-entry-dropdown-toggle { - color: $white-normal; - background-color: $blue-500; +.ide-new-btn { + display: none; + + .btn { + padding: 2px 5px; + } + + .dropdown.show .ide-entry-dropdown-toggle { + color: $white-normal; + background-color: $blue-500; + } } .ide-preview-header { @@ -1442,3 +1366,54 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; top: 50%; transform: translateY(-50%); } + +.ide-file-templates { + padding: $grid-size $gl-padding; + background-color: $gray-light; + border-bottom: 1px solid $white-dark; + + .dropdown { + min-width: 180px; + } + + .dropdown-content { + max-height: 222px; + } +} + +.ide-commit-editor-header { + height: 65px; + padding: 8px 16px; + background-color: $theme-gray-50; + box-shadow: inset 0 -1px $white-dark; +} + +.ide-commit-list-changed-icon { + width: $ide-commit-row-height; + height: $ide-commit-row-height; +} + +.ide-file-icon-holder { + display: flex; + align-items: center; + color: $theme-gray-700; +} + +.ide-file-changed-icon { + margin-left: auto; + + > svg { + display: block; + } +} + +.file-row:hover, +.file-row:focus { + .ide-new-btn { + display: block; + } + + .folder-icon { + fill: $gl-text-color-secondary; + } +} diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/page_bundles/xterm.scss index 7d40c61da26..7f040ac9b96 100644 --- a/app/assets/stylesheets/pages/xterm.scss +++ b/app/assets/stylesheets/page_bundles/xterm.scss @@ -1,3 +1,5 @@ +@import 'framework/variables'; + .build-page { // color codes are based on http://en.wikipedia.org/wiki/File:Xterm_256color_chart.svg // see also: https://gist.github.com/jasonm23/2868981 diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss index 6c555aee20a..f0acb78f731 100644 --- a/app/assets/stylesheets/pages/admin.scss +++ b/app/assets/stylesheets/pages/admin.scss @@ -4,3 +4,7 @@ padding-bottom: 46px; } } + +.usage-data { + max-height: 400px; +} diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss index 49fe50977f5..38fec3f0aa8 100644 --- a/app/assets/stylesheets/pages/branches.scss +++ b/app/assets/stylesheets/pages/branches.scss @@ -23,7 +23,7 @@ .bar { position: absolute; height: 4px; - background-color: $divergence-graph-bar-bg; + background-color: $gl-gray-200; } .bar-behind { @@ -61,7 +61,7 @@ height: 18px; margin: 5px 0 0; float: left; - background-color: $divergence-graph-separator-bg; + background-color: $gl-gray-200; } } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 0f22fe21143..71a3fd544f2 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -4,9 +4,60 @@ } } -.cluster-applications-table { - // Wait for the Vue to kick-in and render the applications block - min-height: 628px; +.cluster-application-row { + background: $gray-lighter; + + &.cluster-application-installed { + background: none; + } + + .settings-message { + padding: $gl-vert-padding $gl-padding-8; + } +} + +@media (min-width: map-get($grid-breakpoints, md)) { + .cluster-application-list { + border: 1px solid $border-color; + border-radius: $border-radius-default; + } + + .cluster-application-row { + border-bottom: 1px solid $border-color; + padding: $gl-padding; + } +} + +.cluster-application-logo { + border: 3px solid $white-light; + box-shadow: 0 0 0 1px $gray-normal; + + &.avatar:hover { + border-color: $white-light; + } +} + +.cluster-application-warning { + font-weight: bold; + text-align: center; + padding: $gl-padding; + border-bottom: 1px solid $white-normal; + + .svg-container { + display: inline-block; + vertical-align: middle; + margin-right: $gl-padding-8; + width: 40px; + height: 40px; + } +} + +.cluster-application-description { + flex: 1; +} + +.cluster-application-disabled { + opacity: 0.5; } .clusters-dropdown-menu { diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7d7143631f2..987dcd32e3a 100644 --- a/app/assets/stylesheets/pages/diff.scss +++ b/app/assets/stylesheets/pages/diff.scss @@ -31,7 +31,7 @@ .file-mode-changed { padding: 10px; - color: $file-mode-changed; + color: $gl-gray-500; } .suppressed-container { @@ -72,6 +72,7 @@ .line_holder td { line-height: $code-line-height; font-size: $code-font-size; + vertical-align: top; &.noteable_line { position: relative; @@ -244,7 +245,7 @@ .swipe-wrap { overflow: hidden; - border-left: 1px solid $diff-swipe-border; + border-left: 1px solid $gl-gray-400; position: absolute; display: block; top: 13px; @@ -749,6 +750,10 @@ left: $gl-padding; } + .dropdown-input .dropdown-input-search { + pointer-events: all; + } + .diff-changed-file { display: flex; padding-top: 8px; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 196f6ae6d8c..79984c1a546 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -153,7 +153,7 @@ .x-axis path, .y-axis path { - stroke: $stat-graph-axis-fill; + stroke: $gl-gray-350; } .label-x-axis-line, @@ -163,7 +163,7 @@ .y-axis { line { - stroke: $stat-graph-axis-fill; + stroke: $gl-gray-350; stroke-width: 1; } } diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index da0c9b44498..a91d44805ee 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -87,7 +87,7 @@ border: 0; background: $gray-light; border-radius: 0; - color: $events-pre-color; + color: $gl-gray-500; overflow: hidden; } @@ -104,7 +104,7 @@ } .event-note-icon { - color: $events-pre-color; + color: $gl-gray-500; float: left; font-size: $gl-font-size; line-height: 16px; diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss index 22fce893fd7..4fb1a956fab 100644 --- a/app/assets/stylesheets/pages/graph.scss +++ b/app/assets/stylesheets/pages/graph.scss @@ -20,7 +20,7 @@ .graphs { .graph-author-email { float: right; - color: $graph-author-email-color; + color: $gl-gray-500; } .graph-additions { @@ -58,7 +58,7 @@ .y-axis-label { line { - stroke: $stat-graph-axis-fill; + stroke: $gl-gray-350; } text { diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 60b4d39bb1a..fe792a53b44 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -3,7 +3,6 @@ } .dashboard .side .card .card-header .input-group { - .form-control { height: 42px; } @@ -30,14 +29,15 @@ } } +.group-nav-container .group-search, .group-nav-container .nav-controls { display: flex; align-items: flex-start; - padding: $gl-padding-top 0; - border-bottom: 1px solid $border-color; + padding: $gl-padding-top 0 0; .group-filter-form { - flex: 1; + flex: 1 1 auto; + margin-right: $gl-padding-8; } .dropdown-menu-right { @@ -106,7 +106,7 @@ &, .dropdown, .dropdown .dropdown-toggle, - .btn-new { + .btn-success { display: block; } @@ -118,7 +118,7 @@ .group-filter-form, .dropdown .dropdown-toggle, - .btn-new { + .btn-success { width: 100%; } @@ -136,6 +136,10 @@ flex: 1; } + .dropdown-toggle { + width: auto; + } + .dropdown-menu { width: 100%; max-width: inherit; @@ -145,38 +149,14 @@ } } -.groups-empty-state { - padding: 50px 100px; - overflow: hidden; - - @include media-breakpoint-down(sm) { - padding: 50px 0; - } - - svg { - float: right; - - @include media-breakpoint-down(sm) { - float: none; - display: block; - width: 250px; - position: relative; - left: 50%; - margin-left: -125px; - } - } - - .text-content { - float: left; - width: 460px; - margin-top: 120px; +.group-nav-container .group-search { + padding: $gl-padding 0; + border-bottom: 1px solid $border-color; +} - @include media-breakpoint-down(sm) { - float: none; - margin-top: 60px; - width: auto; - text-align: center; - } +.groups-listing { + .group-list-tree .group-row:first-child { + border-top: 0; } } @@ -278,12 +258,12 @@ } &::after { - content: ""; + content: ''; position: absolute; height: 100%; width: 100%; background-color: transparent; - border: 2px outset $kdb-border; + border: 2px outset $gl-gray-200; border-radius: 50%; animation: spin-avatar 3s infinite linear; } @@ -346,7 +326,7 @@ position: relative; &::before { - content: ""; + content: ''; display: block; width: 10px; height: 0; diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss index 0350fe5752e..2c23f31c240 100644 --- a/app/assets/stylesheets/pages/help.scss +++ b/app/assets/stylesheets/pages/help.scss @@ -1,6 +1,6 @@ .shortcut-mappings { font-size: 12px; - color: $help-shortcut-mapping-color; + color: $gl-gray-700; tbody:first-child tr:first-child { padding-top: 0; @@ -22,7 +22,7 @@ .shortcut { padding-right: 10px; - color: $help-shortcut-color; + color: $gl-gray-400; text-align: right; white-space: nowrap; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index 9ac47a771a5..62a9f97caa9 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -701,6 +701,11 @@ align-self: center; overflow: hidden; text-overflow: ellipsis; + + .user-status-emoji { + margin-left: $gl-padding-4; + margin-right: 0; + } } .js-issuable-selector-wrap { @@ -721,13 +726,13 @@ display: flex; } - .issue-info-container { + .issuable-info-container { -webkit-flex: 1; flex: 1; display: flex; padding-right: $gl-padding; - .issue-main-info { + .issuable-main-info { flex: 1 auto; margin-right: 10px; } @@ -763,7 +768,7 @@ margin-bottom: 10px; min-width: 15px; - .selected_issue { + .selected-issuable { vertical-align: text-top; } } @@ -795,7 +800,7 @@ } .issuable-list li, -.issue-info-container .controls { +.issuable-info-container .controls { .avatar-counter { display: inline-block; vertical-align: middle; diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss index c9e5fb9c579..fa0ab1a3bae 100644 --- a/app/assets/stylesheets/pages/login.scss +++ b/app/assets/stylesheets/pages/login.scss @@ -100,6 +100,22 @@ p { margin: 0; } + + .omniauth-btn { + margin-bottom: $gl-padding; + width: 48%; + padding: $gl-padding-8; + + @include media-breakpoint-down(md) { + width: 100%; + } + + img { + width: $default-icon-size; + height: $default-icon-size; + margin-right: $gl-padding; + } + } } .new-session-tabs { @@ -169,10 +185,6 @@ } } - label { - font-weight: $gl-font-weight-normal; - } - .submit-container { margin-top: 16px; } @@ -200,15 +212,6 @@ } } -.oauth-image-link { - margin-right: 10px; - - img { - width: 32px; - height: 32px; - } -} - .devise-layout-html { margin: 0; padding: 0; diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss index 5fdb2b4a90a..99609a96976 100644 --- a/app/assets/stylesheets/pages/members.scss +++ b/app/assets/stylesheets/pages/members.scss @@ -4,7 +4,7 @@ } .users-project-form { - .btn-create { + .btn-success { margin-right: 10px; } } diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 7b8cad254c7..97b131687d3 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -460,7 +460,7 @@ display: -webkit-flex; display: flex; - .issue-info-container { + .issuable-info-container { -webkit-flex: 1; flex: 1; } @@ -910,7 +910,7 @@ opacity: .65; &:hover { - color: $file-mode-changed; + color: $gl-gray-500; text-decoration: none; } } diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss index ac7b701c2e2..4268e194ed7 100644 --- a/app/assets/stylesheets/pages/note_form.scss +++ b/app/assets/stylesheets/pages/note_form.scss @@ -2,7 +2,7 @@ * Note Form */ .comment-btn { - @extend .btn-create; + @extend .btn-success; } .diff-file .diff-content { diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index dbe9f0c03fb..c9e0899425f 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -94,8 +94,8 @@ ul.notes { opacity: 0.5; .dummy-avatar { - background-color: $kdb-border; - border: 1px solid darken($kdb-border, 25%); + background-color: $gl-gray-200; + border: 1px solid darken($gl-gray-200, 25%); } .note-headline-light, @@ -334,20 +334,6 @@ ul.notes { border: 1px solid $white-normal; border-left: 0; - &.notes_line { - vertical-align: middle; - text-align: center; - padding: 10px 0; - background: $gray-light; - color: $text-color; - } - - &.notes_line2 { - text-align: center; - padding: 10px 0; - border-left: 1px solid $note-line2-border !important; - } - &.notes_content { background-color: $gray-light; border-width: 1px 0; diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 17f34319050..caa839c32a5 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -279,6 +279,10 @@ table.u2f-registrations { } } +.codes { + padding-top: 14px; +} + .oauth-application-show { .scope-name { font-weight: $gl-font-weight-bold; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a95e78931b1..7c42dcad959 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -115,7 +115,7 @@ .project-feature-controls { display: flex; align-items: center; - margin: 8px 0; + margin: $gl-padding-8 0; max-width: 432px; .toggle-wrapper { @@ -144,12 +144,8 @@ .group-home-panel { padding-top: 24px; padding-bottom: 24px; + border-bottom: 1px solid $border-color; - @include media-breakpoint-up(sm) { - border-bottom: 1px solid $border-color; - } - - .project-avatar, .group-avatar { float: none; margin: 0 auto; @@ -175,7 +171,6 @@ } } - .project-home-desc, .group-home-desc { margin-left: auto; margin-right: auto; @@ -199,6 +194,62 @@ } } +.project-home-panel { + padding-top: $gl-padding-8; + padding-bottom: $gl-padding-24; + + .project-title-row { + margin-right: $gl-padding-8; + } + + .project-avatar { + width: $project-title-row-height; + height: $project-title-row-height; + flex-shrink: 0; + flex-basis: $project-title-row-height; + margin: 0 $gl-padding-8 0 0; + } + + .project-title { + font-size: 20px; + line-height: $project-title-row-height; + font-weight: bold; + } + + .project-metadata { + font-weight: normal; + font-size: 14px; + line-height: $gl-btn-line-height; + color: $gl-text-color-secondary; + + .icon { + margin-right: $gl-padding-4; + font-size: 16px; + } + + .project-visibility, + .project-license, + .project-tag-list { + margin-right: $gl-padding-8; + } + + .project-license { + .btn { + line-height: 0; + border-width: 0; + } + } + + .project-tag-list, + .project-license { + .icon { + position: relative; + top: 2px; + } + } + } +} + .nav > .project-repo-buttons { margin-top: 0; } @@ -206,8 +257,6 @@ .project-repo-buttons, .group-buttons { .btn { - padding: 3px 10px; - &:last-child { margin-left: 0; } @@ -222,11 +271,15 @@ .fa-caret-down { margin-left: 3px; + + &.dropdown-btn-icon { + margin-left: 0; + } } } .project-action-button { - margin: 15px 5px 0; + margin: $gl-padding $gl-padding-8 0 0; vertical-align: top; } @@ -243,82 +296,45 @@ .count-buttons { display: inline-block; vertical-align: top; - margin-top: 15px; - } + margin-top: $gl-padding; - .project-clone-holder { - display: inline-block; - margin: 15px 5px 0 0; + .count-badge { + height: $input-height; - input { - height: 28px; + .icon { + top: -1px; + } } - } - .count-with-arrow { - display: inline-block; - position: relative; - margin-left: 4px; + .count-badge-count, + .count-badge-button { + border: 1px solid $border-color; + line-height: 1; + } - .arrow { - &::before { - content: ''; - display: inline-block; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 0; - margin-top: -6px; - border-width: 7px 5px 7px 0; - border-right-color: $count-arrow-border; - pointer-events: none; - } + .count, + .count-badge-button { + color: $gl-text-color; + } - &::after { - content: ''; - position: absolute; - width: 0; - height: 0; - border-color: transparent; - border-style: solid; - top: 50%; - left: 1px; - margin-top: -9px; - border-width: 10px 7px 10px 0; - border-right-color: $white-light; - pointer-events: none; - } + .count-badge-count { + padding: 0 12px; + border-right: 0; + border-radius: $border-radius-base 0 0 $border-radius-base; + background: $gray-light; } - .count { - @include btn-white; - display: inline-block; - background: $white-light; - border-radius: 2px; - border-width: 1px; - border-style: solid; - font-size: 13px; - font-weight: $gl-font-weight-bold; - line-height: 13px; - letter-spacing: 0.4px; - padding: 6px 14px; - text-align: center; - vertical-align: middle; - touch-action: manipulation; - background-image: none; - white-space: nowrap; - margin: 0 10px 0 4px; + .count-badge-button { + border-radius: 0 $border-radius-base $border-radius-base 0; + } + } - a { - color: inherit; - } + .project-clone-holder { + display: inline-block; + margin: $gl-padding $gl-padding-8 0 0; - &:hover { - background: $white-light; - } + input { + height: $input-height; } } @@ -333,6 +349,14 @@ min-width: 320px; } } + + .mobile-git-clone { + margin-top: $gl-padding-8; + + .dropdown-menu-inner-content { + @extend .monospace; + } + } } .split-one { @@ -347,7 +371,7 @@ .save-project-loader { margin-top: 50px; margin-bottom: 50px; - color: $save-project-loader-color; + color: $gl-gray-700; } .transfer-project .select2-container { @@ -423,7 +447,7 @@ > li + li::before { padding: 0 3px; - color: $project-breadcrumb-color; + color: $gl-gray-400; } a { @@ -511,7 +535,6 @@ .controls { margin-left: auto; } - } .choose-template { @@ -574,7 +597,7 @@ flex-wrap: wrap; .btn { - padding: 8px; + padding: $gl-padding-8; margin-right: 10px; } @@ -651,7 +674,7 @@ left: -10px; top: 50%; z-index: 10; - padding: 8px 0; + padding: $gl-padding-8 0; text-align: center; background-color: $white-light; color: $gl-text-color-tertiary; @@ -665,7 +688,7 @@ left: 50%; top: 0; transform: translateX(-50%); - padding: 0 8px; + padding: 0 $gl-padding-8; } } @@ -699,17 +722,51 @@ .project-stats { font-size: 0; text-align: center; - max-width: 100%; border-bottom: 1px solid $border-color; - .nav { - margin-top: $gl-padding-8; - margin-bottom: $gl-padding-8; + .scrolling-tabs-container { + .scrolling-tabs { + margin-top: $gl-padding-8; + margin-bottom: $gl-padding-8; + flex-wrap: wrap; + border-bottom: 0; + } + .fade-left, + .fade-right { + top: 0; + height: 100%; + + .fa { + top: 50%; + margin-top: -$gl-padding-8; + } + } + + .nav { + flex-basis: 100%; + + + .nav { + margin: $gl-padding-8 0; + } + } + + @include media-breakpoint-down(md) { + flex-direction: column; + + .nav { + flex-wrap: nowrap; + } + + .nav:first-child { + margin-right: $gl-padding-8; + } + } + } + + .nav { > li { display: inline-block; - margin-top: $gl-padding-4; - margin-bottom: $gl-padding-4; &:not(:last-child) { margin-right: $gl-padding; @@ -732,13 +789,17 @@ font-size: $gl-font-size; line-height: $gl-btn-line-height; color: $gl-text-color-secondary; + white-space: nowrap; } .stat-link { + border-bottom: 0; + &:hover, &:focus { color: $gl-text-color; text-decoration: underline; + border-bottom: 0; } } @@ -868,7 +929,7 @@ pre.light-well { } .git-clone-holder { - width: 380px; + width: 320px; .btn-clipboard { border: 1px solid $border-color; diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss index 77119aea9e2..04151b1cd59 100644 --- a/app/assets/stylesheets/pages/search.scss +++ b/app/assets/stylesheets/pages/search.scss @@ -218,7 +218,7 @@ input[type='checkbox']:hover { } .btn-search, - .btn-new { + .btn-success { width: 100%; margin-top: 5px; diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index e351dd7c0bb..dbf8692d69b 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -106,7 +106,7 @@ .settings-list-icon { color: $gl-text-color-secondary; - font-size: $settings-icon-size; + font-size: $default-icon-size; line-height: 42px; } @@ -249,7 +249,7 @@ } .loading-metrics .metrics-load-spinner { - color: $loading-color; + color: $gl-gray-700; } .metrics-list { diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 5d3b7b21ce4..3fc37e20c36 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -143,7 +143,7 @@ border: 0; background: $gray-light; border-radius: 0; - color: $todo-body-pre-color; + color: $gl-gray-500; margin: 0 20px; overflow: hidden; } @@ -205,7 +205,7 @@ .todo-body { margin: 0; - border-left: 2px solid $todo-body-border; + border-left: 2px solid $gl-gray-100; padding-left: 10px; } } diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss index 48ac5b21db8..84c617c7ec0 100644 --- a/app/assets/stylesheets/pages/ui_dev_kit.scss +++ b/app/assets/stylesheets/pages/ui_dev_kit.scss @@ -6,7 +6,7 @@ .example { padding: 15px; - border: 1px dashed $ui-dev-kit-example-border; + border: 1px dashed $gl-gray-100; margin-bottom: 15px; &::before { diff --git a/app/assets/stylesheets/performance_bar.scss b/app/assets/stylesheets/performance_bar.scss index 57d43beaf21..2e2ab8532d2 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -11,10 +11,10 @@ height: $performance-bar-height; background: $black; line-height: $performance-bar-height; - color: $perf-bar-text; + color: $gl-gray-400; select { - color: $perf-bar-text; + color: $gl-gray-400; width: 200px; } @@ -53,7 +53,7 @@ padding: 4px 6px; font-family: Consolas, 'Liberation Mono', Courier, monospace; line-height: 1; - color: $perf-bar-bucket-color; + color: $gl-gray-200; border-radius: 3px; box-shadow: 0 1px 0 $perf-bar-bucket-box-shadow-from, inset 0 1px 2px $perf-bar-bucket-box-shadow-to; diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb index ed13ead63f9..68e14f0c2e5 100644 --- a/app/controllers/abuse_reports_controller.rb +++ b/app/controllers/abuse_reports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AbuseReportsController < ApplicationController before_action :set_user, only: [:new] @@ -30,6 +32,7 @@ class AbuseReportsController < ApplicationController )) end + # rubocop: disable CodeReuse/ActiveRecord def set_user @user = User.find_by(id: params[:user_id]) @@ -39,4 +42,5 @@ class AbuseReportsController < ApplicationController redirect_to @user, alert: "Cannot create the abuse report. This user has been blocked." end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb index dc9a6df5f75..d5537023b26 100644 --- a/app/controllers/admin/abuse_reports_controller.rb +++ b/app/controllers/admin/abuse_reports_controller.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + class Admin::AbuseReportsController < Admin::ApplicationController + # rubocop: disable CodeReuse/ActiveRecord def index @abuse_reports = AbuseReport.order(id: :desc).page(params[:page]) @abuse_reports.includes(:reporter, :user) end + # rubocop: enable CodeReuse/ActiveRecord def destroy abuse_report = AbuseReport.find(params[:id]) diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index 9aaec905734..fdd3b4126ff 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::AppearancesController < Admin::ApplicationController before_action :set_appearance, except: :create diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index a4648b33cfa..ef182b981f1 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Provides a base class for Admin controllers to subclass # # Automatically sets the layout and ensures an administrator is logged in diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 9723e400574..875e46969fe 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -1,19 +1,58 @@ +# frozen_string_literal: true + class Admin::ApplicationSettingsController < Admin::ApplicationController + include InternalRedirect before_action :set_application_setting def show end + def integrations + end + + def repository + end + + def templates + end + + def ci_cd + end + + def reporting + end + + def metrics_and_profiling + end + + def network + end + + def geo + end + + def preferences + end + def update successful = ApplicationSettings::UpdateService .new(@application_setting, current_user, application_setting_params) .execute - if successful - redirect_to admin_application_settings_path, - notice: 'Application settings saved successfully' - else - render :show + if recheck_user_consent? + session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent? + end + + redirect_path = referer_path(request) || admin_application_settings_path + + respond_to do |format| + if successful + format.json { head :ok } + format.html { redirect_to redirect_path, notice: 'Application settings saved successfully' } + else + format.json { head :bad_request } + format.html { render :show } + end end end @@ -76,6 +115,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ) end + def recheck_user_consent? + return false unless session[:ask_for_usage_stats_consent] + return false unless params[:application_setting] + + params[:application_setting].key?(:usage_ping_enabled) || params[:application_setting].key?(:version_check_enabled) + end + def visible_application_setting_attributes ApplicationSettingsHelper.visible_attributes + [ :domain_blacklist_file, diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 5be23c76a95..00d2cc01192 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -1,12 +1,16 @@ +# frozen_string_literal: true + class Admin::ApplicationsController < Admin::ApplicationController include OauthApplications before_action :set_application, only: [:show, :edit, :update, :destroy] before_action :load_scopes, only: [:new, :create, :edit, :update] + # rubocop: disable CodeReuse/ActiveRecord def index @applications = Doorkeeper::Application.where("owner_id IS NULL") end + # rubocop: enable CodeReuse/ActiveRecord def show end @@ -45,9 +49,11 @@ class Admin::ApplicationsController < Admin::ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def set_application @application = Doorkeeper::Application.where("owner_id IS NULL").find(params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord # Only allow a trusted parameter "white list" through. def application_params diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb index 5f90ad7137d..7701f2e645b 100644 --- a/app/controllers/admin/background_jobs_controller.rb +++ b/app/controllers/admin/background_jobs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::BackgroundJobsController < Admin::ApplicationController def show ps_output, _ = Gitlab::Popen.popen(%W(ps ww -U #{Gitlab.config.gitlab.user} -o pid,pcpu,pmem,stat,start,command)) diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index a9109a1d4d0..a91d9a534cd 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -1,12 +1,16 @@ +# frozen_string_literal: true + class Admin::BroadcastMessagesController < Admin::ApplicationController include BroadcastMessagesHelper before_action :finder, only: [:edit, :update, :destroy] + # rubocop: disable CodeReuse/ActiveRecord def index @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) @broadcast_message = BroadcastMessage.new end + # rubocop: enable CodeReuse/ActiveRecord def edit end diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb index 737942f3eb2..b5fb5511638 100644 --- a/app/controllers/admin/dashboard_controller.rb +++ b/app/controllers/admin/dashboard_controller.rb @@ -1,13 +1,17 @@ +# frozen_string_literal: true + class Admin::DashboardController < Admin::ApplicationController include CountHelper COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest, Note, Snippet, Key, Milestone].freeze + # rubocop: disable CodeReuse/ActiveRecord def index @counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS) @projects = Project.order_id_desc.without_deleted.with_route.limit(10) @users = User.order_id_desc.limit(10) @groups = Group.order_id_desc.with_route.limit(10) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index 5c2025c1988..49ce275ad14 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::DeployKeysController < Admin::ApplicationController before_action :deploy_keys, only: [:index] before_action :deploy_key, only: [:destroy, :edit, :update] diff --git a/app/controllers/admin/gitaly_servers_controller.rb b/app/controllers/admin/gitaly_servers_controller.rb index 11c4dfe3d8d..0a5566bfe70 100644 --- a/app/controllers/admin/gitaly_servers_controller.rb +++ b/app/controllers/admin/gitaly_servers_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::GitalyServersController < Admin::ApplicationController def index @gitaly_servers = Gitaly::Server.all diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index d7a5b745d3f..46e85e1424f 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::GroupsController < Admin::ApplicationController include MembersPresentation @@ -10,6 +12,7 @@ class Admin::GroupsController < Admin::ApplicationController @groups = @groups.page(params[:page]) end + # rubocop: disable CodeReuse/ActiveRecord def show @group = Group.with_statistics.joins(:route).group('routes.path').find_by_full_path(params[:id]) @members = present_members( @@ -18,6 +21,7 @@ class Admin::GroupsController < Admin::ApplicationController AccessRequestsFinder.new(@group).execute(current_user)) @projects = @group.projects.with_statistics.page(params[:projects_page]) end + # rubocop: enable CodeReuse/ActiveRecord def new @group = Group.new diff --git a/app/controllers/admin/health_check_controller.rb b/app/controllers/admin/health_check_controller.rb index 61247b280b3..44864f9c7d0 100644 --- a/app/controllers/admin/health_check_controller.rb +++ b/app/controllers/admin/health_check_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::HealthCheckController < Admin::ApplicationController def show @errors = HealthCheck::Utils.process_checks(['standard']) diff --git a/app/controllers/admin/hook_logs_controller.rb b/app/controllers/admin/hook_logs_controller.rb index 3017f96c26f..8301b3aa880 100644 --- a/app/controllers/admin/hook_logs_controller.rb +++ b/app/controllers/admin/hook_logs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::HookLogsController < Admin::ApplicationController include HooksExecution diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index a98c355c7ba..d0abdec50ae 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::HooksController < Admin::ApplicationController include HooksExecution diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index ceb45865804..b51c2f678ca 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::IdentitiesController < Admin::ApplicationController before_action :user before_action :identity, except: [:index, :new, :create] @@ -44,9 +46,11 @@ class Admin::IdentitiesController < Admin::ApplicationController protected + # rubocop: disable CodeReuse/ActiveRecord def user @user ||= User.find_by!(username: params[:user_id]) end + # rubocop: enable CodeReuse/ActiveRecord def identity @identity ||= user.identities.find(params[:id]) diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index a7b562b1d8e..f5825ecb19a 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::ImpersonationTokensController < Admin::ApplicationController before_action :user @@ -30,9 +32,11 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def user @user ||= User.find_by!(username: params[:user_id]) end + # rubocop: enable CodeReuse/ActiveRecord def finder(options = {}) PersonalAccessTokensFinder.new({ user: user, impersonation: true }.merge(options)) @@ -42,6 +46,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController params.require(:personal_access_token).permit(:name, :expires_at, :impersonation, scopes: []) end + # rubocop: disable CodeReuse/ActiveRecord def set_index_vars @scopes = Gitlab::Auth.available_scopes(current_user) @@ -49,4 +54,5 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController @inactive_impersonation_tokens = finder(state: 'inactive').execute @active_impersonation_tokens = finder(state: 'active').execute.order(:expires_at) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb index d2f947d2c66..08d7e3b4fa2 100644 --- a/app/controllers/admin/impersonations_controller.rb +++ b/app/controllers/admin/impersonations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::ImpersonationsController < Admin::ApplicationController skip_before_action :authenticate_admin! before_action :authenticate_impersonator! diff --git a/app/controllers/admin/jobs_controller.rb b/app/controllers/admin/jobs_controller.rb index e355d5fdea7..0c1afdc3d3b 100644 --- a/app/controllers/admin/jobs_controller.rb +++ b/app/controllers/admin/jobs_controller.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + class Admin::JobsController < Admin::ApplicationController + # rubocop: disable CodeReuse/ActiveRecord def index @scope = params[:scope] @all_builds = Ci::Build @@ -16,6 +19,7 @@ class Admin::JobsController < Admin::ApplicationController end @builds = @builds.page(params[:page]).per(30) end + # rubocop: enable CodeReuse/ActiveRecord def cancel_all Ci::Build.running_or_pending.each(&:cancel) diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index 0b76193a90e..4e9262ccc96 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::KeysController < Admin::ApplicationController before_action :user, only: [:show, :destroy] @@ -24,9 +26,11 @@ class Admin::KeysController < Admin::ApplicationController protected + # rubocop: disable CodeReuse/ActiveRecord def user @user ||= User.find_by!(username: params[:user_id]) end + # rubocop: enable CodeReuse/ActiveRecord def key_params params.require(:user_id, :id) diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb index 7eb8f758807..aa5eae7a474 100644 --- a/app/controllers/admin/labels_controller.rb +++ b/app/controllers/admin/labels_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::LabelsController < Admin::ApplicationController before_action :set_label, only: [:show, :edit, :update, :destroy] diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb index 12a27cede75..06b0e6a15a3 100644 --- a/app/controllers/admin/logs_controller.rb +++ b/app/controllers/admin/logs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::LogsController < Admin::ApplicationController before_action :loggers @@ -12,7 +14,8 @@ class Admin::LogsController < Admin::ApplicationController Gitlab::GitLogger, Gitlab::EnvironmentLogger, Gitlab::SidekiqLogger, - Gitlab::RepositoryCheckLogger + Gitlab::RepositoryCheckLogger, + Gitlab::ProjectServiceLogger ] end end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 3afe66c3566..550f29a58d2 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::ProjectsController < Admin::ApplicationController include MembersPresentation @@ -19,6 +21,7 @@ class Admin::ProjectsController < Admin::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def show if @group @group_members = present_members( @@ -30,7 +33,9 @@ class Admin::ProjectsController < Admin::ApplicationController @requesters = present_members( AccessRequestsFinder.new(@project).execute(current_user)) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def transfer namespace = Namespace.find_by(id: params[:new_namespace_id]) ::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace) @@ -38,6 +43,7 @@ class Admin::ProjectsController < Admin::ApplicationController @project.reload redirect_to admin_project_path(@project) end + # rubocop: enable CodeReuse/ActiveRecord def repository_check RepositoryCheck::SingleRepositoryWorker.perform_async(@project.id) diff --git a/app/controllers/admin/requests_profiles_controller.rb b/app/controllers/admin/requests_profiles_controller.rb index a478176e138..64d74ae4231 100644 --- a/app/controllers/admin/requests_profiles_controller.rb +++ b/app/controllers/admin/requests_profiles_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::RequestsProfilesController < Admin::ApplicationController def index @profile_token = Gitlab::RequestProfiler.profile_token diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb index 51d5799cd89..774ce04d079 100644 --- a/app/controllers/admin/runner_projects_controller.rb +++ b/app/controllers/admin/runner_projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::RunnerProjectsController < Admin::ApplicationController before_action :project, only: [:create] diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 6c76c55a9d4..0b6ff491c66 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + class Admin::RunnersController < Admin::ApplicationController before_action :runner, except: :index def index - sort = params[:sort] == 'contacted_asc' ? { contacted_at: :asc } : { id: :desc } - @runners = Ci::Runner.order(sort) - @runners = @runners.search(params[:search]) if params[:search].present? - @runners = @runners.page(params[:page]).per(30) - @active_runners_cnt = Ci::Runner.online.count + finder = Admin::RunnersFinder.new(params: params) + @runners = finder.execute + @active_runners_count = Ci::Runner.online.count + @sort = finder.sort_key end def show @@ -57,6 +58,7 @@ class Admin::RunnersController < Admin::ApplicationController params.require(:runner).permit(Ci::Runner::FORM_EDITABLE) end + # rubocop: disable CodeReuse/ActiveRecord def assign_builds_and_projects @builds = runner.builds.order('id DESC').first(30) @projects = @@ -69,4 +71,5 @@ class Admin::RunnersController < Admin::ApplicationController @projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? @projects = @projects.page(params[:page]).per(30) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/admin/services_controller.rb b/app/controllers/admin/services_controller.rb index 91a36af34f3..c455930c044 100644 --- a/app/controllers/admin/services_controller.rb +++ b/app/controllers/admin/services_controller.rb @@ -30,16 +30,20 @@ class Admin::ServicesController < Admin::ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def services_templates Service.available_services_names.map do |service_name| service_template = "#{service_name}_service".camelize.constantize service_template.where(template: true).first_or_create end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def service @service ||= Service.where(id: params[:id], template: true).first end + # rubocop: enable CodeReuse/ActiveRecord def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42430') diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index d52d67a67a5..18d22c95b61 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -1,7 +1,11 @@ +# frozen_string_literal: true + class Admin::SpamLogsController < Admin::ApplicationController + # rubocop: disable CodeReuse/ActiveRecord def index @spam_logs = SpamLog.order(id: :desc).page(params[:page]) end + # rubocop: enable CodeReuse/ActiveRecord def destroy spam_log = SpamLog.find(params[:id]) diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb index 99039724521..244fc2b31bb 100644 --- a/app/controllers/admin/system_info_controller.rb +++ b/app/controllers/admin/system_info_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::SystemInfoController < Admin::ApplicationController EXCLUDED_MOUNT_OPTIONS = [ 'nobrowse', diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index a51a8c3ed4a..b783c0e2a6f 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::UsersController < Admin::ApplicationController before_action :user, except: [:index, :new, :create] @@ -174,9 +176,11 @@ class Admin::UsersController < Admin::ApplicationController user == current_user end + # rubocop: disable CodeReuse/ActiveRecord def user @user ||= User.find_by!(username: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord def redirect_back_or_admin_user(options = {}) redirect_back_or_default(default: default_route, options: options) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index e5b38898a67..838527aaa41 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'gon' require 'fogbugz' @@ -22,6 +24,7 @@ class ApplicationController < ActionController::Base before_action :add_gon_variables, unless: [:peek_request?, :json_request?] before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? + before_action :set_usage_stats_consent_flag around_action :set_locale @@ -110,6 +113,7 @@ class ApplicationController < ActionController::Base def append_info_to_payload(payload) super + payload[:ua] = request.env["HTTP_USER_AGENT"] payload[:remote_ip] = request.remote_ip logged_user = auth_user @@ -433,4 +437,29 @@ class ApplicationController < ActionController::Base !(peek_request? || devise_controller?) end + + def set_usage_stats_consent_flag + return unless current_user + return if sessionless_user? + return if session.has_key?(:ask_for_usage_stats_consent) + + session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent? + + if session[:ask_for_usage_stats_consent] + disable_usage_stats + end + end + + def disable_usage_stats + application_setting_params = { + usage_ping_enabled: false, + version_check_enabled: false, + skip_usage_stats_user: true + } + settings = Gitlab::CurrentSettings.current_application_settings + + ApplicationSettings::UpdateService + .new(settings, current_user, application_setting_params) + .execute + end end diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb index 9e30b982b06..3766b64a091 100644 --- a/app/controllers/autocomplete_controller.rb +++ b/app/controllers/autocomplete_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AutocompleteController < ApplicationController skip_before_action :authenticate_user!, only: [:users, :award_emojis] diff --git a/app/controllers/boards/application_controller.rb b/app/controllers/boards/application_controller.rb index b2675025fc0..eab908ba5ed 100644 --- a/app/controllers/boards/application_controller.rb +++ b/app/controllers/boards/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Boards class ApplicationController < ::ApplicationController respond_to :json diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 7dd19f87ef5..4f3d737e3ce 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Boards class IssuesController < Boards::ApplicationController include BoardsResponses @@ -11,6 +13,7 @@ module Boards before_action :authorize_update_issue, only: [:update] skip_before_action :authenticate_user!, only: [:index] + # rubocop: disable CodeReuse/ActiveRecord def index list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params) issues = list_service.execute @@ -25,6 +28,7 @@ module Boards render_issues(issues, list_service.metadata) end + # rubocop: enable CodeReuse/ActiveRecord def create service = Boards::Issues::CreateService.new(board_parent, project, current_user, issue_params) diff --git a/app/controllers/boards/lists_controller.rb b/app/controllers/boards/lists_controller.rb index e8b5934f2a9..ccd02144671 100644 --- a/app/controllers/boards/lists_controller.rb +++ b/app/controllers/boards/lists_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Boards class ListsController < Boards::ApplicationController include BoardsResponses diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb index 738a6a5173e..99ce24bd435 100644 --- a/app/controllers/ci/lints_controller.rb +++ b/app/controllers/ci/lints_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Ci class LintsController < ::ApplicationController before_action :authenticate_user! diff --git a/app/controllers/concerns/accepts_pending_invitations.rb b/app/controllers/concerns/accepts_pending_invitations.rb index 6e8aef52b52..cb66c1a055d 100644 --- a/app/controllers/concerns/accepts_pending_invitations.rb +++ b/app/controllers/concerns/accepts_pending_invitations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AcceptsPendingInvitations extend ActiveSupport::Concern diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb index dfa1da7872c..5507328f8ae 100644 --- a/app/controllers/concerns/authenticates_with_two_factor.rb +++ b/app/controllers/concerns/authenticates_with_two_factor.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == AuthenticatesWithTwoFactor # # Controller concern to handle two-factor authentication @@ -88,6 +90,7 @@ module AuthenticatesWithTwoFactor # Setup in preparation of communication with a U2F (universal 2nd factor) device # Actual communication is performed using a Javascript API + # rubocop: disable CodeReuse/ActiveRecord def setup_u2f_authentication(user) key_handles = user.u2f_registrations.pluck(:key_handle) u2f = U2F::U2F.new(u2f_app_id) @@ -99,4 +102,5 @@ module AuthenticatesWithTwoFactor sign_requests: sign_requests }) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/boards_responses.rb b/app/controllers/concerns/boards_responses.rb index da830ec2cb1..b7e4f9b81f1 100644 --- a/app/controllers/concerns/boards_responses.rb +++ b/app/controllers/concerns/boards_responses.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BoardsResponses include Gitlab::Utils::StrongMemoize diff --git a/app/controllers/concerns/checks_collaboration.rb b/app/controllers/concerns/checks_collaboration.rb index 81367663a06..1fa82f7dcd4 100644 --- a/app/controllers/concerns/checks_collaboration.rb +++ b/app/controllers/concerns/checks_collaboration.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ChecksCollaboration def can_collaborate_with_project?(project, ref: nil) return true if can?(current_user, :push_code, project) diff --git a/app/controllers/concerns/continue_params.rb b/app/controllers/concerns/continue_params.rb index 8b7355974df..f0e6adf4dec 100644 --- a/app/controllers/concerns/continue_params.rb +++ b/app/controllers/concerns/continue_params.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ContinueParams include InternalRedirect extend ActiveSupport::Concern diff --git a/app/controllers/concerns/controller_with_cross_project_access_check.rb b/app/controllers/concerns/controller_with_cross_project_access_check.rb index a45c3384578..3f72f092683 100644 --- a/app/controllers/concerns/controller_with_cross_project_access_check.rb +++ b/app/controllers/concerns/controller_with_cross_project_access_check.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ControllerWithCrossProjectAccessCheck extend ActiveSupport::Concern diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb index b26a76d2b62..b3777fd2b0f 100644 --- a/app/controllers/concerns/creates_commit.rb +++ b/app/controllers/concerns/creates_commit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CreatesCommit extend ActiveSupport::Concern include Gitlab::Utils::StrongMemoize @@ -65,7 +67,7 @@ module CreatesCommit flash[:notice] = nil else target = different_project? ? "project" : "branch" - flash[:notice] << " You can now submit a merge request to get this change into the original #{target}." + flash[:notice] = flash[:notice] + " You can now submit a merge request to get this change into the original #{target}." end end end @@ -99,6 +101,7 @@ module CreatesCommit end # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def merge_request_exists? strong_memoize(:merge_request) do MergeRequestsFinder.new(current_user, project_id: @project.id) @@ -110,6 +113,7 @@ module CreatesCommit target_branch: @start_branch) end end + # rubocop: enable CodeReuse/ActiveRecord # rubocop:enable Gitlab/ModuleWithInstanceVariables def different_project? diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb index 1ab107168c0..c1ef848e1e7 100644 --- a/app/controllers/concerns/cycle_analytics_params.rb +++ b/app/controllers/concerns/cycle_analytics_params.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CycleAnalyticsParams extend ActiveSupport::Concern diff --git a/app/controllers/concerns/diff_for_path.rb b/app/controllers/concerns/diff_for_path.rb index d5388c4cd20..6be7a2a18a2 100644 --- a/app/controllers/concerns/diff_for_path.rb +++ b/app/controllers/concerns/diff_for_path.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DiffForPath extend ActiveSupport::Concern diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb index 997af4ab9e9..71bdef8ce03 100644 --- a/app/controllers/concerns/enforces_two_factor_authentication.rb +++ b/app/controllers/concerns/enforces_two_factor_authentication.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # == EnforcesTwoFactorAuthentication # # Controller concern to enforce two-factor authentication requirements @@ -24,6 +26,7 @@ module EnforcesTwoFactorAuthentication current_user.try(:require_two_factor_authentication_from_group?) end + # rubocop: disable CodeReuse/ActiveRecord def two_factor_authentication_reason(global: -> {}, group: -> {}) if two_factor_authentication_required? if Gitlab::CurrentSettings.require_two_factor_authentication? @@ -34,6 +37,7 @@ module EnforcesTwoFactorAuthentication end end end + # rubocop: enable CodeReuse/ActiveRecord def two_factor_grace_period periods = [Gitlab::CurrentSettings.two_factor_grace_period] diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb index 6ec6897e707..4f56346832c 100644 --- a/app/controllers/concerns/group_tree.rb +++ b/app/controllers/concerns/group_tree.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + module GroupTree # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def render_group_tree(groups) groups = groups.sort_by_attribute(@sort = params[:sort]) @@ -23,7 +26,9 @@ module GroupTree end # rubocop:enable Gitlab/ModuleWithInstanceVariables end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def filtered_groups_with_ancestors(groups) filtered_groups = groups.search(params[:filter]).page(params[:page]) @@ -40,4 +45,5 @@ module GroupTree filtered_groups end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/hooks_execution.rb b/app/controllers/concerns/hooks_execution.rb index a22e46b4860..e8add1f4055 100644 --- a/app/controllers/concerns/hooks_execution.rb +++ b/app/controllers/concerns/hooks_execution.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module HooksExecution extend ActiveSupport::Concern diff --git a/app/controllers/concerns/internal_redirect.rb b/app/controllers/concerns/internal_redirect.rb index 10b9852e329..6785e6972d0 100644 --- a/app/controllers/concerns/internal_redirect.rb +++ b/app/controllers/concerns/internal_redirect.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module InternalRedirect extend ActiveSupport::Concern @@ -36,4 +38,10 @@ module InternalRedirect path_with_query = [uri.path, uri.query].compact.join('?') [path_with_query, uri.fragment].compact.join("#") end + + def referer_path(request) + return unless request.referer.presence + + URI(request.referer).path + end end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 37e03d70b6f..07e01e903ea 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module IssuableActions extend ActiveSupport::Concern @@ -89,12 +91,14 @@ module IssuableActions render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } end + # rubocop: disable CodeReuse/ActiveRecord def discussions notes = issuable.discussion_notes .inc_relations_for_view .includes(:noteable) .fresh + notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes) notes = prepare_notes_for_rendering(notes) notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } @@ -102,6 +106,7 @@ module IssuableActions render json: discussion_serializer.represent(discussions, context: self) end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index a2c96f5d635..5217b4be928 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module IssuableCollections extend ActiveSupport::Concern include CookiesHelper @@ -48,9 +50,11 @@ module IssuableCollections false end + # rubocop: disable CodeReuse/ActiveRecord def issuables_collection finder.execute.preload(preload_for_collection) end + # rubocop: enable CodeReuse/ActiveRecord def redirect_out_of_range(total_pages) return false if total_pages.nil? || total_pages.zero? @@ -81,6 +85,7 @@ module IssuableCollections end # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def filter_params set_sort_order_from_cookie set_default_state @@ -101,6 +106,7 @@ module IssuableCollections @filter_params.permit(finder_type.valid_params) end + # rubocop: enable CodeReuse/ActiveRecord # rubocop:enable Gitlab/ModuleWithInstanceVariables def set_default_state diff --git a/app/controllers/concerns/issues_action.rb b/app/controllers/concerns/issues_action.rb index 9d58656773d..a75590457d6 100644 --- a/app/controllers/concerns/issues_action.rb +++ b/app/controllers/concerns/issues_action.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module IssuesAction extend ActiveSupport::Concern include IssuableCollections diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb index 671a204621d..1fdfde4c869 100644 --- a/app/controllers/concerns/issues_calendar.rb +++ b/app/controllers/concerns/issues_calendar.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + module IssuesCalendar extend ActiveSupport::Concern # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def render_issues_calendar(issuables) @issues = issuables .non_archived @@ -20,5 +23,6 @@ module IssuesCalendar end end end + # rubocop: enable CodeReuse/ActiveRecord # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb index 4584ff782a3..9576eb14fdd 100644 --- a/app/controllers/concerns/lfs_request.rb +++ b/app/controllers/concerns/lfs_request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This concern assumes: # - a `#project` accessor # - a `#user` accessor diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb index 215e0bdf3cb..c6c3598a976 100644 --- a/app/controllers/concerns/members_presentation.rb +++ b/app/controllers/concerns/members_presentation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MembersPresentation extend ActiveSupport::Concern @@ -10,10 +12,12 @@ module MembersPresentation ).fabricate! end + # rubocop: disable CodeReuse/ActiveRecord def preload_associations(members) ActiveRecord::Associations::Preloader.new.preload(members, :user) ActiveRecord::Associations::Preloader.new.preload(members, :source) ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 409e6d4c4d2..ca713192c9e 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MembershipActions include MembersPresentation extend ActiveSupport::Concern @@ -57,6 +59,7 @@ module MembershipActions redirect_to members_page_url end + # rubocop: disable CodeReuse/ActiveRecord def leave member = membershipable.members_and_requesters.find_by!(user_id: current_user.id) Members::DestroyService.new(current_user).execute(member) @@ -77,6 +80,7 @@ module MembershipActions format.json { render json: { notice: notice } } end end + # rubocop: enable CodeReuse/ActiveRecord def resend_invite member = membershipable.members.find(params[:id]) diff --git a/app/controllers/concerns/merge_requests_action.rb b/app/controllers/concerns/merge_requests_action.rb index b70db99b157..285f2c3a8a0 100644 --- a/app/controllers/concerns/merge_requests_action.rb +++ b/app/controllers/concerns/merge_requests_action.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MergeRequestsAction extend ActiveSupport::Concern include IssuableCollections diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb index d92cf8b4894..eccbe35577b 100644 --- a/app/controllers/concerns/milestone_actions.rb +++ b/app/controllers/concerns/milestone_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MilestoneActions extend ActiveSupport::Concern diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index 5127db3f5fb..3a45d6205ab 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module NotesActions include RendersNotes include Gitlab::Utils::StrongMemoize @@ -18,6 +20,7 @@ module NotesActions notes = notes_finder.execute .inc_relations_for_view + notes = ResourceEvents::MergeIntoNotesService.new(noteable, current_user, last_fetched_at: current_fetched_at).execute(notes) notes = prepare_notes_for_rendering(notes) notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } @@ -40,12 +43,26 @@ module NotesActions @note = Notes::CreateService.new(note_project, current_user, create_params).execute - if @note.is_a?(Note) - prepare_notes_for_rendering([@note], noteable) - end - respond_to do |format| - format.json { render json: note_json(@note) } + format.json do + json = { + commands_changes: @note.commands_changes + } + + if @note.persisted? && return_discussion? + json[:valid] = true + + discussion = @note.discussion + prepare_notes_for_rendering(discussion.notes) + json[:discussion] = discussion_serializer.represent(discussion, context: self) + else + prepare_notes_for_rendering([@note]) + + json.merge!(note_json(@note)) + end + + render json: json + end format.html { redirect_back_or_default } end end @@ -54,10 +71,7 @@ module NotesActions # rubocop:disable Gitlab/ModuleWithInstanceVariables def update @note = Notes::UpdateService.new(project, current_user, note_params).execute(note) - - if @note.is_a?(Note) - prepare_notes_for_rendering([@note]) - end + prepare_notes_for_rendering([@note]) respond_to do |format| format.json { render json: note_json(@note) } @@ -88,14 +102,17 @@ module NotesActions end def note_json(note) - attrs = { - commands_changes: note.commands_changes - } + attrs = {} if note.persisted? attrs[:valid] = true - if use_note_serializer? + if return_discussion? + discussion = note.discussion + prepare_notes_for_rendering(discussion.notes) + + attrs[:discussion] = discussion_serializer.represent(discussion, context: self) + elsif use_note_serializer? attrs.merge!(note_serializer.represent(note)) else attrs.merge!( @@ -215,6 +232,10 @@ module NotesActions ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user) end + def discussion_serializer + DiscussionSerializer.new(project: project, noteable: noteable, current_user: current_user, note_entity: ProjectNoteEntity) + end + def note_project strong_memoize(:note_project) do next nil unless project @@ -234,6 +255,10 @@ module NotesActions end end + def return_discussion? + Gitlab::Utils.to_boolean(params[:return_discussion]) + end + def use_note_serializer? return false if params['html'] diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb index f0a68f23566..d97e22df472 100644 --- a/app/controllers/concerns/oauth_applications.rb +++ b/app/controllers/concerns/oauth_applications.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module OauthApplications extend ActiveSupport::Concern diff --git a/app/controllers/concerns/params_backward_compatibility.rb b/app/controllers/concerns/params_backward_compatibility.rb index b0e3d9c7b34..c972d6e3161 100644 --- a/app/controllers/concerns/params_backward_compatibility.rb +++ b/app/controllers/concerns/params_backward_compatibility.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ParamsBackwardCompatibility private diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb index 99123fcb3b0..c61b9fabe9e 100644 --- a/app/controllers/concerns/preview_markdown.rb +++ b/app/controllers/concerns/preview_markdown.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PreviewMarkdown extend ActiveSupport::Concern diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb index ba7adcfea86..b8026c7a01d 100644 --- a/app/controllers/concerns/renders_blob.rb +++ b/app/controllers/concerns/renders_blob.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RendersBlob extend ActiveSupport::Concern diff --git a/app/controllers/concerns/renders_commits.rb b/app/controllers/concerns/renders_commits.rb index b1c9b1e532f..f48e0586211 100644 --- a/app/controllers/concerns/renders_commits.rb +++ b/app/controllers/concerns/renders_commits.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RendersCommits def limited_commits(commits) if commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb index d640378c24d..955ac1a1bc8 100644 --- a/app/controllers/concerns/renders_member_access.rb +++ b/app/controllers/concerns/renders_member_access.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RendersMemberAccess def prepare_groups_for_rendering(groups) preload_max_member_access_for_collection(Group, groups) @@ -13,6 +15,7 @@ module RendersMemberAccess private + # rubocop: disable CodeReuse/ActiveRecord def preload_max_member_access_for_collection(klass, collection) return if !current_user || collection.blank? @@ -20,4 +23,5 @@ module RendersMemberAccess current_user.public_send(method_name, collection.ids) # rubocop:disable GitlabSecurity/PublicSend end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb index cf04023080a..ce36da6b715 100644 --- a/app/controllers/concerns/renders_notes.rb +++ b/app/controllers/concerns/renders_notes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RendersNotes # rubocop:disable Gitlab/ModuleWithInstanceVariables def prepare_notes_for_rendering(notes, noteable = nil) @@ -20,9 +22,11 @@ module RendersNotes project.team.max_member_access_for_user_ids(user_ids) end + # rubocop: disable CodeReuse/ActiveRecord def preload_noteable_for_regular_notes(notes) ActiveRecord::Associations::Preloader.new.preload(notes.reject(&:for_commit?), :noteable) end + # rubocop: enable CodeReuse/ActiveRecord def preload_first_time_contribution_for_authors(noteable, notes) return unless noteable.is_a?(Issuable) && noteable.first_contribution? @@ -30,7 +34,9 @@ module RendersNotes notes.each {|n| n.specialize_for_first_contribution!(noteable)} end + # rubocop: disable CodeReuse/ActiveRecord def preload_author_status(notes) ActiveRecord::Associations::Preloader.new.preload(notes, { author: :status }) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/concerns/repository_settings_redirect.rb b/app/controllers/concerns/repository_settings_redirect.rb index f3db3cd563b..0f18735c29e 100644 --- a/app/controllers/concerns/repository_settings_redirect.rb +++ b/app/controllers/concerns/repository_settings_redirect.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RepositorySettingsRedirect extend ActiveSupport::Concern diff --git a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb index 88d1b34bb06..426f224d26b 100644 --- a/app/controllers/concerns/requires_whitelisted_monitoring_client.rb +++ b/app/controllers/concerns/requires_whitelisted_monitoring_client.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RequiresWhitelistedMonitoringClient extend ActiveSupport::Concern diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb index 0931bdf4c04..88939b002b2 100644 --- a/app/controllers/concerns/routable_actions.rb +++ b/app/controllers/concerns/routable_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RoutableActions extend ActiveSupport::Concern diff --git a/app/controllers/concerns/send_file_upload.rb b/app/controllers/concerns/send_file_upload.rb index 237c93daee8..0bb7b7efed0 100644 --- a/app/controllers/concerns/send_file_upload.rb +++ b/app/controllers/concerns/send_file_upload.rb @@ -1,7 +1,13 @@ +# frozen_string_literal: true + module SendFileUpload def send_upload(file_upload, send_params: {}, redirect_params: {}, attachment: nil, disposition: 'attachment') if attachment - redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}" } + # Response-Content-Type will not override an existing Content-Type in + # Google Cloud Storage, so the metadata needs to be cleared on GCS for + # this to work. However, this override works with AWS. + redirect_params[:query] = { "response-content-disposition" => "#{disposition};filename=#{attachment.inspect}", + "response-content-type" => guess_content_type(attachment) } # By default, Rails will send uploads with an extension of .js with a # content-type of text/javascript, which will trigger Rails' # cross-origin JavaScript protection. @@ -18,4 +24,14 @@ module SendFileUpload redirect_to file_upload.url(**redirect_params) end end + + def guess_content_type(filename) + types = MIME::Types.type_for(filename) + + if types.present? + types.first.content_type + else + "application/octet-stream" + end + end end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index c1acb50b76c..8bd93a349ef 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ServiceParams extend ActiveSupport::Concern diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 120614739aa..8c22490700c 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SnippetsActions extend ActiveSupport::Concern diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb index 922aa58a00f..c3a1b12af84 100644 --- a/app/controllers/concerns/spammable_actions.rb +++ b/app/controllers/concerns/spammable_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SpammableActions extend ActiveSupport::Concern diff --git a/app/controllers/concerns/todos_actions.rb b/app/controllers/concerns/todos_actions.rb index c0acdb3498d..78b65f7961b 100644 --- a/app/controllers/concerns/todos_actions.rb +++ b/app/controllers/concerns/todos_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosActions extend ActiveSupport::Concern diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb index ae0b815f85e..97b343f8b1a 100644 --- a/app/controllers/concerns/toggle_award_emoji.rb +++ b/app/controllers/concerns/toggle_award_emoji.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ToggleAwardEmoji extend ActiveSupport::Concern diff --git a/app/controllers/concerns/toggle_subscription_action.rb b/app/controllers/concerns/toggle_subscription_action.rb index 776583579e8..e613bfaeef2 100644 --- a/app/controllers/concerns/toggle_subscription_action.rb +++ b/app/controllers/concerns/toggle_subscription_action.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ToggleSubscriptionAction extend ActiveSupport::Concern diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb index 434459a225a..7a1c7abfb8f 100644 --- a/app/controllers/concerns/uploads_actions.rb +++ b/app/controllers/concerns/uploads_actions.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module UploadsActions extend ActiveSupport::Concern @@ -53,6 +55,8 @@ module UploadsActions maximum_size: Gitlab::CurrentSettings.max_attachment_size.megabytes.to_i) render json: authorized + rescue SocketError + render json: "Error uploading file", status: :internal_server_error end private @@ -87,6 +91,7 @@ module UploadsActions end end + # rubocop: disable CodeReuse/ActiveRecord def build_uploader_from_upload return unless uploader = build_uploader @@ -94,6 +99,7 @@ module UploadsActions upload = Upload.find_by(uploader: uploader_class.to_s, path: upload_paths) upload&.build_uploader end + # rubocop: enable CodeReuse/ActiveRecord def build_uploader_from_params return unless uploader = build_uploader diff --git a/app/controllers/concerns/with_performance_bar.rb b/app/controllers/concerns/with_performance_bar.rb index 6a8b1a4de7b..c12839c7bbd 100644 --- a/app/controllers/concerns/with_performance_bar.rb +++ b/app/controllers/concerns/with_performance_bar.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module WithPerformanceBar extend ActiveSupport::Concern diff --git a/app/controllers/concerns/workhorse_request.rb b/app/controllers/concerns/workhorse_request.rb index 43c0f1b173c..028f10e866a 100644 --- a/app/controllers/concerns/workhorse_request.rb +++ b/app/controllers/concerns/workhorse_request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module WorkhorseRequest extend ActiveSupport::Concern diff --git a/app/controllers/confirmations_controller.rb b/app/controllers/confirmations_controller.rb index 7bc46a6ccc0..2c4aab67448 100644 --- a/app/controllers/confirmations_controller.rb +++ b/app/controllers/confirmations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ConfirmationsController < Devise::ConfirmationsController include AcceptsPendingInvitations @@ -20,7 +22,7 @@ class ConfirmationsController < Devise::ConfirmationsController after_sign_in(resource) else Gitlab::AppLogger.info("Email Confirmed: username=#{resource.username} email=#{resource.email} ip=#{request.remote_ip}") - flash[:notice] += " Please sign in." + flash[:notice] = flash[:notice] + " Please sign in." new_session_path(:user, anchor: 'login-pane') end end diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 0469e7e1e1f..78f7f6d4e23 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -22,7 +22,7 @@ class Dashboard::MilestonesController < Dashboard::ApplicationController private def group_milestones - groups = GroupsFinder.new(current_user, all_available: true).execute + groups = GroupsFinder.new(current_user, all_available: false).execute DashboardGroupMilestone.build_collection(groups) end diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index ccfcbbdc776..e8f796f17ca 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -23,6 +23,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def starred @projects = load_projects(params.merge(starred: true)) .includes(:forked_from_project, :tags) @@ -38,6 +39,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord private @@ -46,6 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController @sort = params[:sort] end + # rubocop: disable CodeReuse/ActiveRecord def load_projects(finder_params) projects = ProjectsFinder .new(params: finder_params, current_user: current_user) @@ -55,6 +58,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController prepare_projects_for_rendering(projects) end + # rubocop: enable CodeReuse/ActiveRecord def load_events projects = load_projects(params.merge(non_public: true)) diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index bd7111e28bc..231a23427f7 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -73,6 +73,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id) end + # rubocop: disable CodeReuse/ActiveRecord def redirect_out_of_range(todos) total_pages = if todo_params.except(:sort, :page).empty? @@ -91,4 +92,5 @@ class Dashboard::TodosController < Dashboard::ApplicationController out_of_range end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb index ff133001b84..241753a505a 100644 --- a/app/controllers/dashboard_controller.rb +++ b/app/controllers/dashboard_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class DashboardController < Dashboard::ApplicationController include IssuesAction include MergeRequestsAction diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index c7273606a85..03a2ee07fea 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -34,6 +34,7 @@ class Explore::ProjectsController < Explore::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def starred @projects = load_projects.reorder('star_count DESC') @@ -46,9 +47,11 @@ class Explore::ProjectsController < Explore::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def load_projects projects = ProjectsFinder.new(current_user: current_user, params: params) .execute @@ -58,4 +61,5 @@ class Explore::ProjectsController < Explore::ApplicationController prepare_projects_for_rendering(projects) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb index 0a1cf169aca..a1ec144410b 100644 --- a/app/controllers/graphql_controller.rb +++ b/app/controllers/graphql_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GraphqlController < ApplicationController # Unauthenticated users have access to the API for public data skip_before_action :authenticate_user! diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 3e0076ac935..ae31313db64 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -2,7 +2,6 @@ class Groups::LabelsController < Groups::ApplicationController include ToggleSubscriptionAction before_action :label, only: [:edit, :update, :destroy] - before_action :available_labels, only: [:index] before_action :authorize_admin_labels!, only: [:new, :create, :edit, :update, :destroy] before_action :save_previous_label_path, only: [:edit] @@ -11,10 +10,10 @@ class Groups::LabelsController < Groups::ApplicationController def index respond_to do |format| format.html do - @labels = @available_labels.page(params[:page]) + @labels = GroupLabelsFinder.new(@group, params.merge(sort: sort)).execute end format.json do - render json: LabelSerializer.new.represent_appearance(@available_labels) + render json: LabelSerializer.new.represent_appearance(available_labels) end end end @@ -116,4 +115,8 @@ class Groups::LabelsController < Groups::ApplicationController include_descendant_groups: params[:include_descendant_groups], search: params[:search]).execute end + + def sort + @sort ||= params[:sort] || 'name_asc' + end end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index e57b9ff23a7..062c8c4e9e1 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GroupsController < Groups::ApplicationController include API::Helpers::RelatedResourcesHelpers include IssuesAction @@ -17,7 +19,7 @@ class GroupsController < Groups::ApplicationController before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :event_filter, only: [:activity] - before_action :user_actions, only: [:show, :subgroups] + before_action :user_actions, only: [:show] skip_cross_project_access_check :index, :new, :create, :edit, :update, :destroy, :projects @@ -53,11 +55,7 @@ class GroupsController < Groups::ApplicationController def show respond_to do |format| - format.html do - @has_children = GroupDescendantsFinder.new(current_user: current_user, - parent_group: @group, - params: params).has_children? - end + format.html format.atom do load_events @@ -101,6 +99,7 @@ class GroupsController < Groups::ApplicationController redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion." end + # rubocop: disable CodeReuse/ActiveRecord def transfer parent_group = Group.find_by(id: params[:new_parent_group_id]) service = ::Groups::TransferService.new(@group, current_user) @@ -113,9 +112,11 @@ class GroupsController < Groups::ApplicationController render :edit end end + # rubocop: enable CodeReuse/ActiveRecord protected + # rubocop: disable CodeReuse/ActiveRecord def authorize_create_group! allowed = if params[:parent_id].present? parent = Group.find_by(id: params[:parent_id]) @@ -126,6 +127,7 @@ class GroupsController < Groups::ApplicationController render_404 unless allowed end + # rubocop: enable CodeReuse/ActiveRecord def determine_layout if [:new, :create].include?(action_name.to_sym) @@ -160,6 +162,7 @@ class GroupsController < Groups::ApplicationController ] end + # rubocop: disable CodeReuse/ActiveRecord def load_events params[:sort] ||= 'latest_activity_desc' @@ -179,6 +182,7 @@ class GroupsController < Groups::ApplicationController .new(current_user) .execute(@events, atom_request: request.format.atom?) end + # rubocop: enable CodeReuse/ActiveRecord def user_actions if current_user diff --git a/app/controllers/health_check_controller.rb b/app/controllers/health_check_controller.rb index c3d18991fd4..a2abed7ba4e 100644 --- a/app/controllers/health_check_controller.rb +++ b/app/controllers/health_check_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HealthCheckController < HealthCheck::HealthCheckController include RequiresWhitelistedMonitoringClient end diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb index 3fedd5bfb29..ab4bc911e17 100644 --- a/app/controllers/health_controller.rb +++ b/app/controllers/health_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HealthController < ActionController::Base protect_from_forgery with: :exception, except: :storage_check, prepend: true include RequiresWhitelistedMonitoringClient diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb index a394521698c..e5a1fc9d6ff 100644 --- a/app/controllers/help_controller.rb +++ b/app/controllers/help_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class HelpController < ApplicationController skip_before_action :authenticate_user! diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb index 96bb2237d90..eeeebe430a7 100644 --- a/app/controllers/ide_controller.rb +++ b/app/controllers/ide_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class IdeController < ApplicationController layout 'fullscreen' diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb index 5766c6924cd..14b8c6e4137 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,16 +1,20 @@ class Import::BaseController < ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def find_already_added_projects(import_type) current_user.created_projects.where(import_type: import_type).includes(:import_state) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def find_jobs(import_type) current_user.created_projects .includes(:import_state) .where(import_type: import_type) .to_json(only: [:id], methods: [:import_status]) end + # rubocop: enable CodeReuse/ActiveRecord def find_or_create_namespace(names, owner) names = params[:target_namespace].presence || names diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb index fa31933e778..f885e04b198 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -16,6 +16,7 @@ class Import::BitbucketController < Import::BaseController redirect_to status_import_bitbucket_url end + # rubocop: disable CodeReuse/ActiveRecord def status bitbucket_client = Bitbucket::Client.new(credentials) repos = bitbucket_client.repos @@ -27,6 +28,7 @@ class Import::BitbucketController < Import::BaseController @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) } end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs('bitbucket') diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb index 798daeca6c9..fdd1078cdf7 100644 --- a/app/controllers/import/bitbucket_server_controller.rb +++ b/app/controllers/import/bitbucket_server_controller.rb @@ -52,6 +52,7 @@ class Import::BitbucketServerController < Import::BaseController redirect_to status_import_bitbucket_server_path end + # rubocop: disable CodeReuse/ActiveRecord def status repos = bitbucket_client.repos @@ -66,6 +67,7 @@ class Import::BitbucketServerController < Import::BaseController clear_session_data redirect_to new_import_bitbucket_server_path end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs('bitbucket_server') diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb index 2d665e05ac3..df96d181153 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -39,6 +39,7 @@ class Import::FogbugzController < Import::BaseController redirect_to status_import_fogbugz_path end + # rubocop: disable CodeReuse/ActiveRecord def status unless client.valid? return redirect_to new_import_fogbugz_path @@ -51,6 +52,7 @@ class Import::FogbugzController < Import::BaseController @repos.reject! { |repo| already_added_projects_names.include? repo.name } end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs('fogbugz') diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index c9870332c0f..f8b43b4fde5 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -22,6 +22,7 @@ class Import::GithubController < Import::BaseController redirect_to status_import_url end + # rubocop: disable CodeReuse/ActiveRecord def status @repos = client.repos @already_added_projects = find_already_added_projects(provider) @@ -29,6 +30,7 @@ class Import::GithubController < Import::BaseController @repos.reject! { |repo| already_added_projects_names.include? repo.full_name } end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs(provider) @@ -104,9 +106,11 @@ class Import::GithubController < Import::BaseController :github end + # rubocop: disable CodeReuse/ActiveRecord def logged_in_with_provider? current_user.identities.exists?(provider: provider) end + # rubocop: enable CodeReuse/ActiveRecord def provider_auth if session[access_token_key].blank? diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb index 53f70446d95..bdc402b0a77 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -12,6 +12,7 @@ class Import::GitlabController < Import::BaseController redirect_to status_import_gitlab_url end + # rubocop: disable CodeReuse/ActiveRecord def status @repos = client.projects(starting_page: 1, page_limit: MAX_PROJECT_PAGES, per_page: PER_PAGE_PROJECTS) @@ -20,6 +21,7 @@ class Import::GitlabController < Import::BaseController @repos = @repos.to_a.reject { |repo| already_added_projects_names.include? repo["path_with_namespace"] } end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs('gitlab') diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb index f22df992fe9..f21c38a4c27 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -11,7 +11,7 @@ class Import::GitlabProjectsController < Import::BaseController def create unless file_is_valid? - return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive." }) + return redirect_back_or_default(options: { alert: "You need to upload a GitLab project export archive (ending in .gz)." }) end @project = ::Projects::GitlabProjectsImportService.new(current_user, project_params).execute @@ -29,7 +29,11 @@ class Import::GitlabProjectsController < Import::BaseController private def file_is_valid? - project_params[:file] && project_params[:file].respond_to?(:read) + return false unless project_params[:file] && project_params[:file].respond_to?(:read) + + filename = project_params[:file].original_filename + + ImportExportUploader::EXTENSION_WHITELIST.include?(File.extname(filename).delete('.')) end def verify_gitlab_project_import_enabled diff --git a/app/controllers/import/google_code_controller.rb b/app/controllers/import/google_code_controller.rb index 3bce27e810a..e9387d0ad14 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -65,6 +65,7 @@ class Import::GoogleCodeController < Import::BaseController redirect_to status_import_google_code_path end + # rubocop: disable CodeReuse/ActiveRecord def status unless client.valid? return redirect_to new_import_google_code_path @@ -78,6 +79,7 @@ class Import::GoogleCodeController < Import::BaseController @repos.reject! { |repo| already_added_projects_names.include? repo.name } end + # rubocop: enable CodeReuse/ActiveRecord def jobs render json: find_jobs('google_code') diff --git a/app/controllers/import/manifest_controller.rb b/app/controllers/import/manifest_controller.rb index e5a719fa0df..4ed9dca2475 100644 --- a/app/controllers/import/manifest_controller.rb +++ b/app/controllers/import/manifest_controller.rb @@ -6,6 +6,7 @@ class Import::ManifestController < Import::BaseController def new end + # rubocop: disable CodeReuse/ActiveRecord def status @already_added_projects = find_already_added_projects already_added_import_urls = @already_added_projects.pluck(:import_url) @@ -14,6 +15,7 @@ class Import::ManifestController < Import::BaseController already_added_import_urls.include?(repository[:url]) end end + # rubocop: enable CodeReuse/ActiveRecord def upload group = Group.find(params[:group_id]) @@ -64,9 +66,11 @@ class Import::ManifestController < Import::BaseController end end + # rubocop: disable CodeReuse/ActiveRecord def group @group ||= Group.find_by(id: session[:manifest_import_group_id]) end + # rubocop: enable CodeReuse/ActiveRecord def repositories @repositories ||= session[:manifest_import_repositories] @@ -76,12 +80,14 @@ class Import::ManifestController < Import::BaseController find_already_added_projects.to_json(only: [:id], methods: [:import_status]) end + # rubocop: disable CodeReuse/ActiveRecord def find_already_added_projects group.all_projects .where(import_type: 'manifest') .where(creator_id: current_user) .includes(:import_state) end + # rubocop: enable CodeReuse/ActiveRecord def verify_import_enabled render_404 unless manifest_import_enabled? diff --git a/app/controllers/instance_statistics/cohorts_controller.rb b/app/controllers/instance_statistics/cohorts_controller.rb index 7eba0a5ecdd..4b4e39db2e1 100644 --- a/app/controllers/instance_statistics/cohorts_controller.rb +++ b/app/controllers/instance_statistics/cohorts_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationController + before_action :authenticate_usage_ping_enabled_or_admin! + def index if Gitlab::CurrentSettings.usage_ping_enabled cohorts_results = Rails.cache.fetch('cohorts', expires_in: 1.day) do @@ -10,4 +12,8 @@ class InstanceStatistics::CohortsController < InstanceStatistics::ApplicationCon @cohorts = CohortsSerializer.new.represent(cohorts_results) end end + + def authenticate_usage_ping_enabled_or_admin! + render_404 unless Gitlab::CurrentSettings.usage_ping_enabled || current_user.admin? + end end diff --git a/app/controllers/instance_statistics/conversational_development_index_controller.rb b/app/controllers/instance_statistics/conversational_development_index_controller.rb index d6d2191849f..306c16d559c 100644 --- a/app/controllers/instance_statistics/conversational_development_index_controller.rb +++ b/app/controllers/instance_statistics/conversational_development_index_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class InstanceStatistics::ConversationalDevelopmentIndexController < InstanceStatistics::ApplicationController + # rubocop: disable CodeReuse/ActiveRecord def index @metric = ConversationalDevelopmentIndex::Metric.order(:created_at).last&.present end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 025d8270b7c..315d1375e02 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class InvitesController < ApplicationController before_action :member skip_before_action :authenticate_user!, only: :decline @@ -50,9 +52,9 @@ class InvitesController < ApplicationController def authenticate_user! return if current_user - notice = "To accept this invitation, sign in" - notice << " or create an account" if Gitlab::CurrentSettings.allow_signup? - notice << "." + notice = ["To accept this invitation, sign in"] + notice << "or create an account" if Gitlab::CurrentSettings.allow_signup? + notice = notice.join(' ') + "." store_location_for :user, request.fullpath redirect_to new_user_session_path, notice: notice diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb index d172aee5436..f9008a5b67e 100644 --- a/app/controllers/jwt_controller.rb +++ b/app/controllers/jwt_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JwtController < ApplicationController skip_before_action :authenticate_user! skip_before_action :verify_authenticity_token diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb index 745abf3c0f5..72aa9d4f17f 100644 --- a/app/controllers/koding_controller.rb +++ b/app/controllers/koding_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class KodingController < ApplicationController before_action :check_integration! layout 'koding' diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb index 0400ffcfee5..7353be478e1 100644 --- a/app/controllers/metrics_controller.rb +++ b/app/controllers/metrics_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MetricsController < ActionController::Base include RequiresWhitelistedMonitoringClient diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb index 461f26561f1..84dce74ace8 100644 --- a/app/controllers/notification_settings_controller.rb +++ b/app/controllers/notification_settings_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotificationSettingsController < ApplicationController before_action :authenticate_user! diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb index a1fe02dc852..9e700f648f4 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -4,7 +4,7 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include PageLayoutHelper include OauthApplications - before_action :verify_user_oauth_applications_enabled + before_action :verify_user_oauth_applications_enabled, except: :index before_action :authenticate_user! before_action :add_gon_variables before_action :load_scopes, only: [:index, :create, :edit] diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 1547d4b5972..30be50d4595 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OmniauthCallbacksController < Devise::OmniauthCallbacksController include AuthenticatesWithTwoFactor include Devise::Controllers::Rememberable @@ -135,14 +137,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController def handle_signup_error label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider']) - message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed." + message = ["Signing in using your #{label} account without a pre-existing GitLab account is not allowed."] if Gitlab::CurrentSettings.allow_signup? - message << " Create a GitLab account first, and then connect it to your #{label} account." + message << "Create a GitLab account first, and then connect it to your #{label} account." end - flash[:notice] = message - + flash[:notice] = message.join(' ') redirect_to new_user_session_path end diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb index 331583c49e6..2912a22411e 100644 --- a/app/controllers/passwords_controller.rb +++ b/app/controllers/passwords_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PasswordsController < Devise::PasswordsController skip_before_action :require_no_authentication, only: [:edit, :update] @@ -5,6 +7,7 @@ class PasswordsController < Devise::PasswordsController before_action :check_password_authentication_available, only: [:create] before_action :throttle_reset, only: [:create] + # rubocop: disable CodeReuse/ActiveRecord def edit super reset_password_token = Devise.token_generator.digest( @@ -24,6 +27,7 @@ class PasswordsController < Devise::PasswordsController end end end + # rubocop: enable CodeReuse/ActiveRecord def update super do |resource| diff --git a/app/controllers/profiles/accounts_controller.rb b/app/controllers/profiles/accounts_controller.rb index 7d1aa8d1ce0..fd9cb9fca3e 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -5,6 +5,7 @@ class Profiles::AccountsController < Profiles::ApplicationController @user = current_user end + # rubocop: disable CodeReuse/ActiveRecord def unlink provider = params[:provider] identity = current_user.identities.find_by(provider: provider) @@ -19,4 +20,5 @@ class Profiles::AccountsController < Profiles::ApplicationController redirect_to profile_account_path end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 8a38ba65d4c..00bd2040b9a 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,10 +1,12 @@ class Profiles::NotificationsController < Profiles::ApplicationController + # rubocop: disable CodeReuse/ActiveRecord def show @user = current_user @group_notifications = current_user.notification_settings.for_groups.order(:id) @project_notifications = current_user.notification_settings.for_projects.order(:id) @global_notification_setting = current_user.global_notification_setting end + # rubocop: enable CodeReuse/ActiveRecord def update result = Users::UpdateService.new(current_user, user_params.merge(user: current_user)).execute diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 346eab4ba19..b357741e3fb 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -38,6 +38,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController params.require(:personal_access_token).permit(:name, :expires_at, scopes: []) end + # rubocop: disable CodeReuse/ActiveRecord def set_index_vars @scopes = Gitlab::Auth.available_scopes(current_user) @@ -46,4 +47,5 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController @new_personal_access_token = PersonalAccessToken.redis_getdel(current_user.id) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 6f50cbb4a36..15248d2d08f 100644 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProfilesController < Profiles::ApplicationController include ActionView::Helpers::SanitizeHelper @@ -44,11 +46,13 @@ class ProfilesController < Profiles::ApplicationController redirect_to profile_personal_access_tokens_path end + # rubocop: disable CodeReuse/ActiveRecord def audit_log @events = AuditEvent.where(entity_type: "User", entity_id: current_user.id) .order("created_at DESC") .page(params[:page]) end + # rubocop: enable CodeReuse/ActiveRecord def update_username result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute @@ -94,6 +98,7 @@ class ProfilesController < Profiles::ApplicationController :location, :name, :public_email, + :commit_email, :skype, :twitter, :username, @@ -101,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController :organization, :preferred_language, :private_profile, + :include_private_contributions, status: [:emoji, :message] ) end diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 6484a713f8e..3e8ffa485dd 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -82,16 +82,20 @@ class Projects::ArtifactsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def build_from_id project.builds.find_by(id: params[:job_id]) if params[:job_id] end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def build_from_ref return unless @ref_name builds = project.latest_successful_builds_for(@ref_name) builds.find_by(name: params[:job]) end + # rubocop: enable CodeReuse/ActiveRecord def artifacts_file @artifacts_file ||= build.artifacts_file diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 56dafa31332..bfe4e7f934f 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -177,6 +177,7 @@ class Projects::BlobController < Projects::ApplicationController render_404 end + # rubocop: disable CodeReuse/ActiveRecord def after_edit_path from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid]) if from_merge_request && @branch_name == @ref @@ -186,6 +187,7 @@ class Projects::BlobController < Projects::ApplicationController project_blob_path(@project, File.join(@branch_name, @path)) end end + # rubocop: enable CodeReuse/ActiveRecord def editor_variables @branch_name = params[:branch_name] diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d1dc9fe9600..d14795e787b 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -48,6 +48,7 @@ class Projects::BranchesController < Projects::ApplicationController @branches = @repository.recent_branches end + # rubocop: disable CodeReuse/ActiveRecord def create branch_name = sanitize(strip_tags(params[:branch_name])) branch_name = Addressable::URI.unescape(branch_name) @@ -88,6 +89,7 @@ class Projects::BranchesController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def destroy @branch_name = Addressable::URI.unescape(params[:id]) diff --git a/app/controllers/projects/build_artifacts_controller.rb b/app/controllers/projects/build_artifacts_controller.rb index b45e5d7ff43..9e99a84fac7 100644 --- a/app/controllers/projects/build_artifacts_controller.rb +++ b/app/controllers/projects/build_artifacts_controller.rb @@ -42,14 +42,18 @@ class Projects::BuildArtifactsController < Projects::ApplicationController @job ||= job_from_id || job_from_ref end + # rubocop: disable CodeReuse/ActiveRecord def job_from_id project.builds.find_by(id: params[:build_id]) if params[:build_id] end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def job_from_ref return unless @ref_name jobs = project.latest_successful_builds_for(@ref_name) jobs.find_by(name: params[:job]) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index a5c82caa897..8c9df51981a 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -4,6 +4,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll before_action :authorize_read_cluster! before_action :authorize_create_cluster!, only: [:create] + # rubocop: disable CodeReuse/ActiveRecord def create application = @application_class.find_or_initialize_by(cluster: @cluster) @@ -23,6 +24,7 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll rescue StandardError head :bad_request end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb index 358fe59618b..eb0fad6cbb2 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -141,7 +141,8 @@ class Projects::ClustersController < Projects::ApplicationController :gcp_project_id, :zone, :num_nodes, - :machine_type + :machine_type, + :legacy_abac ]).merge( provider_type: :gcp, platform_type: :kubernetes @@ -157,7 +158,8 @@ class Projects::ClustersController < Projects::ApplicationController :namespace, :api_url, :token, - :ca_cert + :ca_cert, + :authorization_type ]).merge( provider_type: :user, platform_type: :kubernetes diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb index 53637780a07..81f375875b2 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -38,6 +38,7 @@ class Projects::CommitController < Projects::ApplicationController render_diff_for_path(@commit.diffs(diff_options)) end + # rubocop: disable CodeReuse/ActiveRecord def pipelines @pipelines = @commit.pipelines.order(id: :desc) @pipelines = @pipelines.where(ref: params[:ref]) if params[:ref] @@ -58,6 +59,7 @@ class Projects::CommitController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def merge_requests @merge_requests = @commit.merge_requests.map do |mr| @@ -144,6 +146,7 @@ class Projects::CommitController < Projects::ApplicationController @environment = EnvironmentsFinder.new(@project, current_user, commit: @commit).execute.last end + # rubocop: disable CodeReuse/ActiveRecord def define_note_vars @noteable = @commit @note = @project.build_commit_note(commit) @@ -176,6 +179,7 @@ class Projects::CommitController < Projects::ApplicationController @notes = (@grouped_diff_discussions.values.flatten + @discussions).flat_map(&:notes) @notes = prepare_notes_for_rendering(@notes, @commit) end + # rubocop: enable CodeReuse/ActiveRecord def assign_change_commit_vars @start_branch = params[:start_branch] diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb index 5546bef850b..cd9c9aa30f1 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -15,6 +15,7 @@ class Projects::CommitsController < Projects::ApplicationController redirect_to project_commits_path(@project, @project.default_branch) end + # rubocop: disable CodeReuse/ActiveRecord def show @merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref) @@ -32,6 +33,7 @@ class Projects::CommitsController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def signatures respond_to do |format| diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb index a1e12821caf..cca77903250 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -96,8 +96,10 @@ class Projects::CompareController < Projects::ApplicationController @diff_notes_disabled = compare.present? end + # rubocop: disable CodeReuse/ActiveRecord def merge_request @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened .find_by(source_project: @project, source_branch: head_ref, target_branch: start_ref) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 28fea322334..2555139cd2c 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -52,6 +52,7 @@ class Projects::DeployKeysController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def disable deploy_key_project = @project.deploy_keys_projects.find_by(deploy_key_id: params[:id]) return render_404 unless deploy_key_project @@ -63,6 +64,7 @@ class Projects::DeployKeysController < Projects::ApplicationController format.json { head :ok } end end + # rubocop: enable CodeReuse/ActiveRecord protected diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index b68cdc39cb8..5a2da7274d1 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -2,6 +2,7 @@ class Projects::DeploymentsController < Projects::ApplicationController before_action :authorize_read_environment! before_action :authorize_read_deployment! + # rubocop: disable CodeReuse/ActiveRecord def index deployments = environment.deployments.reorder(created_at: :desc) deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time @@ -9,6 +10,7 @@ class Projects::DeploymentsController < Projects::ApplicationController render json: { deployments: DeploymentSerializer.new(project: project) .represent_concise(deployments) } end + # rubocop: enable CodeReuse/ActiveRecord def metrics return render_404 unless deployment.has_metrics? @@ -41,9 +43,11 @@ class Projects::DeploymentsController < Projects::ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def deployment @deployment ||= environment.deployments.find_by(iid: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord def environment @environment ||= project.environments.find(params[:environment_id]) diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb index 78b9d53a780..efdddb24290 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -50,9 +50,11 @@ class Projects::DiscussionsController < Projects::ApplicationController } end + # rubocop: disable CodeReuse/ActiveRecord def merge_request @merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id]) end + # rubocop: enable CodeReuse/ActiveRecord def discussion @discussion ||= @merge_request.find_discussion(params[:id]) || render_404 diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 68353e6a210..be22950286e 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -31,6 +31,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def folder folder_environments = project.environments.where(environment_type: params[:id]) @environments = folder_environments.with_state(params[:scope] || :available) @@ -51,10 +52,13 @@ class Projects::EnvironmentsController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def show @deployments = environment.deployments.order(id: :desc).page(params[:page]) end + # rubocop: enable CodeReuse/ActiveRecord def new @environment = project.environments.new diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index f43bba18d81..b709edc8f10 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -7,6 +7,7 @@ class Projects::ForksController < Projects::ApplicationController before_action :authorize_download_code! before_action :authenticate_user!, only: [:new, :create] + # rubocop: disable CodeReuse/ActiveRecord def index base_query = project.forks.includes(:creator) @@ -27,12 +28,14 @@ class Projects::ForksController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def new @namespaces = current_user.manageable_namespaces @namespaces.delete(@project.namespace) end + # rubocop: disable CodeReuse/ActiveRecord def create namespace = Namespace.find(params[:namespace_key]) @@ -55,6 +58,7 @@ class Projects::ForksController < Projects::ApplicationController render :error end end + # rubocop: enable CodeReuse/ActiveRecord def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42335') diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c3ac8e107fb..632e498e4ba 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -161,6 +161,7 @@ class Projects::IssuesController < Projects::ApplicationController protected + # rubocop: disable CodeReuse/ActiveRecord def issue return @issue if defined?(@issue) @@ -172,6 +173,7 @@ class Projects::IssuesController < Projects::ApplicationController @issue end + # rubocop: enable CodeReuse/ActiveRecord alias_method :subscribable_resource, :issue alias_method :issuable, :issue alias_method :awardable, :issue diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb index e69faae754a..62b74e84c2c 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -11,6 +11,7 @@ class Projects::JobsController < Projects::ApplicationController layout 'project' + # rubocop: disable CodeReuse/ActiveRecord def index @scope = params[:scope] @all_builds = project.builds.relevant @@ -33,6 +34,7 @@ class Projects::JobsController < Projects::ApplicationController ]) @builds = @builds.page(params[:page]).per(30).without_count end + # rubocop: enable CodeReuse/ActiveRecord def cancel_all return access_denied! unless can?(current_user, :update_build, project) @@ -44,6 +46,7 @@ class Projects::JobsController < Projects::ApplicationController redirect_to project_jobs_path(project) end + # rubocop: disable CodeReuse/ActiveRecord def show @pipeline = @build.pipeline @builds = @pipeline.builds @@ -61,6 +64,7 @@ class Projects::JobsController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def trace build.trace.read do |stream| diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 8a2bce6e7b5..1fd4f0721a7 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -90,6 +90,7 @@ class Projects::LabelsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def set_priorities Label.transaction do available_labels_ids = @available_labels.where(id: params[:label_ids]).pluck(:id) @@ -105,6 +106,7 @@ class Projects::LabelsController < Projects::ApplicationController format.json { render json: { message: 'success' } } end end + # rubocop: enable CodeReuse/ActiveRecord def promote promote_service = Labels::PromoteService.new(@project, @current_user) @@ -163,7 +165,12 @@ class Projects::LabelsController < Projects::ApplicationController LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups], - search: params[:search]).execute + search: params[:search], + sort: sort).execute + end + + def sort + @sort ||= params[:sort] || 'name_asc' end def authorize_admin_labels! diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb index a01351ba292..6d6f88c1075 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -41,11 +41,13 @@ class Projects::LfsApiController < Projects::GitHttpClientController params[:operation] == 'upload' end + # rubocop: disable CodeReuse/ActiveRecord def existing_oids @existing_oids ||= begin project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid) end end + # rubocop: enable CodeReuse/ActiveRecord def download_objects! objects.each do |object| diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index dd7e673ec75..930d9a05c50 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -56,6 +56,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController params[:size].to_i end + # rubocop: disable CodeReuse/ActiveRecord def store_file!(oid, size) object = LfsObject.find_by(oid: oid, size: size) unless object&.file&.exists? @@ -66,6 +67,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController link_to_project!(object) end + # rubocop: enable CodeReuse/ActiveRecord def create_file!(oid, size) uploaded_file = UploadedFile.from_params( @@ -75,9 +77,11 @@ class Projects::LfsStorageController < Projects::GitHttpClientController LfsObject.create!(oid: oid, size: size, file: uploaded_file) end + # rubocop: disable CodeReuse/ActiveRecord def link_to_project!(object) if object && !object.projects.exists?(storage_project.id) object.lfs_objects_projects.create!(project: storage_project) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index fead81dd472..aa2008722ec 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -5,9 +5,11 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont private + # rubocop: disable CodeReuse/ActiveRecord def merge_request @issuable = @merge_request ||= @project.merge_requests.includes(author: :status).find_by!(iid: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord def merge_request_params params.require(:merge_request).permit(merge_request_params_attributes) diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 03d0290ac1d..2ccb3896857 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -109,6 +109,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap set_pipeline_variables end + # rubocop: disable CodeReuse/ActiveRecord def selected_target_project if @project.id.to_s == params[:target_project_id] || !@project.forked? @project @@ -119,6 +120,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @project.forked_from_project end end + # rubocop: enable CodeReuse/ActiveRecord def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42384') diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 48e02581d54..666e65b6c5e 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -21,6 +21,8 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def render_diffs @environment = @merge_request.environments_for(current_user).last + @diffs.write_cache + render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes) end @@ -32,13 +34,16 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic @diffs = @compare.diffs(diff_options) end + # rubocop: disable CodeReuse/ActiveRecord def commit return nil unless commit_id = params[:commit_id].presence return nil unless @merge_request.all_commits.exists?(sha: commit_id) @commit ||= @project.commit(commit_id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def find_merge_request_diff_compare @merge_request_diff = if diff_id = params[:diff_id].presence @@ -66,6 +71,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic @merge_request_diff end end + # rubocop: enable CodeReuse/ActiveRecord def additional_attributes { diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index d31b58972ca..75a85fafa3f 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -330,6 +330,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo @source_project = @merge_request.source_project @target_project = @merge_request.target_project @target_branches = @merge_request.target_project.repository.branch_names + @noteable = @merge_request end def finder_type diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b9b3dcd5a85..e2c05171cd6 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -118,9 +118,11 @@ class Projects::MilestonesController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def milestone @milestone ||= @project.milestones.find_by!(iid: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord def authorize_admin_milestone! return render_404 unless can?(current_user, :admin_milestone, @project) diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index ff49911d892..e1eba4f8327 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -5,9 +5,11 @@ class Projects::PagesController < Projects::ApplicationController before_action :authorize_read_pages!, only: [:show] before_action :authorize_update_pages!, except: [:show] + # rubocop: disable CodeReuse/ActiveRecord def show @domains = @project.pages_domains.order(:domain) end + # rubocop: enable CodeReuse/ActiveRecord def destroy project.remove_pages diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb index 4856be61e88..c29b3c953a6 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -70,7 +70,9 @@ class Projects::PagesDomainsController < Projects::ApplicationController params.require(:pages_domain).permit(:key, :certificate) end + # rubocop: disable CodeReuse/ActiveRecord def domain @domain ||= @project.pages_domains.find_by!(domain: params[:id].to_s) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/projects/pipeline_schedules_controller.rb b/app/controllers/projects/pipeline_schedules_controller.rb index aeda7b3edf5..d8adeffd0b2 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -8,12 +8,14 @@ class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :authorize_update_pipeline_schedule!, except: [:index, :new, :create, :play] before_action :authorize_admin_pipeline_schedule!, only: [:destroy] + # rubocop: disable CodeReuse/ActiveRecord def index @scope = params[:scope] @all_schedules = PipelineSchedulesFinder.new(@project).execute @schedules = PipelineSchedulesFinder.new(@project).execute(scope: params[:scope]) .includes(:last_pipeline) end + # rubocop: enable CodeReuse/ActiveRecord def new @schedule = project.pipeline_schedules.new diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb index b5db646bf57..5b2091d68f8 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -96,7 +96,7 @@ class Projects::PipelinesController < Projects::ApplicationController render json: StageSerializer .new(project: @project, current_user: @current_user) - .represent(@stage, details: true) + .represent(@stage, details: true, retried: params[:retried]) end # TODO: This endpoint is used by mini-pipeline-graph @@ -159,6 +159,7 @@ class Projects::PipelinesController < Projects::ApplicationController params.require(:pipeline).permit(:ref, variables_attributes: %i[key secret_value]) end + # rubocop: disable CodeReuse/ActiveRecord def pipeline @pipeline ||= project .pipelines @@ -166,6 +167,7 @@ class Projects::PipelinesController < Projects::ApplicationController .find_by!(id: params[:id]) .present(current_user: current_user) end + # rubocop: enable CodeReuse/ActiveRecord def whitelist_query_limiting # Also see https://gitlab.com/gitlab-org/gitlab-ce/issues/42343 diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cfa5e72af64..08d5e377941 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -6,6 +6,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController # Authorize before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access] + # rubocop: disable CodeReuse/ActiveRecord def index @sort = params[:sort].presence || sort_value_name @group_links = @project.project_group_links @@ -25,6 +26,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user)) @project_member = @project.project_members.new end + # rubocop: enable CodeReuse/ActiveRecord def import @projects = current_user.authorized_projects.order_id_desc diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 48a09e1ddb8..0fed7f6576c 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -36,54 +36,47 @@ class Projects::RefsController < Projects::ApplicationController end def logs_tree - @offset = if params[:offset].present? - params[:offset].to_i - else - 0 - end + summary = ::Gitlab::TreeSummary.new( + @commit, + @project, + path: @path, + offset: params[:offset], + limit: 25 + ) - @limit = 25 - - @path = params[:path] - - contents = [] - contents.push(*tree.trees) - contents.push(*tree.blobs) - contents.push(*tree.submodules) - - # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37433 - @logs = Gitlab::GitalyClient.allow_n_plus_1_calls do - contents[@offset, @limit].to_a.map do |content| - file = @path ? File.join(@path, content.name) : content.name - last_commit = @repo.last_commit_for_path(@commit.id, file) - commit_path = project_commit_path(@project, last_commit) if last_commit - { - file_name: content.name, - commit: last_commit, - type: content.type, - commit_path: commit_path - } - end - end - - offset = (@offset + @limit) - if contents.size > offset - @more_log_url = logs_file_project_ref_path(@project, @ref, @path || '', offset: offset) - end + @logs, commits = summary.summarize + @more_log_url = more_url(summary.next_offset) if summary.more? respond_to do |format| format.html { render_404 } format.json do - response.headers["More-Logs-Url"] = @more_log_url - + response.headers["More-Logs-Url"] = @more_log_url if summary.more? render json: @logs end - format.js + + # The commit titles must be rendered and redacted before being shown. + # Doing it here allows us to apply performance optimizations that avoid + # N+1 problems + format.js do + prerender_commit_full_titles!(commits) + end end end private + def more_url(offset) + logs_file_project_ref_path(@project, @ref, @path, offset: offset) + end + + def prerender_commit_full_titles!(commits) + # Preload commit authors as they are used in rendering + commits.each(&:lazy_author) + + renderer = Banzai::ObjectRenderer.new(user: current_user, default_project: @project) + renderer.render(commits, :full_title) + end + def validate_ref_id return not_found! if params[:id].present? && params[:id] !~ Gitlab::PathRegex.git_reference_regex end diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 32c0fc6d14a..ef0433795f4 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -18,14 +18,10 @@ module Projects end def destroy - if image.destroy - respond_to do |format| - format.json { head :no_content } - end - else - respond_to do |format| - format.json { head :bad_request } - end + DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id) + + respond_to do |format| + format.json { head :no_content } end end @@ -41,10 +37,10 @@ module Projects # Needed to maintain a backwards compatibility. # def ensure_root_container_repository! - ContainerRegistry::Path.new(@project.full_path).tap do |path| + ::ContainerRegistry::Path.new(@project.full_path).tap do |path| break if path.has_repository? - ContainerRepository.build_from_path(path).tap do |repository| + ::ContainerRepository.build_from_path(path).tap do |repository| repository.save! if repository.has_tags? end end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 19e09b3af6f..caf400ecd92 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -28,9 +28,11 @@ class Projects::ReleasesController < Projects::ApplicationController @tag ||= @repository.find_tag(params[:tag_id]) end + # rubocop: disable CodeReuse/ActiveRecord def release @release ||= @project.releases.find_or_initialize_by(tag: @tag.name) end + # rubocop: enable CodeReuse/ActiveRecord def release_params params.require(:release).permit(:description) diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index 4697af4f26a..ccd481b4dbd 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -31,6 +31,7 @@ module Projects render 'show' end + # rubocop: disable CodeReuse/ActiveRecord def define_protected_refs @protected_branches = @project.protected_branches.order(:name).page(params[:page]) @protected_tags = @project.protected_tags.order(:name).page(params[:page]) @@ -42,6 +43,7 @@ module Projects load_gon_index end + # rubocop: enable CodeReuse/ActiveRecord def remote_mirror @remote_mirror = project.remote_mirrors.first_or_initialize diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index 7f2c3ca38ad..74bba97987f 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -7,6 +7,7 @@ class Projects::TagsController < Projects::ApplicationController before_action :authorize_push_code!, only: [:new, :create] before_action :authorize_admin_project!, only: [:destroy] + # rubocop: disable CodeReuse/ActiveRecord def index params[:sort] = params[:sort].presence || sort_value_recently_updated @@ -23,7 +24,9 @@ class Projects::TagsController < Projects::ApplicationController format.atom { render layout: 'xml.atom' } end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def show @tag = @repository.find_tag(params[:id]) @@ -32,6 +35,7 @@ class Projects::TagsController < Projects::ApplicationController @release = @project.releases.find_or_initialize_by(tag: @tag.name) @commit = @repository.commit(@tag.dereferenced_target) end + # rubocop: enable CodeReuse/ActiveRecord def create result = Tags::CreateService.new(@project, current_user) diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 0eaf9f94e37..7352c5e9bec 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ProjectsController < Projects::ApplicationController include API::Helpers::RelatedResourcesHelpers include IssuableCollections @@ -25,12 +27,14 @@ class ProjectsController < Projects::ApplicationController redirect_to(current_user ? root_path : explore_root_path) end + # rubocop: disable CodeReuse/ActiveRecord def new namespace = Namespace.find_by(id: params[:namespace_id]) if params[:namespace_id] return access_denied! if namespace && !can?(current_user, :create_projects, namespace) @project = Project.new(namespace_id: namespace&.id) end + # rubocop: enable CodeReuse/ActiveRecord def edit @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) @@ -75,6 +79,7 @@ class ProjectsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def transfer return access_denied! unless can?(current_user, :change_namespace, @project) @@ -85,6 +90,7 @@ class ProjectsController < Projects::ApplicationController flash[:alert] = @project.errors[:new_namespace].first end end + # rubocop: enable CodeReuse/ActiveRecord def remove_fork return access_denied! unless can?(current_user, :remove_fork_project, @project) @@ -191,10 +197,8 @@ class ProjectsController < Projects::ApplicationController end def download_export - if export_project_object_storage? - send_upload(@project.import_export_upload.export_file) - elsif export_project_path - send_file export_project_path, disposition: 'attachment' + if @project.export_file_exists? + send_upload(@project.export_file, attachment: @project.export_file.filename) else redirect_to( edit_project_path(@project, anchor: 'js-export-project'), @@ -233,6 +237,7 @@ class ProjectsController < Projects::ApplicationController } end + # rubocop: disable CodeReuse/ActiveRecord def refs find_refs = params['find'] @@ -267,6 +272,7 @@ class ProjectsController < Projects::ApplicationController render json: options.to_json end + # rubocop: enable CodeReuse/ActiveRecord # Render project landing depending of which features are available # So if page is not availble in the list it renders the next page @@ -305,6 +311,7 @@ class ProjectsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def load_events projects = Project.where(id: @project.id) @@ -314,6 +321,7 @@ class ProjectsController < Projects::ApplicationController Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?) end + # rubocop: enable CodeReuse/ActiveRecord def project_params params.require(:project) @@ -425,12 +433,4 @@ class ProjectsController < Projects::ApplicationController def whitelist_query_limiting Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42440') end - - def export_project_path - @export_project_path ||= @project.export_project_path - end - - def export_project_object_storage? - @project.export_project_object_exists? - end end diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index e6d6965036e..8b8d87524a8 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify include AcceptsPendingInvitations diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb index 651b82f04f4..ebf70f25bda 100644 --- a/app/controllers/root_controller.rb +++ b/app/controllers/root_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # RootController # # This controller exists solely to handle requests to `root_url`. When a user is diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 983f888b8ec..1b22907c10f 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SearchController < ApplicationController include ControllerWithCrossProjectAccessCheck include SearchHelper @@ -31,6 +33,7 @@ class SearchController < ApplicationController check_single_commit_result end + # rubocop: disable CodeReuse/ActiveRecord def autocomplete term = params[:term] @@ -43,6 +46,7 @@ class SearchController < ApplicationController render json: search_autocomplete_opts(term).to_json end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/controllers/sent_notifications_controller.rb b/app/controllers/sent_notifications_controller.rb index 93a71103a09..2b76921ebd8 100644 --- a/app/controllers/sent_notifications_controller.rb +++ b/app/controllers/sent_notifications_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SentNotificationsController < ApplicationController skip_before_action :authenticate_user! diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index ab8e2e35b98..643eb75c83c 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SessionsController < Devise::SessionsController include InternalRedirect include AuthenticatesWithTwoFactor @@ -107,6 +109,7 @@ class SessionsController < Devise::SessionsController # Handle an "initial setup" state, where there's only one user, it's an admin, # and they require a password change. + # rubocop: disable CodeReuse/ActiveRecord def check_initial_setup return unless User.limit(2).count == 1 # Count as much 2 to know if we have exactly one @@ -121,6 +124,7 @@ class SessionsController < Devise::SessionsController redirect_to edit_user_password_path(reset_password_token: @token), notice: "Please create a password for your new account." end + # rubocop: enable CodeReuse/ActiveRecord def user_params params.require(:user).permit(:login, :password, :remember_me, :otp_attempt, :device_response) diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index 217da89a1fd..e992afc0026 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -17,9 +17,11 @@ class Snippets::NotesController < ApplicationController nil end + # rubocop: disable CodeReuse/ActiveRecord def snippet PersonalSnippet.find_by(id: params[:snippet_id]) end + # rubocop: enable CodeReuse/ActiveRecord alias_method :noteable, :snippet def note_params diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index dcf18c1f751..694c3a59e2b 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class SnippetsController < ApplicationController include RendersNotes include ToggleAwardEmoji @@ -24,6 +26,7 @@ class SnippetsController < ApplicationController layout 'snippets' respond_to :html + # rubocop: disable CodeReuse/ActiveRecord def index if params[:username].present? @user = User.find_by(username: params[:username]) @@ -38,6 +41,7 @@ class SnippetsController < ApplicationController redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path) end end + # rubocop: enable CodeReuse/ActiveRecord def new @snippet = PersonalSnippet.new @@ -94,9 +98,11 @@ class SnippetsController < ApplicationController protected + # rubocop: disable CodeReuse/ActiveRecord def snippet @snippet ||= PersonalSnippet.inc_relations_for_view.find_by(id: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord alias_method :awardable, :snippet alias_method :spammable, :snippet diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 3d227b0a955..fa5d84633b5 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UploadsController < ApplicationController include UploadsActions diff --git a/app/controllers/user_callouts_controller.rb b/app/controllers/user_callouts_controller.rb index 18cde4a7b1a..ebf1dd8ca02 100644 --- a/app/controllers/user_callouts_controller.rb +++ b/app/controllers/user_callouts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UserCalloutsController < ApplicationController def create if ensure_callout.persisted? @@ -13,9 +15,11 @@ class UserCalloutsController < ApplicationController private + # rubocop: disable CodeReuse/ActiveRecord def ensure_callout current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name]) end + # rubocop: enable CodeReuse/ActiveRecord def feature_name params.require(:feature_name) diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2f65f4a7403..e509098d778 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class UsersController < ApplicationController include RoutableActions include RendersMemberAccess diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb index b6ee49df99b..2cc8a978877 100644 --- a/app/finders/access_requests_finder.rb +++ b/app/finders/access_requests_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AccessRequestsFinder attr_accessor :source diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb index 543bf1a1415..e2b9b0b44c1 100644 --- a/app/finders/admin/projects_finder.rb +++ b/app/finders/admin/projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Admin::ProjectsFinder attr_reader :params, :current_user @@ -6,6 +8,7 @@ class Admin::ProjectsFinder @current_user = current_user end + # rubocop: disable CodeReuse/ActiveRecord def execute items = Project.without_deleted.with_statistics.with_route items = by_namespace_id(items) @@ -19,6 +22,7 @@ class Admin::ProjectsFinder items = items.includes(namespace: [:owner, :route]) sort(items).page(params[:page]) end + # rubocop: enable CodeReuse/ActiveRecord private @@ -26,9 +30,11 @@ class Admin::ProjectsFinder params[:namespace_id].present? ? items.in_namespace(params[:namespace_id]) : items end + # rubocop: disable CodeReuse/ActiveRecord def by_visibilty_level(items) params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items end + # rubocop: enable CodeReuse/ActiveRecord def by_with_push(items) params[:with_push].present? ? items.with_push : items @@ -38,9 +44,11 @@ class Admin::ProjectsFinder params[:abandoned].present? ? items.abandoned : items end + # rubocop: disable CodeReuse/ActiveRecord def by_last_repository_check_failed(items) params[:last_repository_check_failed].present? ? items.where(last_repository_check_failed: true) : items end + # rubocop: enable CodeReuse/ActiveRecord def by_archived(items) if params[:archived] == 'only' diff --git a/app/finders/admin/runners_finder.rb b/app/finders/admin/runners_finder.rb new file mode 100644 index 00000000000..3c2d7ee7d76 --- /dev/null +++ b/app/finders/admin/runners_finder.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +class Admin::RunnersFinder < UnionFinder + NUMBER_OF_RUNNERS_PER_PAGE = 30 + + def initialize(params:) + @params = params + end + + def execute + search! + filter_by_status! + sort! + paginate! + + @runners + end + + def sort_key + if @params[:sort] == 'contacted_asc' + 'contacted_asc' + else + 'created_date' + end + end + + private + + def search! + @runners = + if @params[:search].present? + Ci::Runner.search(@params[:search]) + else + Ci::Runner.all + end + end + + def filter_by_status! + status = @params[:status_status] + if status.present? && Ci::Runner::AVAILABLE_STATUSES.include?(status) + @runners = @runners.public_send(status) # rubocop:disable GitlabSecurity/PublicSend + end + end + + def sort! + @runners = @runners.order_by(sort_key) + end + + def paginate! + @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) + end +end diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb index b2557469079..e2283f3266e 100644 --- a/app/finders/autocomplete/users_finder.rb +++ b/app/finders/autocomplete/users_finder.rb @@ -44,6 +44,7 @@ module Autocomplete # Returns the users based on the input parameters, as an Array. # # This method is separate so it is easier to extend in EE. + # rubocop: disable CodeReuse/ActiveRecord def limited_users # When changing the order of these method calls, make sure that # reorder_by_name() is called _before_ optionally_search(), otherwise @@ -61,6 +62,7 @@ module Autocomplete .limit(LIMIT) .to_a end + # rubocop: enable CodeReuse/ActiveRecord def prepend_current_user? filter_by_current_user.present? && current_user @@ -70,6 +72,7 @@ module Autocomplete author_id.present? && current_user end + # rubocop: disable CodeReuse/ActiveRecord def find_users if project project.authorized_users.union_with_user(author_id) @@ -81,5 +84,6 @@ module Autocomplete User.none end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb index 8bb1366867c..970efa79dfb 100644 --- a/app/finders/branches_finder.rb +++ b/app/finders/branches_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class BranchesFinder def initialize(repository, params = {}) @repository = repository diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb index c13f98257bf..b40d6c41b71 100644 --- a/app/finders/clusters_finder.rb +++ b/app/finders/clusters_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ClustersFinder def initialize(project, user, scope) @project = project diff --git a/app/finders/concerns/created_at_filter.rb b/app/finders/concerns/created_at_filter.rb index ac9ac77732c..6b5863a5c53 100644 --- a/app/finders/concerns/created_at_filter.rb +++ b/app/finders/concerns/created_at_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CreatedAtFilter def by_created_at(items) items = items.created_before(params[:created_before]) if params[:created_before].present? diff --git a/app/finders/concerns/custom_attributes_filter.rb b/app/finders/concerns/custom_attributes_filter.rb index 5bbf9ca242d..825c3a6b5b7 100644 --- a/app/finders/concerns/custom_attributes_filter.rb +++ b/app/finders/concerns/custom_attributes_filter.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module CustomAttributesFilter + # rubocop: disable CodeReuse/ActiveRecord def by_custom_attributes(items) return items unless params[:custom_attributes].is_a?(Hash) return items unless Ability.allowed?(current_user, :read_custom_attribute) @@ -17,4 +20,5 @@ module CustomAttributesFilter scope.where('EXISTS (?)', custom_attributes.where(key: key, value: value)) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/concerns/finder_methods.rb b/app/finders/concerns/finder_methods.rb index 2e905fa5750..5290313585f 100644 --- a/app/finders/concerns/finder_methods.rb +++ b/app/finders/concerns/finder_methods.rb @@ -1,11 +1,17 @@ +# frozen_string_literal: true + module FinderMethods + # rubocop: disable CodeReuse/ActiveRecord def find_by!(*args) raise_not_found_unless_authorized execute.find_by!(*args) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def find_by(*args) if_authorized execute.find_by(*args) end + # rubocop: enable CodeReuse/ActiveRecord def find(*args) raise_not_found_unless_authorized model.find(*args) diff --git a/app/finders/concerns/finder_with_cross_project_access.rb b/app/finders/concerns/finder_with_cross_project_access.rb index 92bf98d7cd2..e038636f0c4 100644 --- a/app/finders/concerns/finder_with_cross_project_access.rb +++ b/app/finders/concerns/finder_with_cross_project_access.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Module to prepend into finders to specify wether or not the finder requires # cross project access # @@ -14,6 +16,7 @@ module FinderWithCrossProjectAccess end override :execute + # rubocop: disable CodeReuse/ActiveRecord def execute(*args) check = Gitlab::CrossProjectAccess.find_check(self) original = super @@ -27,6 +30,7 @@ module FinderWithCrossProjectAccess original end end + # rubocop: enable CodeReuse/ActiveRecord # We can skip the cross project check for finding indivitual records. # this would be handled by the `can?(:read_*, result)` call in `FinderMethods` diff --git a/app/finders/contributed_projects_finder.rb b/app/finders/contributed_projects_finder.rb index a685719555c..c1ef9dfefa7 100644 --- a/app/finders/contributed_projects_finder.rb +++ b/app/finders/contributed_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ContributedProjectsFinder < UnionFinder def initialize(user) @user = user @@ -10,11 +12,13 @@ class ContributedProjectsFinder < UnionFinder # visible by this user. # # Returns an ActiveRecord::Relation. + # rubocop: disable CodeReuse/ActiveRecord def execute(current_user = nil) segments = all_projects(current_user) find_union(segments, Project).includes(:namespace).order_id_desc end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/environments_finder.rb b/app/finders/environments_finder.rb index a59f8c1efa3..419be46fafe 100644 --- a/app/finders/environments_finder.rb +++ b/app/finders/environments_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EnvironmentsFinder attr_reader :project, :current_user, :params @@ -5,6 +7,7 @@ class EnvironmentsFinder @project, @current_user, @params = project, current_user, params end + # rubocop: disable CodeReuse/ActiveRecord def execute deployments = project.deployments deployments = @@ -42,6 +45,7 @@ class EnvironmentsFinder environments end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index 8676925a540..fd7aeca0d8b 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class EventsFinder prepend FinderMethods prepend FinderWithCrossProjectAccess @@ -36,32 +38,42 @@ class EventsFinder private + # rubocop: disable CodeReuse/ActiveRecord def by_current_user_access(events) - events.merge(ProjectsFinder.new(current_user: current_user).execute) + events.merge(ProjectsFinder.new(current_user: current_user).execute) # rubocop: disable CodeReuse/Finder .joins(:project) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_action(events) return events unless Event::ACTIONS[params[:action]] events.where(action: Event::ACTIONS[params[:action]]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_target_type(events) return events unless Event::TARGET_TYPES[params[:target_type]] events.where(target_type: Event::TARGET_TYPES[params[:target_type]]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_created_at_before(events) return events unless params[:before] events.where('events.created_at < ?', params[:before].beginning_of_day) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_created_at_after(events) return events unless params[:after] events.where('events.created_at > ?', params[:after].end_of_day) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/fork_projects_finder.rb b/app/finders/fork_projects_finder.rb index 28d1b31868e..03ace7e8057 100644 --- a/app/finders/fork_projects_finder.rb +++ b/app/finders/fork_projects_finder.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + class ForkProjectsFinder < ProjectsFinder + # rubocop: disable CodeReuse/ActiveRecord def initialize(project, params: {}, current_user: nil) project_ids = project.forks.includes(:creator).select(:id) super(params: params, current_user: current_user, project_ids_relation: project_ids) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb index 051ea108e06..9d57d2d3bc9 100644 --- a/app/finders/group_descendants_finder.rb +++ b/app/finders/group_descendants_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupDescendantsFinder # # Used to find and filter all subgroups and projects of a passed parent group @@ -61,12 +63,16 @@ class GroupDescendantsFinder end def direct_child_groups + # rubocop: disable CodeReuse/Finder GroupsFinder.new(current_user, parent: parent_group, all_available: true).execute + # rubocop: enable CodeReuse/Finder end + # rubocop: disable CodeReuse/ActiveRecord def all_visible_descendant_groups + # rubocop: disable CodeReuse/Finder groups_table = Group.arel_table visible_to_user = groups_table[:visibility_level] .in(Gitlab::VisibilityLevel.levels_for_user(current_user)) @@ -84,7 +90,9 @@ class GroupDescendantsFinder hierarchy_for_parent .descendants .where(visible_to_user) + # rubocop: enable CodeReuse/Finder end + # rubocop: enable CodeReuse/ActiveRecord def subgroups_matching_filter all_visible_descendant_groups @@ -101,24 +109,29 @@ class GroupDescendantsFinder # # So when searching 'project', on the 'subgroup' page we want to preload # 'nested-group' but not 'subgroup' or 'root' + # rubocop: disable CodeReuse/ActiveRecord def ancestors_of_groups(base_for_ancestors) group_ids = base_for_ancestors.except(:select, :sort).select(:id) Gitlab::GroupHierarchy.new(Group.where(id: group_ids)) .base_and_ancestors(upto: parent_group.id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def ancestors_of_filtered_projects projects_to_load_ancestors_of = projects.where.not(namespace: parent_group) groups_to_load_ancestors_of = Group.where(id: projects_to_load_ancestors_of.select(:namespace_id)) ancestors_of_groups(groups_to_load_ancestors_of) .with_selects_for_list(archived: params[:archived]) end + # rubocop: enable CodeReuse/ActiveRecord def ancestors_of_filtered_subgroups ancestors_of_groups(subgroups) .with_selects_for_list(archived: params[:archived]) end + # rubocop: disable CodeReuse/ActiveRecord def subgroups return Group.none unless Group.supports_nested_groups? @@ -132,22 +145,29 @@ class GroupDescendantsFinder groups.with_selects_for_list(archived: params[:archived]).order_by(sort) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/Finder def direct_child_projects - GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params) + GroupProjectsFinder.new(group: parent_group, current_user: current_user, params: params, options: { only_owned: true }) .execute end + # rubocop: enable CodeReuse/Finder # Finds all projects nested under `parent_group` or any of its descendant # groups + # rubocop: disable CodeReuse/ActiveRecord def projects_matching_filter + # rubocop: disable CodeReuse/Finder projects_nested_in_group = Project.where(namespace_id: hierarchy_for_parent.base_and_descendants.select(:id)) params_with_search = params.merge(search: params[:filter]) ProjectsFinder.new(params: params_with_search, current_user: current_user, project_ids_relation: projects_nested_in_group).execute + # rubocop: enable CodeReuse/Finder end + # rubocop: enable CodeReuse/ActiveRecord def projects projects = if params[:filter] @@ -163,7 +183,9 @@ class GroupDescendantsFinder params.fetch(:sort, 'id_asc') end + # rubocop: disable CodeReuse/ActiveRecord def hierarchy_for_parent @hierarchy ||= Gitlab::GroupHierarchy.new(Group.where(id: parent_group.id)) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/group_finder.rb b/app/finders/group_finder.rb index 24c84d2d1aa..d2ad8a372b1 100644 --- a/app/finders/group_finder.rb +++ b/app/finders/group_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GroupFinder include Gitlab::Allowable @@ -5,6 +7,7 @@ class GroupFinder @current_user = current_user end + # rubocop: disable CodeReuse/ActiveRecord def execute(*params) group = Group.find_by(*params) @@ -14,4 +17,5 @@ class GroupFinder nil end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/group_labels_finder.rb b/app/finders/group_labels_finder.rb new file mode 100644 index 00000000000..903023033ed --- /dev/null +++ b/app/finders/group_labels_finder.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class GroupLabelsFinder + attr_reader :group, :params + + def initialize(group, params = {}) + @group = group + @params = params + end + + def execute + group.labels + .optionally_search(params[:search]) + .order_by(params[:sort]) + .page(params[:page]) + end +end diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb index 2a656c0d31c..eebc67cfa9e 100644 --- a/app/finders/group_members_finder.rb +++ b/app/finders/group_members_finder.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + class GroupMembersFinder def initialize(group) @group = group end + # rubocop: disable CodeReuse/ActiveRecord def execute(include_descendants: false) group_members = @group.members wheres = [] @@ -29,4 +32,5 @@ class GroupMembersFinder GroupMember.where(wheres.join(' OR ')) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb index b6bdb2b7b0f..4155b6af8da 100644 --- a/app/finders/group_projects_finder.rb +++ b/app/finders/group_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupProjectsFinder # # Used to filter Projects by set of params @@ -82,6 +84,7 @@ class GroupProjectsFinder < ProjectsFinder options.fetch(:include_subgroups, false) end + # rubocop: disable CodeReuse/ActiveRecord def owned_projects if include_subgroups? Project.where(namespace_id: group.self_and_descendants.select(:id)) @@ -89,6 +92,7 @@ class GroupProjectsFinder < ProjectsFinder group.projects end end + # rubocop: enable CodeReuse/ActiveRecord def shared_projects group.shared_projects diff --git a/app/finders/groups_finder.rb b/app/finders/groups_finder.rb index 0eeba1d2428..a35a3ed6142 100644 --- a/app/finders/groups_finder.rb +++ b/app/finders/groups_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # GroupsFinder # # Used to filter Groups by a set of params @@ -38,6 +40,7 @@ class GroupsFinder < UnionFinder attr_reader :current_user, :params + # rubocop: disable CodeReuse/ActiveRecord def all_groups return [owned_groups] if params[:owned] return [groups_with_min_access_level] if min_access_level? @@ -49,6 +52,7 @@ class GroupsFinder < UnionFinder groups << Group.none if groups.empty? groups end + # rubocop: enable CodeReuse/ActiveRecord def groups_for_ancestors current_user.authorized_groups @@ -58,6 +62,7 @@ class GroupsFinder < UnionFinder current_user.groups end + # rubocop: disable CodeReuse/ActiveRecord def groups_with_min_access_level groups = current_user .groups @@ -67,16 +72,21 @@ class GroupsFinder < UnionFinder .new(groups) .base_and_descendants end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_parent(groups) return groups unless params[:parent] groups.where(parent: params[:parent]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def owned_groups current_user&.owned_groups || Group.none end + # rubocop: enable CodeReuse/ActiveRecord def include_public_groups? current_user.nil? || all_available? diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index 372e2a96c2c..251a559878a 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # IssuableFinder # # Used to filter Issues and MergeRequests collections by set of params @@ -109,6 +111,7 @@ class IssuableFinder # (even if that query is slower than any of the individual state queries) and # grouping and counting within that query. # + # rubocop: disable CodeReuse/ActiveRecord def count_by_state count_params = params.merge(state: nil, sort: nil) finder = self.class.new(current_user, count_params) @@ -132,6 +135,7 @@ class IssuableFinder counts.with_indifferent_access end + # rubocop: enable CodeReuse/ActiveRecord def group return @group if defined?(@group) @@ -157,6 +161,7 @@ class IssuableFinder @project = project end + # rubocop: disable CodeReuse/ActiveRecord def projects(items = nil) return @projects = project if project? @@ -165,13 +170,14 @@ class IssuableFinder current_user.authorized_projects elsif group finder_options = { include_subgroups: params[:include_subgroups], only_owned: true } - GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute + GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder else - ProjectsFinder.new(current_user: current_user).execute + ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder end @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) end + # rubocop: enable CodeReuse/ActiveRecord def search params[:search].presence @@ -185,6 +191,7 @@ class IssuableFinder milestones? && params[:milestone_title] == Milestone::None.title end + # rubocop: disable CodeReuse/ActiveRecord def milestones return @milestones if defined?(@milestones) @@ -200,11 +207,12 @@ class IssuableFinder search_params = { title: params[:milestone_title], project_ids: project_id, group_ids: group_id } - MilestonesFinder.new(search_params).execute + MilestonesFinder.new(search_params).execute # rubocop: disable CodeReuse/Finder else Milestone.none end end + # rubocop: enable CodeReuse/ActiveRecord def labels? params[:label_name].present? @@ -214,16 +222,18 @@ class IssuableFinder labels? && params[:label_name].include?(Label::None.title) end + # rubocop: disable CodeReuse/ActiveRecord def labels return @labels if defined?(@labels) @labels = if labels? && !filter_by_no_label? - LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) + LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true) # rubocop: disable CodeReuse/Finder else Label.none end end + # rubocop: enable CodeReuse/ActiveRecord def assignee_id? params[:assignee_id].present? && params[:assignee_id] != NONE @@ -238,6 +248,7 @@ class IssuableFinder params[:assignee_id] == NONE || params[:assignee_username] == NONE end + # rubocop: disable CodeReuse/ActiveRecord def assignee return @assignee if defined?(@assignee) @@ -250,6 +261,7 @@ class IssuableFinder nil end end + # rubocop: enable CodeReuse/ActiveRecord def author_id? params[:author_id].present? && params[:author_id] != NONE @@ -264,6 +276,7 @@ class IssuableFinder params[:author_id] == NONE || params[:author_username] == NONE end + # rubocop: disable CodeReuse/ActiveRecord def author return @author if defined?(@author) @@ -276,6 +289,7 @@ class IssuableFinder nil end end + # rubocop: enable CodeReuse/ActiveRecord private @@ -283,6 +297,7 @@ class IssuableFinder klass.all end + # rubocop: disable CodeReuse/ActiveRecord def by_scope(items) return items.none if current_user_related? && !current_user @@ -295,6 +310,7 @@ class IssuableFinder items end end + # rubocop: enable CodeReuse/ActiveRecord def by_updated_at(items) items = items.updated_after(params[:updated_after]) if params[:updated_after].present? @@ -303,6 +319,7 @@ class IssuableFinder items end + # rubocop: disable CodeReuse/ActiveRecord def by_state(items) case params[:state].to_s when 'closed' @@ -317,12 +334,14 @@ class IssuableFinder items end end + # rubocop: enable CodeReuse/ActiveRecord def by_group(items) # Selection by group is already covered by `by_project` and `projects` items end + # rubocop: disable CodeReuse/ActiveRecord def by_project(items) items = if project? @@ -335,6 +354,7 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord def use_cte_for_search? return false unless search @@ -343,6 +363,7 @@ class IssuableFinder params[:use_cte_for_search] end + # rubocop: disable CodeReuse/ActiveRecord def by_search(items) return items unless search @@ -355,17 +376,23 @@ class IssuableFinder items.full_search(search) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_iids(items) params[:iids].present? ? items.where(iid: params[:iids]) : items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def sort(items) # Ensure we always have an explicit sort order (instead of inheriting # multiple orders when combining ActiveRecord::Relation objects). params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) if assignee items = items.where(assignee_id: assignee.id) @@ -377,7 +404,9 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_author(items) if author items = items.where(author_id: author.id) @@ -389,6 +418,7 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord def filter_by_upcoming_milestone? params[:milestone_title] == Milestone::Upcoming.name @@ -398,6 +428,7 @@ class IssuableFinder params[:milestone_title] == Milestone::Started.name end + # rubocop: disable CodeReuse/ActiveRecord def by_milestone(items) if milestones? if filter_by_no_milestone? @@ -414,6 +445,7 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord def by_label(items) return items unless labels? diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb index 24a6b9349a0..770e0bfe1a3 100644 --- a/app/finders/issues_finder.rb +++ b/app/finders/issues_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Finders::Issues class # # Used to filter Issues collections by set of params @@ -29,10 +31,13 @@ class IssuesFinder < IssuableFinder @scalar_params ||= super + [:due_date] end + # rubocop: disable CodeReuse/ActiveRecord def klass Issue.includes(:author) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def with_confidentiality_access_check return Issue.all if user_can_see_all_confidential_issues? return Issue.where('issues.confidential IS NOT TRUE') if user_cannot_see_confidential_issues? @@ -46,6 +51,7 @@ class IssuesFinder < IssuableFinder user_id: current_user.id, project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id)) end + # rubocop: enable CodeReuse/ActiveRecord private @@ -125,6 +131,7 @@ class IssuesFinder < IssuableFinder current_user.blank? end + # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) if assignee items.assigned_to(assignee) @@ -136,4 +143,5 @@ class IssuesFinder < IssuableFinder items end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/joined_groups_finder.rb b/app/finders/joined_groups_finder.rb index 47174980258..18cc6891ca4 100644 --- a/app/finders/joined_groups_finder.rb +++ b/app/finders/joined_groups_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JoinedGroupsFinder < UnionFinder def initialize(user) @user = user diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 1d05bf28438..08fc2968e77 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LabelsFinder < UnionFinder prepend FinderWithCrossProjectAccess include FinderMethods @@ -10,6 +12,7 @@ class LabelsFinder < UnionFinder @params = params end + # rubocop: disable CodeReuse/ActiveRecord def execute(skip_authorization: false) @skip_authorization = skip_authorization items = find_union(label_ids, Label) || Label.none @@ -17,11 +20,13 @@ class LabelsFinder < UnionFinder items = by_search(items) sort(items) end + # rubocop: enable CodeReuse/ActiveRecord private attr_reader :current_user, :params, :skip_authorization + # rubocop: disable CodeReuse/ActiveRecord def label_ids label_ids = [] @@ -52,17 +57,26 @@ class LabelsFinder < UnionFinder label_ids end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def sort(items) - items.reorder(title: :asc) + if params[:sort] + items.order_by(params[:sort]) + else + items.reorder(title: :asc) + end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def with_title(items) return items if title.nil? return items.none if title.blank? items.where(title: title) end + # rubocop: enable CodeReuse/ActiveRecord def by_search(labels) return labels unless search? @@ -134,13 +148,14 @@ class LabelsFinder < UnionFinder @project end + # rubocop: disable CodeReuse/ActiveRecord def projects return @projects if defined?(@projects) @projects = if skip_authorization Project.all else - ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute + ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute # rubocop: disable CodeReuse/Finder end @projects = @projects.in_namespace(params[:group_id]) if group? @@ -149,6 +164,7 @@ class LabelsFinder < UnionFinder @projects end + # rubocop: enable CodeReuse/ActiveRecord def authorized_to_read_labels?(label_parent) return true if skip_authorization diff --git a/app/finders/license_template_finder.rb b/app/finders/license_template_finder.rb index fad33f0eca2..196922709f7 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # LicenseTemplateFinder # # Used to find license templates, which may come from a variety of external diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb index 4c893ae2de6..f90a7868102 100644 --- a/app/finders/members_finder.rb +++ b/app/finders/members_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MembersFinder attr_reader :project, :current_user, :group @@ -7,15 +9,16 @@ class MembersFinder @group = project.group end + # rubocop: disable CodeReuse/ActiveRecord def execute(include_descendants: false) project_members = project.project_members project_members = project_members.non_invite unless can?(current_user, :admin_project, project) if group - group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants) + group_members = GroupMembersFinder.new(group).execute(include_descendants: include_descendants) # rubocop: disable CodeReuse/Finder group_members = group_members.non_invite - union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false) + union = Gitlab::SQL::Union.new([project_members, group_members], remove_duplicates: false) # rubocop: disable Gitlab/Union sql = distinct_on(union) @@ -24,6 +27,7 @@ class MembersFinder project_members end end + # rubocop: enable CodeReuse/ActiveRecord def can?(*args) Ability.allowed?(*args) diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index 188ec447a94..5f0589f6c8b 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class MergeRequestTargetProjectFinder include FinderMethods @@ -8,6 +10,7 @@ class MergeRequestTargetProjectFinder @source_project = source_project end + # rubocop: disable CodeReuse/ActiveRecord def execute if @source_project.fork_network @source_project.fork_network.projects @@ -18,4 +21,5 @@ class MergeRequestTargetProjectFinder Project.where(id: source_project) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb index 40089c082c1..b698a3c7b09 100644 --- a/app/finders/merge_requests_finder.rb +++ b/app/finders/merge_requests_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Finders::MergeRequest class # # Used to filter MergeRequests collections by set of params @@ -41,19 +43,23 @@ class MergeRequestsFinder < IssuableFinder @source_branch ||= params[:source_branch].presence end + # rubocop: disable CodeReuse/ActiveRecord def by_source_branch(items) return items unless source_branch items.where(source_branch: source_branch) end + # rubocop: enable CodeReuse/ActiveRecord def target_branch @target_branch ||= params[:target_branch].presence end + # rubocop: disable CodeReuse/ActiveRecord def by_target_branch(items) return items unless target_branch items.where(target_branch: target_branch) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/milestones_finder.rb b/app/finders/milestones_finder.rb index f5d2b9f253a..47231ea80c7 100644 --- a/app/finders/milestones_finder.rb +++ b/app/finders/milestones_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Search for milestones # # params - Hash @@ -18,6 +20,7 @@ class MilestonesFinder @params = params end + # rubocop: disable CodeReuse/ActiveRecord def execute return Milestone.none if project_ids.empty? && group_ids.empty? @@ -28,6 +31,7 @@ class MilestonesFinder order(items) end + # rubocop: enable CodeReuse/ActiveRecord private @@ -35,6 +39,7 @@ class MilestonesFinder items.for_projects_and_groups(project_ids, group_ids) end + # rubocop: disable CodeReuse/ActiveRecord def by_title(items) if params[:title] items.where(title: params[:title]) @@ -42,13 +47,16 @@ class MilestonesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord def by_state(items) Milestone.filter_by_state(items, params[:state]) end + # rubocop: disable CodeReuse/ActiveRecord def order(items) order_statement = Gitlab::Database.nulls_last_order('due_date', 'ASC') items.reorder(order_statement).order('title ASC') end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb index 9b7a35fb3b5..c67c2065440 100644 --- a/app/finders/notes_finder.rb +++ b/app/finders/notes_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class NotesFinder FETCH_OVERLAP = 5.seconds @@ -65,21 +67,23 @@ class NotesFinder @params[:target_type] end + # rubocop: disable CodeReuse/ActiveRecord def notes_of_any_type types = %w(commit issue merge_request snippet) note_relations = types.map { |t| notes_for_type(t) } note_relations.map! { |notes| search(notes) } - UnionFinder.new.find_union(note_relations, Note.includes(:author)) + UnionFinder.new.find_union(note_relations, Note.includes(:author)) # rubocop: disable CodeReuse/Finder end + # rubocop: enable CodeReuse/ActiveRecord def noteables_for_type(noteable_type) case noteable_type when "issue" - IssuesFinder.new(@current_user, project_id: @project.id).execute + IssuesFinder.new(@current_user, project_id: @project.id).execute # rubocop: disable CodeReuse/Finder when "merge_request" - MergeRequestsFinder.new(@current_user, project_id: @project.id).execute + MergeRequestsFinder.new(@current_user, project_id: @project.id).execute # rubocop: disable CodeReuse/Finder when "snippet", "project_snippet" - SnippetsFinder.new(@current_user, project: @project).execute + SnippetsFinder.new(@current_user, project: @project).execute # rubocop: disable CodeReuse/Finder when "personal_snippet" PersonalSnippet.all else @@ -87,6 +91,7 @@ class NotesFinder end end + # rubocop: disable CodeReuse/ActiveRecord def notes_for_type(noteable_type) if noteable_type == "commit" if Ability.allowed?(@current_user, :download_code, @project) @@ -99,6 +104,7 @@ class NotesFinder @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil)) end end + # rubocop: enable CodeReuse/ActiveRecord def notes_on_target if target.respond_to?(:related_notes) diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb index d975f354a88..5beea92689f 100644 --- a/app/finders/personal_access_tokens_finder.rb +++ b/app/finders/personal_access_tokens_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonalAccessTokensFinder attr_accessor :params @@ -16,11 +18,13 @@ class PersonalAccessTokensFinder private + # rubocop: disable CodeReuse/ActiveRecord def by_user(tokens) return tokens unless @params[:user] tokens.where(user: @params[:user]) end + # rubocop: enable CodeReuse/ActiveRecord def by_impersonation(tokens) case @params[:impersonation] diff --git a/app/finders/personal_projects_finder.rb b/app/finders/personal_projects_finder.rb index a56a3a1e1a9..20f5b221a89 100644 --- a/app/finders/personal_projects_finder.rb +++ b/app/finders/personal_projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PersonalProjectsFinder < UnionFinder include Gitlab::Allowable @@ -15,6 +17,7 @@ class PersonalProjectsFinder < UnionFinder # min_access_level: integer # # Returns an ActiveRecord::Relation. + # rubocop: disable CodeReuse/ActiveRecord def execute(current_user = nil) return Project.none unless can?(current_user, :read_user_profile, @user) @@ -22,6 +25,7 @@ class PersonalProjectsFinder < UnionFinder find_union(segments, Project).includes(:namespace).order_updated_desc end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/finders/pipeline_schedules_finder.rb b/app/finders/pipeline_schedules_finder.rb index 2ac4289fbbe..3beee608268 100644 --- a/app/finders/pipeline_schedules_finder.rb +++ b/app/finders/pipeline_schedules_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelineSchedulesFinder attr_reader :project, :pipeline_schedules @@ -6,6 +8,7 @@ class PipelineSchedulesFinder @pipeline_schedules = project.pipeline_schedules end + # rubocop: disable CodeReuse/ActiveRecord def execute(scope: nil) scoped_schedules = case scope @@ -19,4 +22,5 @@ class PipelineSchedulesFinder scoped_schedules.order(id: :desc) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb index a99a889a7e9..3d0d3219a94 100644 --- a/app/finders/pipelines_finder.rb +++ b/app/finders/pipelines_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class PipelinesFinder attr_reader :project, :pipelines, :params, :current_user @@ -10,6 +12,7 @@ class PipelinesFinder @params = params end + # rubocop: disable CodeReuse/ActiveRecord def execute unless Ability.allowed?(current_user, :read_pipeline, project) return Ci::Pipeline.none @@ -25,16 +28,21 @@ class PipelinesFinder items = by_yaml_errors(items) sort_items(items) end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def ids_for_ref(refs) pipelines.where(ref: refs).group(:ref).select('max(id)') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def from_ids(ids) pipelines.unscoped.where(id: ids) end + # rubocop: enable CodeReuse/ActiveRecord def branches project.repository.branch_names @@ -61,12 +69,15 @@ class PipelinesFinder end end + # rubocop: disable CodeReuse/ActiveRecord def by_status(items) return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status]) items.where(status: params[:status]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_ref(items) if params[:ref].present? items.where(ref: params[:ref]) @@ -74,7 +85,9 @@ class PipelinesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_sha(items) if params[:sha].present? items.where(sha: params[:sha]) @@ -82,7 +95,9 @@ class PipelinesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_name(items) if params[:name].present? items.joins(:user).where(users: { name: params[:name] }) @@ -90,7 +105,9 @@ class PipelinesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_username(items) if params[:username].present? items.joins(:user).where(users: { username: params[:username] }) @@ -98,7 +115,9 @@ class PipelinesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_yaml_errors(items) case Gitlab::Utils.to_boolean(params[:yaml_errors]) when true @@ -109,7 +128,9 @@ class PipelinesFinder items end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def sort_items(items) order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by]) params[:order_by] @@ -125,4 +146,5 @@ class PipelinesFinder items.order(order_by => sort) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb index cac6643eff3..c2404412006 100644 --- a/app/finders/projects_finder.rb +++ b/app/finders/projects_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # ProjectsFinder # # Used to filter Projects by set of params @@ -35,7 +37,7 @@ class ProjectsFinder < UnionFinder user = params.delete(:user) collection = if user - PersonalProjectsFinder.new(user, finder_params).execute(current_user) + PersonalProjectsFinder.new(user, finder_params).execute(current_user) # rubocop: disable CodeReuse/Finder else init_collection end @@ -49,6 +51,7 @@ class ProjectsFinder < UnionFinder collection = by_search(collection) collection = by_archived(collection) collection = by_custom_attributes(collection) + collection = by_deleted_status(collection) sort(collection) end @@ -63,6 +66,7 @@ class ProjectsFinder < UnionFinder end end + # rubocop: disable CodeReuse/ActiveRecord def collection_with_user if owned_projects? current_user.owned_projects @@ -76,8 +80,10 @@ class ProjectsFinder < UnionFinder end end end + # rubocop: enable CodeReuse/ActiveRecord # Builds a collection for an anonymous user. + # rubocop: disable CodeReuse/ActiveRecord def collection_without_user if private_only? || owned_projects? || min_access_level? Project.none @@ -85,6 +91,7 @@ class ProjectsFinder < UnionFinder Project.public_to_user end end + # rubocop: enable CodeReuse/ActiveRecord def owned_projects? params[:owned].present? @@ -98,9 +105,11 @@ class ProjectsFinder < UnionFinder params[:min_access_level].present? end + # rubocop: disable CodeReuse/ActiveRecord def by_ids(items) project_ids_relation ? items.where(id: project_ids_relation) : items end + # rubocop: enable CodeReuse/ActiveRecord def union(items) find_union(items, Project).with_route @@ -118,9 +127,11 @@ class ProjectsFinder < UnionFinder params[:trending].present? ? items.trending : items end + # rubocop: disable CodeReuse/ActiveRecord def by_visibilty_level(items) params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items end + # rubocop: enable CodeReuse/ActiveRecord def by_tags(items) params[:tag].present? ? items.tagged_with(params[:tag]) : items @@ -131,6 +142,10 @@ class ProjectsFinder < UnionFinder params[:search].present? ? items.search(params[:search]) : items end + def by_deleted_status(items) + params[:without_deleted].present? ? items.without_deleted : items + end + def sort(items) params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc end diff --git a/app/finders/runner_jobs_finder.rb b/app/finders/runner_jobs_finder.rb index 52340f94523..4fca4ec94f3 100644 --- a/app/finders/runner_jobs_finder.rb +++ b/app/finders/runner_jobs_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class RunnerJobsFinder attr_reader :runner, :params @@ -14,9 +16,11 @@ class RunnerJobsFinder private + # rubocop: disable CodeReuse/ActiveRecord def by_status(items) return items unless HasStatus::AVAILABLE_STATUSES.include?(params[:status]) items.where(status: params[:status]) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb index 9d3772d7541..3528e4228b2 100644 --- a/app/finders/snippets_finder.rb +++ b/app/finders/snippets_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Snippets Finder # # Used to filter Snippets collections by a set of params @@ -41,6 +43,7 @@ class SnippetsFinder < UnionFinder end end + # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets_from_project if can?(current_user, :read_project_snippet, project) if project.team.member?(current_user) @@ -52,7 +55,9 @@ class SnippetsFinder < UnionFinder Snippet.none end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def authorized_snippets # This query was intentionally converted to a raw one to get it work in Rails 5.0. # In Rails 5.0 and 5.1 there's a bug: https://github.com/rails/arel/issues/531 @@ -60,6 +65,7 @@ class SnippetsFinder < UnionFinder Snippet.where("#{feature_available_projects} OR #{not_project_related}") .public_or_visible_to_user(current_user) end + # rubocop: enable CodeReuse/ActiveRecord # Returns a collection of projects that is either public or visible to the # logged in user. @@ -68,6 +74,7 @@ class SnippetsFinder < UnionFinder # the query, e.g. to apply .with_feature_available_for_user on top of it. # This is useful for performance as we can stick those additional filters # at the bottom of e.g. the UNION. + # rubocop: disable CodeReuse/ActiveRecord def projects_for_user return yield(Project.public_to_user) unless current_user @@ -82,10 +89,9 @@ class SnippetsFinder < UnionFinder # We use a UNION here instead of OR clauses since this results in better # performance. - union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')]) - - Project.from("(#{union.to_sql}) AS #{Project.table_name}") + Project.from_union([authorized_projects, visible_projects]) end + # rubocop: enable CodeReuse/ActiveRecord def feature_available_projects # Don't return any project related snippets if the user cannot read cross project @@ -109,6 +115,7 @@ class SnippetsFinder < UnionFinder Snippet.arel_table end + # rubocop: disable CodeReuse/ActiveRecord def by_visibility(items) visibility = params[:visibility] || visibility_from_scope @@ -116,12 +123,15 @@ class SnippetsFinder < UnionFinder items.where(visibility_level: visibility) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_author(items) return items unless params[:author] items.where(author_id: params[:author].id) end + # rubocop: enable CodeReuse/ActiveRecord def visibility_from_scope case params[:scope].to_s diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb index b474f0805dc..2ffd46245e9 100644 --- a/app/finders/tags_finder.rb +++ b/app/finders/tags_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TagsFinder def initialize(repository, params) @repository = repository diff --git a/app/finders/template_finder.rb b/app/finders/template_finder.rb index ea0251bffb6..c92ee9ca9ac 100644 --- a/app/finders/template_finder.rb +++ b/app/finders/template_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class TemplateFinder VENDORED_TEMPLATES = { dockerfiles: ::Gitlab::Template::DockerfileTemplate, @@ -8,7 +10,7 @@ class TemplateFinder class << self def build(type, params = {}) if type == :licenses - LicenseTemplateFinder.new(params) + LicenseTemplateFinder.new(params) # rubocop: disable CodeReuse/Finder else new(type, params) end diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 6e9c8ea6fde..74baf79e4f2 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # TodosFinder # # Used to filter Todos by set of params @@ -120,6 +122,7 @@ class TodosFinder params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc end + # rubocop: disable CodeReuse/ActiveRecord def by_action(items) if action? items = items.where(action: to_action_id) @@ -127,7 +130,9 @@ class TodosFinder items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_action_id(items) if action_id? items = items.where(action: action_id) @@ -135,7 +140,9 @@ class TodosFinder items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_author(items) if author? items = items.where(author_id: author.try(:id)) @@ -143,7 +150,9 @@ class TodosFinder items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_project(items) if project? items = items.where(project: project) @@ -151,19 +160,19 @@ class TodosFinder items end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_group(items) - if group? - groups = group.self_and_descendants - project_todos = items.where(project_id: Project.where(group: groups).select(:id)) - group_todos = items.where(group_id: groups.select(:id)) + return items unless group? - union = Gitlab::SQL::Union.new([project_todos, group_todos]) - items = Todo.from("(#{union.to_sql}) #{Todo.table_name}") - end + groups = group.self_and_descendants + project_todos = items.where(project_id: Project.where(group: groups).select(:id)) + group_todos = items.where(group_id: groups.select(:id)) - items + Todo.from_union([project_todos, group_todos]) end + # rubocop: enable CodeReuse/ActiveRecord def by_state(items) case params[:state].to_s @@ -174,6 +183,7 @@ class TodosFinder end end + # rubocop: disable CodeReuse/ActiveRecord def by_type(items) if type? items = items.where(target_type: type) @@ -181,4 +191,5 @@ class TodosFinder items end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/finders/union_finder.rb b/app/finders/union_finder.rb index 33cd1a491f3..c3b02f7e52f 100644 --- a/app/finders/union_finder.rb +++ b/app/finders/union_finder.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + class UnionFinder def find_union(segments, klass) - if segments.length > 1 - union = Gitlab::SQL::Union.new(segments.map { |s| s.select(:id) }) + unless klass < FromUnion + raise TypeError, "#{klass.inspect} must include the FromUnion module" + end - klass.where("#{klass.table_name}.id IN (#{union.to_sql})") + if segments.length > 1 + klass.from_union(segments) else segments.first end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb index 484a93c9873..815388c894e 100644 --- a/app/finders/user_finder.rb +++ b/app/finders/user_finder.rb @@ -14,9 +14,11 @@ class UserFinder end # Tries to find a User, returning nil if none could be found. + # rubocop: disable CodeReuse/ActiveRecord def execute User.find_by(id: params[:id]) end + # rubocop: enable CodeReuse/ActiveRecord # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could # not be found. diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb index 876f086a3ef..a4daf5b5841 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Get user activity feed for projects common for a user and a logged in user # # - current_user: The user viewing the events @@ -21,6 +23,7 @@ class UserRecentEventsFinder @params = params end + # rubocop: disable CodeReuse/ActiveRecord def execute return Event.none unless can?(current_user, :read_user_profile, target_user) @@ -29,9 +32,11 @@ class UserRecentEventsFinder .with_associations .limit_recent(LIMIT, params[:offset]) end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def recent_events(offset) sql = <<~SQL (#{projects}) AS projects_for_join @@ -42,26 +47,15 @@ class UserRecentEventsFinder # Workaround for https://github.com/rails/rails/issues/24193 Event.from([Arel.sql(sql)]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def target_events Event.where(author: target_user) end + # rubocop: enable CodeReuse/ActiveRecord def projects - # Compile a list of projects `current_user` interacted with - # and `target_user` is allowed to see. - - authorized = target_user - .project_interactions - .joins(:project_authorizations) - .where(project_authorizations: { user: current_user }) - .select(:id) - - visible = target_user - .project_interactions - .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user)) - .select(:id) - - Gitlab::SQL::Union.new([authorized, visible]).to_sql + target_user.project_interactions.to_sql end end diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb index 65824a51919..f2ad9b4bda5 100644 --- a/app/finders/users_finder.rb +++ b/app/finders/users_finder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # UsersFinder # # Used to filter users by set of params @@ -41,11 +43,13 @@ class UsersFinder private + # rubocop: disable CodeReuse/ActiveRecord def by_username(users) return users unless params[:username] users.where(username: params[:username]) end + # rubocop: enable CodeReuse/ActiveRecord def by_search(users) return users unless params[:search].present? @@ -65,18 +69,22 @@ class UsersFinder users.active end + # rubocop: disable CodeReuse/ActiveRecord def by_external_identity(users) return users unless current_user&.admin? && params[:extern_uid] && params[:provider] users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid])) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def by_external(users) return users = users.where.not(external: true) unless current_user&.admin? return users unless params[:external] users.external end + # rubocop: enable CodeReuse/ActiveRecord def by_2fa(users) case params[:two_factor] diff --git a/app/graphql/functions/base_function.rb b/app/graphql/functions/base_function.rb index 42fb8f99acc..2512ecbd255 100644 --- a/app/graphql/functions/base_function.rb +++ b/app/graphql/functions/base_function.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Functions class BaseFunction < GraphQL::Function end diff --git a/app/graphql/functions/echo.rb b/app/graphql/functions/echo.rb index e5bf109b8d7..3104486faac 100644 --- a/app/graphql/functions/echo.rb +++ b/app/graphql/functions/echo.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Functions class Echo < BaseFunction argument :text, GraphQL::STRING_TYPE diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb index 8755a1a62e7..06d26309b5b 100644 --- a/app/graphql/gitlab_schema.rb +++ b/app/graphql/gitlab_schema.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class GitlabSchema < GraphQL::Schema use BatchLoader::GraphQL use Gitlab::Graphql::Authorize diff --git a/app/graphql/mutations/concerns/mutations/resolves_project.rb b/app/graphql/mutations/concerns/mutations/resolves_project.rb index 0dd1f264a52..da9814e88b0 100644 --- a/app/graphql/mutations/concerns/mutations/resolves_project.rb +++ b/app/graphql/mutations/concerns/mutations/resolves_project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mutations module ResolvesProject extend ActiveSupport::Concern diff --git a/app/graphql/mutations/merge_requests/base.rb b/app/graphql/mutations/merge_requests/base.rb index 2149e72e2df..54f01c99d78 100644 --- a/app/graphql/mutations/merge_requests/base.rb +++ b/app/graphql/mutations/merge_requests/base.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Mutations module MergeRequests class Base < BaseMutation diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb index 89b7f9dad6f..459933af9d3 100644 --- a/app/graphql/resolvers/base_resolver.rb +++ b/app/graphql/resolvers/base_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class BaseResolver < GraphQL::Schema::Resolver end diff --git a/app/graphql/resolvers/concerns/resolves_pipelines.rb b/app/graphql/resolvers/concerns/resolves_pipelines.rb index 9ec45378d8e..8fd26d85994 100644 --- a/app/graphql/resolvers/concerns/resolves_pipelines.rb +++ b/app/graphql/resolvers/concerns/resolves_pipelines.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ResolvesPipelines extend ActiveSupport::Concern diff --git a/app/graphql/resolvers/full_path_resolver.rb b/app/graphql/resolvers/full_path_resolver.rb index 4eb28aaed6c..8d3da33e8d2 100644 --- a/app/graphql/resolvers/full_path_resolver.rb +++ b/app/graphql/resolvers/full_path_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers module FullPathResolver extend ActiveSupport::Concern diff --git a/app/graphql/resolvers/merge_request_pipelines_resolver.rb b/app/graphql/resolvers/merge_request_pipelines_resolver.rb index 00b51ee1381..b371f1335f8 100644 --- a/app/graphql/resolvers/merge_request_pipelines_resolver.rb +++ b/app/graphql/resolvers/merge_request_pipelines_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class MergeRequestPipelinesResolver < BaseResolver include ::ResolvesPipelines diff --git a/app/graphql/resolvers/merge_request_resolver.rb b/app/graphql/resolvers/merge_request_resolver.rb index 9f2d348e95f..b87c95217f7 100644 --- a/app/graphql/resolvers/merge_request_resolver.rb +++ b/app/graphql/resolvers/merge_request_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class MergeRequestResolver < BaseResolver argument :iid, GraphQL::ID_TYPE, @@ -8,6 +10,7 @@ module Resolvers alias_method :project, :object + # rubocop: disable CodeReuse/ActiveRecord def resolve(iid:) return unless project.present? @@ -16,5 +19,6 @@ module Resolvers results.each { |mr| loader.call(mr.iid.to_s, mr) } end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb index 7f175a3b26c..86094c46c2a 100644 --- a/app/graphql/resolvers/project_pipelines_resolver.rb +++ b/app/graphql/resolvers/project_pipelines_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class ProjectPipelinesResolver < BaseResolver include ResolvesPipelines diff --git a/app/graphql/resolvers/project_resolver.rb b/app/graphql/resolvers/project_resolver.rb index ec115bad896..ac7c9b0ce2e 100644 --- a/app/graphql/resolvers/project_resolver.rb +++ b/app/graphql/resolvers/project_resolver.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Resolvers class ProjectResolver < BaseResolver prepend FullPathResolver diff --git a/app/graphql/types/base_enum.rb b/app/graphql/types/base_enum.rb index b45a845f74f..cf43fea45e6 100644 --- a/app/graphql/types/base_enum.rb +++ b/app/graphql/types/base_enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseEnum < GraphQL::Schema::Enum end diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb index c5740a334d7..2b2ea64c00b 100644 --- a/app/graphql/types/base_field.rb +++ b/app/graphql/types/base_field.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseField < GraphQL::Schema::Field prepend Gitlab::Graphql::Authorize diff --git a/app/graphql/types/base_input_object.rb b/app/graphql/types/base_input_object.rb index 309e336e6c8..aebed035d3b 100644 --- a/app/graphql/types/base_input_object.rb +++ b/app/graphql/types/base_input_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseInputObject < GraphQL::Schema::InputObject end diff --git a/app/graphql/types/base_interface.rb b/app/graphql/types/base_interface.rb index 69e72dc5808..3451a195c33 100644 --- a/app/graphql/types/base_interface.rb +++ b/app/graphql/types/base_interface.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module BaseInterface include GraphQL::Schema::Interface diff --git a/app/graphql/types/base_object.rb b/app/graphql/types/base_object.rb index 754adf4c04d..82b78abd573 100644 --- a/app/graphql/types/base_object.rb +++ b/app/graphql/types/base_object.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseObject < GraphQL::Schema::Object prepend Gitlab::Graphql::Present diff --git a/app/graphql/types/base_scalar.rb b/app/graphql/types/base_scalar.rb index c0aa38be239..719bc808f47 100644 --- a/app/graphql/types/base_scalar.rb +++ b/app/graphql/types/base_scalar.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseScalar < GraphQL::Schema::Scalar end diff --git a/app/graphql/types/base_union.rb b/app/graphql/types/base_union.rb index 36337fc6ee5..30a5668c0bb 100644 --- a/app/graphql/types/base_union.rb +++ b/app/graphql/types/base_union.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class BaseUnion < GraphQL::Schema::Union end diff --git a/app/graphql/types/ci/pipeline_status_enum.rb b/app/graphql/types/ci/pipeline_status_enum.rb index 2c12e5001d8..c19ddf5bb25 100644 --- a/app/graphql/types/ci/pipeline_status_enum.rb +++ b/app/graphql/types/ci/pipeline_status_enum.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module Ci class PipelineStatusEnum < BaseEnum diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index bbb7d9354d0..2bbffad4563 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module Ci class PipelineType < BaseObject diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 88cd2adc6dc..fb740b6fb1c 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class MergeRequestType < BaseObject expose_permissions Types::PermissionTypes::MergeRequest diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb index 934ed572e56..26a71e2bfbb 100644 --- a/app/graphql/types/permission_types/base_permission_type.rb +++ b/app/graphql/types/permission_types/base_permission_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class BasePermissionType < BaseObject diff --git a/app/graphql/types/permission_types/ci/pipeline.rb b/app/graphql/types/permission_types/ci/pipeline.rb index 942539c7cf7..73e44a33eba 100644 --- a/app/graphql/types/permission_types/ci/pipeline.rb +++ b/app/graphql/types/permission_types/ci/pipeline.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes module Ci diff --git a/app/graphql/types/permission_types/merge_request.rb b/app/graphql/types/permission_types/merge_request.rb index 5c21f6ee9c6..13995d3ea8f 100644 --- a/app/graphql/types/permission_types/merge_request.rb +++ b/app/graphql/types/permission_types/merge_request.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class MergeRequest < BasePermissionType diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb index 755699a4415..066ce64a254 100644 --- a/app/graphql/types/permission_types/project.rb +++ b/app/graphql/types/permission_types/project.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types module PermissionTypes class Project < BasePermissionType diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index 97707215b4e..7b879608b34 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class ProjectType < BaseObject expose_permissions Types::PermissionTypes::Project diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb index 010ec2d7942..7c41716b82a 100644 --- a/app/graphql/types/query_type.rb +++ b/app/graphql/types/query_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class QueryType < BaseObject graphql_name 'Query' diff --git a/app/graphql/types/time_type.rb b/app/graphql/types/time_type.rb index 2333d82ad1e..f045a50e672 100644 --- a/app/graphql/types/time_type.rb +++ b/app/graphql/types/time_type.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Types class TimeType < BaseScalar graphql_name 'Time' diff --git a/app/helpers/accounts_helper.rb b/app/helpers/accounts_helper.rb index 5d27d30eaa3..a4f19480539 100644 --- a/app/helpers/accounts_helper.rb +++ b/app/helpers/accounts_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AccountsHelper def incoming_email_token_enabled? current_user.incoming_email_token && Gitlab::IncomingEmail.supports_issue_creation? diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb index 97b6dac67c5..84aa1160f12 100644 --- a/app/helpers/active_sessions_helper.rb +++ b/app/helpers/active_sessions_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ActiveSessionsHelper # Maps a device type as defined in `ActiveSession` to an svg icon name and # outputs the icon html. diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb index f48db024e3f..ed13c5cfdd6 100644 --- a/app/helpers/appearances_helper.rb +++ b/app/helpers/appearances_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AppearancesHelper def brand_title current_appearance&.title.presence || 'GitLab Community Edition' diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0190aa90763..32fc8e5e9ce 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,15 +1,27 @@ +# frozen_string_literal: true + require 'digest/md5' require 'uri' module ApplicationHelper # See https://docs.gitlab.com/ee/development/ee_features.html#code-in-app-views + # rubocop: disable CodeReuse/ActiveRecord def render_if_exists(partial, locals = {}) - render(partial, locals) if lookup_context.exists?(partial, [], true) + render(partial, locals) if partial_exists?(partial) + end + + def partial_exists?(partial) + lookup_context.exists?(partial, [], true) end + def template_exists?(template) + lookup_context.exists?(template, [], false) + end + # rubocop: enable CodeReuse/ActiveRecord + # Check if a particular controller is the current one # - # args - One or more controller names to check + # args - One or more controller names to check (using path notation when inside namespaces) # # Examples # @@ -17,6 +29,11 @@ module ApplicationHelper # current_controller?(:tree) # => true # current_controller?(:commits) # => false # current_controller?(:commits, :tree) # => true + # + # # On Admin::ApplicationController + # current_controller?(:application) # => true + # current_controller?('admin/application') # => true + # current_controller?('gitlab/application') # => false def current_controller?(*args) args.any? do |v| v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path @@ -49,6 +66,7 @@ module ApplicationHelper # Define whenever show last push event # with suggestion to create MR + # rubocop: disable CodeReuse/ActiveRecord def show_last_push_widget?(event) # Skip if event is not about added or modified non-master branch return false unless event && event.last_push_to_non_root? && !event.rm_ref? @@ -66,6 +84,7 @@ module ApplicationHelper true end + # rubocop: enable CodeReuse/ActiveRecord def hexdigest(string) Digest::SHA1.hexdigest string @@ -106,11 +125,11 @@ module ApplicationHelper # # Returns an HTML-safe String def time_ago_with_tooltip(time, placement: 'top', html_class: '', short_format: false) - css_classes = short_format ? 'js-short-timeago' : 'js-timeago' - css_classes << " #{html_class}" unless html_class.blank? + css_classes = [short_format ? 'js-short-timeago' : 'js-timeago'] + css_classes << html_class unless html_class.blank? element = content_tag :time, l(time, format: "%b %d, %Y"), - class: css_classes, + class: css_classes.join(' '), title: l(time.to_time.in_time_zone, format: :timeago_tooltip), datetime: time.to_time.getutc.iso8601, data: { diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 684c84c3006..dc393968786 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ApplicationSettingsHelper extend self @@ -73,12 +75,12 @@ module ApplicationSettingsHelper def oauth_providers_checkboxes button_based_providers.map do |source| disabled = Gitlab::CurrentSettings.disabled_oauth_sign_in_sources.include?(source.to_s) - css_class = 'btn' - css_class << ' active' unless disabled + css_class = ['btn'] + css_class << 'active' unless disabled checkbox_name = 'application_setting[enabled_oauth_sign_in_sources][]' name = Gitlab::Auth::OAuth::Provider.label_for(source) - label_tag(checkbox_name, class: css_class) do + label_tag(checkbox_name, class: css_class.join(' ')) do check_box_tag(checkbox_name, source, !disabled, autocomplete: 'off', id: name.tr(' ', '_')) + name @@ -220,6 +222,7 @@ module ApplicationSettingsHelper :recaptcha_enabled, :recaptcha_private_key, :recaptcha_site_key, + :receive_max_input_size, :repository_checks_enabled, :repository_storages, :require_two_factor_authentication, @@ -261,4 +264,8 @@ module ApplicationSettingsHelper :web_ide_clientside_preview_enabled ] end + + def expanded_by_default? + Rails.env.test? + end end diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb index 18f0979fc86..c158cf20dd6 100644 --- a/app/helpers/auth_helper.rb +++ b/app/helpers/auth_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AuthHelper PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2 facebook azure_oauth2 authentiq).freeze LDAP_PROVIDER = /\Aldap/ @@ -64,9 +66,11 @@ module AuthHelper end end + # rubocop: disable CodeReuse/ActiveRecord def auth_active?(provider) current_user.identities.exists?(provider: provider.to_s) end + # rubocop: enable CodeReuse/ActiveRecord def unlink_allowed?(provider) %w(saml cas3).exclude?(provider.to_s) diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb index 7b076728685..516c8a353ea 100644 --- a/app/helpers/auto_devops_helper.rb +++ b/app/helpers/auto_devops_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AutoDevopsHelper def show_auto_devops_callout?(project) Feature.get(:auto_devops_banner_disabled).off? && @@ -24,6 +26,7 @@ module AutoDevopsHelper end end + # rubocop: disable CodeReuse/ActiveRecord def cluster_ingress_ip(project) project .cluster_ingresses @@ -32,6 +35,7 @@ module AutoDevopsHelper .pluck(:external_ip) .first end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb index 494f785e305..321811a3ca3 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AvatarsHelper def project_icon(project_id, options = {}) source_icon(Project, project_id, options) @@ -125,9 +127,9 @@ module AvatarsHelper def source_identicon(source, options = {}) bg_key = (source.id % 7) + 1 - options[:class] ||= '' - options[:class] << ' identicon' - options[:class] << " bg#{bg_key}" + + options[:class] = + [*options[:class], "identicon bg#{bg_key}"].join(' ') content_tag(:div, class: options[:class].strip) do source.name[0, 1].upcase diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb index 86b19368cfd..b97a95629f7 100644 --- a/app/helpers/award_emoji_helper.rb +++ b/app/helpers/award_emoji_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module AwardEmojiHelper def toggle_award_url(awardable) return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note) diff --git a/app/helpers/blame_helper.rb b/app/helpers/blame_helper.rb index 089d9e3e387..82c74e2416d 100644 --- a/app/helpers/blame_helper.rb +++ b/app/helpers/blame_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BlameHelper def age_map_duration(blame_groups, project) now = Time.zone.now diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index 96f7415ae98..9cbd5b5f785 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BlobHelper def highlight(blob_name, blob_content, repository: nil, plain: false) plain ||= blob_content.length > Blob::MAXIMUM_TEXT_HIGHLIGHT_SIZE diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index af878bcf9a0..e3b74f443f7 100644 --- a/app/helpers/boards_helper.rb +++ b/app/helpers/boards_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BoardsHelper def board @board ||= @board || @boards.first diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb index 07b1fc3d7cf..eadf48205fc 100644 --- a/app/helpers/branches_helper.rb +++ b/app/helpers/branches_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BranchesHelper def project_branches options_for_select(@project.repository.branch_names, @project.default_branch) diff --git a/app/helpers/breadcrumbs_helper.rb b/app/helpers/breadcrumbs_helper.rb index e88fe6bcd7e..b067376cea0 100644 --- a/app/helpers/breadcrumbs_helper.rb +++ b/app/helpers/breadcrumbs_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BreadcrumbsHelper def add_to_breadcrumbs(text, link) @breadcrumbs_extra_links ||= [] diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb index 0a15c29cfb5..289cb44f1e8 100644 --- a/app/helpers/broadcast_messages_helper.rb +++ b/app/helpers/broadcast_messages_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BroadcastMessagesHelper def broadcast_message(message) return unless message.present? @@ -8,18 +10,17 @@ module BroadcastMessagesHelper end def broadcast_message_style(broadcast_message) - style = '' + style = [] if broadcast_message.color.present? style << "background-color: #{broadcast_message.color}" - style << '; ' if broadcast_message.font.present? end if broadcast_message.font.present? style << "color: #{broadcast_message.font}" end - style + style.join('; ') end def broadcast_message_status(broadcast_message) diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb index 4ec63fdaffc..3c8caec3fe5 100644 --- a/app/helpers/builds_helper.rb +++ b/app/helpers/builds_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module BuildsHelper def build_summary(build, skip: false) if build.has_trace? @@ -12,10 +14,10 @@ module BuildsHelper end def sidebar_build_class(build, current_build) - build_class = '' - build_class += ' active' if build.id === current_build.id - build_class += ' retried' if build.retried? - build_class + build_class = [] + build_class << 'active' if build.id === current_build.id + build_class << 'retried' if build.retried? + build_class.join(' ') end def javascript_build_options diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb index 26e3850a540..7f071d55a6b 100644 --- a/app/helpers/button_helper.rb +++ b/app/helpers/button_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ButtonHelper # Output a "Copy to Clipboard" button # @@ -61,7 +63,7 @@ module ButtonHelper dropdown_description = http_dropdown_description(protocol) append_url = project.http_url_to_repo if append_link - dropdown_item_with_description(protocol, dropdown_description, href: append_url) + dropdown_item_with_description(protocol, dropdown_description, href: append_url, data: { clone_type: 'http' }) end def http_dropdown_description(protocol) @@ -80,16 +82,17 @@ module ButtonHelper append_url = project.ssh_url_to_repo if append_link - dropdown_item_with_description('SSH', dropdown_description, href: append_url) + dropdown_item_with_description('SSH', dropdown_description, href: append_url, data: { clone_type: 'ssh' }) end - def dropdown_item_with_description(title, description, href: nil) + def dropdown_item_with_description(title, description, href: nil, data: nil) button_content = content_tag(:strong, title, class: 'dropdown-menu-inner-title') button_content << content_tag(:span, description, class: 'dropdown-menu-inner-content') if description content_tag (href ? :a : :span), (href ? button_content : title), class: "#{title.downcase}-selector", - href: (href if href) + href: (href if href), + data: (data if data) end end diff --git a/app/helpers/calendar_helper.rb b/app/helpers/calendar_helper.rb index c54b91b0ce5..ad4116fc3da 100644 --- a/app/helpers/calendar_helper.rb +++ b/app/helpers/calendar_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CalendarHelper def calendar_url_options { format: :ics, diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb index 330959e536d..136772e1ec3 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # DEPRECATED # @@ -121,11 +123,6 @@ module CiStatusHelper render_status_with_link('pipeline', pipeline.status, path, tooltip_placement: tooltip_placement) end - def no_runners_for_project?(project) - project.runners.blank? && - Ci::Runner.instance_type.blank? - end - def render_status_with_link(type, status, path = nil, tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16) klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}" title = "#{type.titleize}: #{ci_label_for_status(status)}" diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 8fd0b6f14c6..a67c91b21d7 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ClustersHelper def has_multiple_clusters?(project) false @@ -11,4 +13,8 @@ module ClustersHelper render 'projects/clusters/gcp_signup_offer_banner' end end + + def rbac_clusters_feature_enabled? + Feature.enabled?(:rbac_clusters) + end end diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb index 7a942c44ac4..d52cfd6e37a 100644 --- a/app/helpers/commits_helper.rb +++ b/app/helpers/commits_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CommitsHelper # Returns a link to the commit author. If the author has a matching user and # is a member of the current @project it will link to the team member page. diff --git a/app/helpers/compare_helper.rb b/app/helpers/compare_helper.rb index 2df5b5d1695..9ece8b0bc5b 100644 --- a/app/helpers/compare_helper.rb +++ b/app/helpers/compare_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CompareHelper def create_mr_button?(from = params[:from], to = params[:to], project = @project) from.present? && diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 8893209b314..d0ef86851ad 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ComponentsHelper def gitlab_workhorse_version if request.headers['Gitlab-Workhorse'].present? diff --git a/app/helpers/conversational_development_index_helper.rb b/app/helpers/conversational_development_index_helper.rb index 1ff54415811..37e5bb325fb 100644 --- a/app/helpers/conversational_development_index_helper.rb +++ b/app/helpers/conversational_development_index_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ConversationalDevelopmentIndexHelper def score_level(score) if score < 33.33 diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb index 5cd98f40f78..e16223a82c9 100644 --- a/app/helpers/count_helper.rb +++ b/app/helpers/count_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module CountHelper def approximate_count_with_delimiters(count_data, model) count = count_data[model] diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb index 31551fba862..33c53021c11 100644 --- a/app/helpers/dashboard_helper.rb +++ b/app/helpers/dashboard_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DashboardHelper def assigned_issues_dashboard_path issues_dashboard_path(assignee_id: current_user.id) diff --git a/app/helpers/defer_script_tag_helper.rb b/app/helpers/defer_script_tag_helper.rb index e1567556e5e..d91c6d52683 100644 --- a/app/helpers/defer_script_tag_helper.rb +++ b/app/helpers/defer_script_tag_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DeferScriptTagHelper # Override the default ActionView `javascript_include_tag` helper to support page specific deferred loading def javascript_include_tag(*sources) diff --git a/app/helpers/deploy_tokens_helper.rb b/app/helpers/deploy_tokens_helper.rb index bd921322476..80a5bb44c69 100644 --- a/app/helpers/deploy_tokens_helper.rb +++ b/app/helpers/deploy_tokens_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DeployTokensHelper def expand_deploy_tokens_section?(deploy_token) deploy_token.persisted? || diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb index 1bb82fd8150..b6844d36052 100644 --- a/app/helpers/diff_helper.rb +++ b/app/helpers/diff_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DiffHelper def mark_inline_diffs(old_line, new_line) old_diffs, new_diffs = Gitlab::Diff::InlineDiff.new(old_line, new_line).inline_diffs @@ -39,7 +41,8 @@ module DiffHelper line_num_class = %w[diff-line-num unfold js-unfold] line_num_class << 'js-unfold-bottom' if bottom - html = '' + html = [] + if old_pos html << content_tag(:td, '...', class: [*line_num_class, 'old_line'], data: { linenumber: old_pos }) html << content_tag(:td, text, class: [*content_line_class, 'left-side']) if view == :parallel @@ -50,7 +53,7 @@ module DiffHelper html << content_tag(:td, text, class: [*content_line_class, ('right-side' if view == :parallel)]) end - html.html_safe + html.join.html_safe end def diff_line_content(line) @@ -210,14 +213,14 @@ module DiffHelper params[:w] == '1' end + # rubocop: disable CodeReuse/ActiveRecord def params_with_whitespace hide_whitespace? ? request.query_parameters.except(:w) : request.query_parameters.merge(w: 1) end + # rubocop: enable CodeReuse/ActiveRecord def toggle_whitespace_link(url, options) - options[:class] ||= '' - options[:class] << ' btn btn-default' - + options[:class] = [*options[:class], 'btn btn-default'].join(' ') link_to "#{hide_whitespace? ? 'Show' : 'Hide'} whitespace changes", url, class: options[:class] end diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb index 5a2360b4661..4b6c5b215e8 100644 --- a/app/helpers/dropdowns_helper.rb +++ b/app/helpers/dropdowns_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module DropdownsHelper def dropdown_tag(toggle_text, options: {}, &block) content_tag :div, class: "dropdown #{options[:wrapper_class] if options.key?(:wrapper_class)}" do @@ -10,7 +12,7 @@ module DropdownsHelper dropdown_output = dropdown_toggle(toggle_text, data_attr, options) dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}") do - output = "" + output = [] if options.key?(:title) output << dropdown_title(options[:title]) @@ -31,8 +33,7 @@ module DropdownsHelper end output << dropdown_loading - - output.html_safe + output.join.html_safe end dropdown_output.html_safe @@ -50,7 +51,7 @@ module DropdownsHelper def dropdown_title(title, options: {}) content_tag :div, class: "dropdown-title" do - title_output = "" + title_output = [] if options.fetch(:back, false) title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back", aria: { label: "Go back" }, type: "button") do @@ -66,7 +67,7 @@ module DropdownsHelper end end - title_output.html_safe + title_output.join.html_safe end end diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb index c86a26ac30f..2d2e89a2a50 100644 --- a/app/helpers/emails_helper.rb +++ b/app/helpers/emails_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module EmailsHelper include AppearancesHelper @@ -49,8 +51,8 @@ module EmailsHelper def reset_token_expire_message link_tag = link_to('request a new one', new_user_password_url(user_email: @user.email)) - msg = "This link is valid for #{password_reset_token_valid_time}. " - msg << "After it expires, you can #{link_tag}." + "This link is valid for #{password_reset_token_valid_time}. " \ + "After it expires, you can #{link_tag}." end def header_logo diff --git a/app/helpers/emoji_helper.rb b/app/helpers/emoji_helper.rb index 482f68f412b..51b7fd7f352 100644 --- a/app/helpers/emoji_helper.rb +++ b/app/helpers/emoji_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module EmojiHelper def emoji_icon(*args) raw Gitlab::Emoji.gl_emoji_tag(*args) diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb index 1e78a189c08..2b7320817ed 100644 --- a/app/helpers/environment_helper.rb +++ b/app/helpers/environment_helper.rb @@ -1,9 +1,13 @@ +# frozen_string_literal: true + module EnvironmentHelper + # rubocop: disable CodeReuse/ActiveRecord def environment_for_build(project, build) return unless build.environment project.environments.find_by(name: build.expanded_environment_name) end + # rubocop: enable CodeReuse/ActiveRecord def environment_link_for_build(project, build) environment = environment_for_build(project, build) diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb index c005ecbb56b..7b22bc8f98f 100644 --- a/app/helpers/environments_helper.rb +++ b/app/helpers/environments_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module EnvironmentsHelper def environments_list_data { diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index 269acf5b2e2..c94946a04e7 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module EventsHelper ICON_NAMES_BY_EVENT_TYPE = { 'pushed to' => 'commit', @@ -19,7 +21,7 @@ module EventsHelper name = self_added ? 'You' : author.name link_to name, user_path(author.username), title: name else - event.author_name + escape_once(event.author_name) end end diff --git a/app/helpers/explore_helper.rb b/app/helpers/explore_helper.rb index f062a91a166..62be591ec47 100644 --- a/app/helpers/explore_helper.rb +++ b/app/helpers/explore_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ExploreHelper def filter_projects_path(options = {}) exist_opts = { diff --git a/app/helpers/external_wiki_helper.rb b/app/helpers/external_wiki_helper.rb index 8cf890b74a8..e36d63b2946 100644 --- a/app/helpers/external_wiki_helper.rb +++ b/app/helpers/external_wiki_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ExternalWikiHelper def get_project_wiki_path(project) external_wiki_service = project.external_wiki diff --git a/app/helpers/favicon_helper.rb b/app/helpers/favicon_helper.rb index 3a5342a8d9d..4a809731d97 100644 --- a/app/helpers/favicon_helper.rb +++ b/app/helpers/favicon_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FaviconHelper def favicon_extension_whitelist FaviconUploader::EXTENSION_WHITELIST diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb index 905e2002592..5705ee54cee 100644 --- a/app/helpers/form_helper.rb +++ b/app/helpers/form_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module FormHelper def form_errors(model, type: 'form') return unless model.errors.any? diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb index 8ab394384f3..5edc6dcf454 100644 --- a/app/helpers/git_helper.rb +++ b/app/helpers/git_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module GitHelper def strip_gpg_signature(text) text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "") diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 61e12b0f31e..04cf43be452 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Shorter routing method for some project items module GitlabRoutingHelper extend ActiveSupport::Concern diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb index 1022070ab6f..49b15cde009 100644 --- a/app/helpers/graph_helper.rb +++ b/app/helpers/graph_helper.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + module GraphHelper def refs(repo, commit) - refs = commit.ref_names(repo).join(' ') + refs = [commit.ref_names(repo).join(' ')] # append note count notes_count = @graph.notes[commit.id] refs << "[#{pluralize(notes_count, 'note')}]" if notes_count > 0 - refs + refs.join end def parents_zip_spaces(parents, parent_spaces) diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb index 5b51d2f2425..f573fd399a5 100644 --- a/app/helpers/groups_helper.rb +++ b/app/helpers/groups_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module GroupsHelper def group_nav_link_paths %w[groups#projects groups#edit badges#index ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index] @@ -43,22 +45,22 @@ module GroupsHelper def group_title(group, name = nil, url = nil) @has_group_title = true - full_title = '' + full_title = [] group.ancestors.reverse.each_with_index do |parent, index| if index > 0 add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before) else - full_title += breadcrumb_list_item group_title_link(parent, hidable: false) + full_title << breadcrumb_list_item(group_title_link(parent, hidable: false)) end end - full_title += render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups") + full_title << render("layouts/nav/breadcrumbs/collapsed_dropdown", location: :before, title: _("Show parent subgroups")) - full_title += breadcrumb_list_item group_title_link(group) - full_title += ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name + full_title << breadcrumb_list_item(group_title_link(group)) + full_title << ' · '.html_safe + link_to(simple_sanitize(name), url, class: 'group-path breadcrumb-item-text js-breadcrumb-item-text') if name - full_title.html_safe + full_title.join.html_safe end def projects_lfs_status(group) @@ -138,15 +140,8 @@ module GroupsHelper def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) link_to(group_path(group), class: "group-path #{'breadcrumb-item-text' unless for_dropdown} js-breadcrumb-item-text #{'hidable' if hidable}") do - output = - if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? - group_icon(group, class: "avatar-tile", width: 15, height: 15) - else - "" - end - - output << simple_sanitize(group.name) - output.html_safe + icon = group_icon(group, class: "avatar-tile", width: 15, height: 15) if (group.try(:avatar_url) || show_avatar) && !Rails.env.test? + [icon, simple_sanitize(group.name)].join.html_safe end end diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb index 0a356ba55d2..c4b39939192 100644 --- a/app/helpers/hooks_helper.rb +++ b/app/helpers/hooks_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module HooksHelper def link_to_test_hook(hook, trigger) path = case hook diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb index a8a10c98d69..037004327b9 100644 --- a/app/helpers/icons_helper.rb +++ b/app/helpers/icons_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'json' module IconsHelper @@ -47,9 +49,10 @@ module IconsHelper end end - css_classes = size ? "s#{size}" : "" - css_classes << " #{css_class}" unless css_class.blank? - content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes) + css_classes = [] + css_classes << "s#{size}" if size + css_classes << "#{css_class}" unless css_class.blank? + content_tag(:svg, content_tag(:use, "", { "xlink:href" => "#{sprite_icon_path}##{icon_name}" } ), class: css_classes.empty? ? nil : css_classes.join(' ')) end def external_snippet_icon(name) @@ -70,10 +73,10 @@ module IconsHelper end def spinner(text = nil, visible = false) - css_class = 'loading' - css_class << ' hide' unless visible + css_class = ['loading'] + css_class << 'hide' unless visible - content_tag :div, class: css_class do + content_tag :div, class: css_class.join(' ') do icon('spinner spin') + text end end @@ -86,7 +89,7 @@ module IconsHelper end end - def visibility_level_icon(level, fw: true) + def visibility_level_icon(level, fw: true, options: {}) name = case level when Gitlab::VisibilityLevel::PRIVATE @@ -97,9 +100,10 @@ module IconsHelper 'globe' end - name << " fw" if fw + name = [name] + name << "fw" if fw - icon(name) + icon(name.join(' '), options) end def file_type_icon_class(type, mode, name) diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb index c65f1565425..3d0eb3d0d51 100644 --- a/app/helpers/import_helper.rb +++ b/app/helpers/import_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ImportHelper include ::Gitlab::Utils::StrongMemoize diff --git a/app/helpers/instance_configuration_helper.rb b/app/helpers/instance_configuration_helper.rb index cee319f20bc..f695be32743 100644 --- a/app/helpers/instance_configuration_helper.rb +++ b/app/helpers/instance_configuration_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module InstanceConfigurationHelper def instance_configuration_cell_html(value, &block) return '-' unless value.to_s.presence diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index c84ed8091c3..56f6686da57 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module IssuablesHelper include GitlabRoutingHelper @@ -105,6 +107,7 @@ module IssuablesHelper end end + # rubocop: disable CodeReuse/ActiveRecord def user_dropdown_label(user_id, default_label) return default_label if user_id.nil? return "Unassigned" if user_id == "0" @@ -117,7 +120,9 @@ module IssuablesHelper default_label end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def project_dropdown_label(project_id, default_label) return default_label if project_id.nil? return "Any project" if project_id == "0" @@ -130,7 +135,9 @@ module IssuablesHelper default_label end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group_dropdown_label(group_id, default_label) return default_label if group_id.nil? return "Any group" if group_id == "0" @@ -143,6 +150,7 @@ module IssuablesHelper default_label end end + # rubocop: enable CodeReuse/ActiveRecord def milestone_dropdown_label(milestone_title, default_label = "Milestone") title = @@ -167,33 +175,35 @@ module IssuablesHelper end def issuable_meta(issuable, project, text) - output = "" + output = [] output << "Opened #{time_ago_with_tooltip(issuable.created_at)} by ".html_safe + output << content_tag(:strong) do author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "d-none d-sm-inline", tooltip: true) author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "d-block d-sm-none") if status = user_status(issuable.author) - author_output << "  #{status}".html_safe + author_output << "#{status}".html_safe end author_output end - output << " ".html_safe output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip', title: _('1st contribution!')) - output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block") + output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block prepend-left-8") output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none") - output.html_safe + output.join.html_safe end + # rubocop: disable CodeReuse/ActiveRecord def issuable_todo(issuable) if current_user current_user.todos.find_by(target: issuable, state: :pending) end end + # rubocop: enable CodeReuse/ActiveRecord def issuable_labels_tooltip(labels, limit: 5) first, last = labels.partition.with_index { |_, i| i < limit } diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 5b27d1d9404..f7d448ea3a7 100644 --- a/app/helpers/issues_helper.rb +++ b/app/helpers/issues_helper.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module IssuesHelper def issue_css_classes(issue) - classes = "issue" - classes << " closed" if issue.closed? - classes << " today" if issue.today? - classes + classes = ["issue"] + classes << "closed" if issue.closed? + classes << "today" if issue.today? + classes.join(' ') end # Returns an OpenStruct object suitable for use by <tt>options_from_collection_for_select</tt> @@ -105,8 +107,8 @@ module IssuesHelper end def link_to_discussions_to_resolve(merge_request, single_discussion = nil) - link_text = merge_request.to_reference - link_text += " (discussion #{single_discussion.first_note.id})" if single_discussion + link_text = [merge_request.to_reference] + link_text << "(discussion #{single_discussion.first_note.id})" if single_discussion path = if single_discussion Gitlab::UrlBuilder.build(single_discussion.first_note) @@ -115,7 +117,7 @@ module IssuesHelper project_merge_request_path(project, merge_request) end - link_to link_text, path + link_to link_text.join(' '), path end def show_new_issue_link?(project) diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb index cd4075b340d..7cb6da26236 100644 --- a/app/helpers/javascript_helper.rb +++ b/app/helpers/javascript_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module JavascriptHelper def page_specific_javascript_tag(js) javascript_include_tag asset_path(js) diff --git a/app/helpers/kerberos_spnego_helper.rb b/app/helpers/kerberos_spnego_helper.rb index f5b0aa7549a..c0eb8f83f56 100644 --- a/app/helpers/kerberos_spnego_helper.rb +++ b/app/helpers/kerberos_spnego_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module KerberosSpnegoHelper def allow_basic_auth? true # different behavior in GitLab Enterprise Edition diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb index c7df25cecef..6c51739ba1a 100644 --- a/app/helpers/labels_helper.rb +++ b/app/helpers/labels_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module LabelsHelper extend self include ActionView::Helpers::TagHelper diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb index 603b9438e35..ac987a04895 100644 --- a/app/helpers/lazy_image_tag_helper.rb +++ b/app/helpers/lazy_image_tag_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module LazyImageTagHelper def placeholder_image "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==" @@ -11,9 +13,11 @@ module LazyImageTagHelper options[:data] ||= {} options[:data][:src] = path_to_image(source) - options[:class] ||= "" - options[:class] << " lazy" + # options[:class] can be either String or Array. + klass_opts = Array.wrap(options[:class]) + klass_opts << "lazy" + options[:class] = klass_opts.join(' ') source = placeholder_image end diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index cbb971cf8b7..0d638b850b4 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'nokogiri' module MarkupHelper @@ -72,14 +74,21 @@ module MarkupHelper # the tag contents are truncated without removing the closing tag. def first_line_in_markdown(object, attribute, max_chars = nil, options = {}) md = markdown_field(object, attribute, options) + return nil unless md.present? - text = truncate_visible(md, max_chars || md.length) if md.present? + tags = %w(a gl-emoji b pre code p span) + tags << 'img' if options[:allow_images] - sanitize( + text = truncate_visible(md, max_chars || md.length) + text = sanitize( text, - tags: %w(a img gl-emoji b pre code p span), + tags: tags, attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version'] ) + + # since <img> tags are stripped, this can leave empty <a> tags hanging around + # (as our markdown wraps images in links) + options[:allow_images] ? text : strip_empty_link_tags(text).html_safe end def markdown(text, context = {}) @@ -107,23 +116,23 @@ module MarkupHelper def markup(file_name, text, context = {}) context[:project] ||= @project - context[:markdown_engine] ||= :redcarpet + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = context.delete(:rendered) || markup_unsafe(file_name, text, context) prepare_for_rendering(html, context) end - def render_wiki_content(wiki_page) + def render_wiki_content(wiki_page, context = {}) text = wiki_page.content return '' unless text.present? - context = { + context.merge!( pipeline: :wiki, project: @project, project_wiki: @project_wiki, page_slug: wiki_page.slug, - issuable_state_filter_enabled: true, - markdown_engine: :redcarpet - } + issuable_state_filter_enabled: true + ) + context[:markdown_engine] ||= :redcarpet unless commonmark_for_repositories_enabled? html = case wiki_page.format @@ -178,6 +187,10 @@ module MarkupHelper end end + def commonmark_for_repositories_enabled? + Feature.enabled?(:commonmark_for_repositories, default_enabled: true) + end + private # Return +text+, truncated to +max_chars+ characters, excluding any HTML @@ -229,6 +242,16 @@ module MarkupHelper end end + def strip_empty_link_tags(text) + scrubber = Loofah::Scrubber.new do |node| + node.remove if node.name == 'a' && node.content.blank? + end + + # Use `Loofah` directly instead of `sanitize` + # as we still use the `rails-deprecated_sanitizer` gem + Loofah.fragment(text).scrub!(scrubber).to_s + end + def markdown_toolbar_button(options = {}) data = options[:data].merge({ container: 'body' }) content_tag :button, diff --git a/app/helpers/mattermost_helper.rb b/app/helpers/mattermost_helper.rb index 27ff4051c8d..b211fe5076a 100644 --- a/app/helpers/mattermost_helper.rb +++ b/app/helpers/mattermost_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MattermostHelper def mattermost_teams_options(teams) teams.map do |team| diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb index a3129cac2b1..5a21403bc5e 100644 --- a/app/helpers/members_helper.rb +++ b/app/helpers/members_helper.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + module MembersHelper def remove_member_message(member, user: nil) user = current_user if defined?(current_user) + text = 'Are you sure you want to' - text = 'Are you sure you want to ' action = if member.request? if member.user == user @@ -16,13 +18,12 @@ module MembersHelper "remove #{member.user.name} from" end - text << action << " the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" + "#{text} #{action} the #{member.source.human_name} #{member.real_source_type.humanize(capitalize: false)}?" end def remove_member_title(member) - text = " from #{member.real_source_type.humanize(capitalize: false)}" - - text.prepend(member.request? ? 'Deny access request' : 'Remove user') + action = member.request? ? 'Deny access request' : 'Remove user' + "#{action} from #{member.real_source_type.humanize(capitalize: false)}" end def leave_confirmation_message(member_source) @@ -32,9 +33,6 @@ module MembersHelper def filter_group_project_member_path(options = {}) options = params.slice(:search, :sort).merge(options) - - path = request.path - path << "?#{options.to_param}" - path + "#{request.path}?#{options.to_param}" end end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index 097be8a0643..87af6fb08f0 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MergeRequestsHelper def new_mr_path_from_push_event(event) target_project = event.project.default_merge_request_target @@ -19,10 +21,10 @@ module MergeRequestsHelper end def mr_css_classes(mr) - classes = "merge-request" - classes << " closed" if mr.closed? - classes << " merged" if mr.merged? - classes + classes = ["merge-request"] + classes << "closed" if mr.closed? + classes << "merged" if mr.merged? + classes.join(' ') end def ci_build_details_path(merge_request) diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb index 95da8f00aff..94a030d9d57 100644 --- a/app/helpers/milestones_helper.rb +++ b/app/helpers/milestones_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MilestonesHelper include EntityDateHelper @@ -51,6 +53,7 @@ module MilestonesHelper # Returns count of milestones for different states # Uses explicit hash keys as the 'opened' state URL params differs from the db value # and we need to add the total + # rubocop: disable CodeReuse/ActiveRecord def milestone_counts(milestones) counts = milestones.reorder(nil).group(:state).count @@ -60,6 +63,7 @@ module MilestonesHelper all: counts.values.sum || 0 } end + # rubocop: enable CodeReuse/ActiveRecord # Show 'active' class if provided GET param matches check # `or_blank` allows the function to return 'active' when given an empty param @@ -119,20 +123,18 @@ module MilestonesHelper title = date_type == :start ? "Start date" : "End date" if date - time_ago = time_ago_in_words(date) - time_ago.slice!("about ") - - time_ago << if date.past? - " ago" - else - " remaining" - end + time_ago = time_ago_in_words(date).sub("about ", "") + state = if date.past? + "ago" + else + "remaining" + end content = [ title, "<br />", date.to_s(:medium), - "(#{time_ago})" + "(#{time_ago} #{state})" ].join(" ") content.html_safe diff --git a/app/helpers/milestones_routing_helper.rb b/app/helpers/milestones_routing_helper.rb index a0b2616f224..a49b561533a 100644 --- a/app/helpers/milestones_routing_helper.rb +++ b/app/helpers/milestones_routing_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MilestonesRoutingHelper def milestone_path(milestone, *args) if milestone.group_milestone? diff --git a/app/helpers/mirror_helper.rb b/app/helpers/mirror_helper.rb index 93ed22513ac..a4025730397 100644 --- a/app/helpers/mirror_helper.rb +++ b/app/helpers/mirror_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MirrorHelper def mirrors_form_data_attributes { project_mirror_endpoint: project_mirror_path(@project) } diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb index 6535afb6425..6c65e573307 100644 --- a/app/helpers/namespaces_helper.rb +++ b/app/helpers/namespaces_helper.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + module NamespacesHelper def namespace_id_from(params) params.dig(:project, :namespace_id) || params[:namespace_id] end + # rubocop: disable CodeReuse/ActiveRecord def namespaces_options(selected = :current_user, display_path: false, groups: nil, extra_group: nil, groups_only: false) groups ||= current_user.manageable_groups .eager_load(:route) @@ -40,6 +43,7 @@ module NamespacesHelper grouped_options_for_select(options, selected_id) end + # rubocop: enable CodeReuse/ActiveRecord def namespace_icon(namespace, size = 40) if namespace.is_a?(Group) @@ -53,6 +57,7 @@ module NamespacesHelper # Many importers create a temporary Group, so use the real # group if one exists by that name to prevent duplicates. + # rubocop: disable CodeReuse/ActiveRecord def dedup_extra_group(extra_group) unless extra_group.persisted? existing_group = Group.find_by(path: extra_group.path) @@ -61,6 +66,7 @@ module NamespacesHelper extra_group end + # rubocop: enable CodeReuse/ActiveRecord def options_for_group(namespaces, display_path:, type:) group_label = type.pluralize diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index a84a39235d8..761f42f2f0f 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module NavHelper def header_links @header_links ||= get_header_links diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index 5404ead44f3..a80c8f273a8 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module NotesHelper def note_target_fields(note) if note.noteable @@ -108,7 +110,7 @@ module NotesHelper end def noteable_note_url(note) - Gitlab::UrlBuilder.build(note) + Gitlab::UrlBuilder.build(note) if note.id end def form_resources diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb index a185f2916d4..5318ab4ddef 100644 --- a/app/helpers/notifications_helper.rb +++ b/app/helpers/notifications_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module NotificationsHelper include IconsHelper diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb index 45bd3606076..3c0b11c4d32 100644 --- a/app/helpers/numbers_helper.rb +++ b/app/helpers/numbers_helper.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + module NumbersHelper + # rubocop: disable CodeReuse/ActiveRecord def limited_counter_with_delimiter(resource, **options) limit = options.fetch(:limit, 1000).to_i count = resource.limit(limit + 1).count(:all) @@ -8,4 +11,5 @@ module NumbersHelper number_with_delimiter(count, options) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb index 68d892393ef..b33c074d1af 100644 --- a/app/helpers/page_layout_helper.rb +++ b/app/helpers/page_layout_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PageLayoutHelper def page_title(*titles) @page_title ||= [] @@ -65,14 +67,14 @@ module PageLayoutHelper end def page_card_meta_tags - tags = '' + tags = [] page_card_attributes.each_with_index do |pair, i| tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0]) tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1]) end - tags.html_safe + tags.join.html_safe end def header_title(title = nil, title_url = nil) @@ -115,16 +117,16 @@ module PageLayoutHelper end def container_class - css_class = "container-fluid" + css_class = ["container-fluid"] unless fluid_layout - css_class += " container-limited" + css_class << "container-limited" end if blank_container - css_class += " container-blank" + css_class << "container-blank" end - css_class + css_class.join(' ') end end diff --git a/app/helpers/pagination_helper.rb b/app/helpers/pagination_helper.rb index 83dd76a01dd..d05153c9d4b 100644 --- a/app/helpers/pagination_helper.rb +++ b/app/helpers/pagination_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PaginationHelper def paginate_collection(collection, remote: nil) if collection.is_a?(Kaminari::PaginatableWithoutCount) diff --git a/app/helpers/performance_bar_helper.rb b/app/helpers/performance_bar_helper.rb index d24efe37f5f..7518cec160c 100644 --- a/app/helpers/performance_bar_helper.rb +++ b/app/helpers/performance_bar_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PerformanceBarHelper # This is a hack since using `alias_method :performance_bar_enabled?, :peek_enabled?` # in WithPerformanceBar breaks tests (but works in the browser). diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index 4b9f6bd2caf..0e166106b32 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module PipelineSchedulesHelper def timezone_data ActiveSupport::TimeZone.all.map do |timezone| diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb index fb523cb865b..ff9842d4cd9 100644 --- a/app/helpers/preferences_helper.rb +++ b/app/helpers/preferences_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Helper methods for per-User preferences module PreferencesHelper def layout_choices diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index e7aa92e6e5c..55674e37a34 100644 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ProfilesHelper def attribute_provider_label(attribute) user_synced_attributes_metadata = current_user.user_synced_attributes_metadata diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 18b3badda8d..8b17e6ef75d 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ProjectsHelper def link_to_project(project) link_to [project.namespace.becomes(Namespace), project], title: h(project.name) do @@ -50,7 +52,7 @@ module ProjectsHelper return "(deleted)" unless author - author_html = "" + author_html = [] # Build avatar image tag author_html << link_to_member_avatar(author, opts) if opts[:avatar] @@ -60,7 +62,7 @@ module ProjectsHelper author_html << capture(&block) if block - author_html = author_html.html_safe + author_html = author_html.join.html_safe if opts[:name] link_to(author_html, user_path(author), class: "author-link #{"#{opts[:extra_class]}" if opts[:extra_class]} #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe @@ -80,15 +82,8 @@ module ProjectsHelper end project_link = link_to project_path(project) do - output = - if project.avatar_url && !Rails.env.test? - project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) - else - "" - end - - output << content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text") - output.html_safe + icon = project_icon(project, alt: project.name, class: 'avatar-tile', width: 15, height: 15) if project.avatar_url && !Rails.env.test? + [icon, content_tag("span", simple_sanitize(project.name), class: "breadcrumb-item-text js-breadcrumb-item-text")].join.html_safe end namespace_link = breadcrumb_list_item(namespace_link) unless project.group @@ -203,6 +198,14 @@ module ProjectsHelper current_user.require_extra_setup_for_git_auth? end + def show_auto_devops_implicitly_enabled_banner?(project) + cookie_key = "hide_auto_devops_implicitly_enabled_banner_#{project.id}" + + project.has_auto_devops_implicitly_enabled? && + cookies[cookie_key.to_sym].blank? && + (project.owner == current_user || project.team.maintainer?(current_user)) + end + def link_to_set_password if current_user.require_password_creation_for_git? link_to s_('SetPasswordToCloneLink|set a password'), edit_profile_password_path @@ -219,6 +222,7 @@ module ProjectsHelper # # If no limit is applied we'll just issue a COUNT since the result set could # be too large to load into memory. + # rubocop: disable CodeReuse/ActiveRecord def any_projects?(projects) return projects.any? if projects.is_a?(Array) @@ -228,6 +232,7 @@ module ProjectsHelper projects.except(:offset).any? end end + # rubocop: enable CodeReuse/ActiveRecord def show_projects?(projects, params) !!(params[:personal] || params[:name] || any_projects?(projects)) @@ -252,6 +257,10 @@ module ProjectsHelper "xcode://clone?repo=#{CGI.escape(default_url_to_repo(project))}" end + def legacy_render_context(params) + params[:legacy_render] ? { markdown_engine: :redcarpet } : {} + end + private def get_project_nav_tabs(project, current_user) @@ -351,6 +360,10 @@ module ProjectsHelper end end + def default_clone_label + _("Copy %{protocol} clone URL") % { protocol: default_clone_protocol.upcase } + end + def default_clone_protocol if allowed_protocols_present? enabled_protocol diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb index 9a842cf5ce0..c1505b52808 100644 --- a/app/helpers/repository_languages_helper.rb +++ b/app/helpers/repository_languages_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RepositoryLanguagesHelper def repository_languages_bar(languages) return if languages.none? diff --git a/app/helpers/rss_helper.rb b/app/helpers/rss_helper.rb index 7d4fa83a67a..67c7d244f11 100644 --- a/app/helpers/rss_helper.rb +++ b/app/helpers/rss_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RssHelper def rss_url_options { format: :atom, feed_token: current_user.try(:feed_token) } diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb index 9fb42487a75..cb21f922401 100644 --- a/app/helpers/runners_helper.rb +++ b/app/helpers/runners_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module RunnersHelper def runner_status_icon(runner) status = runner.status diff --git a/app/helpers/safe_params_helper.rb b/app/helpers/safe_params_helper.rb index b568e8810cc..18bbf3347a8 100644 --- a/app/helpers/safe_params_helper.rb +++ b/app/helpers/safe_params_helper.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + module SafeParamsHelper # Rails 5.0 requires to permit `params` if they're used in url helpers. # Use this helper when generating links with `params.merge(...)` + # rubocop: disable CodeReuse/ActiveRecord def safe_params if params.respond_to?(:permit!) params.except(:host, :port, :protocol).permit! @@ -8,4 +11,5 @@ module SafeParamsHelper params end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index 98074a4c0c5..4f9e1322b56 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SearchHelper def search_autocomplete_opts(term) return unless current_user @@ -99,6 +101,7 @@ module SearchHelper end # Autocomplete results for the current user's groups + # rubocop: disable CodeReuse/ActiveRecord def groups_autocomplete(term, limit = 5) current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group| { @@ -110,8 +113,10 @@ module SearchHelper } end end + # rubocop: enable CodeReuse/ActiveRecord # Autocomplete results for the current user's projects + # rubocop: disable CodeReuse/ActiveRecord def projects_autocomplete(term, limit = 5) current_user.authorized_projects.order_id_desc.search_by_title(term) .sorted_by_stars.non_archived.limit(limit).map do |p| @@ -125,6 +130,7 @@ module SearchHelper } end end + # rubocop: enable CodeReuse/ActiveRecord def search_result_sanitize(str) Sanitize.clean(str) diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb index 6cefcde558a..cf60696ef39 100644 --- a/app/helpers/selects_helper.rb +++ b/app/helpers/selects_helper.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + module SelectsHelper def users_select_tag(id, opts = {}) - css_class = "ajax-users-select " - css_class << "multiselect " if opts[:multiple] - css_class << "skip_ldap " if opts[:skip_ldap] + css_class = ["ajax-users-select"] + css_class << "multiselect" if opts[:multiple] + css_class << "skip_ldap" if opts[:skip_ldap] css_class << (opts[:class] || '') value = opts[:selected] || '' html = { - class: css_class, + class: css_class.join(' '), data: users_select_data_attributes(opts) } @@ -24,20 +26,21 @@ module SelectsHelper end def groups_select_tag(id, opts = {}) - opts[:class] ||= '' - opts[:class] << ' ajax-groups-select' + classes = Array.wrap(opts[:class]) + classes << 'ajax-groups-select' + + opts[:class] = classes.join(' ') + select2_tag(id, opts) end def namespace_select_tag(id, opts = {}) - opts[:class] ||= '' - opts[:class] << ' ajax-namespace-select' + opts[:class] = [*opts[:class], 'ajax-namespace-select'].join(' ') select2_tag(id, opts) end def project_select_tag(id, opts = {}) - opts[:class] ||= '' - opts[:class] << ' ajax-project-select' + opts[:class] = [*opts[:class], 'ajax-project-select'].join(' ') unless opts.delete(:scope) == :all if @group @@ -57,7 +60,10 @@ module SelectsHelper end def select2_tag(id, opts = {}) - opts[:class] << ' multiselect' if opts[:multiple] + klass_opts = [opts[:class]] + klass_opts << 'multiselect' if opts[:multiple] + + opts[:class] = klass_opts.join(' ') value = opts[:selected] || '' hidden_field_tag(id, value, opts) diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb index 3d255df66a0..d53eaef9952 100644 --- a/app/helpers/sentry_helper.rb +++ b/app/helpers/sentry_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SentryHelper def sentry_enabled? Gitlab::Sentry.enabled? diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb index f872990122e..d4b50b7ecfb 100644 --- a/app/helpers/services_helper.rb +++ b/app/helpers/services_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module ServicesHelper def service_event_description(event) case event @@ -30,7 +32,7 @@ module ServicesHelper end def service_save_button(service) - button_tag(class: 'btn btn-save', type: 'submit', disabled: service.deprecated?) do + button_tag(class: 'btn btn-success', type: 'submit', disabled: service.deprecated?) do icon('spinner spin', class: 'hidden js-btn-spinner') + content_tag(:span, 'Save changes', class: 'js-btn-label') end diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb index 50aeb7f4b82..32bf3526571 100644 --- a/app/helpers/sidekiq_helper.rb +++ b/app/helpers/sidekiq_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SidekiqHelper SIDEKIQ_PS_REGEXP = %r{\A (?<pid>\d+)\s+ diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb index a05640773ad..c7d31f3469d 100644 --- a/app/helpers/snippets_helper.rb +++ b/app/helpers/snippets_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SnippetsHelper def reliable_snippet_path(snippet, opts = nil) if snippet.project_id? diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb index 36a311dfa8a..53bd43d4861 100644 --- a/app/helpers/sorting_helper.rb +++ b/app/helpers/sorting_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SortingHelper def sort_options_hash { @@ -22,7 +24,8 @@ module SortingHelper sort_value_recently_updated => sort_title_recently_updated, sort_value_popularity => sort_title_popularity, sort_value_priority => sort_title_priority, - sort_value_upvotes => sort_title_upvotes + sort_value_upvotes => sort_title_upvotes, + sort_value_contacted_date => sort_title_contacted_date } end @@ -32,7 +35,8 @@ module SortingHelper sort_value_name => sort_title_name, sort_value_oldest_activity => sort_title_oldest_activity, sort_value_oldest_created => sort_title_oldest_created, - sort_value_recently_created => sort_title_recently_created + sort_value_recently_created => sort_title_recently_created, + sort_value_most_stars => sort_title_most_stars } if current_controller?('admin/projects') @@ -99,6 +103,17 @@ module SortingHelper } end + def label_sort_options_hash + { + sort_value_name => sort_title_name, + sort_value_name_desc => sort_title_name_desc, + sort_value_recently_created => sort_title_recently_created, + sort_value_oldest_created => sort_title_oldest_created, + sort_value_recently_updated => sort_title_recently_updated, + sort_value_oldest_updated => sort_title_oldest_updated + } + end + def sortable_item(item, path, sorted_by) link_to item, path, class: sorted_by == item ? 'is-active' : '' end @@ -228,6 +243,14 @@ module SortingHelper s_('SortOptions|Most popular') end + def sort_title_contacted_date + s_('SortOptions|Last Contact') + end + + def sort_title_most_stars + s_('SortOptions|Most stars') + end + # Values. def sort_value_access_level_asc 'access_level_asc' @@ -348,4 +371,12 @@ module SortingHelper def sort_value_upvotes 'upvotes_desc' end + + def sort_value_contacted_date + 'contacted_asc' + end + + def sort_value_most_stars + 'stars_desc' + end end diff --git a/app/helpers/storage_health_helper.rb b/app/helpers/storage_health_helper.rb index b76c1228220..182e8e6641b 100644 --- a/app/helpers/storage_health_helper.rb +++ b/app/helpers/storage_health_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module StorageHealthHelper def failing_storage_health_message(storage_health) storage_name = content_tag(:strong, h(storage_health.storage_name)) diff --git a/app/helpers/storage_helper.rb b/app/helpers/storage_helper.rb index e19c67a37ca..be8761db562 100644 --- a/app/helpers/storage_helper.rb +++ b/app/helpers/storage_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module StorageHelper def storage_counter(size_in_bytes) precision = size_in_bytes < 1.megabyte ? 0 : 1 diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb index ec2cf2b16c0..164c69ca50b 100644 --- a/app/helpers/submodule_helper.rb +++ b/app/helpers/submodule_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SubmoduleHelper extend self diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb index 5b4a141dbcf..ac4e8f54260 100644 --- a/app/helpers/system_note_helper.rb +++ b/app/helpers/system_note_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module SystemNoteHelper ICON_NAMES_BY_ACTION = { 'commit' => 'commit', @@ -21,7 +23,8 @@ module SystemNoteHelper 'outdated' => 'pencil-square', 'duplicate' => 'issue-duplicate', 'locked' => 'lock', - 'unlocked' => 'lock-open' + 'unlocked' => 'lock-open', + 'due_date' => 'calendar' }.freeze def system_note_icon_name(note) diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb index ee701076a14..d91f0f78db7 100644 --- a/app/helpers/tab_helper.rb +++ b/app/helpers/tab_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TabHelper # Navigation link helper # @@ -6,7 +8,7 @@ module TabHelper # element is the value passed to the block. # # options - The options hash used to determine if the element is "active" (default: {}) - # :controller - One or more controller names to check (optional). + # :controller - One or more controller names to check, use path notation when namespaced (optional). # :action - One or more action names to check (optional). # :path - A shorthand path, such as 'dashboard#index', to check (optional). # :html_options - Extra options to be passed to the list element (optional). @@ -40,6 +42,20 @@ module TabHelper # nav_link(controller: :tree, html_options: {class: 'home'}) { "Hello" } # # => '<li class="home active">Hello</li>' # + # # For namespaced controllers like Admin::AppearancesController#show + # + # # Controller and namespace matches + # nav_link(controller: 'admin/appearances') { "Hello" } + # # => '<li class="active">Hello</li>' + # + # # Controller and namespace matches but action doesn't + # nav_link(controller: 'admin/appearances', action: :edit) { "Hello" } + # # => '<li>Hello</li>' + # + # # Shorthand path with namespace + # nav_link(path: 'admin/appearances#show') { "Hello"} + # # => '<li class="active">Hello</li>' + # # Returns a list item element String def nav_link(options = {}, &block) klass = active_nav_link?(options) ? 'active' : '' @@ -47,9 +63,7 @@ module TabHelper # Add our custom class into the html_options, which may or may not exist # and which may or may not already have a :class key o = options.delete(:html_options) || {} - o[:class] ||= '' - o[:class] += ' ' + klass - o[:class].strip! + o[:class] = [*o[:class], klass].join(' ').strip if block_given? content_tag(:li, capture(&block), o) diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index d000d6b1c0a..de0b92b6fd7 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TagsHelper def tag_path(tag) "/tags/#{tag}" @@ -14,12 +16,13 @@ module TagsHelper end def tag_list(project) - html = '' + html = [] + project.tag_list.each do |tag| html << link_to(tag, tag_path(tag)) end - html.html_safe + html.join.html_safe end def protected_tag?(project, tag) diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 336385f6798..94044d7b85e 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TimeHelper def time_interval_in_words(interval_in_seconds) interval_in_seconds = interval_in_seconds.to_i diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 7cd74358168..6bd78336ed3 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TodosHelper def todos_pending_count @todos_pending_count ||= current_user.todos_pending_count @@ -94,9 +96,7 @@ module TodosHelper end end - path = request.path - path << "?#{options.to_param}" - path + "#{request.path}?#{options.to_param}" end def todo_actions_options @@ -152,10 +152,11 @@ module TodosHelper '' end - html = "· ".html_safe - html << content_tag(:span, class: css_class) do + content = content_tag(:span, class: css_class) do "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}" end + + "· #{content}".html_safe end private diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb index dc42caa70e5..6d2da5699fb 100644 --- a/app/helpers/tree_helper.rb +++ b/app/helpers/tree_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TreeHelper FILE_LIMIT = 1_000 @@ -5,10 +7,11 @@ module TreeHelper # their corresponding partials # # tree - A `Tree` object for the current tree + # rubocop: disable CodeReuse/ActiveRecord def render_tree(tree) # Sort submodules and folders together by name ahead of files folders, files, submodules = tree.trees, tree.blobs, tree.submodules - tree = '' + tree = [] items = (folders + submodules).sort_by(&:name) + files if items.size > FILE_LIMIT @@ -18,8 +21,9 @@ module TreeHelper end tree << render(partial: 'projects/tree/tree_row', collection: items) if items.present? - tree.html_safe + tree.join.html_safe end + # rubocop: enable CodeReuse/ActiveRecord # Return an image icon depending on the file type and mode # @@ -122,6 +126,7 @@ module TreeHelper end # returns the relative path of the first subdir that doesn't have only one directory descendant + # rubocop: disable CodeReuse/ActiveRecord def flatten_tree(root_path, tree) return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present? @@ -132,6 +137,7 @@ module TreeHelper return tree.name end end + # rubocop: enable CodeReuse/ActiveRecord def selected_branch @branch_name || tree_edit_branch diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb index ce435ca2241..5cfdc0971f0 100644 --- a/app/helpers/triggers_helper.rb +++ b/app/helpers/triggers_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module TriggersHelper def builds_trigger_url(project_id, ref: nil) if ref.nil? diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index da5fe25c07d..bae01d476df 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -1,6 +1,9 @@ +# frozen_string_literal: true + module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze + CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -11,6 +14,10 @@ module UserCalloutsHelper !user_dismissed?(GCP_SIGNUP_OFFER) end + def show_cluster_security_warning? + !user_dismissed?(CLUSTER_SECURITY_WARNING) + end + private def user_dismissed?(feature_name) diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 2c0c4254a0c..bcd91f619c8 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module UsersHelper def user_link(user) link_to(user.name, user_path(user), diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index c20753ece72..75637eb0676 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + module VersionCheckHelper def version_status_badge - if Rails.env.production? && Gitlab::CurrentSettings.version_check_enabled - image_url = VersionCheck.new.url - image_tag image_url, class: 'js-version-status-badge' - end + return unless Rails.env.production? + return unless Gitlab::CurrentSettings.version_check_enabled + return if User.single_user&.requires_usage_stats_consent? + + image_url = VersionCheck.new.url + image_tag image_url, class: 'js-version-status-badge' end end diff --git a/app/helpers/visibility_level_helper.rb b/app/helpers/visibility_level_helper.rb index cf2fe5a2019..e690350a0d1 100644 --- a/app/helpers/visibility_level_helper.rb +++ b/app/helpers/visibility_level_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module VisibilityLevelHelper def visibility_level_color(level) case level @@ -82,7 +84,7 @@ module VisibilityLevelHelper def disallowed_project_visibility_level_description(level, project) level_name = Gitlab::VisibilityLevel.level_name(level).downcase reasons = [] - instructions = '' + instructions = [] unless project.visibility_level_allowed_as_fork?(level) reasons << "the fork source project has lower visibility" @@ -96,7 +98,7 @@ module VisibilityLevelHelper end reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' - "This project cannot be #{level_name}#{reasons}.#{instructions}".html_safe + "This project cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe end # Note: these messages closely mirror the form validation strings found in the group @@ -104,7 +106,7 @@ module VisibilityLevelHelper def disallowed_group_visibility_level_description(level, group) level_name = Gitlab::VisibilityLevel.level_name(level).downcase reasons = [] - instructions = '' + instructions = [] unless group.visibility_level_allowed_by_projects?(level) reasons << "it contains projects with higher visibility" @@ -122,7 +124,7 @@ module VisibilityLevelHelper end reasons = reasons.any? ? ' because ' + reasons.to_sentence : '' - "This group cannot be #{level_name}#{reasons}.#{instructions}".html_safe + "This group cannot be #{level_name}#{reasons}.#{instructions.join}".html_safe end def visibility_icon_description(form_model) @@ -138,7 +140,7 @@ module VisibilityLevelHelper end def project_visibility_icon_description(level) - "#{visibility_level_label(level)} - #{project_visibility_level_description(level)}" + "#{project_visibility_level_description(level)}" end def visibility_level_label(level) diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 72f6b397046..345ddcf023a 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module WebpackHelper def webpack_bundle_tag(bundle) javascript_include_tag(*webpack_entrypoint_paths(bundle)) diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb index 17940aeb900..647f34e57ed 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module WikiHelper include API::Helpers::RelatedResourcesHelpers diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb index fd1d78bd9b8..f19445fca1a 100644 --- a/app/helpers/workhorse_helper.rb +++ b/app/helpers/workhorse_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Helpers to send Git blobs, diffs, patches or archives through Workhorse. # Workhorse will also serve files when using `send_file`. module WorkhorseHelper diff --git a/app/mailers/emails/auto_devops.rb b/app/mailers/emails/auto_devops.rb new file mode 100644 index 00000000000..9705a3052d4 --- /dev/null +++ b/app/mailers/emails/auto_devops.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Emails + module AutoDevops + def autodevops_disabled_email(pipeline, recipient) + @pipeline = pipeline + @project = pipeline.project + + add_project_headers + + mail(to: recipient, + subject: auto_devops_disabled_subject(@project.name)) do |format| + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } + end + end + + private + + def auto_devops_disabled_subject(project_name) + subject("Auto DevOps pipeline was disabled for #{project_name}") + end + end +end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index c8b1ab5033a..602e5afe26b 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -19,6 +19,7 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end + # rubocop: disable CodeReuse/ActiveRecord def reassigned_issue_email(recipient_id, issue_id, previous_assignee_ids, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) @@ -27,6 +28,7 @@ module Emails mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason)) end + # rubocop: enable CodeReuse/ActiveRecord def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 70f65d4e58d..67af0a4eb98 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -22,12 +22,14 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end + # rubocop: disable CodeReuse/ActiveRecord def reassigned_merge_request_email(recipient_id, merge_request_id, previous_assignee_id, updated_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) @previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end + # rubocop: enable CodeReuse/ActiveRecord def relabeled_merge_request_email(recipient_id, merge_request_id, label_names, updated_by_user_id, reason = nil) setup_merge_request_mail(merge_request_id, recipient_id) diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb index 40d7b9ccd7a..2ea1aea1f51 100644 --- a/app/mailers/emails/profile.rb +++ b/app/mailers/emails/profile.rb @@ -9,6 +9,7 @@ module Emails mail(to: @user.notification_email, subject: subject("Account was created for you")) end + # rubocop: disable CodeReuse/ActiveRecord def new_ssh_key_email(key_id) @key = Key.find_by(id: key_id) @@ -18,7 +19,9 @@ module Emails @target_url = user_url(@user) mail(to: @user.notification_email, subject: subject("SSH key was added to your account")) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def new_gpg_key_email(gpg_key_id) @gpg_key = GpgKey.find_by(id: gpg_key_id) @@ -28,5 +31,6 @@ module Emails @target_url = user_url(@user) mail(to: @user.notification_email, subject: subject("GPG key was added to your account")) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index f4eeb85270e..f7347ee61b4 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -12,6 +12,7 @@ class Notify < BaseMailer include Emails::Profile include Emails::Pipelines include Emails::Members + include Emails::AutoDevops helper MergeRequestsHelper helper DiffHelper diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb index df470930e9e..2f5b5483e9d 100644 --- a/app/mailers/previews/notify_preview.rb +++ b/app/mailers/previews/notify_preview.rb @@ -9,7 +9,7 @@ class NotifyPreview < ActionMailer::Preview In this notification email, we expect to see: - The note contents (that's what you're looking at) - - A link to view this note on Gitlab + - A link to view this note on GitLab - An explanation for why the user is receiving this notification MD @@ -26,7 +26,7 @@ class NotifyPreview < ActionMailer::Preview - A line saying who started this discussion - The note contents (that's what you're looking at) - - A link to view this discussion on Gitlab + - A link to view this discussion on GitLab - An explanation for why the user is receiving this notification MD @@ -44,7 +44,7 @@ class NotifyPreview < ActionMailer::Preview - A line saying who started this discussion and on what file - The diff - The note contents (that's what you're looking at) - - A link to view this discussion on Gitlab + - A link to view this discussion on GitLab - An explanation for why the user is receiving this notification MD @@ -125,6 +125,10 @@ class NotifyPreview < ActionMailer::Preview Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) end + def autodevops_disabled_email + Notify.autodevops_disabled_email(pipeline, user.email).message + end + private def project diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb index 4bcf371cfc0..145169be8a6 100644 --- a/app/mailers/repository_check_mailer.rb +++ b/app/mailers/repository_check_mailer.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class RepositoryCheckMailer < BaseMailer + # rubocop: disable CodeReuse/ActiveRecord def notify(failed_count) @message = if failed_count == 1 @@ -14,4 +15,5 @@ class RepositoryCheckMailer < BaseMailer subject: "GitLab Admin | #{@message}" ) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 03bd7fa016e..645adddb000 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -219,6 +219,7 @@ class ApplicationSetting < ActiveRecord::Base validate :terms_exist, if: :enforce_terms? before_validation :ensure_uuid! + before_validation :strip_sentry_values before_save :ensure_runners_registration_token before_save :ensure_health_check_access_token @@ -302,7 +303,8 @@ class ApplicationSetting < ActiveRecord::Base instance_statistics_visibility_private: false, user_default_external: false, user_default_internal_regex: nil, - user_show_add_ssh_key_message: true + user_show_add_ssh_key_message: true, + usage_stats_set_by_user_id: nil } end @@ -381,6 +383,11 @@ class ApplicationSetting < ActiveRecord::Base super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) end + def strip_sentry_values + sentry_dsn.strip! if sentry_dsn.present? + clientside_sentry_dsn.strip! if clientside_sentry_dsn.present? + end + def performance_bar_allowed_group Group.find_by_id(performance_bar_allowed_group_id) end diff --git a/app/models/badge.rb b/app/models/badge.rb index 7e3b6b659e4..f016654206b 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class Badge < ActiveRecord::Base + include FromUnion + # This structure sets the placeholders that the urls # can have. This hash also sets which action to ask when # the placeholder is found. diff --git a/app/models/blob_viewer/gitlab_ci_yml.rb b/app/models/blob_viewer/gitlab_ci_yml.rb index 1a86f04b1b9..655241c2808 100644 --- a/app/models/blob_viewer/gitlab_ci_yml.rb +++ b/app/models/blob_viewer/gitlab_ci_yml.rb @@ -10,16 +10,16 @@ module BlobViewer self.file_types = %i(gitlab_ci) self.binary = false - def validation_message + def validation_message(project, sha) return @validation_message if defined?(@validation_message) prepare! - @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data) + @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data, { project: project, sha: sha }) end - def valid? - validation_message.blank? + def valid?(project, sha) + validation_message(project, sha).blank? end end end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index faa160ad6ba..63aaa0f7bcc 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -40,6 +40,7 @@ module Ci delegate :url, to: :runner_session, prefix: true, allow_nil: true delegate :terminal_specification, to: :runner_session, allow_nil: true delegate :gitlab_deploy_token, to: :project + delegate :trigger_short_token, to: :trigger_request, allow_nil: true ## # The "environment" field for builds is a String, and is the unexpanded name! @@ -139,9 +140,11 @@ module Ci end def retry(build, current_user) + # rubocop: disable CodeReuse/ServiceClass Ci::RetryBuildService .new(build.project, current_user) .execute(build) + # rubocop: enable CodeReuse/ServiceClass end end @@ -224,11 +227,13 @@ module Ci self.when == 'manual' end + # rubocop: disable CodeReuse/ServiceClass def play(current_user) Ci::PlayBuildService .new(project, current_user) .execute(self) end + # rubocop: enable CodeReuse/ServiceClass def cancelable? active? || created? @@ -385,9 +390,11 @@ module Ci update(coverage: coverage) if coverage.present? end + # rubocop: disable CodeReuse/ServiceClass def parse_trace_sections! ExtractSectionsFromBuildTraceService.new(project, user).execute(self) end + # rubocop: enable CodeReuse/ServiceClass def trace Gitlab::Ci::Trace.new(self) @@ -647,8 +654,31 @@ module Ci end end + # Virtual deployment status depending on the environment status. + def deployment_status + return nil unless starts_environment? + + if success? + return successful_deployment_status + elsif complete? && !success? + return :failed + end + + :creating + end + private + def successful_deployment_status + if success? && last_deployment&.last? + return :last + elsif success? && last_deployment.present? + return :out_of_date + end + + :creating + end + def each_test_report Ci::JobArtifact::TEST_REPORT_FILE_TYPES.each do |file_type| public_send("job_artifacts_#{file_type}").each_blob do |blob| # rubocop:disable GitlabSecurity/PublicSend diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 4853b23513c..93fc1b145b2 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -87,7 +87,9 @@ module Ci end def hashed_path? - super || self.try(:file_location).nil? + return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column + + super || self.file_location.nil? end def expire_in diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 526bf7af99b..6dac577c514 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -161,6 +161,12 @@ module Ci PipelineNotificationWorker.perform_async(pipeline.id) end end + + after_transition any => [:failed] do |pipeline| + next unless pipeline.auto_devops_source? + + pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) } + end end scope :internal, -> { where(source: internal_sources) } @@ -381,10 +387,12 @@ module Ci end end + # rubocop: disable CodeReuse/ServiceClass def retry_failed(current_user) Ci::RetryPipelineService.new(project, current_user) .execute(self) end + # rubocop: enable CodeReuse/ServiceClass def mark_as_processable_after_stage(stage_idx) builds.skipped.after_stage(stage_idx).find_each(&:process) @@ -458,7 +466,7 @@ module Ci return @config_processor if defined?(@config_processor) @config_processor ||= begin - Gitlab::Ci::YamlProcessor.new(ci_yaml_file) + ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha }) rescue Gitlab::Ci::YamlProcessor::ValidationError => e self.yaml_errors = e.message nil @@ -519,9 +527,11 @@ module Ci project.notes.for_commit_id(sha) end + # rubocop: disable CodeReuse/ServiceClass def process! Ci::ProcessPipelineService.new(project, user).execute(self) end + # rubocop: enable CodeReuse/ServiceClass def update_status retry_optimistic_lock(self) do diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index f41955f43e7..3e815937f4b 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -7,11 +7,14 @@ module Ci include IgnorableColumn include RedisCacheable include ChronicDurationAttribute + include FromUnion RUNNER_QUEUE_EXPIRY_TIME = 60.minutes ONLINE_CONTACT_TIMEOUT = 1.hour UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes - AVAILABLE_SCOPES = %w[specific shared active paused online].freeze + AVAILABLE_TYPES = %w[specific shared].freeze + AVAILABLE_STATUSES = %w[active paused online offline].freeze + AVAILABLE_SCOPES = (AVAILABLE_TYPES + AVAILABLE_STATUSES).freeze FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze ignore_column :is_shared @@ -29,6 +32,13 @@ module Ci scope :active, -> { where(active: true) } scope :paused, -> { where(active: false) } scope :online, -> { where('contacted_at > ?', contact_time_deadline) } + # The following query using negation is cheaper than using `contacted_at <= ?` + # because there are less runners online than have been created. The + # resulting query is quickly finding online ones and then uses the regular + # indexed search and rejects the ones that are in the previous set. If we + # did `contacted_at <= ?` the query would effectively have to do a seq + # scan. + scope :offline, -> { where.not(id: online) } scope :ordered, -> { order(id: :desc) } # BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb` @@ -48,21 +58,32 @@ module Ci } scope :owned_or_instance_wide, -> (project_id) do - union = Gitlab::SQL::Union.new( - [belonging_to_project(project_id), belonging_to_parent_group_of_project(project_id), instance_type], + from_union( + [ + belonging_to_project(project_id), + belonging_to_parent_group_of_project(project_id), + instance_type + ], remove_duplicates: false ) - from("(#{union.to_sql}) ci_runners") end scope :assignable_for, ->(project) do # FIXME: That `to_sql` is needed to workaround a weird Rails bug. # Without that, placeholders would miss one and couldn't match. + # + # We use "unscoped" here so that any current Ci::Runner filters don't + # apply to the inner query, which is not necessary. + exclude_runners = unscoped { project.runners.select(:id) }.to_sql + where(locked: false) - .where.not("ci_runners.id IN (#{project.runners.select(:id).to_sql})") + .where.not("ci_runners.id IN (#{exclude_runners})") .project_type end + scope :order_contacted_at_asc, -> { order(contacted_at: :asc) } + scope :order_created_at_desc, -> { order(created_at: :desc) } + validate :tag_constraints validates :access_level, presence: true validates :runner_type, presence: true @@ -115,6 +136,14 @@ module Ci ONLINE_CONTACT_TIMEOUT.ago end + def self.order_by(order) + if order == 'contacted_asc' + order_contacted_at_asc + else + order_created_at_desc + end + end + def set_default_values self.token = SecureRandom.hex(15) if self.token.blank? end diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb index 913936a0bcb..0b52c690e93 100644 --- a/app/models/ci/trigger_request.rb +++ b/app/models/ci/trigger_request.rb @@ -8,6 +8,8 @@ module Ci belongs_to :pipeline, foreign_key: :commit_id has_many :builds + delegate :short_token, to: :trigger, prefix: true, allow_nil: true + # We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables. # Ci::TriggerRequest doesn't save variables anymore. validates :variables, absence: true diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 55bbf7cae7e..423071ec024 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -32,7 +32,8 @@ module Clusters def install_command Gitlab::Kubernetes::Helm::InitCommand.new( name: name, - files: files + files: files, + rbac: cluster.platform_kubernetes_rbac? ) end diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb index 93f654e0638..bd0286ee3f9 100644 --- a/app/models/clusters/applications/ingress.rb +++ b/app/models/clusters/applications/ingress.rb @@ -39,6 +39,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) diff --git a/app/models/clusters/applications/jupyter.rb b/app/models/clusters/applications/jupyter.rb index ef1c76c03bd..3d84eeed5a8 100644 --- a/app/models/clusters/applications/jupyter.rb +++ b/app/models/clusters/applications/jupyter.rb @@ -40,6 +40,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 88399dbbb95..46d0388a464 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -48,6 +48,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files ) @@ -71,7 +72,7 @@ module Clusters private def kube_client - cluster&.kubeclient + cluster&.kubeclient&.core_client end end end diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index bde255723c8..a4a2e2b79a6 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -33,6 +33,7 @@ module Clusters Gitlab::Kubernetes::Helm::InstallCommand.new( name: name, version: VERSION, + rbac: cluster.platform_kubernetes_rbac?, chart: chart, files: files, repository: repository diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 7cf75403ab6..d7011ef447a 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -42,6 +42,7 @@ module Clusters delegate :on_creation?, to: :provider, allow_nil: true delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true + delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true diff --git a/app/models/clusters/platforms/kubernetes.rb b/app/models/clusters/platforms/kubernetes.rb index e6ddca0d5d0..3a335909101 100644 --- a/app/models/clusters/platforms/kubernetes.rb +++ b/app/models/clusters/platforms/kubernetes.rb @@ -5,6 +5,7 @@ module Clusters class Kubernetes < ActiveRecord::Base include Gitlab::Kubernetes include ReactiveCaching + include EnumWithNil self.table_name = 'cluster_platforms_kubernetes' self.reactive_cache_key = ->(kubernetes) { [kubernetes.class.model_name.singular, kubernetes.id] } @@ -47,6 +48,12 @@ module Clusters alias_method :active?, :enabled? + enum_with_nil authorization_type: { + unknown_authorization: nil, + rbac: 1, + abac: 2 + } + def actual_namespace if namespace.present? namespace @@ -95,7 +102,7 @@ module Clusters end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end private @@ -115,15 +122,16 @@ module Clusters slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace unless (username && password) || token raise "Either username/password or token is required to access API" end - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -133,7 +141,7 @@ module Clusters # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -157,15 +165,6 @@ module Clusters { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/commit.rb b/app/models/commit.rb index 594972ad344..49c36ad9d3f 100644 --- a/app/models/commit.rb +++ b/app/models/commit.rb @@ -22,6 +22,7 @@ class Commit attr_accessor :project, :author attr_accessor :redacted_description_html attr_accessor :redacted_title_html + attr_accessor :redacted_full_title_html attr_reader :gpg_commit DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines] diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b65d7672973..fe2f144ef03 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -58,9 +58,11 @@ class CommitStatus < ActiveRecord::Base # These are pages deployments and external statuses. # before_create unless: :importing? do + # rubocop: disable CodeReuse/ServiceClass Ci::EnsureStageService.new(project, user).execute(self) do |stage| self.run_after_commit { StageUpdateWorker.perform_async(stage.id) } end + # rubocop: enable CodeReuse/ServiceClass end state_machine :status do @@ -130,10 +132,12 @@ class CommitStatus < ActiveRecord::Base after_transition any => :failed do |commit_status| next unless commit_status.project + # rubocop: disable CodeReuse/ServiceClass commit_status.run_after_commit do MergeRequests::AddTodoWhenBuildFailsService .new(project, nil).execute(self) end + # rubocop: enable CodeReuse/ServiceClass end end diff --git a/app/models/concerns/case_sensitivity.rb b/app/models/concerns/case_sensitivity.rb index 6e80365ee5b..c93b6589ee7 100644 --- a/app/models/concerns/case_sensitivity.rb +++ b/app/models/concerns/case_sensitivity.rb @@ -9,23 +9,46 @@ module CaseSensitivity # # Unlike other ActiveRecord methods this method only operates on a Hash. def iwhere(params) - criteria = self - cast_lower = Gitlab::Database.postgresql? + criteria = self params.each do |key, value| - column = ActiveRecord::Base.connection.quote_table_name(key) + criteria = case value + when Array + criteria.where(value_in(key, value)) + else + criteria.where(value_equal(key, value)) + end + end + + criteria + end - condition = - if cast_lower - "LOWER(#{column}) = LOWER(:value)" - else - "#{column} = :value" - end + private + + def value_equal(column, value) + lower_value = lower_value(value) + + lower_column(arel_table[column]).eq(lower_value).to_sql + end - criteria = criteria.where(condition, value: value) + def value_in(column, values) + lower_values = values.map do |value| + lower_value(value) end - criteria + lower_column(arel_table[column]).in(lower_values).to_sql + end + + def lower_value(value) + return value if Gitlab::Database.mysql? + + Arel::Nodes::NamedFunction.new('LOWER', [Arel::Nodes.build_quoted(value)]) + end + + def lower_column(column) + return column if Gitlab::Database.mysql? + + column.lower end end end diff --git a/app/models/concerns/from_union.rb b/app/models/concerns/from_union.rb new file mode 100644 index 00000000000..9b8595b1211 --- /dev/null +++ b/app/models/concerns/from_union.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module FromUnion + extend ActiveSupport::Concern + + class_methods do + # Produces a query that uses a FROM to select data using a UNION. + # + # Using a FROM for a UNION has in the past lead to better query plans. As + # such, we generally recommend this pattern instead of using a WHERE IN. + # + # Example: + # users = User.from_union([User.where(id: 1), User.where(id: 2)]) + # + # This would produce the following SQL query: + # + # SELECT * + # FROM ( + # SELECT * + # FROM users + # WHERE id = 1 + # + # UNION + # + # SELECT * + # FROM users + # WHERE id = 2 + # ) users; + # + # members - An Array of ActiveRecord::Relation objects to use in the UNION. + # + # remove_duplicates - A boolean indicating if duplicate entries should be + # removed. Defaults to true. + # + # alias_as - The alias to use for the sub query. Defaults to the name of the + # table of the current model. + # rubocop: disable Gitlab/Union + def from_union(members, remove_duplicates: true, alias_as: table_name) + union = Gitlab::SQL::Union + .new(members, remove_duplicates: remove_duplicates) + .to_sql + + # This pattern is necessary as a bug in Rails 4 can cause the use of + # `from("string here").includes(:foo)` to break ActiveRecord. This is + # fixed in https://github.com/rails/rails/pull/25374, which is released as + # part of Rails 5. + from([Arel.sql("(#{union}) #{alias_as}")]) + end + # rubocop: enable Gitlab/Union + end +end diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f881ce2321c..5f65fceb7af 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -49,7 +49,7 @@ module Issuable end end - has_many :label_links, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :label_links, as: :target, dependent: :destroy, inverse_of: :target # rubocop:disable Cop/ActiveRecordDependent has_many :labels, through: :label_links has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent @@ -109,10 +109,6 @@ module Issuable false end - def etag_caching_enabled? - false - end - def has_multiple_assignees? assignees.count > 1 end diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb index 393607e82c4..298d0d42d90 100644 --- a/app/models/concerns/mentionable.rb +++ b/app/models/concerns/mentionable.rb @@ -61,7 +61,7 @@ module Mentionable cache_key: [self, attr], author: author, skip_project_check: skip_project_check? - ) + ).merge(mentionable_params) extractor.analyze(text, options) end @@ -86,12 +86,11 @@ module Mentionable return [] unless matches_cross_reference_regex? refs = all_references(current_user) - refs = (refs.issues + refs.merge_requests + refs.commits) # We're using this method instead of Array diffing because that requires # both of the object's `hash` values to be the same, which may not be the # case for otherwise identical Commit objects. - refs.reject { |ref| ref == local_reference } + extracted_mentionables(refs).reject { |ref| ref == local_reference } end # Uses regex to quickly determine if mentionables might be referenced @@ -134,6 +133,10 @@ module Mentionable private + def extracted_mentionables(refs) + refs.issues + refs.merge_requests + refs.commits + end + # Returns a Hash of changed mentionable fields # # Preference is given to the `changes` Hash, but falls back to @@ -161,4 +164,8 @@ module Mentionable def skip_project_check? false end + + def mentionable_params + {} + end end diff --git a/app/models/concerns/mentionable/reference_regexes.rb b/app/models/concerns/mentionable/reference_regexes.rb index f6fd28bac33..fe8fbb71184 100644 --- a/app/models/concerns/mentionable/reference_regexes.rb +++ b/app/models/concerns/mentionable/reference_regexes.rb @@ -5,13 +5,19 @@ module Mentionable def self.reference_pattern(link_patterns, issue_pattern) Regexp.union(link_patterns, issue_pattern, - Commit.reference_pattern, - MergeRequest.reference_pattern) + *other_patterns) + end + + def self.other_patterns + [ + Commit.reference_pattern, + MergeRequest.reference_pattern + ] end DEFAULT_PATTERN = begin issue_pattern = Issue.reference_pattern - link_patterns = Regexp.union([Issue, Commit, MergeRequest].map(&:link_reference_pattern)) + link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact) reference_pattern(link_patterns, issue_pattern) end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index ce778eae271..098eed137ba 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -82,4 +82,23 @@ module Noteable def lockable? [MergeRequest, Issue].include?(self.class) end + + def etag_caching_enabled? + false + end + + def expire_note_etag_cache + return unless discussions_rendered_on_frontend? + return unless etag_caching_enabled? + + Gitlab::EtagCaching::Store.new.touch(note_etag_key) + end + + def note_etag_key + Gitlab::Routing.url_helpers.project_noteable_notes_path( + project, + target_type: self.class.name.underscore, + target_id: id + ) + end end diff --git a/app/models/concerns/project_services_loggable.rb b/app/models/concerns/project_services_loggable.rb new file mode 100644 index 00000000000..fecd77cdc98 --- /dev/null +++ b/app/models/concerns/project_services_loggable.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module ProjectServicesLoggable + def log_info(message, params = {}) + message = build_message(message, params) + + logger.info(message) + end + + def log_error(message, params = {}) + message = build_message(message, params) + + logger.error(message) + end + + def build_message(message, params = {}) + { + service_class: self.class.name, + project_id: project.id, + project_path: project.full_path, + message: message + }.merge(params) + end + + def logger + Gitlab::ProjectServiceLogger + end +end diff --git a/app/models/concerns/protected_branch_access.rb b/app/models/concerns/protected_branch_access.rb index 744f7f48dc8..58761fce952 100644 --- a/app/models/concerns/protected_branch_access.rb +++ b/app/models/concerns/protected_branch_access.rb @@ -2,18 +2,17 @@ module ProtectedBranchAccess extend ActiveSupport::Concern + include ProtectedRefAccess included do - include ProtectedRefAccess - belongs_to :protected_branch delegate :project, to: :protected_branch + end - def check_access(user) - return false if access_level == Gitlab::Access::NO_ACCESS + def check_access(user) + return false if access_level == Gitlab::Access::NO_ACCESS - super - end + super end end diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index efa666fb3f2..583751ea6ac 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -3,18 +3,22 @@ module ProtectedRefAccess extend ActiveSupport::Concern - ALLOWED_ACCESS_LEVELS = [ - Gitlab::Access::MAINTAINER, - Gitlab::Access::DEVELOPER, - Gitlab::Access::NO_ACCESS - ].freeze - HUMAN_ACCESS_LEVELS = { Gitlab::Access::MAINTAINER => "Maintainers".freeze, Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze, Gitlab::Access::NO_ACCESS => "No one".freeze }.freeze + class_methods do + def allowed_access_levels + [ + Gitlab::Access::MAINTAINER, + Gitlab::Access::DEVELOPER, + Gitlab::Access::NO_ACCESS + ] + end + end + included do scope :master, -> { maintainer } # @deprecated scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) } @@ -26,7 +30,7 @@ module ProtectedRefAccess scope :for_group, -> { where.not(group_id: nil) } validates :access_level, presence: true, if: :role?, inclusion: { - in: ALLOWED_ACCESS_LEVELS + in: self.allowed_access_levels } end diff --git a/app/models/concerns/protected_tag_access.rb b/app/models/concerns/protected_tag_access.rb index 04bd54d6b1c..3f5696c0749 100644 --- a/app/models/concerns/protected_tag_access.rb +++ b/app/models/concerns/protected_tag_access.rb @@ -2,10 +2,9 @@ module ProtectedTagAccess extend ActiveSupport::Concern + include ProtectedRefAccess included do - include ProtectedRefAccess - belongs_to :protected_tag delegate :project, to: :protected_tag diff --git a/app/models/concerns/storage/legacy_namespace.rb b/app/models/concerns/storage/legacy_namespace.rb index 3b745657a9e..9785011720a 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -25,8 +25,6 @@ module Storage Gitlab::PagesTransfer.new.rename_namespace(full_path_was, full_path) end - remove_exports! - # If repositories moved successfully we need to # send update instructions to users. # However we cannot allow rollback since we moved namespace dir @@ -101,8 +99,6 @@ module Storage end end end - - remove_exports! end def remove_legacy_exports! diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb index 41413854d5c..2c08a8e1acf 100644 --- a/app/models/container_repository.rb +++ b/app/models/container_repository.rb @@ -8,8 +8,7 @@ class ContainerRepository < ActiveRecord::Base delegate :client, to: :registry - before_destroy :delete_tags! - + # rubocop: disable CodeReuse/ServiceClass def registry @registry ||= begin token = Auth::ContainerRegistryAuthenticationService.full_access_token(path) @@ -20,6 +19,7 @@ class ContainerRepository < ActiveRecord::Base ContainerRegistry::Registry.new(url, token: token, path: host_port) end end + # rubocop: enable CodeReuse/ServiceClass def path @path ||= [project.full_path, name] diff --git a/app/models/dashboard_group_milestone.rb b/app/models/dashboard_group_milestone.rb index 13807d43265..32e8104125c 100644 --- a/app/models/dashboard_group_milestone.rb +++ b/app/models/dashboard_group_milestone.rb @@ -13,7 +13,11 @@ class DashboardGroupMilestone < GlobalMilestone end def self.build_collection(groups) - MilestonesFinder.new(group_ids: groups.pluck(:id)).execute.map { |m| new(m) } + Milestone.of_groups(groups.select(:id)) + .reorder_by_due_date_asc + .order_by_name_asc + .active + .map { |m| new(m) } end override :group_milestone? diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb index fd5d7726fb6..db501b4b506 100644 --- a/app/models/deploy_key.rb +++ b/app/models/deploy_key.rb @@ -18,7 +18,7 @@ class DeployKey < Key end def orphaned? - self.deploy_keys_projects.length == 0 + self.deploy_keys_projects.empty? end def almost_orphaned? diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb index 716cf6574d3..047d353b4b5 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -131,7 +131,7 @@ class DiffNote < Note # As an extra benefit, the returned `diff_file` already # has `highlighted_diff_lines` data set from Redis on # `Diff::FileCollection::MergeRequestDiff`. - noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first + noteable.diffs(original_position.diff_options).diff_files.first else original_position.diff_file(self.project.repository) end diff --git a/app/models/environment.rb b/app/models/environment.rb index c8d1d378ae0..309bd4f37c9 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -158,9 +158,11 @@ class Environment < ActiveRecord::Base prometheus_adapter.query(:additional_metrics_environment, self) if has_metrics? end + # rubocop: disable CodeReuse/ServiceClass def prometheus_adapter @prometheus_adapter ||= Prometheus::AdapterService.new(project, deployment_platform).prometheus_adapter end + # rubocop: enable CodeReuse/ServiceClass def slug super.presence || generate_slug diff --git a/app/models/epic.rb b/app/models/epic.rb index f027993376c..ccd10593434 100644 --- a/app/models/epic.rb +++ b/app/models/epic.rb @@ -3,6 +3,10 @@ # Placeholder class for model that is implemented in EE # It reserves '&' as a reference prefix, but the table does not exists in CE class Epic < ActiveRecord::Base + def self.link_reference_pattern + nil + end + def self.reference_prefix '&' end diff --git a/app/models/event.rb b/app/models/event.rb index ba28866e8e6..596155a9525 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -3,6 +3,7 @@ class Event < ActiveRecord::Base include Sortable include IgnorableColumn + include FromUnion default_scope { reorder(nil) } CREATED = 1 @@ -151,15 +152,17 @@ class Event < ActiveRecord::Base if push? || commit_note? Ability.allowed?(user, :download_code, project) elsif membership_changed? - true + Ability.allowed?(user, :read_project, project) elsif created_project? - true + Ability.allowed?(user, :read_project, project) elsif issue? || issue_note? Ability.allowed?(user, :read_issue, note? ? note_target : target) elsif merge_request? || merge_request_note? Ability.allowed?(user, :read_merge_request, note? ? note_target : target) + elsif milestone? + Ability.allowed?(user, :read_project, project) else - milestone? + false # No other event types are visible end end diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb index 6e23e811b0e..a6cebabe089 100644 --- a/app/models/global_milestone.rb +++ b/app/models/global_milestone.rb @@ -17,7 +17,7 @@ class GlobalMilestone params = { project_ids: projects.map(&:id), state: params[:state] } - child_milestones = MilestonesFinder.new(params).execute + child_milestones = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder milestones = child_milestones.select(:id, :title).group_by(&:title).map do |title, grouped| milestones_relation = Milestone.where(id: grouped.map(&:id)) @@ -48,7 +48,7 @@ class GlobalMilestone params = { group_ids: [group.id], state: 'all' } - relation = MilestonesFinder.new(params).execute + relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder grouped_by_state = relation.reorder(nil).group(:state).count { @@ -64,7 +64,7 @@ class GlobalMilestone params = { project_ids: projects.map(&:id), state: 'all' } - relation = MilestonesFinder.new(params).execute + relation = MilestonesFinder.new(params).execute # rubocop: disable CodeReuse/Finder project_milestones_by_state_and_title = relation.reorder(nil).group(:state, :title).count opened = count_by_state(project_milestones_by_state_and_title, 'active') diff --git a/app/models/group.rb b/app/models/group.rb index 106a1f4a94c..62af20d2142 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -236,14 +236,18 @@ class Group < Namespace system_hook_service.execute_hooks_for(self, :destroy) end + # rubocop: disable CodeReuse/ServiceClass def system_hook_service SystemHooksService.new end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def refresh_members_authorized_projects(blocking: true) UserProjectAccessChangedService.new(user_ids_for_project_authorizations) .execute(blocking: blocking) end + # rubocop: enable CodeReuse/ServiceClass def user_ids_for_project_authorizations members_with_parents.pluck(:user_id) @@ -300,14 +304,12 @@ class Group < Namespace # 3. They belong to a sub-group or project in such sub-group # 4. They belong to an ancestor group def direct_and_indirect_users - union = Gitlab::SQL::Union.new([ + User.from_union([ User .where(id: direct_and_indirect_members.select(:user_id)) .reorder(nil), project_users_with_descendants ]) - - User.from("(#{union.to_sql}) #{User.table_name}") end # Returns all users that are members of projects diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb index ea046bea368..283e2d680f4 100644 --- a/app/models/hooks/active_hook_filter.rb +++ b/app/models/hooks/active_hook_filter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ActiveHookFilter def initialize(hook) @hook = hook diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb index bda82a116a1..7d9f6d89d44 100644 --- a/app/models/hooks/service_hook.rb +++ b/app/models/hooks/service_hook.rb @@ -4,7 +4,9 @@ class ServiceHook < WebHook belongs_to :service validates :service, presence: true + # rubocop: disable CodeReuse/ServiceClass def execute(data) WebHookService.new(self, data, 'service_hook').execute end + # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb index 20f15c15277..771a61b090f 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -11,13 +11,17 @@ class WebHook < ActiveRecord::Base validates :token, format: { without: /\n/ } validates :push_events_branch_filter, branch_filter: true + # rubocop: disable CodeReuse/ServiceClass def execute(data, hook_name) WebHookService.new(self, data, hook_name).execute end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def async_execute(data, hook_name) WebHookService.new(self, data, hook_name).async_execute end + # rubocop: enable CodeReuse/ServiceClass # Allow urls pointing localhost and the local network def allow_local_requests? diff --git a/app/models/issue.rb b/app/models/issue.rb index d0cd7461daa..d13fbcf002c 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -172,6 +172,7 @@ class Issue < ActiveRecord::Base # All branches containing the current issue's ID, except for # those with a merge request open referencing the current issue. + # rubocop: disable CodeReuse/ServiceClass def related_branches(current_user) branches_with_iid = project.repository.branch_names.select do |branch| branch =~ /\A#{iid}-(?!\d+-stable)/i @@ -185,6 +186,7 @@ class Issue < ActiveRecord::Base branches_with_iid - branches_with_merge_request end + # rubocop: enable CodeReuse/ServiceClass def suggested_branch_name return to_branch_name unless project.repository.branch_exists?(to_branch_name) @@ -278,9 +280,11 @@ class Issue < ActiveRecord::Base true end + # rubocop: disable CodeReuse/ServiceClass def update_project_counter_caches Projects::OpenIssuesCountService.new(project).refresh_cache end + # rubocop: enable CodeReuse/ServiceClass private diff --git a/app/models/key.rb b/app/models/key.rb index 3bb0d2f6f9c..bdb83e12793 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -55,9 +55,11 @@ class Key < ActiveRecord::Base "key-#{id}" end + # rubocop: disable CodeReuse/ServiceClass def update_last_used_at Keys::LastUsedService.new(self).execute end + # rubocop: enable CodeReuse/ServiceClass def add_to_shell GitlabShellWorker.perform_async( @@ -67,9 +69,11 @@ class Key < ActiveRecord::Base ) end + # rubocop: disable CodeReuse/ServiceClass def post_create_hook SystemHooksService.new.execute_hooks_for(self, :create) end + # rubocop: enable CodeReuse/ServiceClass def remove_from_shell GitlabShellWorker.perform_async( @@ -79,15 +83,19 @@ class Key < ActiveRecord::Base ) end + # rubocop: disable CodeReuse/ServiceClass def refresh_user_cache return unless user Users::KeysCountService.new(user).refresh_cache end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def post_destroy_hook SystemHooksService.new.execute_hooks_for(self, :destroy) end + # rubocop: enable CodeReuse/ServiceClass def public_key @public_key ||= Gitlab::SSHPublicKey.new(key) diff --git a/app/models/label.rb b/app/models/label.rb index 96c1515b41a..9ef57a05b3e 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -5,6 +5,9 @@ class Label < ActiveRecord::Base include Referable include Subscribable include Gitlab::SQL::Pattern + include OptionallySearch + include Sortable + include FromUnion # Represents a "No Label" state used for filtering Issues and Merge # Requests that have no label assigned. @@ -40,6 +43,8 @@ class Label < ActiveRecord::Base scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } + scope :order_name_asc, -> { reorder(title: :asc) } + scope :order_name_desc, -> { reorder(title: :desc) } def self.prioritized(project) joins(:priorities) diff --git a/app/models/label_link.rb b/app/models/label_link.rb index 779657b25d5..1d93a55e8e9 100644 --- a/app/models/label_link.rb +++ b/app/models/label_link.rb @@ -3,7 +3,7 @@ class LabelLink < ActiveRecord::Base include Importable - belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations + belongs_to :target, polymorphic: true, inverse_of: :label_links # rubocop:disable Cop/PolymorphicAssociations belongs_to :label validates :target, presence: true, unless: :importing? diff --git a/app/models/label_note.rb b/app/models/label_note.rb new file mode 100644 index 00000000000..680952cf421 --- /dev/null +++ b/app/models/label_note.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +class LabelNote < Note + attr_accessor :resource_parent + attr_reader :events + + def self.from_events(events, resource: nil, resource_parent: nil) + resource ||= events.first.issuable + + attrs = { + system: true, + author: events.first.user, + created_at: events.first.created_at, + discussion_id: events.first.discussion_id, + noteable: resource, + system_note_metadata: SystemNoteMetadata.new(action: 'label'), + events: events, + resource_parent: resource_parent + } + + if resource_parent.is_a?(Project) + attrs[:project_id] = resource_parent.id + end + + LabelNote.new(attrs) + end + + def events=(events) + @events = events + + update_outdated_markdown + end + + def cached_html_up_to_date?(markdown_field) + true + end + + def note + @note ||= note_text + end + + def note_html + @note_html ||= "<p dir=\"auto\">#{note_text(html: true)}</p>" + end + + def project + resource_parent if resource_parent.is_a?(Project) + end + + def group + resource_parent if resource_parent.is_a?(Group) + end + + private + + def update_outdated_markdown + events.each do |event| + if event.outdated_markdown? + event.refresh_invalid_reference + end + end + 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, removed].compact.join(' and ') + 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) + existing_refs = label_refs.select { |ref| ref.present? }.sort + refs_str = existing_refs.empty? ? nil : existing_refs.join(' ') + + deleted = label_refs.count - existing_refs.count + deleted_str = deleted == 0 ? nil : "#{deleted} deleted" + + return nil unless refs_str || deleted_str + + label_list_str = [refs_str, deleted_str].compact.join(' + ') + suffix = 'label'.pluralize(deleted > 0 ? deleted : existing_refs.count) + + "#{prefix} #{label_list_str} #{suffix}" + end + + def label_refs_by_action(action, html) + field = html ? :reference_html : :reference + + events.select { |e| e.action == action }.map(&field) + end +end diff --git a/app/models/license_template.rb b/app/models/license_template.rb index 0ad75b27827..693a6a89fd2 100644 --- a/app/models/license_template.rb +++ b/app/models/license_template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class LicenseTemplate PROJECT_TEMPLATE_REGEX = %r{[\<\{\[] diff --git a/app/models/member.rb b/app/models/member.rb index d9b4e8d2ac6..0696ea46c8b 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -145,6 +145,7 @@ class Member < ActiveRecord::Base end def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false) + # rubocop: disable CodeReuse/ServiceClass # `user` can be either a User object, User ID or an email to be invited member = retrieve_member(source, user, existing_members) access_level = retrieve_access_level(access_level) @@ -171,6 +172,7 @@ class Member < ActiveRecord::Base end member + # rubocop: enable CodeReuse/ServiceClass end def add_users(source, users, access_level, current_user: nil, expires_at: nil) @@ -339,12 +341,14 @@ class Member < ActiveRecord::Base @notification_setting ||= user&.notification_settings_for(source) end + # rubocop: disable CodeReuse/ServiceClass def notifiable?(type, opts = {}) # always notify when there isn't a user yet return true if user.blank? NotificationRecipientService.notifiable?(user, type, notifiable_options.merge(opts)) end + # rubocop: enable CodeReuse/ServiceClass private @@ -374,6 +378,7 @@ class Member < ActiveRecord::Base # in a transaction. Doing so can lead to the job running before the # transaction has been committed, resulting in the job either throwing an # error or not doing any meaningful work. + # rubocop: disable CodeReuse/ServiceClass def refresh_member_authorized_projects # If user/source is being destroyed, project access are going to be # destroyed eventually because of DB foreign keys, so we shouldn't bother @@ -382,6 +387,7 @@ class Member < ActiveRecord::Base UserProjectAccessChangedService.new(user_id).execute end + # rubocop: enable CodeReuse/ServiceClass def after_accept_invite post_create_hook @@ -395,13 +401,17 @@ class Member < ActiveRecord::Base post_create_hook end + # rubocop: disable CodeReuse/ServiceClass def system_hook_service SystemHooksService.new end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def notification_service NotificationService.new end + # rubocop: enable CodeReuse/ServiceClass def notifiable_options {} diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 0154fe5aeba..537f2a3a231 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -138,7 +138,9 @@ class ProjectMember < Member super end + # rubocop: disable CodeReuse/ServiceClass def event_service EventCreateService.new end + # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 396647a14ae..dd5d494997d 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -14,6 +14,7 @@ class MergeRequest < ActiveRecord::Base include Gitlab::Utils::StrongMemoize include LabelEventable include ReactiveCaching + include FromUnion self.reactive_cache_key = ->(model) { [model.project.id, model.iid] } self.reactive_cache_refresh_interval = 10.minutes @@ -137,12 +138,14 @@ class MergeRequest < ActiveRecord::Base Gitlab::Timeless.timeless(merge_request, &block) end + # rubocop: disable CodeReuse/ServiceClass after_transition unchecked: :cannot_be_merged do |merge_request, transition| if merge_request.notify_conflict? NotificationService.new.merge_request_unmergeable(merge_request) TodoService.new.merge_request_became_unmergeable(merge_request) end end + # rubocop: enable CodeReuse/ServiceClass def check_state?(merge_status) [:unchecked, :cannot_be_merged_recheck].include?(merge_status.to_sym) @@ -235,11 +238,10 @@ class MergeRequest < ActiveRecord::Base def self.in_projects(relation) # unscoping unnecessary conditions that'll be applied # when executing `where("merge_requests.id IN (#{union.to_sql})")` - source = unscoped.where(source_project_id: relation).select(:id) - target = unscoped.where(target_project_id: relation).select(:id) - union = Gitlab::SQL::Union.new([source, target]) + source = unscoped.where(source_project_id: relation) + target = unscoped.where(target_project_id: relation) - where("merge_requests.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + from_union([source, target]) end # This is used after project import, to reset the IDs to the correct @@ -623,11 +625,13 @@ class MergeRequest < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def reload_diff(current_user = nil) return unless open? MergeRequests::ReloadDiffsService.new(self, current_user).execute end + # rubocop: enable CodeReuse/ServiceClass def check_if_can_be_merged return unless self.class.state_machines[:merge_status].check_state?(merge_status) && Gitlab::Database.read_write? @@ -736,11 +740,8 @@ class MergeRequest < ActiveRecord::Base # compared to using OR statements. We're using UNION ALL since the queries # used won't produce any duplicates (e.g. a note for a commit can't also be # a note for an MR). - union = Gitlab::SQL::Union - .new([notes, commit_notes], remove_duplicates: false) - .to_sql - - Note.from("(#{union}) #{Note.table_name}") + Note + .from_union([notes, commit_notes], remove_duplicates: false) .includes(:noteable) end @@ -1036,6 +1037,7 @@ class MergeRequest < ActiveRecord::Base actual_head_pipeline&.has_test_reports? end + # rubocop: disable CodeReuse/ServiceClass def compare_test_reports unless has_test_reports? return { status: :error, status_reason: 'This merge request does not have test reports' } @@ -1050,7 +1052,9 @@ class MergeRequest < ActiveRecord::Base data end || { status: :parsing } end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def calculate_reactive_cache(identifier, *args) case identifier.to_sym when :compare_test_results @@ -1060,6 +1064,7 @@ class MergeRequest < ActiveRecord::Base raise NotImplementedError, "Unknown identifier: #{identifier}" end end + # rubocop: enable CodeReuse/ServiceClass def all_commits # MySQL doesn't support LIMIT in a subquery. @@ -1125,6 +1130,7 @@ class MergeRequest < ActiveRecord::Base diff_refs && diff_refs.complete? end + # rubocop: disable CodeReuse/ServiceClass def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil) return unless has_complete_diff_refs? return if new_diff_refs == old_diff_refs @@ -1154,6 +1160,7 @@ class MergeRequest < ActiveRecord::Base .execute(self) end end + # rubocop: enable CodeReuse/ServiceClass def keep_around_commit project.repository.keep_around(self.merge_commit_sha) @@ -1189,9 +1196,11 @@ class MergeRequest < ActiveRecord::Base true end + # rubocop: disable CodeReuse/ServiceClass def update_project_counter_caches Projects::OpenMergeRequestsCountService.new(target_project).refresh_cache end + # rubocop: enable CodeReuse/ServiceClass def first_contribution? return false if project.team.max_member_access(author_id) > Gitlab::Access::GUEST diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb index bbe4f6f7969..02c6b650f33 100644 --- a/app/models/merge_request_diff.rb +++ b/app/models/merge_request_diff.rb @@ -219,12 +219,14 @@ class MergeRequestDiff < ActiveRecord::Base self.id == merge_request.latest_merge_request_diff_id end + # rubocop: disable CodeReuse/ServiceClass def compare_with(sha) # When compare merge request versions we want diff A..B instead of A...B # so we handle cases when user does squash and rebase of the commits between versions. # For this reason we set straight to true by default. CompareService.new(project, head_commit_sha).execute(project, sha, straight: true) end + # rubocop: enable CodeReuse/ServiceClass private diff --git a/app/models/milestone.rb b/app/models/milestone.rb index cb1def1b422..892a680f221 100644 --- a/app/models/milestone.rb +++ b/app/models/milestone.rb @@ -46,6 +46,9 @@ class Milestone < ActiveRecord::Base where(conditions.reduce(:or)) end + scope :order_by_name_asc, -> { order(Arel::Nodes::Ascending.new(arel_table[:title].lower)) } + scope :reorder_by_due_date_asc, -> { reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) } + validates :group, presence: true, unless: :project validates :project, presence: true, unless: :group @@ -149,7 +152,7 @@ class Milestone < ActiveRecord::Base sorted = case method.to_s when 'due_date_asc' - reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC')) + reorder_by_due_date_asc when 'due_date_desc' reorder(Gitlab::Database.nulls_last_order('due_date', 'DESC')) when 'name_asc' diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 0deb44d7916..0289f29211d 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -11,6 +11,7 @@ class Namespace < ActiveRecord::Base include Gitlab::SQL::Pattern include IgnorableColumn include FeatureGate + include FromUnion ignore_column :deleted_at @@ -253,18 +254,6 @@ class Namespace < ActiveRecord::Base end end - # Exports belonging to projects with legacy storage are placed in a common - # subdirectory of the namespace, so a simple `rm -rf` is sufficient to remove - # them. - # - # Exports of projects using hashed storage are placed in a location defined - # only by the project ID, so each must be removed individually. - def remove_exports! - remove_legacy_exports! - - all_projects.with_storage_feature(:repository).find_each(&:remove_exports) - end - def refresh_project_authorizations owner.refresh_authorized_projects end diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb index 6c5a4c56377..1b2369aab18 100644 --- a/app/models/network/commit.rb +++ b/app/models/network/commit.rb @@ -18,7 +18,7 @@ module Network end def space - if @spaces.size > 0 + if @spaces.present? @spaces.first else 0 diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb index 1431dfefc55..6da3bb7bfb7 100644 --- a/app/models/network/graph.rb +++ b/app/models/network/graph.rb @@ -81,7 +81,7 @@ module Network skip = 0 while offset == -1 tmp_commits = find_commits(skip) - if tmp_commits.size > 0 + if tmp_commits.present? index = tmp_commits.index do |c| c.id == @commit.id end @@ -218,7 +218,7 @@ module Network def get_space_base(leaves) space_base = 1 parents = leaves.last.parents(@map) - if parents.size > 0 + if parents.present? if parents.first.space > 0 space_base = parents.first.space end diff --git a/app/models/note.rb b/app/models/note.rb index 2e343b8f9f8..bea02d69b65 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -17,6 +17,7 @@ class Note < ActiveRecord::Base include Editable include Gitlab::SQL::Pattern include ThrottledTouch + include FromUnion module SpecialRole FIRST_TIME_CONTRIBUTOR = :first_time_contributor @@ -181,6 +182,7 @@ class Note < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def cross_reference? return unless system? @@ -190,6 +192,7 @@ class Note < ActiveRecord::Base SystemNoteService.cross_reference?(note) end end + # rubocop: enable CodeReuse/ServiceClass def diff_note? false @@ -389,18 +392,7 @@ class Note < ActiveRecord::Base end def expire_etag_cache - return unless noteable&.discussions_rendered_on_frontend? - return unless noteable&.etag_caching_enabled? - - Gitlab::EtagCaching::Store.new.touch(etag_key) - end - - def etag_key - Gitlab::Routing.url_helpers.project_noteable_notes_path( - project, - target_type: noteable_type.underscore, - target_id: noteable_id - ) + noteable&.expire_note_etag_cache end def touch(*args) diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb index 7739a3894d3..7a33ade826b 100644 --- a/app/models/pages_domain.rb +++ b/app/models/pages_domain.rb @@ -140,9 +140,11 @@ class PagesDomain < ActiveRecord::Base self.verification_code = SecureRandom.hex(16) end + # rubocop: disable CodeReuse/ServiceClass def update_daemon ::Projects::UpdatePagesConfigurationService.new(project).execute end + # rubocop: enable CodeReuse/ServiceClass def pages_config_changed? project_id_changed? || diff --git a/app/models/project.rb b/app/models/project.rb index 97d9fa355ef..0a5099b27b1 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -29,6 +29,7 @@ class Project < ActiveRecord::Base include BatchDestroyDependentAssociations include FeatureGate include OptionallySearch + include FromUnion extend Gitlab::Cache::RequestCache extend Gitlab::ConfigHelper @@ -232,6 +233,8 @@ class Project < ActiveRecord::Base has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' has_many :cluster_ingresses, through: :clusters, source: :application_ingress, class_name: 'Clusters::Applications::Ingress' + has_many :prometheus_metrics + # Container repositories need to remove data from the container registry, # which is not managed by the DB. Hence we're still using dependent: :destroy # here. @@ -328,7 +331,7 @@ class Project < ActiveRecord::Base # last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push scope :sorted_by_activity, -> { reorder("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC") } - scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } + scope :sorted_by_stars, -> { reorder(star_count: :desc) } scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) } scope :personal, ->(user) { where(namespace_id: user.namespace_id) } @@ -478,6 +481,8 @@ class Project < ActiveRecord::Base reorder(last_activity_at: :desc) when 'latest_activity_asc' reorder(last_activity_at: :asc) + when 'stars_desc' + sorted_by_stars else order_by(method) end @@ -567,7 +572,6 @@ class Project < ActiveRecord::Base end def cleanup - @repository&.cleanup @repository = nil end @@ -1113,12 +1117,14 @@ class Project < ActiveRecord::Base find_or_initialize_services.find { |service| service.to_param == name } end + # rubocop: disable CodeReuse/ServiceClass def create_labels Label.templates.each do |label| params = label.attributes.except('id', 'template', 'created_at', 'updated_at') Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true) end end + # rubocop: enable CodeReuse/ServiceClass def find_service(list, name) list.find { |service| service.to_param == name } @@ -1166,6 +1172,7 @@ class Project < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def send_move_instructions(old_path_with_namespace) # New project path needs to be committed to the DB or notification will # retrieve stale information @@ -1173,6 +1180,7 @@ class Project < ActiveRecord::Base NotificationService.new.project_was_moved(self, old_path_with_namespace) end end + # rubocop: enable CodeReuse/ServiceClass def owner if group @@ -1182,6 +1190,7 @@ class Project < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def execute_hooks(data, hooks_scope = :push_hooks) run_after_commit_or_now do hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook| @@ -1190,6 +1199,7 @@ class Project < ActiveRecord::Base SystemHooksService.new.execute_hooks(data, hooks_scope) end end + # rubocop: enable CodeReuse/ServiceClass def execute_services(data, hooks_scope = :push_hooks) # Call only service hooks that are active for this scope @@ -1486,8 +1496,7 @@ class Project < ActiveRecord::Base end def all_runners - union = Gitlab::SQL::Union.new([runners, group_runners, shared_runners]) - Ci::Runner.from("(#{union.to_sql}) ci_runners") + Ci::Runner.from_union([runners, group_runners, shared_runners]) end def active_runners @@ -1504,13 +1513,17 @@ class Project < ActiveRecord::Base self.runners_token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.runners_token) end + # rubocop: disable CodeReuse/ServiceClass def open_issues_count(current_user = nil) Projects::OpenIssuesCountService.new(self, current_user).count end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def open_merge_requests_count Projects::OpenMergeRequestsCountService.new(self).count end + # rubocop: enable CodeReuse/ServiceClass def visibility_level_allowed_as_fork?(level = self.visibility_level) return true unless forked? @@ -1591,6 +1604,7 @@ class Project < ActiveRecord::Base end # TODO: what to do here when not using Legacy Storage? Do we still need to rename and delay removal? + # rubocop: disable CodeReuse/ServiceClass def remove_pages # Projects with a missing namespace cannot have their pages removed return unless namespace @@ -1606,6 +1620,7 @@ class Project < ActiveRecord::Base PagesWorker.perform_in(5.minutes, :remove, namespace.full_path, temp_path) end end + # rubocop: enable CodeReuse/ServiceClass def rename_repo path_before = previous_changes['path'].first @@ -1666,6 +1681,7 @@ class Project < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch return unless default_branch @@ -1686,6 +1702,7 @@ class Project < ActiveRecord::Base ProtectedBranches::CreateService.new(self, creator, params).execute(skip_authorization: true) end end + # rubocop: enable CodeReuse/ServiceClass def remove_import_jid return unless import_jid @@ -1733,16 +1750,12 @@ class Project < ActiveRecord::Base import_export_shared.archive_path end - def export_project_path - Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } - end - def export_status if export_in_progress? :started elsif after_export_in_progress? :after_export_action - elsif export_project_path || export_project_object_exists? + elsif export_file_exists? :finished else :none @@ -1757,21 +1770,19 @@ class Project < ActiveRecord::Base import_export_shared.after_export_in_progress? end - def remove_exports(path = export_path) - if path.present? - FileUtils.rm_rf(path) - elsif export_project_object_exists? - import_export_upload.remove_export_file! - import_export_upload.save - end + def remove_exports + return unless export_file_exists? + + import_export_upload.remove_export_file! + import_export_upload.save end - def remove_exported_project_file - remove_exports(export_project_path) + def export_file_exists? + export_file&.file end - def export_project_object_exists? - Gitlab::ImportExport.object_storage? && import_export_upload&.export_file&.file + def export_file + import_export_upload&.export_file end def full_path_slug @@ -1922,9 +1933,11 @@ class Project < ActiveRecord::Base # @deprecated cannot remove yet because it has an index with its name in elasticsearch alias_method :path_with_namespace, :full_path + # rubocop: disable CodeReuse/ServiceClass def forks_count Projects::ForksCountService.new(self).count end + # rubocop: enable CodeReuse/ServiceClass def legacy_storage? [nil, 0].include?(self.storage_version) @@ -2011,12 +2024,10 @@ class Project < ActiveRecord::Base def badges return project_badges unless group - group_badges_rel = GroupBadge.where(group: group.self_and_ancestors) - - union = Gitlab::SQL::Union.new([project_badges.select(:id), - group_badges_rel.select(:id)]) - - Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + Badge.from_union([ + project_badges, + GroupBadge.where(group: group.self_and_ancestors) + ]) end def merge_requests_allowing_push_to_user(user) @@ -2069,6 +2080,7 @@ class Project < ActiveRecord::Base private + # rubocop: disable CodeReuse/ServiceClass def rename_or_migrate_repository! if Gitlab::CurrentSettings.hashed_storage_enabled? && storage_upgradable? && @@ -2078,6 +2090,7 @@ class Project < ActiveRecord::Base storage.rename_repo end end + # rubocop: enable CodeReuse/ServiceClass def storage_upgradable? storage_version != LATEST_STORAGE_VERSION @@ -2102,6 +2115,7 @@ class Project < ActiveRecord::Base self.project_feature.untrack_statistics_for_deletion! end + # rubocop: disable CodeReuse/ServiceClass def execute_rename_repository_hooks!(full_path_before) # When we import a project overwriting the original project, there # is a move operation. In that case we don't want to send the instructions. @@ -2112,6 +2126,7 @@ class Project < ActiveRecord::Base reload_repository! end + # rubocop: enable CodeReuse/ServiceClass def storage @storage ||= diff --git a/app/models/project_authorization.rb b/app/models/project_authorization.rb index 746bb4584c9..2c590008db2 100644 --- a/app/models/project_authorization.rb +++ b/app/models/project_authorization.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ProjectAuthorization < ActiveRecord::Base + include FromUnion + belongs_to :user belongs_to :project @@ -8,9 +10,9 @@ class ProjectAuthorization < ActiveRecord::Base validates :access_level, inclusion: { in: Gitlab::Access.all_values }, presence: true validates :user, uniqueness: { scope: [:project, :access_level] }, presence: true - def self.select_from_union(union) - select(['project_id', 'MAX(access_level) AS access_level']) - .from("(#{union.to_sql}) #{ProjectAuthorization.table_name}") + def self.select_from_union(relations) + from_union(relations) + .select(['project_id', 'MAX(access_level) AS access_level']) .group(:project_id) end diff --git a/app/models/project_import_state.rb b/app/models/project_import_state.rb index 89ed09af96a..d59cb43dea4 100644 --- a/app/models/project_import_state.rb +++ b/app/models/project_import_state.rb @@ -48,9 +48,11 @@ class ProjectImportState < ActiveRecord::Base project.reset_cache_and_import_attrs if Gitlab::ImportSources.importer_names.include?(project.import_type) && project.repo_exists? + # rubocop: disable CodeReuse/ServiceClass state.run_after_commit do Projects::AfterImportService.new(project).execute end + # rubocop: enable CodeReuse/ServiceClass end end end diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb index 35c19049c04..cc5f1207653 100644 --- a/app/models/project_services/asana_service.rb +++ b/app/models/project_services/asana_service.rb @@ -65,7 +65,7 @@ http://app.asana.com/-/account_api' # check the branch restriction is poplulated and branch is not included branch = Gitlab::Git.ref_name(data[:ref]) branch_restriction = restrict_to_branch.to_s - if branch_restriction.length > 0 && branch_restriction.index(branch).nil? + if branch_restriction.present? && branch_restriction.index(branch).nil? return end @@ -101,7 +101,7 @@ http://app.asana.com/-/account_api' task.update(completed: true) end rescue => e - Rails.logger.error(e.message) + log_error(e.message) next end end diff --git a/app/models/project_services/chat_message/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb index 58631e09538..6b7a35aaa75 100644 --- a/app/models/project_services/chat_message/merge_message.rb +++ b/app/models/project_services/chat_message/merge_message.rb @@ -26,7 +26,7 @@ module ChatMessage def activity { - title: "Merge Request #{state} by #{user_combined_name}", + title: "Merge Request #{state_or_action_text} by #{user_combined_name}", subtitle: "in #{project_link}", text: merge_request_link, image: user_avatar @@ -48,7 +48,7 @@ module ChatMessage end def merge_request_message - "#{user_combined_name} #{state} #{merge_request_link} in #{project_link}: #{title}" + "#{user_combined_name} #{state_or_action_text} #{merge_request_link} in #{project_link}" end def merge_request_link @@ -62,5 +62,10 @@ module ChatMessage def merge_request_url "#{project_url}/merge_requests/#{merge_request_iid}" end + + # overridden in EE + def state_or_action_text + state + end end end diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb index a783a314071..a15780c14f9 100644 --- a/app/models/project_services/irker_service.rb +++ b/app/models/project_services/irker_service.rb @@ -104,7 +104,7 @@ class IrkerService < Service new_recipient = URI.join(default_irc_uri, '/', recipient).to_s uri = consider_uri(URI.parse(new_recipient)) rescue - Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}") + log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient) end end diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index c7520d766a8..e1d342be188 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -88,7 +88,7 @@ class IssueTrackerService < Service rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}" end - Rails.logger.info(message) + log_info(message) result end diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb index cc98b3f5a41..ba7fcb0cf93 100644 --- a/app/models/project_services/jira_service.rb +++ b/app/models/project_services/jira_service.rb @@ -205,7 +205,7 @@ class JiraService < IssueTrackerService begin issue.transitions.build.save!(transition: { id: transition_id }) rescue => error - Rails.logger.info "#{self.class.name} Issue Transition failed message ERROR: #{client_url} - #{error.message}" + log_error("Issue transition failed", error: error.message, client_url: client_url) return false end end @@ -257,9 +257,8 @@ class JiraService < IssueTrackerService new_remote_link.save!(remote_link_props) end - result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}." - Rails.logger.info(result_message) - result_message + log_info("Successfully posted", client_url: client_url) + "SUCCESS: Successfully posted to http://jira.example.net." end end @@ -317,7 +316,7 @@ class JiraService < IssueTrackerService rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e @error = e.message - Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}" + log_error("Error sending message", client_url: client_url, error: @error) nil end diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb index bda1f67b8ff..f119555f16b 100644 --- a/app/models/project_services/kubernetes_service.rb +++ b/app/models/project_services/kubernetes_service.rb @@ -96,10 +96,10 @@ class KubernetesService < DeploymentService # Check we can connect to the Kubernetes API def test(*args) - kubeclient = build_kubeclient! + kubeclient = build_kube_client! - kubeclient.discover - { success: kubeclient.discovered, result: "Checked API discovery endpoint" } + kubeclient.core_client.discover + { success: kubeclient.core_client.discovered, result: "Checked API discovery endpoint" } rescue => err { success: false, result: err } end @@ -144,7 +144,7 @@ class KubernetesService < DeploymentService end def kubeclient - @kubeclient ||= build_kubeclient! + @kubeclient ||= build_kube_client!(api_groups: ['api', 'apis/rbac.authorization.k8s.io']) end def deprecated? @@ -182,11 +182,12 @@ class KubernetesService < DeploymentService slug.gsub(/[^-a-z0-9]/, '-').gsub(/^-+/, '') end - def build_kubeclient!(api_path: 'api', api_version: 'v1') + def build_kube_client!(api_groups: ['api'], api_version: 'v1') raise "Incomplete settings" unless api_url && actual_namespace && token - ::Kubeclient::Client.new( - join_api_url(api_path), + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, api_version, auth_options: kubeclient_auth_options, ssl_options: kubeclient_ssl_options, @@ -196,7 +197,7 @@ class KubernetesService < DeploymentService # Returns a hash of all pods in the namespace def read_pods - kubeclient = build_kubeclient! + kubeclient = build_kube_client! kubeclient.get_pods(namespace: actual_namespace).as_json rescue Kubeclient::HttpError => err @@ -220,15 +221,6 @@ class KubernetesService < DeploymentService { bearer_token: token } end - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - end - def terminal_auth { token: token, diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index e3ab60adefd..bfabc6d262c 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -44,11 +44,15 @@ class SlashCommandsService < Service private + # rubocop: disable CodeReuse/ServiceClass def find_chat_user(params) ChatNames::FindUserService.new(self, params).execute end + # rubocop: enable CodeReuse/ServiceClass + # rubocop: disable CodeReuse/ServiceClass def authorize_chat_name_url(params) ChatNames::AuthorizeUserService.new(self, params).execute end + # rubocop: enable CodeReuse/ServiceClass end diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb index f4b3421f04b..559e4f99294 100644 --- a/app/models/project_wiki.rb +++ b/app/models/project_wiki.rb @@ -80,7 +80,7 @@ class ProjectWiki pages(limit: 1).empty? end - # Returns an Array of Gitlab WikiPage instances or an + # Returns an Array of GitLab WikiPage instances or an # empty Array if this Wiki has no pages. def pages(limit: 0) wiki.pages(limit: limit).map { |page| WikiPage.new(self, page, true) } @@ -184,11 +184,12 @@ class ProjectWiki def commit_details(action, message = nil, title = nil) commit_message = message || default_message(action, title) + git_user = Gitlab::Git::User.from_gitlab(@user) Gitlab::Git::Wiki::CommitDetails.new(@user.id, - @user.username, - @user.name, - @user.email, + git_user.username, + git_user.name, + git_user.email, commit_message) end diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb new file mode 100644 index 00000000000..ce2db9cb44c --- /dev/null +++ b/app/models/prometheus_metric.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +class PrometheusMetric < ActiveRecord::Base + belongs_to :project, validate: true, inverse_of: :prometheus_metrics + + enum group: { + # built-in groups + nginx_ingress: -1, + ha_proxy: -2, + aws_elb: -3, + nginx: -4, + kubernetes: -5, + + # custom/user groups + business: 0, + response: 1, + system: 2 + } + + validates :title, presence: true + validates :query, presence: true + validates :group, presence: true + validates :y_label, presence: true + validates :unit, presence: true + + validates :project, presence: true, unless: :common? + validates :project, absence: true, if: :common? + + scope :common, -> { where(common: true) } + + GROUP_TITLES = { + # built-in groups + nginx_ingress: _('Response metrics (NGINX Ingress)'), + ha_proxy: _('Response metrics (HA Proxy)'), + aws_elb: _('Response metrics (AWS ELB)'), + nginx: _('Response metrics (NGINX)'), + kubernetes: _('System metrics (Kubernetes)'), + + # custom/user groups + business: _('Business metrics (Custom)'), + response: _('Response metrics (Custom)'), + system: _('System metrics (Custom)') + }.freeze + + REQUIRED_METRICS = { + nginx_ingress: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg), + ha_proxy: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total), + aws_elb: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum), + nginx: %w(nginx_server_requests nginx_server_requestMsec), + kubernetes: %w(container_memory_usage_bytes container_cpu_usage_seconds_total) + }.freeze + + def group_title + GROUP_TITLES[group.to_sym] + end + + def required_metrics + REQUIRED_METRICS[group.to_sym].to_a.map(&:to_s) + end + + def to_query_metric + Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries) + end + + def queries + [ + { + query_range: query, + unit: unit, + label: legend, + series: query_series + }.compact + ] + end + + def query_series + case legend + when 'Status Code' + [{ + label: 'status_code', + when: [ + { value: '2xx', color: 'green' }, + { value: '4xx', color: 'orange' }, + { value: '5xx', color: 'red' } + ] + }] + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index cf255c8951f..12fbf7d5d1d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -81,10 +81,6 @@ class Repository alias_method :raw, :raw_repository - def cleanup - @raw_repository&.cleanup - end - # Don't use this! It's going away. Use Gitaly to read or write from repos. def path_to_repo @path_to_repo ||= @@ -580,7 +576,12 @@ class Repository end def rendered_readme - MarkupHelper.markup_unsafe(readme.name, readme.data, project: project, markdown_engine: :redcarpet) if readme + return unless readme + + context = { project: project } + context[:markdown_engine] = :redcarpet unless MarkupHelper.commonmark_for_repositories_enabled? + + MarkupHelper.markup_unsafe(readme.name, readme.data, context) end cache_method :rendered_readme @@ -994,14 +995,6 @@ class Repository remote_branch: merge_request.target_branch) end - def blob_data_at(sha, path) - blob = blob_at(sha, path) - return unless blob - - blob.load_all_data! - blob.data - end - def squash(user, merge_request) raw.squash(user, merge_request.id, branch: merge_request.target_branch, start_sha: merge_request.diff_start_sha, @@ -1010,6 +1003,14 @@ class Repository message: merge_request.title) end + def blob_data_at(sha, path) + blob = blob_at(sha, path) + return unless blob + + blob.load_all_data! + blob.data + end + private # TODO Generice finder, later split this on finders by Ref or Oid diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb index 42c255fcd1e..3fd96b9dc18 100644 --- a/app/models/resource_label_event.rb +++ b/app/models/resource_label_event.rb @@ -3,33 +3,122 @@ # This model is not used yet, it will be used for: # https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 class ResourceLabelEvent < ActiveRecord::Base + include Importable + include Gitlab::Utils::StrongMemoize + include CacheMarkdownField + + cache_markdown_field :reference + belongs_to :user belongs_to :issue belongs_to :merge_request belongs_to :label - validates :user, presence: true, on: :create - validates :label, presence: true, on: :create + scope :created_after, ->(time) { where('created_at > ?', time) } + + validates :user, presence: { unless: :importing? }, on: :create + validates :label, presence: { unless: :importing? }, on: :create validate :exactly_one_issuable + after_save :expire_etag_cache + after_destroy :expire_etag_cache + enum action: { add: 1, remove: 2 } - def self.issuable_columns - %i(issue_id merge_request_id).freeze + def self.issuable_attrs + %i(issue merge_request).freeze end def issuable 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("-")) + end + end + + def project + issuable.project + end + + def group + issuable.group if issuable.respond_to?(:group) + end + + def outdated_markdown? + return true if label_id.nil? && reference.present? + + reference.nil? || latest_cached_markdown_version != cached_markdown_version + end + + def banzai_render_context(field) + super.merge(pipeline: 'label', only_path: true) + end + + def refresh_invalid_reference + # label_id could be nullified on label delete + self.reference = '' if label_id.nil? + + # reference is not set for events which were not rendered yet + self.reference ||= label_reference + + if changed? + save + elsif invalidated_markdown_cache? + refresh_markdown_cache! + end + end + private + def label_reference + if local_label? + label.to_reference(format: :id) + elsif label.is_a?(GroupLabel) + label.to_reference(label.group, target_project: resource_parent, format: :id) + else + label.to_reference(resource_parent, format: :id) + end + end + def exactly_one_issuable - if self.class.issuable_columns.count { |attr| self[attr] } != 1 - errors.add(:base, "Exactly one of #{self.class.issuable_columns.join(', ')} is required") + issuable_count = self.class.issuable_attrs.count { |attr| self["#{attr}_id"] } + + return true if issuable_count == 1 + + # if none of issuable IDs is set, check explicitly if nested issuable + # object is set, this is used during project import + if issuable_count == 0 && importing? + issuable_count = self.class.issuable_attrs.count { |attr| self.public_send(attr) } # rubocop:disable GitlabSecurity/PublicSend + + return true if issuable_count == 1 end + + errors.add(:base, "Exactly one of #{self.class.issuable_attrs.join(', ')} is required") + end + + def expire_etag_cache + issuable.expire_note_etag_cache + end + + def local_label? + params = { include_ancestor_groups: true } + if resource_parent.is_a?(Project) + params[:project_id] = resource_parent.id + else + params[:group_id] = resource_parent.id + end + + LabelsFinder.new(nil, params).execute(skip_authorization: true).where(id: label.id).any? + end + + def resource_parent + issuable.project || issuable.group end end diff --git a/app/models/service.rb b/app/models/service.rb index 140058771ee..4dbda7acab6 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -5,6 +5,7 @@ class Service < ActiveRecord::Base include Sortable include Importable + include ProjectServicesLoggable serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize diff --git a/app/models/snippet.rb b/app/models/snippet.rb index 5b394e3fa79..e9533ee7c77 100644 --- a/app/models/snippet.rb +++ b/app/models/snippet.rb @@ -12,6 +12,7 @@ class Snippet < ActiveRecord::Base include Spammable include Editable include Gitlab::SQL::Pattern + include FromUnion cache_markdown_field :title, pipeline: :single_line cache_markdown_field :description diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 376ef673ca8..6fadbcefa53 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -15,7 +15,7 @@ class SystemNoteMetadata < ActiveRecord::Base commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved opened closed merged duplicate locked unlocked - outdated tag + outdated tag due_date ].freeze validates :note, presence: true diff --git a/app/models/todo.rb b/app/models/todo.rb index 48d92ad04b3..265fb932f7c 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -2,6 +2,7 @@ class Todo < ActiveRecord::Base include Sortable + include FromUnion ASSIGNED = 1 MENTIONED = 2 diff --git a/app/models/user.rb b/app/models/user.rb index f21ca1c569f..eeac87e2e52 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -20,6 +20,7 @@ class User < ActiveRecord::Base include BlocksJsonSerialization include WithUploads include OptionallySearch + include FromUnion DEFAULT_NOTIFICATION_LEVEL = :participating @@ -61,6 +62,7 @@ class User < ActiveRecord::Base # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour + # rubocop: disable CodeReuse/ServiceClass def update_tracked_fields!(request) return if Gitlab::Database.read_only? @@ -71,6 +73,7 @@ class User < ActiveRecord::Base Users::UpdateService.new(self, user: self).execute(validate: false) end + # rubocop: enable CodeReuse/ServiceClass attr_accessor :force_random_password @@ -159,6 +162,7 @@ class User < ActiveRecord::Base validates :notification_email, presence: true validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email } validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true + validates :commit_email, email: true, allow_nil: true, if: ->(user) { user.commit_email != user.email } validates :bio, length: { maximum: 255 }, allow_blank: true validates :projects_limit, presence: true, @@ -171,12 +175,15 @@ class User < ActiveRecord::Base validate :unique_email, if: :email_changed? validate :owns_notification_email, if: :notification_email_changed? validate :owns_public_email, if: :public_email_changed? + validate :owns_commit_email, if: :commit_email_changed? validate :signup_domain_valid?, on: :create, if: ->(user) { !user.created_by_id } before_validation :sanitize_attrs before_validation :set_notification_email, if: :new_record? before_validation :set_public_email, if: :public_email_changed? + before_validation :set_commit_email, if: :commit_email_changed? before_save :set_public_email, if: :public_email_changed? # in case validation is skipped + before_save :set_commit_email, if: :commit_email_changed? # in case validation is skipped before_save :ensure_incoming_email_token before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? } before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) } @@ -257,6 +264,7 @@ class User < ActiveRecord::Base scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } scope :confirmed, -> { where.not(confirmed_at: nil) } + scope :by_username, -> (usernames) { iwhere(username: usernames) } # Limits the users to those that have TODOs, optionally in the given state. # @@ -279,11 +287,9 @@ class User < ActiveRecord::Base # user_id - The ID of the user to include. def self.union_with_user(user_id = nil) if user_id.present? - union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)]) - # We use "unscoped" here so that any inner conditions are not repeated for # the outer query, which would be redundant. - User.unscoped.from("(#{union.to_sql}) #{User.table_name}") + User.unscoped.from_union([all, User.unscoped.where(id: user_id)]) else all end @@ -347,9 +353,8 @@ class User < ActiveRecord::Base emails = joins(:emails).where(emails: { email: email }) emails = emails.confirmed if confirmed - union = Gitlab::SQL::Union.new([users, emails]) - from("(#{union.to_sql}) #{table_name}") + from_union([users, emails]) end def filter(filter_name) @@ -444,17 +449,17 @@ class User < ActiveRecord::Base end def find_by_username(username) - iwhere(username: username).take + by_username(username).take end def find_by_username!(username) - iwhere(username: username).take! + by_username(username).take! end def find_by_personal_access_token(token_string) return unless token_string - PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user + PersonalAccessTokensFinder.new(state: 'active').find_by(token: token_string)&.user # rubocop: disable CodeReuse/Finder end # Returns a user for the given SSH key. @@ -489,6 +494,16 @@ class User < ActiveRecord::Base u.name = 'Ghost User' end end + + # Return true if there is only single non-internal user in the deployment, + # ghost user is ignored. + def single_user? + User.non_internal.limit(2).count == 1 + end + + def single_user + User.non_internal.first if single_user? + end end def full_path @@ -606,6 +621,32 @@ class User < ActiveRecord::Base errors.add(:public_email, "is not an email you own") unless all_emails.include?(public_email) end + def owns_commit_email + return if read_attribute(:commit_email).blank? + + errors.add(:commit_email, "is not an email you own") unless verified_emails.include?(commit_email) + end + + # Define commit_email-related attribute methods explicitly instead of relying + # on ActiveRecord to provide them. Some of the specs use the current state of + # the model code but an older database schema, so we need to guard against the + # possibility of the commit_email column not existing. + + def commit_email + return self.email unless has_attribute?(:commit_email) + + # The commit email is the same as the primary email if undefined + super.presence || self.email + end + + def commit_email=(email) + super if has_attribute?(:commit_email) + end + + def commit_email_changed? + has_attribute?(:commit_email) && super + end + # see if the new email is already a verified secondary email def check_for_verified_email skip_reconfirmation! if emails.confirmed.where(email: self.email).any? @@ -616,6 +657,7 @@ class User < ActiveRecord::Base # hash and `_was` variables getting munged. # By using an `after_commit` instead of `after_update`, we avoid the recursive callback # scenario, though it then requires us to use the `previous_changes` hash + # rubocop: disable CodeReuse/ServiceClass def update_emails_with_primary_email(previous_email) primary_email_record = emails.find_by(email: email) Emails::DestroyService.new(self, user: self).execute(primary_email_record) if primary_email_record @@ -624,6 +666,7 @@ class User < ActiveRecord::Base # have access to the original confirmation values at this point, so just set confirmed_at Emails::CreateService.new(self, user: self, email: previous_email).execute(confirmed_at: confirmed_at) end + # rubocop: enable CodeReuse/ServiceClass def update_invalid_gpg_signatures gpg_keys.each(&:update_invalid_gpg_signatures) @@ -631,10 +674,10 @@ class User < ActiveRecord::Base # Returns the groups a user has access to, either through a membership or a project authorization def authorized_groups - union = Gitlab::SQL::Union - .new([groups.select(:id), authorized_projects.select(:namespace_id)]) - - Group.where("namespaces.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + Group.from_union([ + groups, + authorized_projects.joins(:namespace).select('namespaces.*') + ]) end # Returns the groups a user is a member of, either directly or through a parent group @@ -652,9 +695,11 @@ class User < ActiveRecord::Base all_expanded_groups.where(require_two_factor_authentication: true) end + # rubocop: disable CodeReuse/ServiceClass def refresh_authorized_projects Users::RefreshAuthorizedProjectsService.new(self).execute end + # rubocop: enable CodeReuse/ServiceClass def remove_project_authorizations(project_ids) project_authorizations.where(project_id: project_ids).delete_all @@ -697,7 +742,15 @@ class User < ActiveRecord::Base end def owned_projects - @owned_projects ||= Project.from("(#{owned_projects_union.to_sql}) AS projects") + @owned_projects ||= Project.from_union( + [ + Project.where(namespace: namespace), + Project.joins(:project_authorizations) + .where("projects.namespace_id <> ?", namespace.id) + .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER }) + ], + remove_duplicates: false + ) end # Returns projects which user can admin issues on (for example to move an issue to that project). @@ -707,11 +760,13 @@ class User < ActiveRecord::Base authorized_projects(Gitlab::Access::REPORTER).non_archived.with_issues_enabled end + # rubocop: disable CodeReuse/ServiceClass def require_ssh_key? count = Users::KeysCountService.new(self).count count.zero? && Gitlab::ProtocolAccess.allowed?('ssh') end + # rubocop: enable CodeReuse/ServiceClass def require_password_creation_for_web? allow_password_authentication_for_web? && password_automatically_set? @@ -775,6 +830,7 @@ class User < ActiveRecord::Base projects_limit - personal_projects_count end + # rubocop: disable CodeReuse/ServiceClass def recent_push(project = nil) service = Users::LastPushEventService.new(self) @@ -784,6 +840,7 @@ class User < ActiveRecord::Base service.last_event_for_user end end + # rubocop: enable CodeReuse/ServiceClass def several_namespaces? owned_groups.any? || maintainers_groups.any? @@ -852,10 +909,17 @@ class User < ActiveRecord::Base end end + def set_commit_email + if commit_email.blank? || verified_emails.exclude?(commit_email) + self.commit_email = nil + end + end + def update_secondary_emails! set_notification_email set_public_email - save if notification_email_changed? || public_email_changed? + set_commit_email + save if notification_email_changed? || public_email_changed? || commit_email_changed? end def set_projects_limit @@ -921,9 +985,11 @@ class User < ActiveRecord::Base email.start_with?('temp-email-for-oauth') end + # rubocop: disable CodeReuse/ServiceClass def avatar_url(size: nil, scale: 2, **args) GravatarService.new.execute(email, size, scale, username: username) end + # rubocop: enable CodeReuse/ServiceClass def primary_email_verified? confirmed? && !temp_oauth_email? @@ -989,26 +1055,32 @@ class User < ActiveRecord::Base system_hook_service.execute_hooks_for(self, :destroy) end + # rubocop: disable CodeReuse/ServiceClass def remove_key_cache Users::KeysCountService.new(self).delete_cache end + # rubocop: enable CodeReuse/ServiceClass def delete_async(deleted_by:, params: {}) block if params[:hard_delete] DeleteUserWorker.perform_async(deleted_by.id, id, params.to_h) end + # rubocop: disable CodeReuse/ServiceClass def notification_service NotificationService.new end + # rubocop: enable CodeReuse/ServiceClass def log_info(message) Gitlab::AppLogger.info message end + # rubocop: disable CodeReuse/ServiceClass def system_hook_service SystemHooksService.new end + # rubocop: enable CodeReuse/ServiceClass def starred?(project) starred_projects.exists?(project.id) @@ -1072,17 +1144,17 @@ class User < ActiveRecord::Base def ci_owned_runners @ci_owned_runners ||= begin - project_runner_ids = Ci::RunnerProject + project_runners = Ci::RunnerProject .where(project: authorized_projects(Gitlab::Access::MAINTAINER)) - .select(:runner_id) + .joins(:runner) + .select('ci_runners.*') - group_runner_ids = Ci::RunnerNamespace + group_runners = Ci::RunnerNamespace .where(namespace_id: owned_or_maintainers_groups.select(:id)) - .select(:runner_id) - - union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids]) + .joins(:runner) + .select('ci_runners.*') - Ci::Runner.where("ci_runners.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection + Ci::Runner.from_union([project_runners, group_runners]) end end @@ -1110,13 +1182,13 @@ class User < ActiveRecord::Base def assigned_open_merge_requests_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_merge_requests_count'], force: force, expires_in: 20.minutes) do - MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened').execute.count + MergeRequestsFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count end end def assigned_open_issues_count(force: false) Rails.cache.fetch(['users', id, 'assigned_open_issues_count'], force: force, expires_in: 20.minutes) do - IssuesFinder.new(self, assignee_id: self.id, state: 'opened').execute.count + IssuesFinder.new(self, assignee_id: self.id, state: 'opened', non_archived: true).execute.count end end @@ -1177,6 +1249,7 @@ class User < ActiveRecord::Base # See: # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92> # + # rubocop: disable CodeReuse/ServiceClass def increment_failed_attempts! return if ::Gitlab::Database.read_only? @@ -1189,6 +1262,7 @@ class User < ActiveRecord::Base Users::UpdateService.new(self, user: self).execute(validate: false) end end + # rubocop: enable CodeReuse/ServiceClass def access_level if admin? @@ -1286,6 +1360,10 @@ class User < ActiveRecord::Base !terms_accepted? end + def requires_usage_stats_consent? + !consented_usage_stats? && 7.days.ago > self.created_at && !has_current_license? && User.single_user? + end + # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups @@ -1300,13 +1378,12 @@ class User < ActiveRecord::Base private - def owned_projects_union - Gitlab::SQL::Union.new([ - Project.where(namespace: namespace), - Project.joins(:project_authorizations) - .where("projects.namespace_id <> ?", namespace.id) - .where(project_authorizations: { user_id: id, access_level: Gitlab::Access::OWNER }) - ], remove_duplicates: false) + def has_current_license? + false + end + + def consented_usage_stats? + Gitlab::CurrentSettings.usage_stats_set_by_user_id == self.id end # Added according to https://github.com/plataformatec/devise/blob/7df57d5081f9884849ca15e4fde179ef164a575f/README.md#activejob-integration @@ -1417,7 +1494,7 @@ class User < ActiveRecord::Base &creation_block ) - Users::UpdateService.new(user, user: user).execute(validate: false) + Users::UpdateService.new(user, user: user).execute(validate: false) # rubocop: disable CodeReuse/ServiceClass user ensure Gitlab::ExclusiveLease.cancel(lease_key, uuid) diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 33790afc35e..102907a8bd3 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -51,7 +51,7 @@ class WikiPage validates :title, presence: true validates :content, presence: true - # The Gitlab ProjectWiki instance. + # The GitLab ProjectWiki instance. attr_reader :wiki # The raw Gitlab::Git::WikiPage instance. @@ -127,7 +127,7 @@ class WikiPage version.try(:message) end - # The Gitlab Commit instance for this page. + # The GitLab Commit instance for this page. def version return nil unless persisted? diff --git a/app/policies/application_setting/term_policy.rb b/app/policies/application_setting/term_policy.rb index 17f00f33d35..c0d2ceaa349 100644 --- a/app/policies/application_setting/term_policy.rb +++ b/app/policies/application_setting/term_policy.rb @@ -19,6 +19,7 @@ class ApplicationSetting rule { terms_accepted }.prevent :accept_terms + # rubocop: disable CodeReuse/ActiveRecord def agreement strong_memoize(:agreement) do next nil if @user.nil? || @subject.nil? @@ -26,5 +27,6 @@ class ApplicationSetting @user.term_agreements.find_by(term: @subject) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/policies/ci/runner_policy.rb b/app/policies/ci/runner_policy.rb index c44f22b6ad3..de76b7b2b5b 100644 --- a/app/policies/ci/runner_policy.rb +++ b/app/policies/ci/runner_policy.rb @@ -5,7 +5,9 @@ module Ci with_options scope: :subject, score: 0 condition(:locked, scope: :subject) { @subject.locked? } + # rubocop: disable CodeReuse/ActiveRecord condition(:owned_runner) { @user.ci_owned_runners.exists?(@subject.id) } + # rubocop: enable CodeReuse/ActiveRecord rule { anonymous }.prevent_all diff --git a/app/policies/deploy_key_policy.rb b/app/policies/deploy_key_policy.rb index 204c54a5b20..7f0ec011e79 100644 --- a/app/policies/deploy_key_policy.rb +++ b/app/policies/deploy_key_policy.rb @@ -4,7 +4,9 @@ class DeployKeyPolicy < BasePolicy with_options scope: :subject, score: 0 condition(:private_deploy_key) { @subject.private? } + # rubocop: disable CodeReuse/ActiveRecord condition(:has_deploy_key) { @user.project_deploy_keys.exists?(id: @subject.id) } + # rubocop: enable CodeReuse/ActiveRecord rule { anonymous }.prevent_all diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 273a93a1423..d0e84b1aa38 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -398,6 +398,7 @@ class ProjectPolicy < BasePolicy end end + # rubocop: disable CodeReuse/ActiveRecord def project_group_member? return false if @user.nil? @@ -407,6 +408,7 @@ class ProjectPolicy < BasePolicy project.group.requesters.exists?(user_id: @user.id) ) end + # rubocop: enable CodeReuse/ActiveRecord def team_access_level return -1 if @user.nil? diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index a08f34e2335..65e77ea3f92 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -11,10 +11,16 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated runner_unsupported: 'Your runner is outdated, please upgrade your runner' }.freeze + private_constant :CALLOUT_FAILURE_MESSAGES + presents :build + def self.callout_failure_messages + CALLOUT_FAILURE_MESSAGES + end + def callout_failure_message - CALLOUT_FAILURE_MESSAGES.fetch(failure_reason.to_sym) + self.class.callout_failure_messages.fetch(failure_reason.to_sym) end def recoverable? diff --git a/app/presenters/conversational_development_index/metric_presenter.rb b/app/presenters/conversational_development_index/metric_presenter.rb index e0312c6f431..9639b84cf56 100644 --- a/app/presenters/conversational_development_index/metric_presenter.rb +++ b/app/presenters/conversational_development_index/metric_presenter.rb @@ -139,8 +139,10 @@ module ConversationalDevelopmentIndex ] end + # rubocop: disable CodeReuse/ActiveRecord def average_percentage_score cards.sum(&:percentage_score) / cards.size.to_f end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb index 8c4eac3c31d..3f565b826dd 100644 --- a/app/presenters/merge_request_presenter.rb +++ b/app/presenters/merge_request_presenter.rb @@ -142,6 +142,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def assign_to_closing_issues_link + # rubocop: disable CodeReuse/ServiceClass issues = MergeRequests::AssignIssuesService.new(project, current_user, merge_request: merge_request, @@ -152,6 +153,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated pluralize_this_issue = issues.count > 1 ? "these issues" : "this issue" link_to "Assign yourself to #{pluralize_this_issue}", path, method: :post end + # rubocop: enable CodeReuse/ServiceClass end def can_revert_on_current_merge_request? @@ -202,7 +204,9 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated end def conflicts + # rubocop: disable CodeReuse/ServiceClass @conflicts ||= MergeRequests::Conflicts::ListService.new(merge_request) + # rubocop: enable CodeReuse/ServiceClass end def closing_issues diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 4c2f33213d6..d2434d96fd7 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -11,16 +11,18 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated presents :project + AnchorData = Struct.new(:enabled, :label, :link, :class_modifier) + MAX_TAGS_TO_SHOW = 3 + def statistics_anchors(show_auto_devops_callout:) [ + readme_anchor_data, + changelog_anchor_data, + contribution_guide_anchor_data, files_anchor_data, commits_anchor_data, branches_anchor_data, tags_anchor_data, - readme_anchor_data, - changelog_anchor_data, - license_anchor_data, - contribution_guide_anchor_data, gitlab_ci_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data @@ -31,7 +33,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ readme_anchor_data, changelog_anchor_data, - license_anchor_data, contribution_guide_anchor_data, autodevops_anchor_data(show_auto_devops_callout: show_auto_devops_callout), kubernetes_cluster_anchor_data, @@ -42,6 +43,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated def empty_repo_statistics_anchors [ + files_anchor_data, + commits_anchor_data, + branches_anchor_data, + tags_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.select { |item| item.enabled } @@ -51,7 +56,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated [ new_file_anchor_data, readme_anchor_data, - license_anchor_data, autodevops_anchor_data, kubernetes_cluster_anchor_data ].compact.reject { |item| item.enabled } @@ -182,95 +186,101 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def files_anchor_data - OpenStruct.new(enabled: true, - label: _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, - link: project_tree_path(project)) + AnchorData.new(true, + _('Files (%{human_size})') % { human_size: storage_counter(statistics.total_repository_size) }, + empty_repo? ? nil : project_tree_path(project)) end def commits_anchor_data - OpenStruct.new(enabled: true, - label: n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, - link: project_commits_path(project, repository.root_ref)) + AnchorData.new(true, + n_('Commit (%{commit_count})', 'Commits (%{commit_count})', statistics.commit_count) % { commit_count: number_with_delimiter(statistics.commit_count) }, + empty_repo? ? nil : project_commits_path(project, repository.root_ref)) end def branches_anchor_data - OpenStruct.new(enabled: true, - label: n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, - link: project_branches_path(project)) + AnchorData.new(true, + n_('Branch (%{branch_count})', 'Branches (%{branch_count})', repository.branch_count) % { branch_count: number_with_delimiter(repository.branch_count) }, + empty_repo? ? nil : project_branches_path(project)) end def tags_anchor_data - OpenStruct.new(enabled: true, - label: n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, - link: project_tags_path(project)) + AnchorData.new(true, + n_('Tag (%{tag_count})', 'Tags (%{tag_count})', repository.tag_count) % { tag_count: number_with_delimiter(repository.tag_count) }, + empty_repo? ? nil : project_tags_path(project)) end def new_file_anchor_data if current_user && can_current_user_push_to_default_branch? - OpenStruct.new(enabled: false, - label: _('New file'), - link: project_new_blob_path(project, default_branch || 'master'), - class_modifier: 'new') + AnchorData.new(false, + _('New file'), + project_new_blob_path(project, default_branch || 'master'), + 'new') end end def readme_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.readme.nil? - OpenStruct.new(enabled: false, - label: _('Add Readme'), - link: add_readme_path) + AnchorData.new(false, + _('Add Readme'), + add_readme_path) elsif repository.readme - OpenStruct.new(enabled: true, - label: _('Readme'), - link: default_view != 'readme' ? readme_path : '#readme') + AnchorData.new(true, + _('Readme'), + default_view != 'readme' ? readme_path : '#readme') end end def changelog_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? - OpenStruct.new(enabled: false, - label: _('Add Changelog'), - link: add_changelog_path) + AnchorData.new(false, + _('Add Changelog'), + add_changelog_path) elsif repository.changelog.present? - OpenStruct.new(enabled: true, - label: _('Changelog'), - link: changelog_path) + AnchorData.new(true, + _('Changelog'), + changelog_path) end end def license_anchor_data - if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? - OpenStruct.new(enabled: false, - label: _('Add License'), - link: add_license_path) - elsif repository.license_blob.present? - OpenStruct.new(enabled: true, - label: license_short_name, - link: license_path) + if repository.license_blob.present? + AnchorData.new(true, + license_short_name, + license_path) + else + if current_user && can_current_user_push_to_default_branch? + AnchorData.new(false, + _('Add license'), + add_license_path) + else + AnchorData.new(false, + _('No license. All rights reserved'), + nil) + end end end def contribution_guide_anchor_data if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? - OpenStruct.new(enabled: false, - label: _('Add Contribution guide'), - link: add_contribution_guide_path) + AnchorData.new(false, + _('Add Contribution guide'), + add_contribution_guide_path) elsif repository.contribution_guide.present? - OpenStruct.new(enabled: true, - label: _('Contribution guide'), - link: contribution_guide_path) + AnchorData.new(true, + _('Contribution guide'), + contribution_guide_path) end end def autodevops_anchor_data(show_auto_devops_callout: false) if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout - OpenStruct.new(enabled: auto_devops_enabled?, - label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), - link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + AnchorData.new(auto_devops_enabled?, + auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), + project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) elsif auto_devops_enabled? - OpenStruct.new(enabled: true, - label: _('Auto DevOps enabled'), - link: nil) + AnchorData.new(true, + _('Auto DevOps enabled'), + nil) end end @@ -282,32 +292,48 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated cluster_link = new_project_cluster_path(project) end - OpenStruct.new(enabled: !clusters.empty?, - label: clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), - link: cluster_link) + AnchorData.new(!clusters.empty?, + clusters.empty? ? _('Add Kubernetes cluster') : _('Kubernetes configured'), + cluster_link) end end def gitlab_ci_anchor_data if current_user && can_current_user_push_code? && repository.gitlab_ci_yml.blank? && !auto_devops_enabled? - OpenStruct.new(enabled: false, - label: _('Set up CI/CD'), - link: add_ci_yml_path) + AnchorData.new(false, + _('Set up CI/CD'), + add_ci_yml_path) elsif repository.gitlab_ci_yml.present? - OpenStruct.new(enabled: true, - label: _('CI/CD configuration'), - link: ci_configuration_path) + AnchorData.new(true, + _('CI/CD configuration'), + ci_configuration_path) end end def koding_anchor_data if current_user && can_current_user_push_code? && koding_enabled? && repository.koding_yml.blank? - OpenStruct.new(enabled: false, - label: _('Set up Koding'), - link: add_koding_stack_path) + AnchorData.new(false, + _('Set up Koding'), + add_koding_stack_path) end end + def tags_to_show + project.tag_list.take(MAX_TAGS_TO_SHOW) # rubocop: disable CodeReuse/ActiveRecord + end + + def count_of_extra_tags_not_shown + if project.tag_list.count > MAX_TAGS_TO_SHOW + project.tag_list.count - MAX_TAGS_TO_SHOW + else + 0 + end + end + + def has_extra_tags? + count_of_extra_tags_not_shown > 0 + end + private def filename_path(filename) diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb index 28eaef00a12..85518c9a3a4 100644 --- a/app/presenters/projects/settings/deploy_keys_presenter.rb +++ b/app/presenters/projects/settings/deploy_keys_presenter.rb @@ -12,9 +12,11 @@ module Projects @key ||= DeployKey.new.tap { |dk| dk.deploy_keys_projects.build } end + # rubocop: disable CodeReuse/ActiveRecord def enabled_keys @enabled_keys ||= project.deploy_keys.includes(:projects) end + # rubocop: enable CodeReuse/ActiveRecord def any_keys_enabled? enabled_keys.any? @@ -24,14 +26,17 @@ module Projects @available_keys ||= current_user.accessible_deploy_keys - enabled_keys end + # rubocop: disable CodeReuse/ActiveRecord def available_project_keys @available_project_keys ||= current_user.project_deploy_keys.includes(:projects) - enabled_keys end + # rubocop: enable CodeReuse/ActiveRecord def key_available?(deploy_key) available_keys.include?(deploy_key) end + # rubocop: disable CodeReuse/ActiveRecord def available_public_keys return @available_public_keys if defined?(@available_public_keys) @@ -41,9 +46,10 @@ module Projects # in @available_project_keys. @available_public_keys -= available_project_keys end + # rubocop: enable CodeReuse/ActiveRecord def as_json - serializer = DeployKeySerializer.new + serializer = DeployKeySerializer.new # rubocop: disable CodeReuse/Serializer opts = { user: current_user } { diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index 271ff668eda..00a441a9a1e 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -1,12 +1,26 @@ # frozen_string_literal: true class BuildDetailsEntity < JobEntity + include EnvironmentHelper + include RequestAwareEntity + include CiStatusHelper + expose :coverage, :erased_at, :duration expose :tag_list, as: :tags expose :user, using: UserEntity expose :runner, using: RunnerEntity expose :pipeline, using: PipelineEntity + expose :deployment_status, if: -> (*) { build.has_environment? } do + expose :deployment_status, as: :status + + expose :icon do |build| + ci_label_for_status(build.status) + end + + expose :persisted_environment, as: :environment, with: EnvironmentEntity + end + expose :metadata, using: BuildMetadataEntity expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do @@ -36,6 +50,10 @@ class BuildDetailsEntity < JobEntity erase_project_job_path(project, build) end + expose :terminal_path, if: -> (*) { can_create_build_terminal? } do |build| + terminal_project_job_path(project, build) + end + expose :merge_request, if: -> (*) { can?(current_user, :read_merge_request, build.merge_request) } do expose :iid do |build| build.merge_request.iid @@ -55,6 +73,26 @@ class BuildDetailsEntity < JobEntity raw_project_job_path(project, build) end + expose :trigger, if: -> (*) { build.trigger_request } do + expose :trigger_short_token, as: :short_token + + expose :trigger_variables, as: :variables, using: TriggerVariableEntity + end + + expose :runners do + expose :online do |build| + build.any_runners_online? + end + + expose :available do |build| + project.any_runners? + end + + expose :settings_path, if: -> (*) { can_admin_build? } do |build| + project_runners_path(project) + end + end + private def build_failed_issue_options @@ -69,4 +107,12 @@ class BuildDetailsEntity < JobEntity def project build.project end + + def can_create_build_terminal? + can?(current_user, :create_build_terminal, build) && build.has_terminal? + end + + def can_admin_build? + can?(request.current_user, :admin_build, project) + end end diff --git a/app/serializers/commit_entity.rb b/app/serializers/commit_entity.rb index b3287c66554..ce76659fa46 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -6,7 +6,7 @@ class CommitEntity < API::Entities::Commit expose :author, using: UserEntity expose :author_gravatar_url do |commit| - GravatarService.new.execute(commit.author_email) + GravatarService.new.execute(commit.author_email) # rubocop: disable CodeReuse/ServiceClass end expose :commit_url do |commit| diff --git a/app/serializers/status_entity.rb b/app/serializers/detailed_status_entity.rb index 306c30f0323..c772c807f76 100644 --- a/app/serializers/status_entity.rb +++ b/app/serializers/detailed_status_entity.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -class StatusEntity < Grape::Entity +class DetailedStatusEntity < Grape::Entity include RequestAwareEntity expose :icon, :text, :label, :group @@ -8,6 +8,14 @@ class StatusEntity < Grape::Entity expose :has_details?, as: :has_details expose :details_path + expose :illustration do |status| + begin + status.illustration + rescue NotImplementedError + # ignored + end + end + expose :favicon do |status| Gitlab::Favicon.status_overlay(status.favicon) end diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index f75ace14d9c..878cc5290bd 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -35,13 +35,17 @@ class DiffsEntity < Grape::Entity diffs_project_merge_request_path(merge_request&.project, merge_request) end + # rubocop: disable CodeReuse/ActiveRecord expose :added_lines do |diffs| diffs.diff_files.sum(&:added_lines) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord expose :removed_lines do |diffs| diffs.diff_files.sum(&:removed_lines) end + # rubocop: enable CodeReuse/ActiveRecord expose :render_overflow_warning do |diffs| render_overflow_warning?(diffs.diff_files) diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb index ed09db0f3f4..ebe76c9fcda 100644 --- a/app/serializers/discussion_entity.rb +++ b/app/serializers/discussion_entity.rb @@ -6,6 +6,7 @@ class DiscussionEntity < Grape::Entity expose :id, :reply_id expose :position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? } + expose :original_position, if: -> (d, _) { d.diff_discussion? && !d.legacy_diff_discussion? } expose :line_code, if: -> (d, _) { d.diff_discussion? } expose :expanded?, as: :expanded expose :active?, as: :active, if: -> (d, _) { d.diff_discussion? } diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb index dc1686c30c4..598ce5f9e4f 100644 --- a/app/serializers/environment_serializer.rb +++ b/app/serializers/environment_serializer.rb @@ -29,6 +29,7 @@ class EnvironmentSerializer < BaseSerializer private + # rubocop: disable CodeReuse/ActiveRecord def itemize(resource) items = resource.order('folder ASC') .group('COALESCE(environment_type, name)') @@ -46,4 +47,5 @@ class EnvironmentSerializer < BaseSerializer Item.new(item.folder, item.size, environments[item.last_id]) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb index f6804fe7f6a..20d7032c970 100644 --- a/app/serializers/group_child_entity.rb +++ b/app/serializers/group_child_entity.rb @@ -66,11 +66,13 @@ class GroupChildEntity < Grape::Entity private + # rubocop: disable CodeReuse/ActiveRecord def membership return unless request.current_user @membership ||= request.current_user.members.find_by(source: object) end + # rubocop: enable CodeReuse/ActiveRecord def project? object.is_a?(Project) diff --git a/app/serializers/group_entity.rb b/app/serializers/group_entity.rb index c46c342ee5d..0e1bc9a6b3d 100644 --- a/app/serializers/group_entity.rb +++ b/app/serializers/group_entity.rb @@ -17,9 +17,11 @@ class GroupEntity < Grape::Entity end expose :permissions do + # rubocop: disable CodeReuse/ActiveRecord expose :human_group_access do |group, options| group.group_members.find_by(user_id: request.current_user)&.human_access end + # rubocop: enable CodeReuse/ActiveRecord end expose :edit_path do |group| diff --git a/app/serializers/job_entity.rb b/app/serializers/job_entity.rb index 7bc1d87dea5..26b29993fec 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -27,7 +27,7 @@ class JobEntity < Grape::Entity expose :playable?, as: :playable expose :created_at expose :updated_at - expose :detailed_status, as: :status, with: StatusEntity + expose :detailed_status, as: :status, with: DetailedStatusEntity expose :callout_message, if: -> (*) { failed? && !build.script_failure? } expose :recoverable, if: -> (*) { failed? } diff --git a/app/serializers/job_group_entity.rb b/app/serializers/job_group_entity.rb index 0941a9d36be..0db7624b3f7 100644 --- a/app/serializers/job_group_entity.rb +++ b/app/serializers/job_group_entity.rb @@ -5,7 +5,7 @@ class JobGroupEntity < Grape::Entity expose :name expose :size - expose :detailed_status, as: :status, with: StatusEntity + expose :detailed_status, as: :status, with: DetailedStatusEntity expose :jobs, with: JobEntity private diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index f55d448235a..380e8804f51 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -243,7 +243,7 @@ class MergeRequestWidgetEntity < IssuableEntity def presenter(merge_request) @presenters ||= {} - @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) + @presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter end # Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs, diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb index daa5c24d0f5..c6d27817411 100644 --- a/app/serializers/note_entity.rb +++ b/app/serializers/note_entity.rb @@ -4,6 +4,12 @@ class NoteEntity < API::Entities::Note include RequestAwareEntity include NotesHelper + expose :id do |note| + # resource events are represented as notes too, but don't + # have ID, discussion ID is used for them instead + note.id ? note.id.to_s : note.discussion_id + end + expose :type expose :author, using: NoteUserEntity @@ -46,8 +52,8 @@ class NoteEntity < API::Entities::Note expose :emoji_awardable?, as: :emoji_awardable expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity - expose :report_abuse_path do |note| - new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note)) + expose :report_abuse_path, if: -> (note, _) { note.author_id } do |note| + new_abuse_report_path(user_id: note.author_id, ref_url: Gitlab::UrlBuilder.build(note)) end expose :noteable_note_url do |note| diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb index 6cf1925adda..aef838409e0 100644 --- a/app/serializers/pipeline_entity.rb +++ b/app/serializers/pipeline_entity.rb @@ -30,7 +30,7 @@ class PipelineEntity < Grape::Entity end expose :details do - expose :detailed_status, as: :status, with: StatusEntity + expose :detailed_status, as: :status, with: DetailedStatusEntity expose :duration expose :finished_at end diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb index 3205578b83e..4f31af3c46d 100644 --- a/app/serializers/pipeline_serializer.rb +++ b/app/serializers/pipeline_serializer.rb @@ -4,6 +4,7 @@ class PipelineSerializer < BaseSerializer include WithPagination entity PipelineDetailsEntity + # rubocop: disable CodeReuse/ActiveRecord def represent(resource, opts = {}) if resource.is_a?(ActiveRecord::Relation) resource = resource.preload([ @@ -33,6 +34,7 @@ class PipelineSerializer < BaseSerializer super(resource, opts) end + # rubocop: enable CodeReuse/ActiveRecord def represent_status(resource) return {} unless resource.present? diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb index d7c4d0aacc6..f6cdea1d8b5 100644 --- a/app/serializers/project_note_entity.rb +++ b/app/serializers/project_note_entity.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class ProjectNoteEntity < NoteEntity - expose :human_access do |note| + expose :human_access, if: -> (note, _) { note.project.present? } do |note| note.project.team.human_max_access(note.author_id) end @@ -9,7 +9,7 @@ class ProjectNoteEntity < NoteEntity toggle_award_emoji_project_note_path(note.project, note.id) end - expose :path do |note| + expose :path, if: -> (note, _) { note.id } do |note| project_note_path(note.project, note) end diff --git a/app/serializers/runner_entity.rb b/app/serializers/runner_entity.rb index 04ec80e0efa..97e5b336a35 100644 --- a/app/serializers/runner_entity.rb +++ b/app/serializers/runner_entity.rb @@ -5,8 +5,7 @@ class RunnerEntity < Grape::Entity expose :id, :description - expose :edit_path, - if: -> (*) { can?(request.current_user, :admin_build, project) && runner.project_type? } do |runner| + expose :edit_path, if: -> (*) { can_edit_runner? } do |runner| edit_project_runner_path(project, runner) end @@ -17,4 +16,8 @@ class RunnerEntity < Grape::Entity def project request.project end + + def can_edit_runner? + can?(request.current_user, :update_runner, runner) && runner.project_type? + end end diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb index 00e6d32ee3a..029dd3d0684 100644 --- a/app/serializers/stage_entity.rb +++ b/app/serializers/stage_entity.rb @@ -19,7 +19,13 @@ class StageEntity < Grape::Entity latest_statuses end - expose :detailed_status, as: :status, with: StatusEntity + expose :retried, + if: -> (_, opts) { opts[:retried] }, + with: JobEntity do |stage| + retried_statuses + end + + expose :detailed_status, as: :status, with: DetailedStatusEntity expose :path do |stage| project_pipeline_path( @@ -48,9 +54,19 @@ class StageEntity < Grape::Entity @grouped_statuses ||= stage.statuses.latest_ordered.group_by(&:status) end + def grouped_retried_statuses + @grouped_retried_statuses ||= stage.statuses.retried_ordered.group_by(&:status) + end + def latest_statuses HasStatus::ORDERED_STATUSES.map do |ordered_status| grouped_statuses.fetch(ordered_status, []) end.flatten end + + def retried_statuses + HasStatus::ORDERED_STATUSES.map do |ordered_status| + grouped_retried_statuses.fetch(ordered_status, []) + end.flatten + end end diff --git a/app/serializers/trigger_variable_entity.rb b/app/serializers/trigger_variable_entity.rb new file mode 100644 index 00000000000..56203113631 --- /dev/null +++ b/app/serializers/trigger_variable_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class TriggerVariableEntity < Grape::Entity + include RequestAwareEntity + + expose :key, :value, :public +end diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb index 19cf34e2ac4..2e4643ed668 100644 --- a/app/services/application_settings/update_service.rb +++ b/app/services/application_settings/update_service.rb @@ -11,11 +11,19 @@ module ApplicationSettings params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id end + if usage_stats_updated? && !params.delete(:skip_usage_stats_user) + params[:usage_stats_set_by_user_id] = current_user.id + end + @application_setting.update(@params) end private + def usage_stats_updated? + params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled) + end + def update_terms(terms) return unless terms.present? diff --git a/app/services/applications/create_service.rb b/app/services/applications/create_service.rb index 7db90c0b3c6..3d88c4f064e 100644 --- a/app/services/applications/create_service.rb +++ b/app/services/applications/create_service.rb @@ -2,10 +2,12 @@ module Applications class CreateService + # rubocop: disable CodeReuse/ActiveRecord def initialize(current_user, params) @current_user = current_user @params = params.except(:ip_address) end + # rubocop: enable CodeReuse/ActiveRecord def execute(request) Doorkeeper::Application.create(@params) diff --git a/app/services/boards/create_service.rb b/app/services/boards/create_service.rb index 4caf5ffa3cb..1b796cef3e2 100644 --- a/app/services/boards/create_service.rb +++ b/app/services/boards/create_service.rb @@ -9,7 +9,7 @@ module Boards private def can_create_board? - parent.boards.size == 0 + parent.boards.empty? end def create_board! diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 0db1418b37a..0b69661bbd0 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -9,6 +9,7 @@ module Boards fetch_issues.order_by_position_and_priority end + # rubocop: disable CodeReuse/ActiveRecord def metadata keys = metadata_fields.keys columns = metadata_fields.values_at(*keys).join(', ') @@ -16,6 +17,7 @@ module Boards Hash[keys.zip(results.flatten)] end + # rubocop: enable CodeReuse/ActiveRecord private @@ -24,6 +26,7 @@ module Boards end # We memoize the query here since the finder methods we use are quite complex. This does not memoize the result of the query. + # rubocop: disable CodeReuse/ActiveRecord def fetch_issues strong_memoize(:fetch_issues) do issues = IssuesFinder.new(current_user, filter_params).execute @@ -31,6 +34,7 @@ module Boards filter(issues).reorder(nil) end end + # rubocop: enable CodeReuse/ActiveRecord def filter(issues) issues = without_board_labels(issues) unless list&.movable? || list&.closed? @@ -52,6 +56,7 @@ module Boards set_parent set_state set_scope + set_non_archived params end @@ -72,24 +77,36 @@ module Boards params[:include_subgroups] = board.group_board? end + def set_non_archived + params[:non_archived] = parent.is_a?(Group) + end + + # rubocop: disable CodeReuse/ActiveRecord def board_label_ids @board_label_ids ||= board.lists.movable.pluck(:label_id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def without_board_labels(issues) return issues unless board_label_ids.any? issues.where.not('EXISTS (?)', issues_label_links.limit(1)) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def issues_label_links LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id").where(label_id: board_label_ids) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def with_list_label(issues) issues.where('EXISTS (?)', LabelLink.where("label_links.target_type = 'Issue' AND label_links.target_id = issues.id") .where("label_links.label_id = ?", list.label_id).limit(1)) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 6fd8a23b2a1..7dd87034410 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -21,13 +21,17 @@ module Boards moving_from_list != moving_to_list end + # rubocop: disable CodeReuse/ActiveRecord def moving_from_list @moving_from_list ||= board.lists.find_by(id: params[:from_list_id]) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def moving_to_list @moving_to_list ||= board.lists.find_by(id: params[:to_list_id]) end + # rubocop: enable CodeReuse/ActiveRecord def update(issue) ::Issues::UpdateService.new(issue.project, current_user, issue_params(issue)).execute(issue) @@ -61,6 +65,7 @@ module Boards [moving_to_list.label_id].compact end + # rubocop: disable CodeReuse/ActiveRecord def remove_label_ids label_ids = if moving_to_list.movable? @@ -73,6 +78,7 @@ module Boards Array(label_ids).compact end + # rubocop: enable CodeReuse/ActiveRecord def move_between_ids return unless params[:move_after_id] || params[:move_before_id] diff --git a/app/services/boards/lists/destroy_service.rb b/app/services/boards/lists/destroy_service.rb index e12d4f46e19..609c430caed 100644 --- a/app/services/boards/lists/destroy_service.rb +++ b/app/services/boards/lists/destroy_service.rb @@ -18,10 +18,12 @@ module Boards attr_reader :board + # rubocop: disable CodeReuse/ActiveRecord def decrement_higher_lists(list) board.lists.movable.where('position > ?', list.position) .update_all('position = position - 1') end + # rubocop: enable CodeReuse/ActiveRecord def remove_list(list) list.destroy diff --git a/app/services/boards/lists/move_service.rb b/app/services/boards/lists/move_service.rb index 27a36051662..93f81837d1a 100644 --- a/app/services/boards/lists/move_service.rb +++ b/app/services/boards/lists/move_service.rb @@ -34,17 +34,21 @@ module Boards end end + # rubocop: disable CodeReuse/ActiveRecord def decrement_intermediate_lists board.lists.movable.where('position > ?', old_position) .where('position <= ?', new_position) .update_all('position = position - 1') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def increment_intermediate_lists board.lists.movable.where('position >= ?', new_position) .where('position < ?', old_position) .update_all('position = position + 1') end + # rubocop: enable CodeReuse/ActiveRecord def update_list_position(list) list.update_attribute(:position, new_position) diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb index 854b191c45c..c91738fa4c7 100644 --- a/app/services/chat_names/find_user_service.rb +++ b/app/services/chat_names/find_user_service.rb @@ -17,6 +17,7 @@ module ChatNames private + # rubocop: disable CodeReuse/ActiveRecord def find_chat_name ChatName.find_by( service: @service, @@ -24,5 +25,6 @@ module ChatNames chat_id: @params[:user_id] ) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/ci/compare_test_reports_service.rb b/app/services/ci/compare_test_reports_service.rb index ec25e934a27..2293f95f56b 100644 --- a/app/services/ci/compare_test_reports_service.rb +++ b/app/services/ci/compare_test_reports_service.rb @@ -3,6 +3,7 @@ module Ci class CompareTestReportsService < ::BaseService def execute(base_pipeline, head_pipeline) + # rubocop: disable CodeReuse/Serializer comparer = Gitlab::Ci::Reports::TestReportsComparer .new(base_pipeline&.test_reports, head_pipeline.test_reports) @@ -19,6 +20,7 @@ module Ci key: key(base_pipeline, head_pipeline), status_reason: e.message } + # rubocop: enable CodeReuse/Serializer end def latest?(base_pipeline, head_pipeline, data) diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 85df8bcff8c..92a8438ab2f 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -65,6 +65,7 @@ module Ci end end + # rubocop: disable CodeReuse/ActiveRecord def auto_cancelable_pipelines project.pipelines .where(ref: pipeline.ref) @@ -72,6 +73,7 @@ module Ci .where.not(sha: project.commit(pipeline.ref).try(:id)) .created_or_pending end + # rubocop: enable CodeReuse/ActiveRecord def pipeline_created_counter @pipeline_created_counter ||= Gitlab::Metrics @@ -84,8 +86,10 @@ module Ci end end + # rubocop: disable CodeReuse/ActiveRecord def related_merge_requests pipeline.project.source_of_merge_requests.opened.where(source_branch: pipeline.ref) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/ci/ensure_stage_service.rb b/app/services/ci/ensure_stage_service.rb index 3d0e39d1b9f..cbb3a2e4709 100644 --- a/app/services/ci/ensure_stage_service.rb +++ b/app/services/ci/ensure_stage_service.rb @@ -38,9 +38,11 @@ module Ci EOS end + # rubocop: disable CodeReuse/ActiveRecord def find_stage @build.pipeline.stages.find_by(name: @build.stage) end + # rubocop: enable CodeReuse/ActiveRecord def create_stage Ci::Stage.create!(name: @build.stage, diff --git a/app/services/ci/extract_sections_from_build_trace_service.rb b/app/services/ci/extract_sections_from_build_trace_service.rb index 693f6d55be3..97f9918fdb7 100644 --- a/app/services/ci/extract_sections_from_build_trace_service.rb +++ b/app/services/ci/extract_sections_from_build_trace_service.rb @@ -11,11 +11,13 @@ module Ci private + # rubocop: disable CodeReuse/ActiveRecord def find_or_create_name(name) project.build_trace_section_names.find_or_create_by!(name: name) rescue ActiveRecord::RecordInvalid project.build_trace_section_names.find_by!(name: name) end + # rubocop: enable CodeReuse/ActiveRecord def extract_sections(build) build.trace.extract_sections.map do |attr| diff --git a/app/services/ci/fetch_kubernetes_token_service.rb b/app/services/ci/fetch_kubernetes_token_service.rb deleted file mode 100644 index 15eda56cac6..00000000000 --- a/app/services/ci/fetch_kubernetes_token_service.rb +++ /dev/null @@ -1,75 +0,0 @@ -# frozen_string_literal: true - -## -# TODO: -# Almost components in this class were copied from app/models/project_services/kubernetes_service.rb -# We should dry up those classes not to repeat the same code. -# Maybe we should have a special facility (e.g. lib/kubernetes_api) to maintain all Kubernetes API caller. -module Ci - class FetchKubernetesTokenService - attr_reader :api_url, :ca_pem, :username, :password - - def initialize(api_url, ca_pem, username, password) - @api_url = api_url - @ca_pem = ca_pem - @username = username - @password = password - end - - def execute - read_secrets.each do |secret| - name = secret.dig('metadata', 'name') - if /default-token/ =~ name - token_base64 = secret.dig('data', 'token') - return Base64.decode64(token_base64) if token_base64 - end - end - - nil - end - - private - - def read_secrets - kubeclient = build_kubeclient! - - kubeclient.get_secrets.as_json - rescue Kubeclient::HttpError => err - raise err unless err.error_code == 404 - - [] - end - - def build_kubeclient!(api_path: 'api', api_version: 'v1') - raise "Incomplete settings" unless api_url && username && password - - ::Kubeclient::Client.new( - join_api_url(api_path), - api_version, - auth_options: { username: username, password: password }, - ssl_options: kubeclient_ssl_options, - http_proxy_uri: ENV['http_proxy'] - ) - end - - def join_api_url(api_path) - url = URI.parse(api_url) - prefix = url.path.sub(%r{/+\z}, '') - - url.path = [prefix, api_path].join("/") - - url.to_s - 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 - end -end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index cafee76a33c..69341a6c263 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -60,17 +60,23 @@ module Ci end end + # rubocop: disable CodeReuse/ActiveRecord def status_for_prior_stages(index) pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def stage_indexes_of_created_builds created_builds.order(:stage_idx).pluck('distinct stage_idx') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def created_builds_in_stage(index) created_builds.where(stage_idx: index) end + # rubocop: enable CodeReuse/ActiveRecord def created_builds pipeline.builds.created @@ -80,6 +86,7 @@ module Ci # This replicates what is db/post_migrate/20170416103934_upate_retried_for_ci_build.rb # and ensures that functionality will not be broken before migration is run # this updates only when there are data that needs to be updated, there are two groups with no retried flag + # rubocop: disable CodeReuse/ActiveRecord def update_retried # find the latest builds for each name latest_statuses = pipeline.statuses.latest @@ -93,6 +100,7 @@ module Ci .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) if latest_statuses.any? end + # rubocop: enable CodeReuse/ActiveRecord def enqueue_build(build) Ci::EnqueueBuildService.new(project, @user).execute(build) diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb index 11f85627faf..5a7be921389 100644 --- a/app/services/ci/register_job_service.rb +++ b/app/services/ci/register_job_service.rb @@ -15,6 +15,7 @@ module Ci @runner = runner end + # rubocop: disable CodeReuse/ActiveRecord def execute(params = {}) builds = if runner.instance_type? @@ -63,6 +64,7 @@ module Ci register_failure Result.new(nil, valid) end + # rubocop: enable CodeReuse/ActiveRecord private @@ -84,6 +86,7 @@ module Ci true end + # rubocop: disable CodeReuse/ActiveRecord def builds_for_shared_runner new_builds. # don't run projects which have not enabled shared runners and builds @@ -97,11 +100,15 @@ module Ci joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id") .order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def builds_for_project_runner new_builds.where(project: runner.projects.without_deleted.with_builds_enabled).order('id ASC') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def builds_for_group_runner # Workaround for weird Rails bug, that makes `runner.groups.to_sql` to return `runner_id = NULL` groups = ::Group.joins(:runner_namespaces).merge(runner.runner_namespaces) @@ -113,11 +120,14 @@ module Ci .without_deleted new_builds.where(project: projects).order('id ASC') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def running_builds_for_shared_runners Ci::Build.running.where(runner: Ci::Runner.instance_type) .group(:project_id).select(:project_id, 'count(*) AS running_builds') end + # rubocop: enable CodeReuse/ActiveRecord def new_builds builds = Ci::Build.pending.unstarted @@ -138,6 +148,7 @@ module Ci attempt_counter.increment end + # rubocop: disable CodeReuse/ActiveRecord def jobs_running_for_project(job) return '+Inf' unless runner.instance_type? @@ -146,6 +157,7 @@ module Ci .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1 running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET ? running_jobs_count : "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+" end + # rubocop: enable CodeReuse/ActiveRecord def failed_attempt_counter @failed_attempt_counter ||= Gitlab::Metrics.counter(:job_register_attempts_failed_total, "Counts the times a runner tries to register a job") diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb index 6ceb59e4780..218f1e63d08 100644 --- a/app/services/ci/retry_build_service.rb +++ b/app/services/ci/retry_build_service.rb @@ -19,6 +19,7 @@ module Ci end end + # rubocop: disable CodeReuse/ActiveRecord def reprocess!(build) unless can?(current_user, :update_build, build) raise Gitlab::Access::AccessDeniedError @@ -41,5 +42,6 @@ module Ci project.builds.create!(Hash[attributes]) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/clusters/gcp/finalize_creation_service.rb b/app/services/clusters/gcp/finalize_creation_service.rb index 264419501dc..3ae0a4a19d0 100644 --- a/app/services/clusters/gcp/finalize_creation_service.rb +++ b/app/services/clusters/gcp/finalize_creation_service.rb @@ -9,17 +9,24 @@ module Clusters @provider = provider configure_provider + create_gitlab_service_account! configure_kubernetes cluster.save! rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e provider.make_errored!("Failed to request to CloudPlatform; #{e.message}") + rescue Kubeclient::HttpError => e + provider.make_errored!("Failed to run Kubeclient: #{e.message}") rescue ActiveRecord::RecordInvalid => e provider.make_errored!("Failed to configure Google Kubernetes Engine Cluster: #{e.message}") end private + def create_gitlab_service_account! + Clusters::Gcp::Kubernetes::CreateServiceAccountService.new(kube_client, rbac: create_rbac_cluster?).execute + end + def configure_provider provider.endpoint = gke_cluster.endpoint provider.status_event = :make_created @@ -32,15 +39,54 @@ module Clusters ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), username: gke_cluster.master_auth.username, password: gke_cluster.master_auth.password, + authorization_type: authorization_type, token: request_kubernetes_token) end def request_kubernetes_token - Ci::FetchKubernetesTokenService.new( + Clusters::Gcp::Kubernetes::FetchKubernetesTokenService.new(kube_client).execute + end + + def authorization_type + create_rbac_cluster? ? 'rbac' : 'abac' + end + + def create_rbac_cluster? + !provider.legacy_abac? + end + + def kube_client + @kube_client ||= build_kube_client!( 'https://' + gke_cluster.endpoint, Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate), gke_cluster.master_auth.username, - gke_cluster.master_auth.password).execute + gke_cluster.master_auth.password, + api_groups: ['api', 'apis/rbac.authorization.k8s.io'] + ) + end + + def build_kube_client!(api_url, ca_pem, username, password, api_groups: ['api'], api_version: 'v1') + raise "Incomplete settings" unless api_url && username && password + + Gitlab::Kubernetes::KubeClient.new( + api_url, + api_groups, + api_version, + auth_options: { username: username, password: password }, + ssl_options: kubeclient_ssl_options(ca_pem), + http_proxy_uri: ENV['http_proxy'] + ) + end + + def kubeclient_ssl_options(ca_pem) + 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 gke_cluster diff --git a/app/services/clusters/gcp/kubernetes.rb b/app/services/clusters/gcp/kubernetes.rb new file mode 100644 index 00000000000..d014d73b3e8 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + SERVICE_ACCOUNT_NAME = 'gitlab' + SERVICE_ACCOUNT_NAMESPACE = 'default' + SERVICE_ACCOUNT_TOKEN_NAME = 'gitlab-token' + CLUSTER_ROLE_BINDING_NAME = 'gitlab-admin' + CLUSTER_ROLE_NAME = 'cluster-admin' + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/create_service_account_service.rb b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb new file mode 100644 index 00000000000..d17744591e6 --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/create_service_account_service.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class CreateServiceAccountService + attr_reader :kubeclient, :rbac + + def initialize(kubeclient, rbac:) + @kubeclient = kubeclient + @rbac = rbac + end + + def execute + kubeclient.create_service_account(service_account_resource) + kubeclient.create_secret(service_account_token_resource) + kubeclient.create_cluster_role_binding(cluster_role_binding_resource) if rbac + end + + private + + def service_account_resource + Gitlab::Kubernetes::ServiceAccount.new(service_account_name, service_account_namespace).generate + end + + def service_account_token_resource + Gitlab::Kubernetes::ServiceAccountToken.new( + SERVICE_ACCOUNT_TOKEN_NAME, service_account_name, service_account_namespace).generate + end + + def cluster_role_binding_resource + subjects = [{ kind: 'ServiceAccount', name: service_account_name, namespace: service_account_namespace }] + + Gitlab::Kubernetes::ClusterRoleBinding.new( + CLUSTER_ROLE_BINDING_NAME, + CLUSTER_ROLE_NAME, + subjects + ).generate + end + + def service_account_name + SERVICE_ACCOUNT_NAME + end + + def service_account_namespace + SERVICE_ACCOUNT_NAMESPACE + end + end + end + end +end diff --git a/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb new file mode 100644 index 00000000000..9e09345c8dc --- /dev/null +++ b/app/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Clusters + module Gcp + module Kubernetes + class FetchKubernetesTokenService + attr_reader :kubeclient + + def initialize(kubeclient) + @kubeclient = kubeclient + end + + def execute + token_base64 = get_secret&.dig('data', 'token') + Base64.decode64(token_base64) if token_base64 + end + + private + + def get_secret + kubeclient.get_secret(SERVICE_ACCOUNT_TOKEN_NAME, SERVICE_ACCOUNT_NAMESPACE).as_json + rescue Kubeclient::HttpError => err + raise err unless err.error_code == 404 + + nil + end + end + end + end +end diff --git a/app/services/clusters/gcp/provision_service.rb b/app/services/clusters/gcp/provision_service.rb index ab1bf9c64f6..80040511ec2 100644 --- a/app/services/clusters/gcp/provision_service.rb +++ b/app/services/clusters/gcp/provision_service.rb @@ -27,7 +27,9 @@ module Clusters provider.zone, provider.cluster.name, provider.num_nodes, - machine_type: provider.machine_type) + machine_type: provider.machine_type, + legacy_abac: provider.legacy_abac + ) unless operation.status == 'PENDING' || operation.status == 'RUNNING' return provider.make_errored!("Operation status is unexpected; #{operation.status_message}") diff --git a/app/services/cohorts_service.rb b/app/services/cohorts_service.rb index 7a14e97f749..6d466c2fc9c 100644 --- a/app/services/cohorts_service.rb +++ b/app/services/cohorts_service.rb @@ -78,6 +78,7 @@ class CohortsService # created_at_month can never be nil, but last_activity_on_month can (when a # user has never logged in, just been created). This covers the last # MONTHS_INCLUDED months. + # rubocop: disable CodeReuse/ActiveRecord def counts_by_month @counts_by_month ||= begin @@ -91,6 +92,7 @@ class CohortsService .count end end + # rubocop: enable CodeReuse/ActiveRecord def column_to_date(column) if Gitlab::Database.postgresql? diff --git a/app/services/concerns/issues/resolve_discussions.rb b/app/services/concerns/issues/resolve_discussions.rb index 1563ed965df..f0e9862ca30 100644 --- a/app/services/concerns/issues/resolve_discussions.rb +++ b/app/services/concerns/issues/resolve_discussions.rb @@ -13,12 +13,14 @@ module Issues end # rubocop:enable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def merge_request_to_resolve_discussions_of strong_memoize(:merge_request_to_resolve_discussions_of) do MergeRequestsFinder.new(current_user, project_id: project.id) .find_by(iid: merge_request_to_resolve_discussions_of_iid) end end + # rubocop: enable CodeReuse/ActiveRecord def discussions_to_resolve return [] unless merge_request_to_resolve_discussions_of diff --git a/app/services/create_release_service.rb b/app/services/create_release_service.rb index 09c68390007..8d1fdbe11c3 100644 --- a/app/services/create_release_service.rb +++ b/app/services/create_release_service.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class CreateReleaseService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(tag_name, release_description) repository = project.repository existing_tag = repository.find_tag(tag_name) @@ -21,6 +22,7 @@ class CreateReleaseService < BaseService error('Tag does not exist', 404) end end + # rubocop: enable CodeReuse/ActiveRecord def success(release) super().merge(release: release) diff --git a/app/services/delete_merged_branches_service.rb b/app/services/delete_merged_branches_service.rb index ff3e4783fe3..ced87a1c37a 100644 --- a/app/services/delete_merged_branches_service.rb +++ b/app/services/delete_merged_branches_service.rb @@ -21,10 +21,12 @@ class DeleteMergedBranchesService < BaseService private + # rubocop: disable CodeReuse/ActiveRecord def merge_request_branch_names # reorder(nil) is necessary for SELECT DISTINCT because default scope adds an ORDER BY source_names = project.origin_merge_requests.opened.reorder(nil).uniq.pluck(:source_branch) target_names = project.merge_requests.opened.reorder(nil).uniq.pluck(:target_branch) (source_names + target_names).uniq end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/emails/base_service.rb b/app/services/emails/base_service.rb index ba7b689a9af..988215ffc78 100644 --- a/app/services/emails/base_service.rb +++ b/app/services/emails/base_service.rb @@ -2,6 +2,8 @@ module Emails class BaseService + attr_reader :current_user + def initialize(current_user, params = {}) @current_user, @params = current_user, params.dup @user = params.delete(:user) diff --git a/app/services/emails/create_service.rb b/app/services/emails/create_service.rb index acf575e24e5..56925a724fe 100644 --- a/app/services/emails/create_service.rb +++ b/app/services/emails/create_service.rb @@ -3,7 +3,12 @@ module Emails class CreateService < ::Emails::BaseService def execute(extra_params = {}) - @user.emails.create(@params.merge(extra_params)) + skip_confirmation = @params.delete(:skip_confirmation) + + email = @user.emails.create(@params.merge(extra_params)) + + email&.confirm if skip_confirmation && current_user.admin? + email end end end diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb index fc7b236f7da..39e614d6569 100644 --- a/app/services/files/base_service.rb +++ b/app/services/files/base_service.rb @@ -7,8 +7,10 @@ module Files def initialize(*args) super - @author_email = params[:author_email] || current_user&.email - @author_name = params[:author_name] || current_user&.name + git_user = Gitlab::Git::User.from_gitlab(current_user) if current_user.present? + + @author_email = params[:author_email] || git_user&.email + @author_name = params[:author_name] || git_user&.name @commit_message = params[:commit_message] @last_commit_sha = params[:last_commit_sha] diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb index 26e90e8cf8c..f1883877d56 100644 --- a/app/services/git_push_service.rb +++ b/app/services/git_push_service.rb @@ -94,6 +94,7 @@ class GitPushService < BaseService ProjectCacheWorker.perform_async(project.id, types, [:commit_count, :repository_size]) end + # rubocop: disable CodeReuse/ActiveRecord def update_signatures commit_shas = last_pushed_commits.map(&:sha) @@ -108,6 +109,7 @@ class GitPushService < BaseService CreateGpgSignatureWorker.perform_async(commit_shas, project.id) end + # rubocop: enable CodeReuse/ActiveRecord # Schedules processing of commit messages. def process_commit_messages diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb index 93d84bd8a9c..641111aeadc 100644 --- a/app/services/groups/destroy_service.rb +++ b/app/services/groups/destroy_service.rb @@ -9,6 +9,7 @@ module Groups Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}") end + # rubocop: disable CodeReuse/ActiveRecord def execute group.prepare_for_destroy @@ -30,5 +31,6 @@ module Groups group.destroy end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb index ea7576077f3..5efa746dfb9 100644 --- a/app/services/groups/transfer_service.rb +++ b/app/services/groups/transfer_service.rb @@ -64,9 +64,11 @@ module Groups end end + # rubocop: disable CodeReuse/ActiveRecord def namespace_with_same_path? Namespace.exists?(path: @group.path, parent: @new_parent_group) end + # rubocop: enable CodeReuse/ActiveRecord def update_group_attributes if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level @@ -78,6 +80,7 @@ module Groups @group.save! end + # rubocop: disable CodeReuse/ActiveRecord def update_children_and_projects_visibility descendants = @group.descendants.where("visibility_level > ?", @new_parent_group.visibility_level) @@ -90,6 +93,7 @@ module Groups .where("visibility_level > ?", @new_parent_group.visibility_level) .update_all(visibility_level: @new_parent_group.visibility_level) end + # rubocop: enable CodeReuse/ActiveRecord def raise_transfer_error(message) raise TransferError, ERROR_MESSAGES[message] diff --git a/app/services/import_export_clean_up_service.rb b/app/services/import_export_clean_up_service.rb index e75a951944e..3ecb51b60d0 100644 --- a/app/services/import_export_clean_up_service.rb +++ b/app/services/import_export_clean_up_service.rb @@ -26,10 +26,12 @@ class ImportExportCleanUpService Gitlab::Popen.popen(%W(find #{path} -not -path #{path} -mmin +#{mmin} -delete)) end + # rubocop: disable CodeReuse/ActiveRecord def clean_up_export_object_files ImportExportUpload.where('updated_at < ?', mmin.minutes.ago).each do |upload| upload.remove_export_file! upload.save! end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb index 051d5ba881d..c4beddf2294 100644 --- a/app/services/issuable/bulk_update_service.rb +++ b/app/services/issuable/bulk_update_service.rb @@ -2,6 +2,7 @@ module Issuable class BulkUpdateService < IssuableBaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(type) model_class = type.classify.constantize update_class = type.classify.pluralize.constantize::UpdateService @@ -28,6 +29,7 @@ module Issuable success: !items.count.zero? } end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb index 028b350ca07..765de9c66b0 100644 --- a/app/services/issuable/common_system_notes_service.rb +++ b/app/services/issuable/common_system_notes_service.rb @@ -17,6 +17,7 @@ module Issuable create_labels_note(old_labels) if issuable.labels != old_labels create_discussion_lock_note if issuable.previous_changes.include?('discussion_locked') create_milestone_note if issuable.previous_changes.include?('milestone_id') + create_due_date_note if issuable.previous_changes.include?('due_date') end private @@ -55,7 +56,9 @@ module Issuable added_labels = issuable.labels - old_labels removed_labels = old_labels - issuable.labels - SystemNoteService.change_label(issuable, issuable.project, current_user, added_labels, removed_labels) + ResourceEvents::ChangeLabelsService + .new(issuable, current_user) + .execute(added_labels: added_labels, removed_labels: removed_labels) end def create_title_change_note(old_title) @@ -88,6 +91,10 @@ module Issuable SystemNoteService.change_milestone(issuable, issuable.project, current_user, issuable.milestone) end + def create_due_date_note + SystemNoteService.change_due_date(issuable, issuable.project, current_user, issuable.due_date) + end + def create_discussion_lock_note SystemNoteService.discussion_lock(issuable, current_user) end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 7d60c65bb79..3e8b9f84042 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -68,11 +68,13 @@ class IssuableBaseService < BaseService find_or_create_label_ids end + # rubocop: disable CodeReuse/ActiveRecord def filter_labels_in_param(key) return if params[key].to_a.empty? params[key] = available_labels.where(id: params[key]).pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord def find_or_create_label_ids labels = params.delete(:labels) @@ -129,28 +131,19 @@ class IssuableBaseService < BaseService params.merge!(command_params) end - def create_issuable(issuable, attributes, label_ids:) - issuable.with_transaction_returning_status do - if issuable.save - issuable.update(label_ids: label_ids) - end - end - end - def create(issuable) handle_quick_actions_on_create(issuable) filter_params(issuable) params.delete(:state_event) params[:author] ||= current_user - - label_ids = process_label_ids(params) + params[:label_ids] = issuable.label_ids.to_a + process_label_ids(params) issuable.assign_attributes(params) before_create(issuable) - if params.present? && create_issuable(issuable, params, label_ids: label_ids) + if issuable.save after_create(issuable) execute_hooks(issuable) invalidate_cache_counts(issuable, users: issuable.assignees) @@ -256,6 +249,7 @@ class IssuableBaseService < BaseService end end + # rubocop: disable CodeReuse/ActiveRecord def change_todo(issuable) case params.delete(:todo_event) when 'add' @@ -265,6 +259,7 @@ class IssuableBaseService < BaseService todo_service.mark_todos_as_done_by_ids(todo, current_user) if todo end end + # rubocop: enable CodeReuse/ActiveRecord def toggle_award(issuable) award = params.delete(:emoji_award) diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb index 25389a946bb..ef08adf4f92 100644 --- a/app/services/issues/base_service.rb +++ b/app/services/issues/base_service.rb @@ -31,6 +31,7 @@ module Issues issue.project.execute_services(issue_data, hooks_scope) end + # rubocop: disable CodeReuse/ActiveRecord def filter_assignee(issuable) return if params[:assignee_ids].blank? @@ -48,6 +49,7 @@ module Issues params.delete(:assignee_ids) end end + # rubocop: enable CodeReuse/ActiveRecord def update_project_counter_caches?(issue) super || issue.confidential_changed? diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb index 841bce9949e..d2bdba1e627 100644 --- a/app/services/issues/move_service.rb +++ b/app/services/issues/move_service.rb @@ -36,6 +36,7 @@ module Issues def update_new_issue rewrite_notes + copy_resource_label_events rewrite_issue_award_emoji add_note_moved_from end @@ -57,6 +58,7 @@ module Issues CreateService.new(@new_project, @current_user, new_params).execute end + # rubocop: disable CodeReuse/ActiveRecord def cloneable_label_ids params = { project_id: @new_project.id, @@ -66,6 +68,7 @@ module Issues LabelsFinder.new(current_user, params).execute.pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord def cloneable_milestone_id title = @old_issue.milestone&.title @@ -96,6 +99,20 @@ module Issues end end + # rubocop: disable CodeReuse/ActiveRecord + def copy_resource_label_events + @old_issue.resource_label_events.find_in_batches do |batch| + events = batch.map do |event| + event.attributes + .except('id', 'reference', 'reference_html') + .merge('issue_id' => @new_issue.id, 'action' => ResourceLabelEvent.actions[event.action]) + end + + Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, events) + end + end + # rubocop: enable CodeReuse/ActiveRecord + def rewrite_issue_award_emoji rewrite_award_emoji(@old_issue, @new_issue) end diff --git a/app/services/issues/referenced_merge_requests_service.rb b/app/services/issues/referenced_merge_requests_service.rb index 40d78502697..a69cd324b1e 100644 --- a/app/services/issues/referenced_merge_requests_service.rb +++ b/app/services/issues/referenced_merge_requests_service.rb @@ -2,6 +2,7 @@ module Issues class ReferencedMergeRequestsService < Issues::BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(issue) referenced = referenced_merge_requests(issue) closed_by = closed_by_merge_requests(issue) @@ -12,6 +13,7 @@ module Issues [sort_by_iid(referenced), sort_by_iid(closed_by)] end + # rubocop: enable CodeReuse/ActiveRecord def referenced_merge_requests(issue) merge_requests = extract_merge_requests(issue) @@ -29,6 +31,7 @@ module Issues ) end + # rubocop: disable CodeReuse/ActiveRecord def closed_by_merge_requests(issue) return [] unless issue.open? @@ -39,6 +42,7 @@ module Issues ids = MergeRequestsClosingIssues.where(merge_request_id: merge_requests.map(&:id), issue_id: issue.id).pluck(:merge_request_id) merge_requests.select { |mr| mr.id.in?(ids) } end + # rubocop: enable CodeReuse/ActiveRecord private @@ -54,10 +58,12 @@ module Issues ext.merge_requests end + # rubocop: disable CodeReuse/ActiveRecord def issue_notes(issue) @issue_notes ||= {} @issue_notes[issue] ||= issue.notes.includes(:author) end + # rubocop: enable CodeReuse/ActiveRecord def sort_by_iid(merge_requests) Gitlab::IssuableSorter.sort(project, merge_requests) { |mr| mr.iid.to_s } diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index faa4c8a5a4f..b54b0bf6ef6 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -67,6 +67,7 @@ module Issues issue.move_between(issue_before, issue_after) end + # rubocop: disable CodeReuse/ActiveRecord def change_issue_duplicate(issue) canonical_issue_id = params.delete(:canonical_issue_id) canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id) @@ -75,6 +76,7 @@ module Issues Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue) end end + # rubocop: enable CodeReuse/ActiveRecord def move_issue_to_new_project(issue) target_project = params.delete(:target_project) @@ -89,6 +91,7 @@ module Issues private + # rubocop: disable CodeReuse/ActiveRecord def get_issue_if_allowed(id, board_group_id = nil) return unless id @@ -101,6 +104,7 @@ module Issues issue if can?(current_user, :update_issue, issue) end + # rubocop: enable CodeReuse/ActiveRecord def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index e4486764a4d..628873519d7 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -29,6 +29,7 @@ module Labels # Only creates the label if current_user can do so, if the label does not exist # and the user can not create the label, nil is returned + # rubocop: disable CodeReuse/ActiveRecord def find_or_create_label new_label = available_labels.find_by(title: title) @@ -39,6 +40,7 @@ module Labels new_label end + # rubocop: enable CodeReuse/ActiveRecord def title params[:title] || params[:name] diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb index 623a5f0950e..f30ad706c63 100644 --- a/app/services/labels/promote_service.rb +++ b/app/services/labels/promote_service.rb @@ -4,6 +4,7 @@ module Labels class PromoteService < BaseService BATCH_SIZE = 1000 + # rubocop: disable CodeReuse/ActiveRecord def execute(label) return unless project.group && label.is_a?(ProjectLabel) @@ -13,6 +14,7 @@ module Labels label_ids_for_merge(new_label).find_in_batches(batch_size: BATCH_SIZE) do |batched_ids| update_issuables(new_label, batched_ids) + update_resource_label_events(new_label, batched_ids) update_issue_board_lists(new_label, batched_ids) update_priorities(new_label, batched_ids) subscribe_users(new_label, batched_ids) @@ -26,9 +28,11 @@ module Labels new_label end end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def subscribe_users(new_label, label_ids) # users can be subscribed to multiple labels that will be merged into the group one # we want to keep only one subscription / user @@ -37,7 +41,9 @@ module Labels .pluck('MAX(id)') Subscription.where(id: ids_to_update).update_all(subscribable_id: new_label.id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def label_ids_for_merge(new_label) LabelsFinder .new(current_user, title: new_label.title, group_id: project.group.id) @@ -45,28 +51,45 @@ module Labels .where.not(id: new_label) .select(:id) # Can't use pluck() to avoid object-creation because of the batching end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_issuables(new_label, label_ids) LabelLink .where(label: label_ids) .update_all(label_id: new_label) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord + def update_resource_label_events(new_label, label_ids) + ResourceLabelEvent + .where(label: label_ids) + .update_all(label_id: new_label) + end + # rubocop: enable CodeReuse/ActiveRecord + + # rubocop: disable CodeReuse/ActiveRecord def update_issue_board_lists(new_label, label_ids) List .where(label: label_ids) .update_all(label_id: new_label) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_priorities(new_label, label_ids) LabelPriority .where(label: label_ids) .update_all(label_id: new_label) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_project_labels(label_ids) Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll end + # rubocop: enable CodeReuse/ActiveRecord def clone_label_to_group_label(label) params = label.attributes.slice('title', 'description', 'color') diff --git a/app/services/labels/transfer_service.rb b/app/services/labels/transfer_service.rb index 1bd8d9fc325..52360f775dc 100644 --- a/app/services/labels/transfer_service.rb +++ b/app/services/labels/transfer_service.rb @@ -32,16 +32,19 @@ module Labels attr_reader :current_user, :old_group, :project + # rubocop: disable CodeReuse/ActiveRecord def labels_to_transfer - label_ids = [] - label_ids << group_labels_applied_to_issues.select(:id) - label_ids << group_labels_applied_to_merge_requests.select(:id) - - union = Gitlab::SQL::Union.new(label_ids) - - Label.where("labels.id IN (#{union.to_sql})").reorder(nil).uniq # rubocop:disable GitlabSecurity/SqlInjection + Label + .from_union([ + group_labels_applied_to_issues, + group_labels_applied_to_merge_requests + ]) + .reorder(nil) + .uniq end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group_labels_applied_to_issues Label.joins(:issues) .where( @@ -49,7 +52,9 @@ module Labels labels: { type: 'GroupLabel', group_id: old_group.id } ) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group_labels_applied_to_merge_requests Label.joins(:merge_requests) .where( @@ -57,6 +62,7 @@ module Labels labels: { type: 'GroupLabel', group_id: old_group.id } ) end + # rubocop: enable CodeReuse/ActiveRecord def find_or_create_label!(label) params = label.attributes.slice('title', 'description', 'color') @@ -65,6 +71,7 @@ module Labels new_label.id end + # rubocop: disable CodeReuse/ActiveRecord def update_label_links(labels, old_label_id:, new_label_id:) # use 'labels' relation to get label_link ids only of issues/MRs # in the project being transferred. @@ -76,10 +83,13 @@ module Labels LabelLink.where(id: link_ids, label_id: old_label_id) .update_all(label_id: new_label_id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_label_priorities(old_label_id:, new_label_id:) LabelPriority.where(project_id: project.id, label_id: old_label_id) .update_all(label_id: new_label_id) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/lfs/file_transformer.rb b/app/services/lfs/file_transformer.rb index c8eccb8e6cd..6ecf583cb6a 100644 --- a/app/services/lfs/file_transformer.rb +++ b/app/services/lfs/file_transformer.rb @@ -55,11 +55,13 @@ module Lfs @cached_attributes ||= Gitlab::Git::AttributesAtRefParser.new(repository, branch_name) end + # rubocop: disable CodeReuse/ActiveRecord def create_lfs_object!(lfs_pointer_file, file_content) LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object| lfs_object.file = CarrierWaveStringFile.new(file_content) end end + # rubocop: enable CodeReuse/ActiveRecord def link_lfs_object!(lfs_object) project.lfs_objects << lfs_object diff --git a/app/services/lfs/lock_file_service.rb b/app/services/lfs/lock_file_service.rb index 78434909d68..c7730d24bdc 100644 --- a/app/services/lfs/lock_file_service.rb +++ b/app/services/lfs/lock_file_service.rb @@ -18,9 +18,11 @@ module Lfs private + # rubocop: disable CodeReuse/ActiveRecord def current_lock project.lfs_file_locks.find_by(path: params[:path]) end + # rubocop: enable CodeReuse/ActiveRecord def create_lock! lock = project.lfs_file_locks.create!(user: current_user, diff --git a/app/services/lfs/locks_finder_service.rb b/app/services/lfs/locks_finder_service.rb index d52cf0e3cc4..4a5b2a52921 100644 --- a/app/services/lfs/locks_finder_service.rb +++ b/app/services/lfs/locks_finder_service.rb @@ -10,10 +10,12 @@ module Lfs private + # rubocop: disable CodeReuse/ActiveRecord def find_locks options = params.slice(:id, :path).compact.symbolize_keys project.lfs_file_locks.where(options) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/lfs/unlock_file_service.rb b/app/services/lfs/unlock_file_service.rb index 4d1443bf772..a42916d86bb 100644 --- a/app/services/lfs/unlock_file_service.rb +++ b/app/services/lfs/unlock_file_service.rb @@ -32,6 +32,7 @@ module Lfs end end + # rubocop: disable CodeReuse/ActiveRecord def lock return @lock if defined?(@lock) @@ -41,5 +42,6 @@ module Lfs project.lfs_file_locks.find_by!(path: params[:path]) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb index e6dd0e12a3a..aa5d8406d0f 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -55,6 +55,7 @@ module MergeRequests end # Returns all origin and fork merge requests from `@project` satisfying passed arguments. + # rubocop: disable CodeReuse/ActiveRecord def merge_requests_for(source_branch, mr_states: [:opened]) MergeRequest .with_state(mr_states) @@ -62,6 +63,7 @@ module MergeRequests .preload(:source_project) # we don't need a #includes since we're just preloading for the #select .select(&:source_project) end + # rubocop: enable CodeReuse/ActiveRecord def pipeline_merge_requests(pipeline) merge_requests_for(pipeline.ref).each do |merge_request| diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb index 55750269bb4..0e76d2cc3ab 100644 --- a/app/services/merge_requests/build_service.rb +++ b/app/services/merge_requests/build_service.rb @@ -20,6 +20,8 @@ module MergeRequests if merge_request.can_be_created compare_branches assign_title_and_description + assign_labels + assign_milestone end merge_request @@ -135,6 +137,20 @@ module MergeRequests append_closes_description end + def assign_labels + return unless target_project.issues_enabled? && issue + return if merge_request.label_ids&.any? + + merge_request.label_ids = issue.try(:label_ids) + end + + def assign_milestone + return unless target_project.issues_enabled? && issue + return if merge_request.milestone_id.present? + + merge_request.milestone_id = issue.try(:milestone_id) + end + def append_closes_description return unless issue&.to_reference.present? @@ -185,7 +201,9 @@ module MergeRequests end def issue - @issue ||= target_project.get_issue(issue_iid, current_user) + strong_memoize(:issue) do + target_project.get_issue(issue_iid, current_user) + end end end end diff --git a/app/services/merge_requests/create_from_issue_service.rb b/app/services/merge_requests/create_from_issue_service.rb index fd91dc4acd0..020af0bb950 100644 --- a/app/services/merge_requests/create_from_issue_service.rb +++ b/app/services/merge_requests/create_from_issue_service.rb @@ -16,8 +16,6 @@ module MergeRequests def execute return error('Invalid issue iid') unless @issue_iid.present? && issue.present? - params[:label_ids] = issue.label_ids if issue.label_ids.any? - result = CreateBranchService.new(project, current_user).execute(branch_name, ref) return result if result[:status] == :error @@ -34,9 +32,11 @@ module MergeRequests private + # rubocop: disable CodeReuse/ActiveRecord def issue @issue ||= IssuesFinder.new(current_user, project_id: project.id).find_by(iid: @issue_iid) end + # rubocop: enable CodeReuse/ActiveRecord def branch_name @branch ||= @branch_name || issue.to_branch_name @@ -58,8 +58,7 @@ module MergeRequests source_project_id: project.id, source_branch: branch_name, target_project_id: project.id, - target_branch: ref, - milestone_id: issue.milestone_id + target_branch: ref } end diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb index c36a2ecbfe3..6081a7d1de0 100644 --- a/app/services/merge_requests/create_service.rb +++ b/app/services/merge_requests/create_service.rb @@ -49,6 +49,7 @@ module MergeRequests merge_request.update(head_pipeline_id: pipeline.id) if pipeline end + # rubocop: disable CodeReuse/ActiveRecord def head_pipeline_for(merge_request) return unless merge_request.source_project @@ -59,6 +60,7 @@ module MergeRequests pipelines.order(id: :desc).first end + # rubocop: enable CodeReuse/ActiveRecord def set_projects! # @project is used to determine whether the user can set the merge request's diff --git a/app/services/merge_requests/delete_non_latest_diffs_service.rb b/app/services/merge_requests/delete_non_latest_diffs_service.rb index 2a8ea316921..d5929446122 100644 --- a/app/services/merge_requests/delete_non_latest_diffs_service.rb +++ b/app/services/merge_requests/delete_non_latest_diffs_service.rb @@ -8,6 +8,7 @@ module MergeRequests @merge_request = merge_request end + # rubocop: disable CodeReuse/ActiveRecord def execute diffs = @merge_request.non_latest_diffs.with_files @@ -16,5 +17,6 @@ module MergeRequests DeleteDiffFilesWorker.bulk_perform_in(index * 5.minutes, ids) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb index 48da796505f..bcdd752ddc4 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -51,6 +51,7 @@ module MergeRequests # and close if push to master include last commit from merge request # We need this to close(as merged) merge requests that were merged into # target branch manually + # rubocop: disable CodeReuse/ActiveRecord def post_merge_manually_merged commit_ids = @commits.map(&:id) merge_requests = @project.merge_requests.preload(:latest_merge_request_diff).opened.where(target_branch: @branch_name).to_a @@ -67,6 +68,7 @@ module MergeRequests .execute(merge_request) end end + # rubocop: enable CodeReuse/ActiveRecord def force_push? Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev) @@ -74,6 +76,7 @@ module MergeRequests # Refresh merge request diff if we push to source or target branch of merge request # Note: we should update merge requests from forks too + # rubocop: disable CodeReuse/ActiveRecord def reload_merge_requests merge_requests = @project.merge_requests.opened .by_source_or_target_branch(@branch_name).to_a @@ -101,6 +104,7 @@ module MergeRequests # @source_merge_requests diffs (for MergeRequest#commit_shas for instance). merge_requests_for_source_branch(reload: true) end + # rubocop: enable CodeReuse/ActiveRecord def reset_merge_when_pipeline_succeeds merge_requests_for_source_branch.each(&:reset_merge_when_pipeline_succeeds) @@ -197,11 +201,13 @@ module MergeRequests # If the merge requests closes any issues, save this information in the # `MergeRequestsClosingIssues` model (as a performance optimization). + # rubocop: disable CodeReuse/ActiveRecord def cache_merge_requests_closing_issues @project.merge_requests.where(source_branch: @branch_name).each do |merge_request| merge_request.cache_merge_request_closes_issues!(@current_user) end end + # rubocop: enable CodeReuse/ActiveRecord def filter_merge_requests(merge_requests) merge_requests.uniq.select(&:source_project) diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb index 8d85dc9eb5f..b4d48fe92ad 100644 --- a/app/services/merge_requests/reload_diffs_service.rb +++ b/app/services/merge_requests/reload_diffs_service.rb @@ -27,10 +27,11 @@ module MergeRequests current_user: current_user) end + # rubocop: disable CodeReuse/ActiveRecord def clear_cache(new_diff) # Executing the iteration we cache highlighted diffs for each diff file of # MergeRequestDiff. - new_diff.diffs_collection.diff_files.to_a + cacheable_collection(new_diff).write_cache # Remove cache for all diffs on this MR. Do not use the association on the # model, as that will interfere with other actions happening when @@ -38,8 +39,15 @@ module MergeRequests MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff| next if merge_request_diff == new_diff - merge_request_diff.diffs_collection.clear_cache! + cacheable_collection(merge_request_diff).clear_cache end end + # rubocop: enable CodeReuse/ActiveRecord + + def cacheable_collection(diff) + # There are scenarios where we don't need to request Diff Stats. + # Mainly when clearing / writing diff caches. + diff.diffs(include_stats: false) + end end end diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb index 660b4faaec0..39071b5dc14 100644 --- a/app/services/milestones/promote_service.rb +++ b/app/services/milestones/promote_service.rb @@ -26,6 +26,7 @@ module Milestones private + # rubocop: disable CodeReuse/ActiveRecord def milestone_ids_for_merge(group_milestone) # Pluck need to be used here instead of select so the array of ids # is persistent after old milestones gets deleted. @@ -35,6 +36,7 @@ module Milestones milestones.pluck(:id) end end + # rubocop: enable CodeReuse/ActiveRecord def move_children_to_group_milestone(group_milestone) milestone_ids_for_merge(group_milestone).in_groups_of(100, false) do |milestone_ids| @@ -59,6 +61,7 @@ module Milestones milestone end + # rubocop: disable CodeReuse/ActiveRecord def update_children(group_milestone, milestone_ids) issues = Issue.where(project_id: group_project_ids, milestone_id: milestone_ids) merge_requests = MergeRequest.where(source_project_id: group_project_ids, milestone_id: milestone_ids) @@ -67,18 +70,23 @@ module Milestones issuable_collection.update_all(milestone_id: group_milestone.id) end end + # rubocop: enable CodeReuse/ActiveRecord def group @group ||= parent.group || raise_error('Project does not belong to a group.') end + # rubocop: disable CodeReuse/ActiveRecord def destroy_old_milestones(milestone) Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group_project_ids @group_project_ids ||= group.projects.pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord def raise_error(message) raise PromoteMilestoneError, "Promotion failed - #{message}" diff --git a/app/services/milestones/update_service.rb b/app/services/milestones/update_service.rb index 81b20943bab..01ab8b37bac 100644 --- a/app/services/milestones/update_service.rb +++ b/app/services/milestones/update_service.rb @@ -2,6 +2,7 @@ module Milestones class UpdateService < Milestones::BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(milestone) state = params[:state_event] @@ -18,5 +19,6 @@ module Milestones milestone end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb index 5c0e8a35cb0..9c236d7f41d 100644 --- a/app/services/notification_recipient_service.rb +++ b/app/services/notification_recipient_service.rb @@ -58,6 +58,7 @@ module NotificationRecipientService @recipients ||= [] end + # rubocop: disable CodeReuse/ActiveRecord def add_recipients(users, type, reason) if users.is_a?(ActiveRecord::Relation) users = users.includes(:notification_settings) @@ -66,10 +67,13 @@ module NotificationRecipientService users = Array(users).compact recipients.concat(users.map { |u| make_recipient(u, type, reason) }) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def user_scope User.includes(:notification_settings) end + # rubocop: enable CodeReuse/ActiveRecord def make_recipient(user, type, reason) NotificationRecipient.new( @@ -112,6 +116,7 @@ module NotificationRecipientService end # Get project/group users with CUSTOM notification level + # rubocop: disable CodeReuse/ActiveRecord def add_custom_notifications user_ids = [] @@ -128,6 +133,7 @@ module NotificationRecipientService add_recipients(user_scope.where(id: user_ids), :watch, nil) end + # rubocop: enable CodeReuse/ActiveRecord def add_project_watchers add_recipients(project_watchers, :watch, nil) if project @@ -138,6 +144,7 @@ module NotificationRecipientService end # Get project users with WATCH notification level + # rubocop: disable CodeReuse/ActiveRecord def project_watchers project_members_ids = user_ids_notifiable_on(project) @@ -151,7 +158,9 @@ module NotificationRecipientService user_scope.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group_watchers user_ids_with_group_global = user_ids_notifiable_on(group, :global) user_ids = user_ids_with_global_level_watch(user_ids_with_group_global) @@ -159,6 +168,7 @@ module NotificationRecipientService user_scope.where(id: user_ids_with_group_setting) end + # rubocop: enable CodeReuse/ActiveRecord def add_subscribed_users return unless target.respond_to? :subscribers @@ -166,6 +176,7 @@ module NotificationRecipientService add_recipients(target.subscribers(project), :subscription, nil) end + # rubocop: disable CodeReuse/ActiveRecord def user_ids_notifiable_on(resource, notification_level = nil) return [] unless resource @@ -177,6 +188,7 @@ module NotificationRecipientService scope.pluck(:user_id) end + # rubocop: enable CodeReuse/ActiveRecord # Build a list of user_ids based on project notification settings def select_project_members_ids(global_setting, user_ids_global_level_watch) @@ -194,14 +206,19 @@ module NotificationRecipientService uids + (global_setting & user_ids_global_level_watch) - project_members end + # rubocop: disable CodeReuse/ActiveRecord def user_ids_with_global_level_watch(ids) settings_with_global_level_of(:watch, ids).pluck(:user_id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def user_ids_with_global_level_custom(ids, action) settings_with_global_level_of(:custom, ids).pluck(:user_id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def settings_with_global_level_of(level, ids) NotificationSetting.where( user_id: ids, @@ -209,6 +226,7 @@ module NotificationRecipientService level: NotificationSetting.levels[level] ) end + # rubocop: enable CodeReuse/ActiveRecord def add_labels_subscribers(labels: nil) return unless target.respond_to? :labels diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 4511c500fca..50fa373025b 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -407,6 +407,12 @@ class NotificationService end end + def autodevops_disabled(pipeline, recipients) + recipients.each do |recipient| + mailer.autodevops_disabled_email(pipeline, recipient).deliver_later + end + end + def pages_domain_verification_succeeded(domain) recipients_for_pages_domain(domain).each do |user| mailer.pages_domain_verification_succeeded_email(domain, user).deliver_later diff --git a/app/services/preview_markdown_service.rb b/app/services/preview_markdown_service.rb index 11b996ed4b6..de8757006f1 100644 --- a/app/services/preview_markdown_service.rb +++ b/app/services/preview_markdown_service.rb @@ -43,6 +43,10 @@ class PreviewMarkdownService < BaseService end def markdown_engine - CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + if params[:legacy_render] + :redcarpet + else + CacheMarkdownField::MarkdownEngine.from_version(params[:markdown_version].to_i) + end end end diff --git a/app/services/projects/auto_devops/disable_service.rb b/app/services/projects/auto_devops/disable_service.rb new file mode 100644 index 00000000000..1b578a3c5ce --- /dev/null +++ b/app/services/projects/auto_devops/disable_service.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Projects + module AutoDevops + class DisableService < BaseService + def execute + return false unless implicitly_enabled_and_first_pipeline_failure? + + disable_auto_devops + end + + private + + def implicitly_enabled_and_first_pipeline_failure? + project.has_auto_devops_implicitly_enabled? && + first_pipeline_failure? + end + + # We're using `limit` to optimize `auto_devops pipeline` query, + # since we only care about the first element, and using only `.count` + # is an expensive operation. See + # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/21172#note_99037378 + # for more context. + # rubocop: disable CodeReuse/ActiveRecord + def first_pipeline_failure? + auto_devops_pipelines.success.limit(1).count.zero? && + auto_devops_pipelines.failed.limit(1).count.nonzero? + end + # rubocop: enable CodeReuse/ActiveRecord + + def disable_auto_devops + project.auto_devops_attributes = { enabled: false } + project.save! + end + + def auto_devops_pipelines + @auto_devops_pipelines ||= project.pipelines.auto_devops_source + end + end + end +end diff --git a/app/services/projects/base_move_relations_service.rb b/app/services/projects/base_move_relations_service.rb index 78cc2869b72..24dec1f3a45 100644 --- a/app/services/projects/base_move_relations_service.rb +++ b/app/services/projects/base_move_relations_service.rb @@ -13,6 +13,7 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord def prepare_relation(relation, id_param = :id) if Gitlab::Database.postgresql? relation @@ -20,5 +21,6 @@ module Projects relation.model.where("#{id_param}": relation.pluck(id_param)) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/batch_forks_count_service.rb b/app/services/projects/batch_forks_count_service.rb index 9bf369df999..6467744a435 100644 --- a/app/services/projects/batch_forks_count_service.rb +++ b/app/services/projects/batch_forks_count_service.rb @@ -5,6 +5,7 @@ # because the service use maps to retrieve the project ids module Projects class BatchForksCountService < Projects::BatchCountService + # rubocop: disable CodeReuse/ActiveRecord def global_count @global_count ||= begin count_service.query(project_ids) @@ -12,6 +13,7 @@ module Projects .count end end + # rubocop: enable CodeReuse/ActiveRecord def count_service ::Projects::ForksCountService diff --git a/app/services/projects/batch_open_issues_count_service.rb b/app/services/projects/batch_open_issues_count_service.rb index d375fcf9dbd..d6ff2291af8 100644 --- a/app/services/projects/batch_open_issues_count_service.rb +++ b/app/services/projects/batch_open_issues_count_service.rb @@ -5,11 +5,13 @@ # because the service use maps to retrieve the project ids module Projects class BatchOpenIssuesCountService < Projects::BatchCountService + # rubocop: disable CodeReuse/ActiveRecord def global_count @global_count ||= begin count_service.query(project_ids).group(:project_id).count end end + # rubocop: enable CodeReuse/ActiveRecord def count_service ::Projects::OpenIssuesCountService diff --git a/app/services/projects/container_repository/destroy_service.rb b/app/services/projects/container_repository/destroy_service.rb new file mode 100644 index 00000000000..1f5af7970d6 --- /dev/null +++ b/app/services/projects/container_repository/destroy_service.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Projects + module ContainerRepository + class DestroyService < BaseService + def execute(container_repository) + return false unless can?(current_user, :update_container_image, project) + + # Delete tags outside of the transaction to avoid hitting an idle-in-transaction timeout + container_repository.delete_tags! + container_repository.destroy + end + end + end +end diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 02a3a3eb096..0e6a7e8da54 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -79,17 +79,21 @@ module Projects @project.errors.add(:namespace, "is not valid") end + # rubocop: disable CodeReuse/ActiveRecord def allowed_fork?(source_project_id) return true if source_project_id.nil? source_project = Project.find_by(id: source_project_id) current_user.can?(:fork_project, source_project) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def allowed_namespace?(user, namespace_id) namespace = Namespace.find_by(id: namespace_id) current_user.can?(:create_projects, namespace) end + # rubocop: enable CodeReuse/ActiveRecord def after_create_actions log_info("#{@project.owner.name} created a new project \"#{@project.full_name}\"") @@ -167,12 +171,14 @@ module Projects @project end + # rubocop: disable CodeReuse/ActiveRecord def create_services_from_active_templates(project) Service.where(template: true, active: true).each do |template| service = Service.build_from_template(project.id, template) service.save! end end + # rubocop: enable CodeReuse/ActiveRecord def set_project_name_from_path # Set project name from path diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 76e22507698..210571b6b4e 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -107,15 +107,19 @@ module Projects mv_repository(old_path, new_path) end + # rubocop: disable CodeReuse/ActiveRecord def repo_exists?(path) gitlab_shell.exists?(project.repository_storage, path + '.git') end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def mv_repository(from_path, to_path) return true unless gitlab_shell.exists?(project.repository_storage, from_path + '.git') gitlab_shell.mv_repository(project.repository_storage, from_path, to_path) end + # rubocop: enable CodeReuse/ActiveRecord def attempt_rollback(project, message) return unless project @@ -129,11 +133,11 @@ module Projects end def attempt_destroy_transaction(project) - Project.transaction do - unless remove_legacy_registry_tags - raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') - end + unless remove_registry_tags + raise_error('Failed to remove some tags in project container registry. Please try again or contact administrator.') + end + Project.transaction do log_destroy_event trash_repositories! @@ -152,6 +156,17 @@ module Projects log_info("Attempting to destroy #{project.full_path} (#{project.id})") end + def remove_registry_tags + return false unless remove_legacy_registry_tags + + project.container_repositories.find_each do |container_repository| + service = Projects::ContainerRepository::DestroyService.new(project, current_user) + service.execute(container_repository) + end + + true + end + ## # This method makes sure that we correctly remove registry tags # for legacy image repository (when repository path equals project path). @@ -159,7 +174,7 @@ module Projects def remove_legacy_registry_tags return true unless Gitlab.config.registry.enabled - ContainerRepository.build_root_repository(project).tap do |repository| + ::ContainerRepository.build_root_repository(project).tap do |repository| break repository.has_tags? ? repository.delete_tags! : true end end diff --git a/app/services/projects/detect_repository_languages_service.rb b/app/services/projects/detect_repository_languages_service.rb index 3488b9ce47e..4a837a4fb6a 100644 --- a/app/services/projects/detect_repository_languages_service.rb +++ b/app/services/projects/detect_repository_languages_service.rb @@ -4,6 +4,7 @@ module Projects class DetectRepositoryLanguagesService < BaseService attr_reader :detected_repository_languages, :programming_languages + # rubocop: disable CodeReuse/ActiveRecord def execute repository_languages = project.repository_languages detection = Gitlab::LanguageDetection.new(repository, repository_languages) @@ -28,9 +29,11 @@ module Projects project.repository_languages.reload end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def ensure_programming_languages(detection) existing_languages = ProgrammingLanguage.where(name: detection.languages) return existing_languages if detection.languages.size == existing_languages.size @@ -42,7 +45,9 @@ module Projects existing_languages + created_languages end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def create_language(name, color) ProgrammingLanguage.transaction do ProgrammingLanguage.where(name: name).first_or_create(color: color) @@ -50,5 +55,6 @@ module Projects rescue ActiveRecord::RecordNotUnique retry end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/enable_deploy_key_service.rb b/app/services/projects/enable_deploy_key_service.rb index b7c172028e9..102088e9557 100644 --- a/app/services/projects/enable_deploy_key_service.rb +++ b/app/services/projects/enable_deploy_key_service.rb @@ -2,6 +2,7 @@ module Projects class EnableDeployKeyService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute key = accessible_keys.find_by(id: params[:key_id] || params[:id]) return unless key @@ -12,6 +13,7 @@ module Projects key end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/services/projects/forks_count_service.rb b/app/services/projects/forks_count_service.rb index b570c6d4754..00e73148358 100644 --- a/app/services/projects/forks_count_service.rb +++ b/app/services/projects/forks_count_service.rb @@ -7,11 +7,13 @@ module Projects 'forks_count' end + # rubocop: disable CodeReuse/ActiveRecord def self.query(project_ids) # We can't directly change ForkedProjectLink to ForkNetworkMember here # Nowadays, when a call using v3 to projects/:id/fork is made, # the relationship to ForkNetworkMember is not updated ForkedProjectLink.where(forked_from_project: project_ids) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb index 044afa1d5e1..a315adf42f0 100644 --- a/app/services/projects/gitlab_projects_import_service.rb +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -32,11 +32,13 @@ module Projects Project.find_by_full_path("#{current_namespace.full_path}/#{params[:path]}").present? end + # rubocop: disable CodeReuse/ActiveRecord def current_namespace strong_memoize(:current_namespace) do Namespace.find_by(id: params[:namespace_id]) end end + # rubocop: enable CodeReuse/ActiveRecord def overwrite? strong_memoize(:overwrite) do diff --git a/app/services/projects/hashed_storage/migrate_repository_service.rb b/app/services/projects/hashed_storage/migrate_repository_service.rb index 641d46e6591..4462d504071 100644 --- a/app/services/projects/hashed_storage/migrate_repository_service.rb +++ b/app/services/projects/hashed_storage/migrate_repository_service.rb @@ -47,10 +47,13 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord def has_wiki? gitlab_shell.exists?(project.repository_storage, "#{old_wiki_disk_path}.git") end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def move_repository(from_name, to_name) from_exists = gitlab_shell.exists?(project.repository_storage, "#{from_name}.git") to_exists = gitlab_shell.exists?(project.repository_storage, "#{to_name}.git") @@ -67,6 +70,7 @@ module Projects gitlab_shell.mv_repository(project.repository_storage, from_name, to_name) end + # rubocop: enable CodeReuse/ActiveRecord def rollback_folder_move move_repository(new_disk_path, old_disk_path) diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb index 7d4fa4e08df..1c4a8d05be6 100644 --- a/app/services/projects/lfs_pointers/lfs_download_service.rb +++ b/app/services/projects/lfs_pointers/lfs_download_service.rb @@ -4,6 +4,7 @@ module Projects module LfsPointers class LfsDownloadService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(oid, url) return unless project&.lfs_enabled? && oid.present? && url.present? @@ -20,6 +21,7 @@ module Projects rescue StandardError => e Rails.logger.error("LFS file with oid #{oid} could't be downloaded from #{sanitized_uri.sanitized_url}: #{e.message}") end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb index 97ce681a911..9215fa0a7bf 100644 --- a/app/services/projects/lfs_pointers/lfs_import_service.rb +++ b/app/services/projects/lfs_pointers/lfs_import_service.rb @@ -41,6 +41,7 @@ module Projects project.update(lfs_enabled: false) end + # rubocop: disable CodeReuse/ActiveRecord def get_download_links existent_lfs = LfsListService.new(project).execute linked_oids = LfsLinkService.new(project).execute(existent_lfs.keys) @@ -50,6 +51,7 @@ module Projects LfsDownloadLinkListService.new(project, remote_uri: current_endpoint_uri).execute(not_linked_lfs) end + # rubocop: enable CodeReuse/ActiveRecord def lfsconfig_endpoint_uri strong_memoize(:lfsconfig_endpoint_uri) do diff --git a/app/services/projects/lfs_pointers/lfs_link_service.rb b/app/services/projects/lfs_pointers/lfs_link_service.rb index a2eba8e124e..8401f3d1d89 100644 --- a/app/services/projects/lfs_pointers/lfs_link_service.rb +++ b/app/services/projects/lfs_pointers/lfs_link_service.rb @@ -16,6 +16,7 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord def link_existing_lfs_objects(oids) existent_lfs_objects = LfsObject.where(oid: oids) @@ -26,6 +27,7 @@ module Projects existent_lfs_objects.pluck(:oid) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb index 9f3f44f30ea..b6a3af8c7b8 100644 --- a/app/services/projects/move_deploy_keys_projects_service.rb +++ b/app/services/projects/move_deploy_keys_projects_service.rb @@ -20,11 +20,13 @@ module Projects .update_all(project_id: @project.id) end + # rubocop: disable CodeReuse/ActiveRecord def non_existent_deploy_keys_projects source_project.deploy_keys_projects .joins(:deploy_key) .where.not(keys: { fingerprint: @project.deploy_keys.select(:fingerprint) }) end + # rubocop: enable CodeReuse/ActiveRecord def remove_remaining_deploy_keys_projects source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll diff --git a/app/services/projects/move_forks_service.rb b/app/services/projects/move_forks_service.rb index 076a7a50aa9..2948555a17c 100644 --- a/app/services/projects/move_forks_service.rb +++ b/app/services/projects/move_forks_service.rb @@ -17,6 +17,7 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord def move_forked_project_links # Update ancestor ForkedProjectLink.where(forked_to_project: source_project) @@ -26,16 +27,21 @@ module Projects ForkedProjectLink.where(forked_from_project: source_project) .update_all(forked_from_project_id: @project.id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def move_fork_network_members ForkNetworkMember.where(project: source_project).update_all(project_id: @project.id) ForkNetworkMember.where(forked_from_project: source_project).update_all(forked_from_project_id: @project.id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_root_project # Update root network project ForkNetwork.where(root_project: source_project).update_all(root_project_id: @project.id) end + # rubocop: enable CodeReuse/ActiveRecord def refresh_forks_count Projects::ForksCountService.new(@project).refresh_cache diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb index f78546a1e9c..308a54ad06e 100644 --- a/app/services/projects/move_lfs_objects_projects_service.rb +++ b/app/services/projects/move_lfs_objects_projects_service.rb @@ -24,8 +24,10 @@ module Projects source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll end + # rubocop: disable CodeReuse/ActiveRecord def non_existent_lfs_objects_projects source_project.lfs_objects_projects.where.not(lfs_object: @project.lfs_objects) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb index 109a00dd6d9..e740c44bd26 100644 --- a/app/services/projects/move_notification_settings_service.rb +++ b/app/services/projects/move_notification_settings_service.rb @@ -31,10 +31,12 @@ module Projects end # Look for notification_settings in source_project that are not in the target project + # rubocop: disable CodeReuse/ActiveRecord def non_existent_notifications source_project.notification_settings .select(:id) .where.not(user_id: users_in_target_project) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/move_project_authorizations_service.rb b/app/services/projects/move_project_authorizations_service.rb index 60f2af88e99..2060a263751 100644 --- a/app/services/projects/move_project_authorizations_service.rb +++ b/app/services/projects/move_project_authorizations_service.rb @@ -33,10 +33,12 @@ module Projects end # Look for authorizations in source_project that are not in the target project + # rubocop: disable CodeReuse/ActiveRecord def non_existent_authorization source_project.project_authorizations .select(:user_id) .where.not(user: @project.authorized_users) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb index 1efafdce36d..fb395ecb9a1 100644 --- a/app/services/projects/move_project_group_links_service.rb +++ b/app/services/projects/move_project_group_links_service.rb @@ -34,9 +34,11 @@ module Projects end # Look for groups in source_project that are not in the target project + # rubocop: disable CodeReuse/ActiveRecord def non_existent_group_links source_project.project_group_links .where.not(group_id: group_links_in_target_project) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb index ec983582d94..f28f44adc03 100644 --- a/app/services/projects/move_project_members_service.rb +++ b/app/services/projects/move_project_members_service.rb @@ -33,10 +33,12 @@ module Projects end # Look for members in source_project that are not in the target project + # rubocop: disable CodeReuse/ActiveRecord def non_existent_members source_project.members .select(:id) .where.not(user_id: @project.project_members.select(:user_id)) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/open_issues_count_service.rb b/app/services/projects/open_issues_count_service.rb index 5d6620c3c54..ee9884e9042 100644 --- a/app/services/projects/open_issues_count_service.rb +++ b/app/services/projects/open_issues_count_service.rb @@ -42,6 +42,7 @@ module Projects cache_key(TOTAL_COUNT_KEY) end + # rubocop: disable CodeReuse/ActiveRecord def refresh_cache(&block) if block_given? super(&block) @@ -59,11 +60,13 @@ module Projects end end end + # rubocop: enable CodeReuse/ActiveRecord # We only show total issues count for reporters # which are allowed to view confidential issues # This will still show a discrepancy on issues number but should be less than before. # Check https://gitlab.com/gitlab-org/gitlab-ce/issues/38418 description. + # rubocop: disable CodeReuse/ActiveRecord def self.query(projects, public_only: true) if public_only Issue.opened.public_only.where(project: projects) @@ -71,5 +74,6 @@ module Projects Issue.opened.where(project: projects) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/projects/propagate_service_template.rb b/app/services/projects/propagate_service_template.rb index fdfa91801ab..633a263af7b 100644 --- a/app/services/projects/propagate_service_template.rb +++ b/app/services/projects/propagate_service_template.rb @@ -70,6 +70,7 @@ module Projects ) end + # rubocop: disable CodeReuse/ActiveRecord def service_hash @service_hash ||= begin @@ -83,7 +84,9 @@ module Projects end end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def run_callbacks(batch) if active_external_issue_tracker? Project.where(id: batch).update_all(has_external_issue_tracker: true) @@ -93,6 +96,7 @@ module Projects Project.where(id: batch).update_all(has_external_wiki: true) end end + # rubocop: enable CodeReuse/ActiveRecord def active_external_issue_tracker? @template.issue_tracker? && !@template.default diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb index 3746cfef702..9d40ab166ff 100644 --- a/app/services/projects/transfer_service.rb +++ b/app/services/projects/transfer_service.rb @@ -37,6 +37,7 @@ module Projects private + # rubocop: disable CodeReuse/ActiveRecord def transfer(project) @old_path = project.full_path @old_group = project.group @@ -54,6 +55,7 @@ module Projects attempt_transfer_transaction end + # rubocop: enable CodeReuse/ActiveRecord def attempt_transfer_transaction Project.transaction do diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index 2c0d91fe34f..a8b7c7f136a 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -2,6 +2,7 @@ module Projects class UnlinkForkService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute return unless @project.forked? @@ -26,6 +27,7 @@ module Projects @project.fork_network_member.destroy @project.forked_project_link.destroy end + # rubocop: enable CodeReuse/ActiveRecord def refresh_forks_count(project) Projects::ForksCountService.new(project).refresh_cache diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 591b38b8151..9d0877d1ab2 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -5,10 +5,10 @@ module Projects attr_reader :errors def execute(remote_mirror) - @errors = [] - return success unless remote_mirror.enabled? + errors = [] + begin remote_mirror.ensure_remote! repository.fetch_remote(remote_mirror.remote_name, no_tags: true) diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index e390d7a04c3..d6d9bacf232 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -6,6 +6,7 @@ module Projects ValidationError = Class.new(StandardError) + # rubocop: disable CodeReuse/ActiveRecord def execute validate! @@ -26,6 +27,7 @@ module Projects rescue ValidationError => e error(e.message) end + # rubocop: enable CodeReuse/ActiveRecord def run_auto_devops_pipeline? return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled') diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index a4c4c9e4812..02d68c3add3 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -111,10 +111,12 @@ module QuickActions end desc 'Assign' + # rubocop: disable CodeReuse/ActiveRecord explanation do |users| users = issuable.allows_multiple_assignees? ? users : users.take(1) "Assigns #{users.map(&:to_reference).to_sentence}." end + # rubocop: enable CodeReuse/ActiveRecord params do issuable.allows_multiple_assignees? ? '@user1 @user2' : '@user' end @@ -124,6 +126,7 @@ module QuickActions parse_params do |assignee_param| extract_users(assignee_param) end + # rubocop: disable CodeReuse/ActiveRecord command :assign do |users| next if users.empty? @@ -134,6 +137,7 @@ module QuickActions [users.first.id] end end + # rubocop: enable CodeReuse/ActiveRecord desc do if issuable.allows_multiple_assignees? @@ -160,6 +164,7 @@ module QuickActions # When multiple users are assigned, all will be unassigned if multiple assignees are no longer allowed extract_users(unassign_param) if issuable.allows_multiple_assignees? end + # rubocop: disable CodeReuse/ActiveRecord command :unassign do |users = nil| @updates[:assignee_ids] = if users&.any? @@ -168,6 +173,7 @@ module QuickActions [] end end + # rubocop: enable CodeReuse/ActiveRecord desc 'Set milestone' explanation do |milestone| @@ -489,6 +495,30 @@ module QuickActions "#{comment} #{TABLEFLIP}" end + desc "Lock the discussion" + explanation "Locks the discussion" + condition do + issuable.is_a?(Issuable) && + issuable.persisted? && + !issuable.discussion_locked? && + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :lock do + @updates[:discussion_locked] = true + end + + desc "Unlock the discussion" + explanation "Unlocks the discussion" + condition do + issuable.is_a?(Issuable) && + issuable.persisted? && + issuable.discussion_locked? && + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + end + command :unlock do + @updates[:discussion_locked] = false + end + # This is a dummy command, so that it appears in the autocomplete commands desc 'CC' params '@user' @@ -522,6 +552,7 @@ module QuickActions current_user.can?(:"update_#{issuable.to_ability_name}", issuable) && issuable.project.boards.count == 1 end + # rubocop: disable CodeReuse/ActiveRecord command :board_move do |target_list_name| label_ids = find_label_ids(target_list_name) @@ -536,6 +567,7 @@ module QuickActions @updates[:add_label_ids] = [label_id] end end + # rubocop: enable CodeReuse/ActiveRecord desc 'Mark this issue as a duplicate of another issue' explanation do |duplicate_reference| @@ -601,6 +633,7 @@ module QuickActions @updates[:tag_message] = message end + # rubocop: disable CodeReuse/ActiveRecord def extract_users(params) return [] if params.nil? @@ -617,6 +650,7 @@ module QuickActions users end + # rubocop: enable CodeReuse/ActiveRecord def find_milestones(project, params = {}) MilestonesFinder.new(params.merge(project_ids: [project.id], group_ids: [project.group&.id])).execute @@ -653,6 +687,7 @@ module QuickActions end end + # rubocop: disable CodeReuse/ActiveRecord def extract_references(arg, type) ext = Gitlab::ReferenceExtractor.new(project, current_user) @@ -660,5 +695,6 @@ module QuickActions ext.references(type) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/quick_actions/target_service.rb b/app/services/quick_actions/target_service.rb index d8ba52c6e50..69464c3c1ae 100644 --- a/app/services/quick_actions/target_service.rb +++ b/app/services/quick_actions/target_service.rb @@ -15,13 +15,17 @@ module QuickActions private + # rubocop: disable CodeReuse/ActiveRecord def issue(type_id) IssuesFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.issues.build end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def merge_request(type_id) MergeRequestsFinder.new(current_user, project_id: project.id).find_by(iid: type_id) || project.merge_requests.build end + # rubocop: enable CodeReuse/ActiveRecord def commit(type_id) project.commit(type_id) diff --git a/app/services/resource_events/change_labels_service.rb b/app/services/resource_events/change_labels_service.rb index 8edb0ddb3ed..039d6e2ebad 100644 --- a/app/services/resource_events/change_labels_service.rb +++ b/app/services/resource_events/change_labels_service.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -# This service is not used yet, it will be used for: -# https://gitlab.com/gitlab-org/gitlab-ce/issues/48483 module ResourceEvents class ChangeLabelsService attr_reader :resource, :user @@ -25,6 +23,7 @@ module ResourceEvents end Gitlab::Database.bulk_insert(ResourceLabelEvent.table_name, labels) + resource.expire_note_etag_cache end private diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb new file mode 100644 index 00000000000..596c0105ea0 --- /dev/null +++ b/app/services/resource_events/merge_into_notes_service.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +# We store events about issuable label changes in a separate table (not as +# other system notes), but we still want to display notes about label changes +# as classic system notes in UI. This service generates "synthetic" notes for +# label event changes and merges them with classic notes and sorts them by +# creation time. + +module ResourceEvents + class MergeIntoNotesService + include Gitlab::Utils::StrongMemoize + + attr_reader :resource, :current_user, :params + + def initialize(resource, current_user, params = {}) + @resource = resource + @current_user = current_user + @params = params + end + + def execute(notes = []) + (notes + label_notes).sort_by { |n| n.created_at } + end + + private + + def label_notes + label_events_by_discussion_id.map do |discussion_id, events| + LabelNote.from_events(events, resource: resource, resource_parent: resource_parent) + end + end + + # rubocop: disable CodeReuse/ActiveRecord + def label_events_by_discussion_id + return [] unless resource.respond_to?(:resource_label_events) + + events = resource.resource_label_events.includes(:label, :user) + events = since_fetch_at(events) + + events.group_by { |event| event.discussion_id } + end + # rubocop: enable CodeReuse/ActiveRecord + + def since_fetch_at(events) + return events unless params[:last_fetched_at].present? + + last_fetched_at = Time.at(params.fetch(:last_fetched_at).to_i) + events.created_after(last_fetched_at - NotesFinder::FETCH_OVERLAP) + end + + def resource_parent + strong_memoize(:resource_parent) do + resource.project || resource.group + end + end + end +end diff --git a/app/services/search/group_service.rb b/app/services/search/group_service.rb index 34803d005e3..00372887985 100644 --- a/app/services/search/group_service.rb +++ b/app/services/search/group_service.rb @@ -11,11 +11,13 @@ module Search @group = group end + # rubocop: disable CodeReuse/ActiveRecord def projects return Project.none unless group return @projects if defined? @projects @projects = super.inside_path(group.full_path) end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/services/search_service.rb b/app/services/search_service.rb index 1b707d79b43..e0cbfac2420 100644 --- a/app/services/search_service.rb +++ b/app/services/search_service.rb @@ -8,6 +8,7 @@ class SearchService @params = params.dup end + # rubocop: disable CodeReuse/ActiveRecord def project return @project if defined?(@project) @@ -19,7 +20,9 @@ class SearchService nil end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def group return @group if defined?(@group) @@ -31,6 +34,7 @@ class SearchService nil end end + # rubocop: enable CodeReuse/ActiveRecord def show_snippets? return @show_snippets if defined?(@show_snippets) diff --git a/app/services/spam_check_service.rb b/app/services/spam_check_service.rb index 895261925ba..51d300d4f1d 100644 --- a/app/services/spam_check_service.rb +++ b/app/services/spam_check_service.rb @@ -22,6 +22,7 @@ module SpamCheckService # a dirty instance, which means it should be already assigned with the new # attribute values. # rubocop:disable Gitlab/ModuleWithInstanceVariables + # rubocop: disable CodeReuse/ActiveRecord def spam_check(spammable, user) spam_service = SpamService.new(spammable, @request) @@ -29,5 +30,6 @@ module SpamCheckService user.spam_logs.find_by(id: @spam_log_id)&.update!(recaptcha_verified: true) end end + # rubocop: enable CodeReuse/ActiveRecord # rubocop:enable Gitlab/ModuleWithInstanceVariables end diff --git a/app/services/submit_usage_ping_service.rb b/app/services/submit_usage_ping_service.rb index 93c2e222963..62222d3fd2a 100644 --- a/app/services/submit_usage_ping_service.rb +++ b/app/services/submit_usage_ping_service.rb @@ -15,6 +15,7 @@ class SubmitUsagePingService def execute return false unless Gitlab::CurrentSettings.usage_ping_enabled? + return false if User.single_user&.requires_usage_stats_consent? response = Gitlab::HTTP.post( URL, diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index dda89830179..575678da1fa 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -98,66 +98,45 @@ module SystemNoteService create_note(NoteSummary.new(issue, project, author, body, action: 'assignee')) end - # Called when one or more labels on a Noteable are added and/or removed + # Called when the milestone of a Noteable is changed # - # noteable - Noteable object - # project - Project owning noteable - # author - User performing the change - # added_labels - Array of Labels added - # removed_labels - Array of Labels removed + # noteable - Noteable object + # project - Project owning noteable + # author - User performing the change + # milestone - Milestone being assigned, or nil # # Example Note text: # - # "added ~1 and removed ~2 ~3 labels" - # - # "added ~4 label" + # "removed milestone" # - # "removed ~5 label" + # "changed milestone to 7.11" # # Returns the created Note object - def change_label(noteable, project, author, added_labels, removed_labels) - labels_count = added_labels.count + removed_labels.count - - references = ->(label) { label.to_reference(format: :id) } - added_labels = added_labels.map(&references).join(' ') - removed_labels = removed_labels.map(&references).join(' ') - - text_parts = [] - - if added_labels.present? - text_parts << "added #{added_labels}" - text_parts << 'and' if removed_labels.present? - end - - if removed_labels.present? - text_parts << "removed #{removed_labels}" - end - - text_parts << 'label'.pluralize(labels_count) - body = text_parts.join(' ') + def change_milestone(noteable, project, author, milestone) + format = milestone&.group_milestone? ? :name : :iid + body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}" - create_note(NoteSummary.new(noteable, project, author, body, action: 'label')) + create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone')) end - # Called when the milestone of a Noteable is changed + # Called when the due_date of a Noteable is changed # # noteable - Noteable object # project - Project owning noteable # author - User performing the change - # milestone - Milestone being assigned, or nil + # due_date - Due date being assigned, or nil # # Example Note text: # - # "removed milestone" + # "removed due date" # - # "changed milestone to 7.11" + # "changed due date to September 20, 2018" # # Returns the created Note object - def change_milestone(noteable, project, author, milestone) - format = milestone&.group_milestone? ? :name : :iid - body = milestone.nil? ? 'removed milestone' : "changed milestone to #{milestone.to_reference(project, format: format)}" + def change_due_date(noteable, project, author, due_date) + body = due_date ? "changed due date to #{due_date.to_s(:long)}" : 'removed due date' - create_note(NoteSummary.new(noteable, project, author, body, action: 'milestone')) + create_note(NoteSummary.new(noteable, project, author, body, action: 'due_date')) end # Called when the estimated time of a Noteable is changed @@ -601,6 +580,7 @@ module SystemNoteService private + # rubocop: disable CodeReuse/ActiveRecord def notes_for_mentioner(mentioner, noteable, notes) if mentioner.is_a?(Commit) text = "#{cross_reference_note_prefix}%#{mentioner.to_reference(nil)}" @@ -611,6 +591,7 @@ module SystemNoteService notes.where(note: [text, text.capitalize]) end end + # rubocop: enable CodeReuse/ActiveRecord def create_note(note_summary) note = Note.create(note_summary.note.merge(system: true)) diff --git a/app/services/tags/destroy_service.rb b/app/services/tags/destroy_service.rb index 800268485a4..6bfef09ac54 100644 --- a/app/services/tags/destroy_service.rb +++ b/app/services/tags/destroy_service.rb @@ -2,6 +2,7 @@ module Tags class DestroyService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(tag_name) repository = project.repository tag = repository.find_tag(tag_name) @@ -26,6 +27,7 @@ module Tags rescue Gitlab::Git::PreReceiveError => ex error(ex.message) end + # rubocop: enable CodeReuse/ActiveRecord def error(message, return_code = 400) super(message).merge(return_code: return_code) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 0df61ad3bce..4fe6c1ec986 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -41,6 +41,7 @@ class TodoService # collects the todo users before the todos themselves are deleted, then # updates the todo counts for those users. # + # rubocop: disable CodeReuse/ActiveRecord def destroy_target(target) todo_users = User.where(id: target.todos.pending.select(:user_id)).to_a @@ -48,6 +49,7 @@ class TodoService todo_users.each(&:update_todos_count_cache) end + # rubocop: enable CodeReuse/ActiveRecord # When we reassign an issue we should: # @@ -198,16 +200,21 @@ class TodoService create_todos(current_user, attributes) end + # rubocop: disable CodeReuse/ActiveRecord def todo_exist?(issuable, current_user) TodosFinder.new(current_user).execute.exists?(target: issuable) end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def todos_by_ids(ids, current_user) current_user.todos.where(id: Array(ids)) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def update_todos_state(todos, current_user, state) # Only update those that are not really on that state todos = todos.where.not(state: state) @@ -216,6 +223,7 @@ class TodoService current_user.update_todos_count_cache todos_ids end + # rubocop: enable CodeReuse/ActiveRecord def create_todos(users, attributes) Array(users).map do |user| @@ -340,8 +348,10 @@ class TodoService end end + # rubocop: disable CodeReuse/ActiveRecord def pending_todos(user, criteria = {}) valid_keys = [:project_id, :target_id, :target_type, :commit_id] user.todos.pending.where(criteria.slice(*valid_keys)) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/services/todos/destroy/base_service.rb b/app/services/todos/destroy/base_service.rb index aeb60e50c64..f3f1dbb5698 100644 --- a/app/services/todos/destroy/base_service.rb +++ b/app/services/todos/destroy/base_service.rb @@ -11,13 +11,17 @@ module Todos private + # rubocop: disable CodeReuse/ActiveRecord def without_authorized(items) items.where('user_id NOT IN (?)', authorized_users) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def authorized_users ProjectAuthorization.select(:user_id).where(project_id: project_ids) end + # rubocop: enable CodeReuse/ActiveRecord def todos raise NotImplementedError diff --git a/app/services/todos/destroy/confidential_issue_service.rb b/app/services/todos/destroy/confidential_issue_service.rb index efec0f22da5..6276e332448 100644 --- a/app/services/todos/destroy/confidential_issue_service.rb +++ b/app/services/todos/destroy/confidential_issue_service.rb @@ -7,18 +7,22 @@ module Todos attr_reader :issue + # rubocop: disable CodeReuse/ActiveRecord def initialize(issue_id) @issue = Issue.find_by(id: issue_id) end + # rubocop: enable CodeReuse/ActiveRecord private override :todos + # rubocop: disable CodeReuse/ActiveRecord def todos Todo.where(target: issue) .where('user_id != ?', issue.author_id) .where('user_id NOT IN (?)', issue.assignees.select(:id)) end + # rubocop: enable CodeReuse/ActiveRecord override :todos_to_remove? def todos_to_remove? @@ -31,11 +35,13 @@ module Todos end override :authorized_users + # rubocop: disable CodeReuse/ActiveRecord def authorized_users ProjectAuthorization.select(:user_id) .where(project_id: project_ids) .where('access_level >= ?', Gitlab::Access::REPORTER) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/todos/destroy/entity_leave_service.rb b/app/services/todos/destroy/entity_leave_service.rb index 4cb9d08713d..e8d1bcdd142 100644 --- a/app/services/todos/destroy/entity_leave_service.rb +++ b/app/services/todos/destroy/entity_leave_service.rb @@ -7,6 +7,7 @@ module Todos attr_reader :user, :entity + # rubocop: disable CodeReuse/ActiveRecord def initialize(user_id, entity_id, entity_type) unless %w(Group Project).include?(entity_type) raise ArgumentError.new("#{entity_type} is not an entity user can leave") @@ -15,6 +16,7 @@ module Todos @user = User.find_by(id: user_id) @entity = entity_type.constantize.find_by(id: entity_id) end + # rubocop: enable CodeReuse/ActiveRecord def execute return unless entity && user @@ -40,21 +42,28 @@ module Todos end end + # rubocop: disable CodeReuse/ActiveRecord def remove_confidential_issue_todos Todo.where( target_id: confidential_issues.select(:id), target_type: Issue, user_id: user.id ).delete_all end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def remove_project_todos Todo.where(project_id: non_authorized_projects, user_id: user.id).delete_all end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def remove_group_todos Todo.where(group_id: non_authorized_groups, user_id: user.id).delete_all end + # rubocop: enable CodeReuse/ActiveRecord override :project_ids + # rubocop: disable CodeReuse/ActiveRecord def project_ids condition = case entity when Project @@ -65,22 +74,29 @@ module Todos Project.where(condition).select(:id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def non_authorized_projects project_ids.where('id NOT IN (?)', user.authorized_projects.select(:id)) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def non_authorized_groups return [] unless entity.is_a?(Namespace) entity.self_and_descendants.select(:id) .where('id NOT IN (?)', GroupsFinder.new(user).execute.select(:id)) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def non_member_groups entity.self_and_descendants.select(:id) .where('id NOT IN (?)', user.membership_groups.select(:id)) end + # rubocop: enable CodeReuse/ActiveRecord def user_has_reporter_access? return unless entity.is_a?(Namespace) @@ -88,6 +104,7 @@ module Todos entity.member?(User.find(user.id), Gitlab::Access::REPORTER) end + # rubocop: disable CodeReuse/ActiveRecord def confidential_issues assigned_ids = IssueAssignee.select(:issue_id).where(user_id: user.id) authorized_reporter_projects = user @@ -98,6 +115,7 @@ module Todos .where('author_id != ?', user.id) .where('id NOT IN (?)', assigned_ids) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/todos/destroy/group_private_service.rb b/app/services/todos/destroy/group_private_service.rb index f67f1d40597..d7ecbb952aa 100644 --- a/app/services/todos/destroy/group_private_service.rb +++ b/app/services/todos/destroy/group_private_service.rb @@ -7,16 +7,20 @@ module Todos attr_reader :group + # rubocop: disable CodeReuse/ActiveRecord def initialize(group_id) @group = Group.find_by(id: group_id) end + # rubocop: enable CodeReuse/ActiveRecord private override :todos + # rubocop: disable CodeReuse/ActiveRecord def todos Todo.where(group_id: group.id) end + # rubocop: enable CodeReuse/ActiveRecord override :authorized_users def authorized_users diff --git a/app/services/todos/destroy/private_features_service.rb b/app/services/todos/destroy/private_features_service.rb index 7e204885b31..a8c3fe0ef5a 100644 --- a/app/services/todos/destroy/private_features_service.rb +++ b/app/services/todos/destroy/private_features_service.rb @@ -10,6 +10,7 @@ module Todos @user_id = user_id end + # rubocop: disable CodeReuse/ActiveRecord def execute ProjectFeature.where(project_id: project_ids).each do |project_features| target_types = [] @@ -22,6 +23,7 @@ module Todos remove_todos(project_features.project_id, target_types) end end + # rubocop: enable CodeReuse/ActiveRecord private @@ -29,6 +31,7 @@ module Todos feature_level == ProjectFeature::PRIVATE end + # rubocop: disable CodeReuse/ActiveRecord def remove_todos(project_id, target_types) items = Todo.where(project_id: project_id) items = items.where(user_id: user_id) if user_id @@ -37,6 +40,7 @@ module Todos .where(target_type: target_types) .delete_all end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/services/todos/destroy/project_private_service.rb b/app/services/todos/destroy/project_private_service.rb index ae8fab3ffca..e00d10c3780 100644 --- a/app/services/todos/destroy/project_private_service.rb +++ b/app/services/todos/destroy/project_private_service.rb @@ -7,16 +7,20 @@ module Todos attr_reader :project + # rubocop: disable CodeReuse/ActiveRecord def initialize(project_id) @project = Project.find_by(id: project_id) end + # rubocop: enable CodeReuse/ActiveRecord private override :todos + # rubocop: disable CodeReuse/ActiveRecord def todos Todo.where(project_id: project.id) end + # rubocop: enable CodeReuse/ActiveRecord override :project_ids def project_ids diff --git a/app/services/update_release_service.rb b/app/services/update_release_service.rb index 422ba668e35..e2228ca026c 100644 --- a/app/services/update_release_service.rb +++ b/app/services/update_release_service.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true class UpdateReleaseService < BaseService + # rubocop: disable CodeReuse/ActiveRecord def execute(tag_name, release_description) repository = project.repository existing_tag = repository.find_tag(tag_name) @@ -19,6 +20,7 @@ class UpdateReleaseService < BaseService error('Tag does not exist', 404) end end + # rubocop: enable CodeReuse/ActiveRecord def success(release) super().merge(release: release) diff --git a/app/services/users/last_push_event_service.rb b/app/services/users/last_push_event_service.rb index a9c9497520b..b3980b8e32c 100644 --- a/app/services/users/last_push_event_service.rb +++ b/app/services/users/last_push_event_service.rb @@ -58,11 +58,13 @@ module Users private + # rubocop: disable CodeReuse/ActiveRecord def find_event_in_database(id) PushEvent .without_existing_merge_requests .find_by(id: id) end + # rubocop: enable CodeReuse/ActiveRecord def user_cache_key "last-push-event/#{@user.id}" diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb index 4d47078bf43..04fd6e37501 100644 --- a/app/services/users/migrate_to_ghost_user_service.rb +++ b/app/services/users/migrate_to_ghost_user_service.rb @@ -54,15 +54,19 @@ module Users migrate_award_emoji end + # rubocop: disable CodeReuse/ActiveRecord def migrate_issues user.issues.update_all(author_id: ghost_user.id) Issue.where(last_edited_by_id: user.id).update_all(last_edited_by_id: ghost_user.id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def migrate_merge_requests user.merge_requests.update_all(author_id: ghost_user.id) MergeRequest.where(merge_user_id: user.id).update_all(merge_user_id: ghost_user.id) end + # rubocop: enable CodeReuse/ActiveRecord def migrate_notes user.notes.update_all(author_id: ghost_user.id) diff --git a/app/services/users/respond_to_terms_service.rb b/app/services/users/respond_to_terms_service.rb index 9efa3b285a8..254480304f9 100644 --- a/app/services/users/respond_to_terms_service.rb +++ b/app/services/users/respond_to_terms_service.rb @@ -6,6 +6,7 @@ module Users @user, @term = user, term end + # rubocop: disable CodeReuse/ActiveRecord def execute(accepted:) agreement = @user.term_agreements.find_or_initialize_by(term: @term) agreement.accepted = accepted @@ -16,6 +17,7 @@ module Users agreement end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb index 30fe0e371a6..df31ad7c8ea 100644 --- a/app/services/wikis/create_attachment_service.rb +++ b/app/services/wikis/create_attachment_service.rb @@ -11,7 +11,7 @@ module Wikis def initialize(*args) super - @file_name = truncate_file_name(params[:file_name]) + @file_name = clean_file_name(params[:file_name]) @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name @commit_message ||= "Upload attachment #{@file_name}" @branch_name ||= wiki.default_branch @@ -23,8 +23,16 @@ module Wikis private - def truncate_file_name(file_name) + def clean_file_name(file_name) return unless file_name.present? + + file_name = truncate_file_name(file_name) + # CommonMark does not allow Urls with whitespaces, so we have to replace them + # Using the same regex Carrierwave use to replace invalid characters + file_name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, '_') + end + + def truncate_file_name(file_name) return file_name if file_name.length <= MAX_FILENAME_LENGTH extension = File.extname(file_name) diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb index b29ef57b071..c0165759203 100644 --- a/app/uploaders/avatar_uploader.rb +++ b/app/uploaders/avatar_uploader.rb @@ -18,6 +18,10 @@ class AvatarUploader < GitlabUploader false end + def absolute_path + self.class.absolute_path(model.avatar.upload) + end + private def dynamic_segment diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb index 52969762b7d..4965bd7f057 100644 --- a/app/uploaders/namespace_file_uploader.rb +++ b/app/uploaders/namespace_file_uploader.rb @@ -6,23 +6,27 @@ class NamespaceFileUploader < FileUploader options.storage_path end - def self.base_dir(model, _store = nil) - File.join(options.base_dir, 'namespace', model_path_segment(model)) + def self.base_dir(model, store = nil) + base_dirs(model)[store || Store::LOCAL] + end + + def self.base_dirs(model) + { + Store::LOCAL => File.join(options.base_dir, 'namespace', model_path_segment(model)), + Store::REMOTE => File.join('namespace', model_path_segment(model)) + } end def self.model_path_segment(model) File.join(model.id.to_s) end + def self.workhorse_local_upload_path + File.join(options.storage_path, 'uploads', TMP_UPLOAD_PATH) + end + # Re-Override def store_dir store_dirs[object_store] end - - def store_dirs - { - Store::LOCAL => File.join(base_dir, dynamic_segment), - Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment) - } - end end diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb index 5795065ae11..0efca895a50 100644 --- a/app/uploaders/records_uploads.rb +++ b/app/uploaders/records_uploads.rb @@ -18,6 +18,7 @@ module RecordsUploads # `Tempfile` object the callback gets. # # Called `after :store` + # rubocop: disable CodeReuse/ActiveRecord def record_upload(_tempfile = nil) return unless model return unless file && file.exists? @@ -29,6 +30,7 @@ module RecordsUploads self.upload = build_upload.tap(&:save!) end end + # rubocop: enable CodeReuse/ActiveRecord def upload_path File.join(store_dir, filename.to_s) @@ -36,9 +38,11 @@ module RecordsUploads private + # rubocop: disable CodeReuse/ActiveRecord def uploads Upload.order(id: :desc).where(uploader: self.class.to_s) end + # rubocop: enable CodeReuse/ActiveRecord def build_upload Upload.new( @@ -53,11 +57,13 @@ module RecordsUploads # Before removing an attachment, destroy any Upload records at the same path # # Called `before :remove` + # rubocop: disable CodeReuse/ActiveRecord def destroy_upload(*args) return unless file && file.exists? self.upload = nil uploads.where(path: upload_path).delete_all end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb index ef482aaaa63..6a0899be850 100644 --- a/app/validators/branch_filter_validator.rb +++ b/app/validators/branch_filter_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # BranchFilterValidator # # Custom validator for branch names. Squishes whitespace and ignores empty diff --git a/app/validators/js_regex_validator.rb b/app/validators/js_regex_validator.rb index a515af7b919..be715967b4a 100644 --- a/app/validators/js_regex_validator.rb +++ b/app/validators/js_regex_validator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class JsRegexValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) return true if value.blank? diff --git a/app/validators/url_validator.rb b/app/validators/url_validator.rb index faaf1283078..216acf79cbd 100644 --- a/app/validators/url_validator.rb +++ b/app/validators/url_validator.rb @@ -41,12 +41,13 @@ class UrlValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) @record = record - if value.present? - value.strip! - else + unless value.present? record.errors.add(attribute, 'must be a valid URL') + return end + value = strip_value!(record, attribute, value) + Gitlab::UrlBlocker.validate!(value, blocker_args) rescue Gitlab::UrlBlocker::BlockedUrlError => e record.errors.add(attribute, "is blocked: #{e.message}") @@ -54,6 +55,13 @@ class UrlValidator < ActiveModel::EachValidator private + def strip_value!(record, attribute, value) + new_value = value.strip + return value if new_value == value + + record.public_send("#{attribute}=", new_value) # rubocop:disable GitlabSecurity/PublicSend + end + def default_options # By default the validator doesn't block any url based on the ip address { diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb index 90193e85f2a..d36a56e81b9 100644 --- a/app/validators/variable_duplicates_validator.rb +++ b/app/validators/variable_duplicates_validator.rb @@ -21,6 +21,7 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator private + # rubocop: disable CodeReuse/ActiveRecord def validate_duplicates(record, attribute, values) duplicates = values.reject(&:marked_for_destruction?).group_by(&:key).select { |_, v| v.many? }.map(&:first) if duplicates.any? @@ -29,4 +30,5 @@ class VariableDuplicatesValidator < ActiveModel::EachValidator record.errors.add(attribute, error_message) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml index 278ad210543..391115a67b5 100644 --- a/app/views/abuse_reports/new.html.haml +++ b/app/views/abuse_reports/new.html.haml @@ -19,4 +19,4 @@ Explain the problem with this user. If appropriate, provide a link to the relevant issue or comment. .form-actions - = f.submit "Send report", class: "btn btn-create" + = f.submit "Send report", class: "btn btn-success" diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index a0861870ba4..cb67079853e 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -5,7 +5,7 @@ %legend Navigation bar: .form-group.row - = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label' + = f.label :header_logo, 'Header logo', class: 'col-sm-2 col-form-label pt-0' .col-sm-10 - if @appearance.header_logo? = image_tag @appearance.header_logo_url, class: 'appearance-light-logo-preview' @@ -22,7 +22,7 @@ %legend Favicon: .form-group.row - = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label' + = f.label :favicon, 'Favicon', class: 'col-sm-2 col-form-label pt-0' .col-sm-10 - if @appearance.favicon? = image_tag @appearance.favicon_url, class: 'appearance-light-logo-preview' @@ -51,7 +51,7 @@ .hint Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. .form-group.row - = f.label :logo, class: 'col-sm-2 col-form-label' + = f.label :logo, class: 'col-sm-2 col-form-label pt-0' .col-sm-10 - if @appearance.logo? = image_tag @appearance.logo_url, class: 'appearance-logo-preview' @@ -75,7 +75,7 @@ Guidelines parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}. .form-actions - = f.submit 'Save', class: 'btn btn-save append-right-10' + = f.submit 'Save', class: 'btn btn-success append-right-10' - if @appearance.persisted? Preview last save: = link_to 'Sign-in page', preview_sign_in_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer' diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml index 1af7dd5bb67..2cd95071c73 100644 --- a/app/views/admin/appearances/preview_sign_in.html.haml +++ b/app/views/admin/appearances/preview_sign_in.html.haml @@ -8,5 +8,5 @@ = label_tag :password = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.' .form-group - = button_tag "Sign in", class: "btn-create btn" + = button_tag "Sign in", class: "btn-success btn" diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml index 9121e44d31b..10bc3452d8b 100644 --- a/app/views/admin/application_settings/_account_and_limit.html.haml +++ b/app/views/admin/application_settings/_account_and_limit.html.haml @@ -14,7 +14,10 @@ = f.label :max_attachment_size, 'Maximum attachment size (MB)', class: 'label-bold' = f.number_field :max_attachment_size, class: 'form-control' .form-group - = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-bold' + = f.label :receive_max_input_size, 'Maximum push size (MB)', class: 'label-light' + = f.number_field :receive_max_input_size, class: 'form-control' + .form-group + = f.label :session_expire_delay, 'Session duration (minutes)', class: 'label-light' = f.number_field :session_expire_delay, class: 'form-control' %span.form-text.text-muted#session_expire_delay_help_block GitLab restart is required to apply changes .form-group diff --git a/app/views/admin/application_settings/_influx.html.haml b/app/views/admin/application_settings/_influx.html.haml index a1eeacd8290..dc5cbb8fa94 100644 --- a/app/views/admin/application_settings/_influx.html.haml +++ b/app/views/admin/application_settings/_influx.html.haml @@ -3,7 +3,7 @@ %fieldset %p - Setup InfluxDB to measure a wide variety of statistics like the time spent + Set up InfluxDB to measure a wide variety of statistics like the time spent in running SQL queries. These settings require a = link_to 'restart', help_page_path('administration/restart_gitlab') to take effect. diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml index c94f4c74820..615aa6317b0 100644 --- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml +++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml @@ -7,9 +7,9 @@ .form-check = f.check_box :mirror_available, class: 'form-check-input' = f.label :mirror_available, class: 'form-check-label' do - Allow mirrors to be setup for projects + Allow mirrors to be set up for projects %span.form-text.text-muted - If disabled, only admins will be able to setup mirrors in projects. + If disabled, only admins will be able to set up mirrors in projects. = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml index 635a6751e5b..5f36358f599 100644 --- a/app/views/admin/application_settings/_signin.html.haml +++ b/app/views/admin/application_settings/_signin.html.haml @@ -31,7 +31,7 @@ .form-check = f.check_box :require_two_factor_authentication, class: 'form-check-input' = f.label :require_two_factor_authentication, class: 'form-check-label' do - Require all users to setup Two-factor authentication + Require all users to set up Two-factor authentication .form-group = f.label :two_factor_authentication, 'Two-factor grace period (hours)', class: 'label-bold' = f.number_field :two_factor_grace_period, min: 0, class: 'form-control', placeholder: '0' diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index 2495defb6a7..788595877ea 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -2,7 +2,7 @@ = form_errors(@application_setting) %fieldset - .form-group + .form-group.mb-2 .form-check = f.check_box :version_check_enabled, class: 'form-check-input' = f.label :version_check_enabled, class: 'form-check-label' do @@ -16,23 +16,26 @@ .form-check = f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input' = f.label :usage_ping_enabled, class: 'form-check-label' do - Enable usage ping + = _('Enable usage ping') .form-text.text-muted - if can_be_configured - To help improve GitLab and its user experience, GitLab will - periodically collect usage information. - = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping") - about what information is shared with GitLab Inc. Visit - = link_to _('Cohorts'), instance_statistics_cohorts_path(anchor: 'usage-ping') - to see the JSON payload sent. + %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.') + + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + %p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + + %button.btn.js-usage-ping-payload-trigger{ type: 'button' } + .js-spinner.d-none= icon('spinner spin') + .js-text.d-inline= _('Preview payload') + %pre.usage-data.js-usage-ping-payload.js-syntax-highlight.code.highlight.mt-2.d-none{ data: { endpoint: usage_data_admin_application_settings_path(format: :html) } } - else - The usage ping is disabled, and cannot be configured through this - form. For more information, see the documentation on - = succeed '.' do - = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') - .form-group + = _('The usage ping is disabled, and cannot be configured through this form.') + - deactivating_usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping') + - deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path } + = s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe } + .form-group.mt-3 = f.label :instance_statistics_visibility_private, _('Instance Statistics visibility') = f.select :instance_statistics_visibility_private, options_for_select({_('All users') => false, _('Only admins') => true}, Gitlab::CurrentSettings.instance_statistics_visibility_private?), {}, class: 'form-control' = f.submit 'Save changes', class: "btn btn-success" - diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml new file mode 100644 index 00000000000..db24c9982f7 --- /dev/null +++ b/app/views/admin/application_settings/ci_cd.html.haml @@ -0,0 +1,26 @@ +- breadcrumb_title _("CI/CD") +- page_title _("CI/CD") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Continuous Integration and Deployment') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Auto DevOps, runners and job artifacts') + .settings-content + = render 'ci_cd' + +- if Gitlab.config.registry.enabled + %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Container Registry') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various container registry settings.') + .settings-content + = render 'registry' diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml new file mode 100644 index 00000000000..310e86b1377 --- /dev/null +++ b/app/views/admin/application_settings/integrations.html.haml @@ -0,0 +1,31 @@ +- breadcrumb_title _("Integrations") +- page_title _("Integrations") +- @content_class = "limit-container-width" unless fluid_layout + += render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded_by_default? + +%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('PlantUML') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Allow rendering of PlantUML diagrams in Asciidoc documents.') + .settings-content + = render 'plantuml' + += render_if_exists 'admin/application_settings/slack', expanded: expanded_by_default? + +%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Third party offers') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Control the display of third party offers.') + .settings-content + = render 'third_party_offers', application_setting: @application_setting + += render_if_exists 'admin/application_settings/snowplow', expanded: expanded_by_default? diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml new file mode 100644 index 00000000000..f50aca32bdf --- /dev/null +++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml @@ -0,0 +1,50 @@ +- breadcrumb_title _("Metrics and profiling") +- page_title _("Metrics and profiling") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Metrics - Influx') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable and configure InfluxDB metrics.') + .settings-content + = render 'influx' + +%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Metrics - Prometheus') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable and configure Prometheus metrics.') + .settings-content + = render 'prometheus' + +%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Profiling - Performance bar') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable the Performance Bar for a given group.') + = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar') + .settings-content + = render 'performance_bar' + +%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header#usage-statistics + %h4 + = _('Usage statistics') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable or disable version check and usage ping.') + .settings-content + = render 'usage' + += render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded_by_default? diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml new file mode 100644 index 00000000000..26fd745f45f --- /dev/null +++ b/app/views/admin/application_settings/network.html.haml @@ -0,0 +1,36 @@ +- breadcrumb_title _("Network") +- page_title _("Network") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Performance optimization') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various settings that affect GitLab performance.') + .settings-content + = render 'performance' + +%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('User and IP Rate Limits') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure limits for web and API requests.') + .settings-content + = render 'ip_limits' + +%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Outbound requests') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Allow requests to the local network from hooks and services.') + .settings-content + = render 'outbound' diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml new file mode 100644 index 00000000000..75f76eea3b4 --- /dev/null +++ b/app/views/admin/application_settings/preferences.html.haml @@ -0,0 +1,69 @@ +- breadcrumb_title _("Preferences") +- page_title _("Preferences") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Email') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Various email settings.') + .settings-content + = render 'email' + +%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Help page') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Help page text and support page url.') + .settings-content + = render 'help_page' + +%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Pages') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Size and domain settings for static websites') + .settings-content + = render 'pages' + +%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Real-time features') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Change this value to influence how frequently the GitLab UI polls for updates.') + .settings-content + = render 'realtime' + +%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Background jobs') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure Sidekiq job throttling.') + .settings-content + = render 'background_jobs' + +%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Gitaly') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure Gitaly timeouts.') + .settings-content + = render 'gitaly' diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml new file mode 100644 index 00000000000..1c2d9ccdb2d --- /dev/null +++ b/app/views/admin/application_settings/reporting.html.haml @@ -0,0 +1,36 @@ +- breadcrumb_title _("Reporting") +- page_title _("Reporting") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Spam and Anti-bot Protection') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable reCAPTCHA or Akismet and set IP limits.') + .settings-content + = render 'spam' + +%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Abuse reports') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Set notification email for abuse reports.') + .settings-content + = render 'abuse' + +%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Error Reporting and Logging') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Enable Sentry for error reporting and logging.') + .settings-content + = render 'logging' diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml new file mode 100644 index 00000000000..d8029e0c54a --- /dev/null +++ b/app/views/admin/application_settings/repository.html.haml @@ -0,0 +1,36 @@ +- breadcrumb_title _("Repository") +- page_title _("Repository") +- @content_class = "limit-container-width" unless fluid_layout + +%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Repository mirror') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? 'Collapse' : 'Expand' + %p + = _('Configure push mirrors.') + .settings-content + = render partial: 'repository_mirrors_form' + +%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Repository storage') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure storage path and circuit breaker settings.') + .settings-content + = render 'repository_storage' + +%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Repository maintenance') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Configure automatic git checks and housekeeping on repositories.') + .settings-content + = render 'repository_check' diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index 194a8157013..e2043183a97 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -1,359 +1,93 @@ -- breadcrumb_title "Settings" -- page_title "Settings" +- breadcrumb_title _("Settings") +- page_title _("Settings") - @content_class = "limit-container-width" unless fluid_layout -- expanded = Rails.env.test? -%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded) } +%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Visibility and access controls') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Set default and restrict visibility levels. Configure import sources and git access protocol.') .settings-content = render 'visibility_and_access' -%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded) } +%section.settings.as-account-limit.no-animate#js-account-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Account and limit') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Session expiration, projects limit and attachment size.') .settings-content = render 'account_and_limit' -%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded) } +%section.settings.as-signup.no-animate#js-signup-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Sign-up restrictions') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Configure the way a user creates a new account.') .settings-content = render 'signup' -%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded) } +%section.settings.as-signin.no-animate#js-signin-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Sign-in restrictions') %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.') .settings-content = render 'signin' -%section.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded) } +%section.qa-terms-settings.settings.as-terms.no-animate#js-terms-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Terms of Service and Privacy Policy') %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Include a Terms of Service agreement and Privacy Policy that all users must accept.') .settings-content = render 'terms' -%section.settings.as-help-page.no-animate#js-help-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Help page') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Help page text and support page url.') - .settings-content - = render 'help_page' - -%section.settings.as-pages.no-animate#js-pages-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Pages') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Size and domain settings for static websites') - .settings-content - = render 'pages' - -%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Continuous Integration and Deployment') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Auto DevOps, runners and job artifacts') - .settings-content - = render 'ci_cd' - -%section.settings.as-influx.no-animate#js-influx-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Metrics - Influx') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable and configure InfluxDB metrics.') - .settings-content - = render 'influx' - -%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Metrics - Prometheus') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable and configure Prometheus metrics.') - .settings-content - = render 'prometheus' - -%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Profiling - Performance bar') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable the Performance Bar for a given group.') - = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar') - .settings-content - = render 'performance_bar' - -%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Background jobs') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Configure Sidekiq job throttling.') - .settings-content - = render 'background_jobs' - -%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Spam and Anti-bot Protection') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable reCAPTCHA or Akismet and set IP limits.') - .settings-content - = render 'spam' - -%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Abuse reports') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Set notification email for abuse reports.') - .settings-content - = render 'abuse' - -%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Error Reporting and Logging') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable Sentry for error reporting and logging.') - .settings-content - = render 'logging' - -%section.qa-repository-storage-settings.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Repository storage') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Configure storage path and circuit breaker settings.') - .settings-content - = render 'repository_storage' - -%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Repository maintenance') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Configure automatic git checks and housekeeping on repositories.') - .settings-content - = render 'repository_check' - -- if Gitlab.config.registry.enabled - %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Container Registry') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Various container registry settings.') - .settings-content - = render 'registry' - - if koding_enabled? - %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) } + %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Koding') %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Online IDE integration settings.') .settings-content = render 'koding' -%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('PlantUML') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Allow rendering of PlantUML diagrams in Asciidoc documents.') - .settings-content - = render 'plantuml' - -%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) } - .settings-header#usage-statistics - %h4 - = _('Usage statistics') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Enable or disable version check and usage ping.') - .settings-content - = render 'usage' - -%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Email') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Various email settings.') - .settings-content - = render 'email' - -%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Gitaly') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Configure Gitaly timeouts.') - .settings-content - = render 'gitaly' += render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded_by_default? -%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) } +%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Web terminal') %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Set max session time for web terminal.') .settings-content = render 'terminal' -%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Real-time features') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Change this value to influence how frequently the GitLab UI polls for updates.') - .settings-content - = render 'realtime' - -%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Performance optimization') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Various settings that affect GitLab performance.') - .settings-content - = render 'performance' - -%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('User and IP Rate Limits') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Configure limits for web and API requests.') - .settings-content - = render 'ip_limits' - -%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Outbound requests') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Allow requests to the local network from hooks and services.') - .settings-content - = render 'outbound' - -%section.settings.as-mirror.no-animate#js-mirror-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Repository mirror') - %button.btn.js-settings-toggle{ type: 'button' } - = expanded ? 'Collapse' : 'Expand' - %p - = _('Configure push mirrors.') - .settings-content - = render partial: 'repository_mirrors_form' - -= render_if_exists 'admin/application_settings/geo', expanded: expanded - -= render_if_exists 'admin/application_settings/external_authorization_service_form', expanded: expanded - -= render_if_exists 'admin/application_settings/elasticsearch_form', expanded: expanded - -= render_if_exists 'admin/application_settings/slack', expanded: expanded - -= render_if_exists 'admin/application_settings/templates', expanded: expanded - -%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) } - .settings-header - %h4 - = _('Third party offers') - %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') - %p - = _('Control the display of third party offers.') - .settings-content - = render 'third_party_offers', application_setting: @application_setting - -= render_if_exists 'admin/application_settings/custom_templates_form', expanded: expanded - -%section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded) } +%section.settings.no-animate#js-web-ide-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 = _('Web IDE') %button.btn.btn-default.js-settings-toggle{ type: 'button' } - = expanded ? _('Collapse') : _('Expand') + = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Manage Web IDE features') .settings-content @@ -370,5 +104,3 @@ = s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation.') = f.submit _('Save changes'), class: "btn btn-success" - -= render_if_exists 'admin/application_settings/pseudonymizer_settings', expanded: expanded diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml index 7f14cddebd8..12690343f6e 100644 --- a/app/views/admin/applications/_form.html.haml +++ b/app/views/admin/applications/_form.html.haml @@ -21,17 +21,17 @@ for local tests = content_tag :div, class: 'form-group row' do - = f.label :trusted, class: 'col-sm-2 col-form-label' + = f.label :trusted, class: 'col-sm-2 col-form-label pt-0' .col-sm-10 = f.check_box :trusted %span.form-text.text-muted Trusted applications are automatically authorized on GitLab OAuth flow. .form-group.row - = f.label :scopes, class: 'col-sm-2 col-form-label' + = f.label :scopes, class: 'col-sm-2 col-form-label pt-0' .col-sm-10 = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes .form-actions - = f.submit 'Submit', class: "btn btn-save wide" + = f.submit 'Submit', class: "btn btn-success wide" = link_to "Cancel", admin_applications_path, class: "btn btn-cancel" diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml index 94d33fa6489..2cdf98075d1 100644 --- a/app/views/admin/applications/index.html.haml +++ b/app/views/admin/applications/index.html.haml @@ -5,7 +5,7 @@ System OAuth applications don't belong to any user and can only be managed by admins %hr %p= link_to 'New application', new_admin_application_path, class: 'btn btn-success' -%table.table.table-striped +%table.table %thead %tr %th Name diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml index 7f34357f147..c465d9f51d6 100644 --- a/app/views/admin/broadcast_messages/_form.html.haml +++ b/app/views/admin/broadcast_messages/_form.html.haml @@ -36,6 +36,6 @@ = f.datetime_select :ends_at, {}, class: 'form-control form-control-inline' .form-actions - if @broadcast_message.persisted? - = f.submit "Update broadcast message", class: "btn btn-create" + = f.submit "Update broadcast message", class: "btn btn-success" - else - = f.submit "Add broadcast message", class: "btn btn-create" + = f.submit "Add broadcast message", class: "btn btn-success" diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index fac61f9d249..85c04f8a01d 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -14,7 +14,7 @@ Projects: = approximate_count_with_delimiters(@counts, Project) %hr - = link_to('New project', new_project_path, class: "btn btn-new") + = link_to('New project', new_project_path, class: "btn btn-success") .col-sm-4 .info-well.dark-well .well-segment.well-centered @@ -24,7 +24,7 @@ = approximate_count_with_delimiters(@counts, User) = render_if_exists 'admin/dashboard/users_statistics' %hr - = link_to 'New user', new_admin_user_path, class: "btn btn-new" + = link_to 'New user', new_admin_user_path, class: "btn btn-success" .col-sm-4 .info-well.dark-well .well-segment.well-centered @@ -33,7 +33,7 @@ Groups: = approximate_count_with_delimiters(@counts, Group) %hr - = link_to 'New group', new_admin_group_path, class: "btn btn-new" + = link_to 'New group', new_admin_group_path, class: "btn btn-success" .row .col-md-4 .info-well diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml index b50adef362f..7c04ef03947 100644 --- a/app/views/admin/deploy_keys/edit.html.haml +++ b/app/views/admin/deploy_keys/edit.html.haml @@ -6,5 +6,5 @@ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Save changes', class: 'btn-save btn' + = f.submit 'Save changes', class: 'btn-success btn' = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml index 52ab8bae119..01013be06d6 100644 --- a/app/views/admin/deploy_keys/index.html.haml +++ b/app/views/admin/deploy_keys/index.html.haml @@ -3,7 +3,7 @@ %h3.page-title.deploy-keys-title Public deploy keys (#{@deploy_keys.count}) .float-right - = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-new btn-sm btn-inverted' + = link_to 'New deploy key', new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted' - if @deploy_keys.any? .table-holder.deploy-keys-list diff --git a/app/views/admin/deploy_keys/new.html.haml b/app/views/admin/deploy_keys/new.html.haml index d4f8e340b69..9a563a5bc78 100644 --- a/app/views/admin/deploy_keys/new.html.haml +++ b/app/views/admin/deploy_keys/new.html.haml @@ -6,5 +6,5 @@ = form_for [:admin, @deploy_key], html: { class: 'deploy-key-form' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Create', class: 'btn-create btn' + = f.submit 'Create', class: 'btn-success btn' = link_to 'Cancel', admin_deploy_keys_path, class: 'btn btn-cancel' diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml index a3773e90cfb..2a117c1414e 100644 --- a/app/views/admin/groups/_form.html.haml +++ b/app/views/admin/groups/_form.html.haml @@ -26,12 +26,12 @@ .alert.alert-info = render 'shared/group_tips' .form-actions - = f.submit _('Create group'), class: "btn btn-create" + = f.submit _('Create group'), class: "btn btn-success" = link_to _('Cancel'), admin_groups_path, class: "btn btn-cancel" - else .form-actions - = f.submit _('Save changes'), class: "btn btn-save" + = f.submit _('Save changes'), class: "btn btn-success" = link_to _('Cancel'), admin_group_path(@group), class: "btn btn-cancel" = render_if_exists 'ldap_group_links/ldap_syncrhonizations', group: @group diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml index 6a9b85b4109..cb833ffd9ac 100644 --- a/app/views/admin/groups/index.html.haml +++ b/app/views/admin/groups/index.html.haml @@ -12,7 +12,7 @@ = search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name' = icon("search", class: "search-icon") = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash - = link_to new_admin_group_path, class: "btn btn-new" do + = link_to new_admin_group_path, class: "btn btn-success" do = _('New group') %ul.content-list = render @groups diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index 72b068ea6b5..0c683f86252 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -111,7 +111,7 @@ .prepend-top-10 = select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2" %hr - = button_tag _('Add users to group'), class: "btn btn-create" + = button_tag _('Add users to group'), class: "btn btn-success" = render 'shared/members/requests', membership_source: @group, requesters: @requesters, force_mobile_view: true .card diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml index b9a650e1f1f..486d0477f20 100644 --- a/app/views/admin/hooks/edit.html.haml +++ b/app/views/admin/hooks/edit.html.haml @@ -12,7 +12,7 @@ = form_for @hook, as: :hook, url: admin_hook_path do |f| = render partial: 'form', locals: { form: f, hook: @hook } .form-actions - = f.submit 'Save changes', class: 'btn btn-create' + = f.submit 'Save changes', class: 'btn btn-success' = render 'shared/web_hooks/test_button', triggers: SystemHook.triggers, hook: @hook = link_to 'Remove', admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' } diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml index 87f9b0e86a7..5d462d7b732 100644 --- a/app/views/admin/hooks/index.html.haml +++ b/app/views/admin/hooks/index.html.haml @@ -10,7 +10,7 @@ .col-lg-8.append-bottom-default = form_for @hook, as: :hook, url: admin_hooks_path do |f| = render partial: 'form', locals: { form: f, hook: @hook } - = f.submit 'Add system hook', class: 'btn btn-create' + = f.submit 'Add system hook', class: 'btn btn-success' %hr diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml index 946d868da01..3ab7990d9e2 100644 --- a/app/views/admin/identities/_form.html.haml +++ b/app/views/admin/identities/_form.html.haml @@ -12,5 +12,5 @@ = f.text_field :extern_uid, class: 'form-control', required: true .form-actions - = f.submit _('Save changes'), class: "btn btn-save" + = f.submit _('Save changes'), class: "btn btn-success" diff --git a/app/views/admin/identities/index.html.haml b/app/views/admin/identities/index.html.haml index df3df159947..9543bbcf977 100644 --- a/app/views/admin/identities/index.html.haml +++ b/app/views/admin/identities/index.html.haml @@ -3,7 +3,7 @@ - page_title _("Identities"), @user.name, _("Users") = render 'admin/users/head' -= link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-new' += link_to _('New identity'), new_admin_user_identity_path, class: 'float-right btn btn-success' - if @identities.present? .table-holder %table.table diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml index ee2d4c8430a..5e7b4817461 100644 --- a/app/views/admin/labels/_form.html.haml +++ b/app/views/admin/labels/_form.html.haml @@ -27,5 +27,5 @@ .form-actions - = f.submit _('Save'), class: 'btn btn-save js-save-button' + = f.submit _('Save'), class: 'btn btn-success js-save-button' = link_to _("Cancel"), admin_labels_path, class: 'btn btn-cancel' diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml index f1b8658f84e..5a5b3d18c5f 100644 --- a/app/views/admin/labels/index.html.haml +++ b/app/views/admin/labels/index.html.haml @@ -1,7 +1,7 @@ - page_title _("Labels") %div - = link_to new_admin_label_path, class: "float-right btn btn-nr btn-new" do + = link_to new_admin_label_path, class: "float-right btn btn-nr btn-success" do = _('New label') %h3.page-title = _('Labels') diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml index 57de792f92d..46bb57c78a8 100644 --- a/app/views/admin/projects/index.html.haml +++ b/app/views/admin/projects/index.html.haml @@ -21,7 +21,7 @@ = dropdown_content = dropdown_loading = render 'shared/projects/dropdown' - = link_to new_project_path, class: 'btn btn-new' do + = link_to new_project_path, class: 'btn btn-success' do New Project = button_tag "Search", class: "btn btn-primary btn-search hide" diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml index 43937b01339..e4fc2985087 100644 --- a/app/views/admin/runners/_runner.html.haml +++ b/app/views/admin/runners/_runner.html.haml @@ -1,51 +1,78 @@ -%tr{ id: dom_id(runner) } - %td - - if runner.instance_type? - %span.badge.badge-success shared - - elsif runner.group_type? - %span.badge.badge-success group - - else - %span.badge.badge-info specific - - if runner.locked? - %span.badge.badge-warning locked - - unless runner.active? - %span.badge.badge-danger paused - - %td - = link_to admin_runner_path(runner) do - = runner.short_sha - %td - = runner.description - %td - = runner.version - %td - = runner.ip_address - %td - - if runner.instance_type? || runner.group_type? - n/a - - else - = runner.projects.count(:all) - %td - #{runner.builds.count(:all)} - %td - - runner.tag_list.sort.each do |tag| - %span.badge.badge-primary - = tag - %td - - if runner.contacted_at - = time_ago_with_tooltip runner.contacted_at - - else - Never - %td.admin-runner-btn-group-cell - .float-right.btn-group - = link_to admin_runner_path(runner), class: 'btn btn-sm btn-default has-tooltip', title: 'Edit', ref: 'tooltip', aria: { label: 'Edit' }, data: { placement: 'top', container: 'body'} do - = icon('pencil') - - - if runner.active? - = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-sm btn-default has-tooltip', title: 'Pause', ref: 'tooltip', aria: { label: 'Pause' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('pause') +.gl-responsive-table-row{ id: dom_id(runner) } + .table-section.section-10.section-wrap + .table-mobile-header{ role: 'rowheader' }= _('Type') + .table-mobile-content + - if runner.instance_type? + %span.badge.badge-success shared + - elsif runner.group_type? + %span.badge.badge-success group - else - = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default btn-sm has-tooltip', title: 'Resume', ref: 'tooltip', aria: { label: 'Resume' }, data: { placement: 'top', container: 'body'} do - = icon('play') - = link_to [:admin, runner], method: :delete, class: 'btn btn-danger btn-sm has-tooltip', title: 'Remove', ref: 'tooltip', aria: { label: 'Remove' }, data: { placement: 'top', container: 'body', confirm: "Are you sure?" } do - = icon('remove') + %span.badge.badge-info specific + - if runner.locked? + %span.badge.badge-warning locked + - unless runner.active? + %span.badge.badge-danger paused + + .table-section.section-10 + .table-mobile-header{ role: 'rowheader' }= _('Runner token') + .table-mobile-content + = link_to runner.short_sha, admin_runner_path(runner) + + .table-section.section-15 + .table-mobile-header{ role: 'rowheader' }= _('Description') + .table-mobile-content.str-truncated.has-tooltip{ title: runner.description } + = runner.description + + .table-section.section-15 + .table-mobile-header{ role: 'rowheader' }= _('Version') + .table-mobile-content.str-truncated.has-tooltip{ title: runner.version } + = runner.version + + .table-section.section-10 + .table-mobile-header{ role: 'rowheader' }= _('IP Address') + .table-mobile-content + = runner.ip_address + + .table-section.section-5 + .table-mobile-header{ role: 'rowheader' }= _('Projects') + .table-mobile-content + - if runner.instance_type? || runner.group_type? + = _('n/a') + - else + = runner.projects.count(:all) + + .table-section.section-5 + .table-mobile-header{ role: 'rowheader' }= _('Jobs') + .table-mobile-content + = runner.builds.count(:all) + + .table-section.section-10.section-wrap + .table-mobile-header{ role: 'rowheader' }= _('Tags') + .table-mobile-content + - runner.tag_list.sort.each do |tag| + %span.badge.badge-primary + = tag + + .table-section.section-10 + .table-mobile-header{ role: 'rowheader' }= _('Last contact') + .table-mobile-content + - if runner.contacted_at + = time_ago_with_tooltip runner.contacted_at + - else + = _('Never') + + .table-section.table-button-footer.section-10 + .btn-group.table-action-buttons + .btn-group + = link_to admin_runner_path(runner), class: 'btn btn-default has-tooltip', title: _('Edit'), ref: 'tooltip', aria: { label: _('Edit') }, data: { placement: 'top', container: 'body'} do + = icon('pencil') + .btn-group + - if runner.active? + = link_to [:pause, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Pause'), ref: 'tooltip', aria: { label: _('Pause') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('pause') + - else + = link_to [:resume, :admin, runner], method: :get, class: 'btn btn-default has-tooltip', title: _('Resume'), ref: 'tooltip', aria: { label: _('Resume') }, data: { placement: 'top', container: 'body'} do + = icon('play') + .btn-group + = link_to [:admin, runner], method: :delete, class: 'btn btn-danger has-tooltip', title: _('Remove'), ref: 'tooltip', aria: { label: _('Remove') }, data: { placement: 'top', container: 'body', confirm: _('Are you sure?') } do + = icon('remove') diff --git a/app/views/admin/runners/_sort_dropdown.html.haml b/app/views/admin/runners/_sort_dropdown.html.haml new file mode 100644 index 00000000000..b201e6bf10e --- /dev/null +++ b/app/views/admin/runners/_sort_dropdown.html.haml @@ -0,0 +1,11 @@ +- sorted_by = sort_options_hash[@sort] + +.dropdown.inline.prepend-left-10 + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } } + = sorted_by + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort + %li + = sortable_item(sort_title_created_date, page_filter_path(sort: sort_value_created_date, label: true), sorted_by) + = sortable_item(sort_title_contacted_date, page_filter_path(sort: sort_value_contacted_date, label: true), sorted_by) + diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 9280ff4d478..ee2e1703fdb 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,77 +1,116 @@ -- breadcrumb_title "Runners" +- breadcrumb_title _('Runners') - @no_container = true %div{ class: container_class } .bs-callout %p - A 'Runner' is a process which runs a job. - You can setup as many Runners as you need. + = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") %br - Runners can be placed on separate users, servers, even on your local machine. + = _('Runners can be placed on separate users, servers, even on your local machine.') %br %div - %span Each Runner can be in one of the following states: + %span= _('Each Runner can be in one of the following states:') %ul %li %span.badge.badge-success shared - \- Runner runs jobs from all unassigned projects + \- + = _('Runner runs jobs from all unassigned projects') %li %span.badge.badge-success group - \- Runner runs jobs from all unassigned projects in its group + \- + = _('Runner runs jobs from all unassigned projects in its group') %li %span.badge.badge-info specific - \- Runner runs jobs from assigned projects + \- + = _('Runner runs jobs from assigned projects') %li %span.badge.badge-warning locked - \- Runner cannot be assigned to other projects + \- + = _('Runner cannot be assigned to other projects') %li %span.badge.badge-danger paused - \- Runner will not receive any new jobs + \- + = _('Runner will not receive any new jobs') .bs-callout.clearfix .float-left %p - You can reset runners registration token by pressing a button below. + = _('You can reset runners registration token by pressing a button below.') .prepend-top-10 - = button_to _("Reset runners registration token"), reset_runners_token_admin_application_settings_path, + = button_to _('Reset runners registration token'), reset_runners_token_admin_application_settings_path, method: :put, class: 'btn btn-default', - data: { confirm: _("Are you sure you want to reset registration token?") } + data: { confirm: _('Are you sure you want to reset registration token?') } = render partial: 'ci/runner/how_to_setup_shared_runner', locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } - .append-bottom-20.clearfix - .float-left - = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do - .form-group - = search_field_tag :search, params[:search], class: 'form-control input-short', placeholder: 'Runner description or token', spellcheck: false - = submit_tag 'Search', class: 'btn' - - .float-right.light - Runners currently online: #{@active_runners_cnt} + .bs-callout + %p + = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - %br + .row-content-block.second-block + = form_tag admin_runners_path, id: 'runners-search', method: :get, class: 'filter-form js-filter-form' do + .filtered-search-wrapper + .filtered-search-box + = dropdown_tag(custom_icon('icon_history'), + options: { wrapper_class: 'filtered-search-history-dropdown-wrapper', + toggle_class: 'filtered-search-history-dropdown-toggle-button', + dropdown_class: 'filtered-search-history-dropdown', + content_class: 'filtered-search-history-dropdown-content', + title: _('Recent searches') }) do + .js-filtered-search-history-dropdown{ data: { full_path: admin_runners_path } } + .filtered-search-box-input-container.droplab-dropdown + .scroll-container + %ul.tokens-container.list-unstyled + %li.input-token + %input.form-control.filtered-search{ { id: 'filtered-search-runners', placeholder: _('Search or filter results...') } } + #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown + %ul{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { action: 'submit' } } + = button_tag class: %w[btn btn-link] do + = sprite_icon('search') + %span + = _('Press Enter or click to search') + %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } + %li.filter-dropdown-item + = button_tag class: %w[btn btn-link] do + -# Encapsulate static class name `{{icon}}` inside #{} to bypass + -# haml lint's ClassAttributeWithStaticValue + %svg + %use{ 'xlink:href': "#{'{{icon}}'}" } + %span.js-filter-hint + {{hint}} + %span.js-filter-tag.dropdown-light-content + {{tag}} + #js-dropdown-admin-runner-status.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + - Ci::Runner::AVAILABLE_STATUSES.each do |status| + %li.filter-dropdown-item{ data: { value: status } } + = button_tag class: %w[btn btn-link] do + = status.titleize + = button_tag class: %w[clear-search hidden] do + = icon('times') + .filter-dropdown-container + = render 'sort_dropdown' - if @runners.any? - .runners-content + .runners-content.content-list .table-holder - %table.table - %thead - %tr - %th Type - %th Runner token - %th Description - %th Version - %th IP Address - %th Projects - %th Jobs - %th Tags - %th= link_to 'Last contact', admin_runners_path(safe_params.slice(:search).merge(sort: 'contacted_asc')) - %th + .gl-responsive-table-row.table-row-header{ role: 'row' } + .table-section.section-10{ role: 'rowheader' }= _('Type') + .table-section.section-10{ role: 'rowheader' }= _('Runner token') + .table-section.section-15{ role: 'rowheader' }= _('Description') + .table-section.section-15{ role: 'rowheader' }= _('Version') + .table-section.section-10{ role: 'rowheader' }= _('IP Address') + .table-section.section-5{ role: 'rowheader' }= _('Projects') + .table-section.section-5{ role: 'rowheader' }= _('Jobs') + .table-section.section-10{ role: 'rowheader' }= _('Tags') + .table-section.section-10{ role: 'rowheader' }= _('Last contact') + .table-section.section-10{ role: 'rowheader' } - - @runners.each do |runner| - = render "admin/runners/runner", runner: runner - = paginate @runners, theme: "gitlab" + - @runners.each do |runner| + = render 'admin/runners/runner', runner: runner + = paginate @runners, theme: 'gitlab' - else - .nothing-here-block No runners found + .nothing-here-block= _('No runners found') diff --git a/app/views/admin/services/_form.html.haml b/app/views/admin/services/_form.html.haml index 993006e8745..1798b44bbb7 100644 --- a/app/views/admin/services/_form.html.haml +++ b/app/views/admin/services/_form.html.haml @@ -7,4 +7,4 @@ = render 'shared/service_settings', form: form, subject: @service .footer-block.row-content-block - = form.submit 'Save', class: 'btn btn-save' + = form.submit 'Save', class: 'btn btn-success' diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml index 7f21bdb91c8..296ef073144 100644 --- a/app/views/admin/users/_form.html.haml +++ b/app/views/admin/users/_form.html.haml @@ -75,8 +75,8 @@ .form-actions - if @user.new_record? - = f.submit 'Create user', class: "btn btn-create" + = f.submit 'Create user', class: "btn btn-success" = link_to 'Cancel', admin_users_path, class: "btn btn-cancel" - else - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-success" = link_to 'Cancel', admin_user_path(@user), class: "btn btn-cancel" diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index faeb82656ba..f910e90d6ca 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -31,7 +31,7 @@ = sort_title_recently_updated = link_to admin_users_path(sort: sort_value_oldest_updated, filter: params[:filter]) do = sort_title_oldest_updated - = link_to 'New user', new_admin_user_path, class: 'btn btn-new btn-search' + = link_to 'New user', new_admin_user_path, class: 'btn btn-success btn-search' .top-area.scrolling-tabs-container.inner-page-scroll-tabs .fade-left diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml index 3d39c1da408..e6da81831ab 100644 --- a/app/views/admin/users/projects.html.haml +++ b/app/views/admin/users/projects.html.haml @@ -7,7 +7,7 @@ .card .card-header Group projects %ul.hover-list - - @user.group_members.includes(:source).each do |group_member| + - @user.group_members.includes(:source).each do |group_member| # rubocop: disable CodeReuse/ActiveRecord - group = group_member.group %li.group_member %strong= link_to group.name, admin_group_path(group) diff --git a/app/views/ci/runner/_how_to_setup_runner.html.haml b/app/views/ci/runner/_how_to_setup_runner.html.haml index c26eb873718..b1b142460b0 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -1,6 +1,6 @@ - link = link_to _("Install GitLab Runner"), 'https://docs.gitlab.com/runner/install/', target: '_blank' .append-bottom-10 - %h4= _("Setup a %{type} Runner manually") % { type: type } + %h4= _("Set up a %{type} Runner manually") % { type: type } %ol %li diff --git a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml index e765a353fe4..afe57bdfa01 100644 --- a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_specific_runner.html.haml @@ -1,6 +1,6 @@ .bs-callout.help-callout .append-bottom-10 - %h4= _('Setup a specific Runner automatically') + %h4= _('Set up a specific Runner automatically') %p - link_to_help_page = link_to(_('Learn more about Kubernetes'), diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml index d8f1e50544c..727784141bb 100644 --- a/app/views/dashboard/_groups_head.html.haml +++ b/app/views/dashboard/_groups_head.html.haml @@ -10,4 +10,4 @@ = render 'shared/groups/search_form' = render 'shared/groups/dropdown' - if current_user.can_create_group? - = link_to _("New group"), new_group_path, class: "btn btn-new" + = link_to _("New group"), new_group_path, class: "btn btn-success" diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml index 9b1d9b659f9..69a2e408073 100644 --- a/app/views/dashboard/_projects_head.html.haml +++ b/app/views/dashboard/_projects_head.html.haml @@ -19,4 +19,4 @@ = render 'shared/projects/search_form' = render 'shared/projects/dropdown' - if current_user.can_create_project? - = link_to "New project", new_project_path, class: "btn btn-new" + = link_to "New project", new_project_path, class: "btn btn-success" diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml index e7e323a8683..4f38339b87a 100644 --- a/app/views/dashboard/_snippets_head.html.haml +++ b/app/views/dashboard/_snippets_head.html.haml @@ -9,4 +9,4 @@ - if current_user .nav-controls.d-none.d-sm-block - = link_to "New snippet", new_snippet_path, class: "btn btn-new", title: "New snippet" + = link_to "New snippet", new_snippet_path, class: "btn btn-success", title: "New snippet" diff --git a/app/views/dashboard/issues.atom.builder b/app/views/dashboard/issues.atom.builder index d7b6fb9a4a1..6034389b897 100644 --- a/app/views/dashboard/issues.atom.builder +++ b/app/views/dashboard/issues.atom.builder @@ -1,3 +1,4 @@ +# rubocop: disable CodeReuse/ActiveRecord xml.title "#{current_user.name} issues" xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: issues_dashboard_url, rel: "alternate", type: "text/html" @@ -5,3 +6,4 @@ xml.id issues_dashboard_url xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? +# rubocop: enable CodeReuse/ActiveRecord diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml index 4391624196b..b11dc2c8e9b 100644 --- a/app/views/dashboard/snippets/index.html.haml +++ b/app/views/dashboard/snippets/index.html.haml @@ -7,7 +7,7 @@ .d-block.d-sm-none - = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-success btn-block", title: "New snippet" do New snippet = render partial: 'snippets/snippets', locals: { link_project: true } diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml index 35dafb3e980..4b8ad5acd5b 100644 --- a/app/views/devise/passwords/edit.html.haml +++ b/app/views/devise/passwords/edit.html.haml @@ -7,12 +7,12 @@ = f.hidden_field :reset_password_token .form-group = f.label 'New password', for: "user_password" - = f.password_field :password, class: "form-control top", required: true, title: 'This field is required' + = f.password_field :password, class: "form-control top qa-password-field", required: true, title: 'This field is required' .form-group = f.label 'Confirm new password', for: "user_password_confirmation" - = f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true + = f.password_field :password_confirmation, class: "form-control bottom qa-password-confirmation", title: 'This field is required', required: true .clearfix - = f.submit "Change your password", class: "btn btn-primary" + = f.submit "Change your password", class: "btn btn-primary qa-change-password-button" .clearfix.prepend-top-20 %p diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 0ee563ac066..7dacd0b1d72 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,10 +1,10 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login" - = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." + = f.label "Username or email", for: "user_login", class: 'label-bold' + = f.text_field :login, class: "form-control top qa-login-field", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required." .form-group - = f.label :password - = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required." + = f.label :password, class: 'label-bold' + = f.password_field :password, class: "form-control bottom qa-password-field", required: true, title: "This field is required." - if devise_mapping.rememberable? .remember-me %label{ for: "user_remember_me" } @@ -17,4 +17,4 @@ = recaptcha_tags .submit-container.move-submit-down - = f.submit "Sign in", class: "btn btn-save" + = f.submit "Sign in", class: "btn btn-success qa-sign-in-button" diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml index 36ff42090be..131544ac0c0 100644 --- a/app/views/devise/sessions/_new_crowd.html.haml +++ b/app/views/devise/sessions/_new_crowd.html.haml @@ -10,4 +10,4 @@ %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = submit_tag "Sign in", class: "btn-save btn" + = submit_tag "Sign in", class: "btn-success btn" diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml index 6bf7349f602..796c0cadda8 100644 --- a/app/views/devise/sessions/_new_ldap.html.haml +++ b/app/views/devise/sessions/_new_ldap.html.haml @@ -1,13 +1,13 @@ = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "gl-show-field-errors") do .form-group = label_tag :username, "#{server['label']} Username" - = text_field_tag :username, nil, { class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true } + = text_field_tag :username, nil, { class: "form-control top qa-username-field", title: "This field is required.", autofocus: "autofocus", required: true } .form-group = label_tag :password - = password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true } + = password_field_tag :password, nil, { class: "form-control bottom qa-password-field", title: "This field is required.", required: true } - if devise_mapping.rememberable? .remember-me %label{ for: "remember_me" } = check_box_tag :remember_me, '1', false, id: 'remember_me' %span Remember me - = submit_tag "Sign in", class: "btn-save btn" + = submit_tag "Sign in", class: "btn-success btn qa-sign-in-button" diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml index ba168c4eab8..fefdf5f9531 100644 --- a/app/views/devise/sessions/two_factor.html.haml +++ b/app/views/devise/sessions/two_factor.html.haml @@ -11,7 +11,7 @@ = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.' %p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. .prepend-top-20 - = f.submit "Verify code", class: "btn btn-save" + = f.submit "Verify code", class: "btn btn-success" - if @user.two_factor_u2f_enabled? = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name } diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml index 3723814debe..269a3721e06 100644 --- a/app/views/devise/shared/_omniauth_box.html.haml +++ b/app/views/devise/shared/_omniauth_box.html.haml @@ -1,14 +1,17 @@ -.omniauth-container - %p - %span.light - Sign in with - - providers = enabled_button_based_providers +.omniauth-container.prepend-top-15 + %label.label-bold.d-block + Sign in with + - providers = enabled_button_based_providers + .d-flex.justify-content-between.flex-wrap - providers.each do |provider| - %span.light - - has_icon = provider_has_icon?(provider) - = link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: 'oauth-login' + (has_icon ? ' oauth-image-link' : ' btn'), id: "oauth-login-#{provider}" - %fieldset.prepend-top-10.remember-me - %label - = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + - has_icon = provider_has_icon?(provider) + = link_to omniauth_authorize_path(:user, provider), method: :post, class: 'btn d-flex align-items-center omniauth-btn text-left oauth-login', id: "oauth-login-#{provider}" do + - if has_icon + = provider_image_tag(provider) %span - Remember me + = label_for_provider(provider) + %fieldset.remember-me + %label + = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox' + %span + Remember me diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index ee7369f54a9..90ed20404c5 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -4,24 +4,24 @@ .devise-errors = devise_error_messages! .form-group - = f.label :name, 'Full name' + = f.label :name, 'Full name', class: 'label-bold' = f.text_field :name, class: "form-control top", required: true, title: "This field is required." .username.form-group - = f.label :username + = f.label :username, class: 'label-bold' = f.text_field :username, class: "form-control middle", pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: 'Please create a username with only alphanumeric characters.' %p.validation-error.hide Username is already taken. %p.validation-success.hide Username is available. %p.validation-pending.hide Checking username availability... .form-group - = f.label :email + = f.label :email, class: 'label-bold' = f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address." .form-group - = f.label :email_confirmation + = f.label :email_confirmation, class: 'label-bold' = f.email_field :email_confirmation, class: "form-control middle", required: true, title: "Please retype the email address." .form-group.append-bottom-20#password-strength - = f.label :password + = f.label :password, class: 'label-bold' = f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters." - %p.gl-field-hint Minimum length is #{@minimum_password_length} characters + %p.gl-field-hint.text-secondary Minimum length is #{@minimum_password_length} characters - if Gitlab::CurrentSettings.current_application_settings.enforce_terms? .form-group = check_box_tag :terms_opt_in, '1', false, required: true @@ -34,8 +34,3 @@ = recaptcha_tags .submit-container = f.submit "Register", class: "btn-register btn" -.clearfix.submit-container - %p - %span.light Didn't receive a confirmation email? - = succeed '.' do - = link_to "Request a new one", new_confirmation_path(:user) diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml index 58c585a29ff..3764e86dd8b 100644 --- a/app/views/devise/shared/_tabs_ldap.html.haml +++ b/app/views/devise/shared/_tabs_ldap.html.haml @@ -4,10 +4,10 @@ = link_to "Crowd", "#crowd", class: 'nav-link active', 'data-toggle' => 'tab' - @ldap_servers.each_with_index do |server, i| %li.nav-item - = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)}", 'data-toggle' => 'tab' + = link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && !crowd_enabled?)} qa-ldap-tab", 'data-toggle' => 'tab' - if password_authentication_enabled_for_web? %li.nav-item - = link_to 'Standard', '#login-pane', class: 'nav-link', 'data-toggle' => 'tab' + = link_to 'Standard', '#login-pane', class: 'nav-link qa-standard-tab', 'data-toggle' => 'tab' - if allow_signup? %li.nav-item = link_to 'Register', '#register-pane', class: 'nav-link', 'data-toggle' => 'tab' diff --git a/app/views/devise/shared/_tabs_normal.html.haml b/app/views/devise/shared/_tabs_normal.html.haml index 284d4fa1b89..8745a4e9d3e 100644 --- a/app/views/devise/shared/_tabs_normal.html.haml +++ b/app/views/devise/shared/_tabs_normal.html.haml @@ -1,6 +1,6 @@ %ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in + %a.nav-link.qa-sign-in-tab.active{ href: '#login-pane', data: { toggle: 'tab' }, role: 'tab' } Sign in - if allow_signup? %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register + %a.nav-link.qa-register-tab{ href: '#register-pane', data: { toggle: 'tab' }, role: 'tab' } Register diff --git a/app/views/discussions/_diff_discussion.html.haml b/app/views/discussions/_diff_discussion.html.haml index 4b6c4581eb3..6b8dd156874 100644 --- a/app/views/discussions/_diff_discussion.html.haml +++ b/app/views/discussions/_diff_discussion.html.haml @@ -4,7 +4,6 @@ -# Text diff discussions - expanded = local_assigns.fetch(:expanded, true) %tr.notes_holder{ class: ('hide' unless expanded) } - %td.notes_line{ colspan: 2 } - %td.notes_content + %td.notes_content{ colspan: 3 } .content{ class: ('hide' unless expanded) } = render partial: "discussions/notes", collection: discussions, as: :discussion, locals: { disable_collapse_class: true } diff --git a/app/views/discussions/_parallel_diff_discussion.html.haml b/app/views/discussions/_parallel_diff_discussion.html.haml index 079d9083dff..2e621c4082d 100644 --- a/app/views/discussions/_parallel_diff_discussion.html.haml +++ b/app/views/discussions/_parallel_diff_discussion.html.haml @@ -1,21 +1,17 @@ - expanded = [*discussions_left, *discussions_right].any?(&:expanded?) %tr.notes_holder{ class: ('hide' unless expanded) } - if discussions_left - %td.notes_line.old - %td.notes_content.parallel.old + %td.notes_content.parallel.old{ colspan: 2 } .content{ class: ('hide' unless discussions_left.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_left, as: :discussion, line_type: 'old', locals: { disable_collapse_class: true } - else - %td.notes_line.old= ("") - %td.notes_content.parallel.old + %td.notes_content.parallel.old{ colspan: 2 } .content - if discussions_right - %td.notes_line.new - %td.notes_content.parallel.new + %td.notes_content.parallel.new{ colspan: 2 } .content{ class: ('hide' unless discussions_right.any?(&:expanded?)) } = render partial: "discussions/notes", collection: discussions_right, as: :discussion, line_type: 'new', locals: { disable_collapse_class: true } - else - %td.notes_line.new= ("") - %td.notes_content.parallel.new + %td.notes_content.parallel.new{ colspan: 2 } .content diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml index 0bc057a8864..78904f550c7 100644 --- a/app/views/doorkeeper/applications/_form.html.haml +++ b/app/views/doorkeeper/applications/_form.html.haml @@ -20,4 +20,4 @@ = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes .prepend-top-default - = f.submit _('Save application'), class: "btn btn-create" + = f.submit _('Save application'), class: "btn btn-success" diff --git a/app/views/doorkeeper/applications/index.html.haml b/app/views/doorkeeper/applications/index.html.haml index ab3a1b100ce..1f5c70a6c6e 100644 --- a/app/views/doorkeeper/applications/index.html.haml +++ b/app/views/doorkeeper/applications/index.html.haml @@ -16,6 +16,9 @@ = _('Add new application') = render 'form', application: @application %hr + - else + .bs-callout.bs-callout-disabled + = _('Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission') - if user_oauth_applications? .oauth-applications %h5 @@ -62,7 +65,7 @@ %th %tbody - @authorized_apps.each do |app| - - token = app.authorized_tokens.order('created_at desc').first + - token = app.authorized_tokens.order('created_at desc').first # rubocop: disable CodeReuse/ActiveRecord %tr{ id: "application_#{app.id}" } %td= app.name %td= token.created_at diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 53a33adc14d..78a1d1a0553 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -11,3 +11,5 @@ = render "events/event/note", event: event - else = render "events/event/common", event: event +- elsif @user&.include_private_contributions? + = render "events/event/private", event: event diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml index 8f7da7d8c4f..98941722434 100644 --- a/app/views/events/_event_scope.html.haml +++ b/app/views/events/_event_scope.html.haml @@ -1,7 +1,7 @@ %span.event-scope = event_preposition(event) - if event.project - = link_to_project event.project + = link_to_project(event.project) - else = event.project_name diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 01e72862114..829a3da1558 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,7 +1,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span{ class: event.action_name } - if event.target = event.action_name diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index d8e59be57bb..6ad7e157131 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,11 +1,11 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span{ class: event.action_name } = event_action_name(event) - if event.project - = link_to_project event.project + = link_to_project(event.project) - else = event.project_name diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index de6383e4097..cdacd998a69 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,7 +1,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) = event.action_name = event_note_title_html(event) diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml new file mode 100644 index 00000000000..ccd2aacb4ea --- /dev/null +++ b/app/views/events/event/_private.html.haml @@ -0,0 +1,10 @@ +.event-inline.event-item + .event-item-timestamp + = time_ago_with_tooltip(event.created_at) + + .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon') + + .event-title + - author_name = capture do + %span.author_name= link_to_author(event) + = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name } diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 85f2d00bde3..5f0ee79cd9b 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -3,7 +3,7 @@ = icon_for_profile_event(event) .event-title - %span.author_name= link_to_author event + %span.author_name= link_to_author(event) %span.pushed #{event.action_name} #{event.ref_type} %strong - commits_link = project_commits_path(project, event.ref_name) diff --git a/app/views/groups/_archived_projects.html.haml b/app/views/groups/_archived_projects.html.haml new file mode 100644 index 00000000000..ed79f5790f0 --- /dev/null +++ b/app/views/groups/_archived_projects.html.haml @@ -0,0 +1,8 @@ +#js-groups-archived-tree + .empty-state.text-center.hidden + %p= _("There are no archived projects yet") + + %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } + .js-groups-list-holder + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/groups/_children.html.haml b/app/views/groups/_children.html.haml deleted file mode 100644 index 742b40784d3..00000000000 --- a/app/views/groups/_children.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -.js-groups-list-holder - #js-groups-tree{ data: { hide_projects: 'false', group_id: group.id, endpoint: group_children_path(group, format: :json), path: group_path(group), form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml index f7cc62c6929..ff59013ed67 100644 --- a/app/views/groups/_group_admin_settings.html.haml +++ b/app/views/groups/_group_admin_settings.html.haml @@ -1,5 +1,5 @@ .form-group.row - = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2' + = f.label :lfs_enabled, 'Large File Storage', class: 'col-form-label col-sm-2 pt-0' .col-sm-10 .form-check = f.check_box :lfs_enabled, checked: @group.lfs_enabled?, class: 'form-check-input' @@ -11,13 +11,13 @@ %span.descr This setting can be overridden in each project. .form-group.row - = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2' + = f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0' .col-sm-10 .form-check = f.check_box :require_two_factor_authentication, class: 'form-check-input' = f.label :require_two_factor_authentication, class: 'form-check-label' do %strong - Require all users in this group to setup Two-factor authentication + Require all users in this group to set up Two-factor authentication = link_to icon('question-circle'), help_page_path('security/two_factor_authentication', anchor: 'enforcing-2fa-for-all-users-in-a-group') .form-group.row .offset-sm-2.col-sm-10 diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml new file mode 100644 index 00000000000..4eb8367f633 --- /dev/null +++ b/app/views/groups/_shared_projects.html.haml @@ -0,0 +1,8 @@ +#js-groups-shared-tree + .empty-state.text-center.hidden + %p= _("There are no projects shared with this group yet") + + %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } + .js-groups-list-holder + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml new file mode 100644 index 00000000000..d53c8026df8 --- /dev/null +++ b/app/views/groups/_subgroups_and_projects.html.haml @@ -0,0 +1,8 @@ +#js-groups-subgroups_and_projects-tree + .empty-state.hidden + = render "shared/groups/empty_state" + + %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } + .js-groups-list-holder + .loading-container.text-center + = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml index aa03f8365f9..04683ec5a9a 100644 --- a/app/views/groups/group_members/_new_group_member.html.haml +++ b/app/views/groups/group_members/_new_group_member.html.haml @@ -19,4 +19,4 @@ On this date, the member(s) will automatically lose access to this group and all of its projects. .col-md-2 - = f.submit 'Add to group', class: "btn btn-create btn-block" + = f.submit 'Add to group', class: "btn btn-success btn-block" diff --git a/app/views/groups/issues.atom.builder b/app/views/groups/issues.atom.builder index 2a385b661e5..2fd96c9d158 100644 --- a/app/views/groups/issues.atom.builder +++ b/app/views/groups/issues.atom.builder @@ -1,3 +1,4 @@ +# rubocop: disable CodeReuse/ActiveRecord xml.title "#{@group.name} issues" xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: issues_group_url, rel: "alternate", type: "text/html" @@ -5,3 +6,4 @@ xml.id issues_group_url xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? +# rubocop: enable CodeReuse/ActiveRecord diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml index e6821009d03..003bd25dd06 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -7,7 +7,7 @@ - if can_admin_label - content_for(:header_content) do .nav-controls - = link_to _('New label'), new_group_label_path(@group), class: "btn btn-new" + = link_to _('New label'), new_group_label_path(@group), class: "btn btn-success" - if @labels.exists? || search.present? #promote-label-modal @@ -22,6 +22,7 @@ %span.input-group-append %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } = icon("search") + = render 'shared/labels/sort_dropdown' .labels-container.prepend-top-5 - if @labels.any? diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml index 6d35457a0ec..39e3af5f6d2 100644 --- a/app/views/groups/milestones/_form.html.haml +++ b/app/views/groups/milestones/_form.html.haml @@ -19,9 +19,9 @@ .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-create btn" + = f.submit 'Create milestone', class: "btn-success btn" = link_to "Cancel", group_milestones_path(@group), class: "btn btn-cancel" - else - = f.submit 'Update milestone', class: "btn-create btn" + = f.submit 'Update milestone', class: "btn-success btn" = link_to "Cancel", group_milestone_path(@group, @milestone), class: "btn btn-cancel" diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml index b6424df55cd..af4fe8f2ef8 100644 --- a/app/views/groups/milestones/index.html.haml +++ b/app/views/groups/milestones/index.html.haml @@ -6,7 +6,7 @@ .nav-controls = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @group) - = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-new" + = link_to "New milestone", new_group_milestone_path(@group), class: "btn btn-success" .milestones %ul.content-list diff --git a/app/views/groups/new.html.haml b/app/views/groups/new.html.haml index 53f54db1ddf..683129fdf6e 100644 --- a/app/views/groups/new.html.haml +++ b/app/views/groups/new.html.haml @@ -36,5 +36,5 @@ = render 'shared/group_tips' .form-actions - = f.submit 'Create group', class: "btn btn-create" + = f.submit 'Create group', class: "btn btn-success" = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml index ffce2d4b14f..8dc88ec446c 100644 --- a/app/views/groups/settings/_permissions.html.haml +++ b/app/views/groups/settings/_permissions.html.haml @@ -10,7 +10,7 @@ = render 'shared/allow_request_access', form: f .form-group.row - %label.col-form-label.col-sm-2 + %label.col-form-label.col-sm-2.pt-0 = s_('GroupSettings|Share with group lock') .col-sm-10 .form-check diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml index 5a88619f769..f1bd817f17a 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -7,11 +7,10 @@ = render 'groups/home_panel' -.groups-header{ class: container_class } - .group-nav-container - .nav-controls.clearfix +.groups-listing{ class: container_class, data: { endpoints: { default: group_children_path(@group, format: :json), shared: group_shared_projects_path(@group, format: :json) } } } + .top-area.group-nav-container + .group-search = render "shared/groups/search_form" - = render "shared/groups/dropdown", show_archive_options: true - if can? current_user, :create_projects, @group - new_project_label = _("New project") - new_subgroup_label = _("New subgroup") @@ -39,7 +38,29 @@ - else = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success" - - if params[:filter].blank? && !@has_children - = render "shared/groups/empty_state" - - else - = render "children", children: @children, group: @group + .scrolling-tabs-container.inner-page-scroll-tabs + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + %ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs + %li.js-subgroups_and_projects-tab + = link_to group_path, data: { target: 'div#subgroups_and_projects', action: 'subgroups_and_projects', toggle: 'tab'} do + = _("Subgroups and projects") + %li.js-shared-tab + = link_to group_shared_path, data: { target: 'div#shared', action: 'shared', toggle: 'tab'} do + = _("Shared projects") + %li.js-archived-tab + = link_to group_archived_path, data: { target: 'div#archived', action: 'archived', toggle: 'tab'} do + = _("Archived projects") + + .nav-controls + = render "shared/groups/dropdown" + + .tab-content + #subgroups_and_projects.tab-pane + = render "subgroups_and_projects", group: @group + + #shared.tab-pane + = render "shared_projects", group: @group + + #archived.tab-pane + = render "archived_projects", group: @group diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml index bdd77730dcc..94c25edaf82 100644 --- a/app/views/help/instance_configuration/_gitlab_pages.html.haml +++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml @@ -8,7 +8,7 @@ %p Below are the settings for - = succeed('.') { link_to('Gitlab Pages', gitlab_pages[:url], target: '_blank') } + = succeed('.') { link_to('GitLab Pages', gitlab_pages[:url], target: '_blank') } .table-responsive %table %thead diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml index b32b602ceb3..506f580b246 100644 --- a/app/views/help/ui.html.haml +++ b/app/views/help/ui.html.haml @@ -189,7 +189,7 @@ %li = link_to 'Sort by date', '#' - = link_to 'New issue', '#', class: 'btn btn-new btn-inverted' + = link_to 'New issue', '#', class: 'btn btn-success btn-inverted' .lead Only nav links without button and search diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml index b54b1af1e0c..626080c284b 100644 --- a/app/views/import/fogbugz/new.html.haml +++ b/app/views/import/fogbugz/new.html.haml @@ -21,4 +21,4 @@ .col-md-4 = password_field_tag :password, nil, class: 'form-control' .form-actions - = submit_tag _('Continue to the next step'), class: 'btn btn-create' + = submit_tag _('Continue to the next step'), class: 'btn btn-success' diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml index ff2f989c509..8ed9dc68bb3 100644 --- a/app/views/import/fogbugz/new_user_map.html.haml +++ b/app/views/import/fogbugz/new_user_map.html.haml @@ -39,4 +39,4 @@ scope: :all, email_user: true, selected: user[:gitlab_user]) .form-actions - = submit_tag _('Continue to the next step'), class: 'btn btn-create' + = submit_tag _('Continue to the next step'), class: 'btn btn-success' diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml index 2b3102f9af9..a88b04eccbb 100644 --- a/app/views/import/gitea/new.html.haml +++ b/app/views/import/gitea/new.html.haml @@ -19,4 +19,4 @@ .col-sm-4 = text_field_tag :personal_access_token, nil, class: 'form-control' .form-actions - = submit_tag _('List Your Gitea Repositories'), class: 'btn btn-create' + = submit_tag _('List Your Gitea Repositories'), class: 'btn btn-success' diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml index 4225ee19217..877d945a09b 100644 --- a/app/views/import/gitlab_projects/new.html.haml +++ b/app/views/import/gitlab_projects/new.html.haml @@ -8,8 +8,11 @@ = form_tag import_gitlab_project_path, class: 'new_project', multipart: true do .row + .form-group.project-name.col-sm-12 + = label_tag :name, _('Project name'), class: 'label-bold' + = text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control input-lg", autofocus: true, required: true .form-group.col-12.col-sm-6 - = label_tag :namespace_id, 'Project path', class: 'label-bold' + = label_tag :namespace_id, _('Project URL'), class: 'label-bold' .form-group .input-group - if current_user.can_select_namespace? @@ -24,8 +27,8 @@ #{user_url(current_user.username)}/ = hidden_field_tag :namespace_id, value: current_user.namespace_id .form-group.col-12.col-sm-6.project-path - = label_tag :path, _('Project name'), class: 'label-bold' - = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, autofocus: true, required: true + = label_tag :path, _('Project slug'), class: 'label-bold' + = text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control", tabindex: 2, required: true .row .form-group.col-md-12 @@ -38,5 +41,5 @@ = file_field_tag :file, class: '' .row .form-actions.col-sm-12 - = submit_tag _('Import project'), class: 'btn btn-create' + = submit_tag _('Import project'), class: 'btn btn-success' = link_to _('Cancel'), new_project_path, class: 'btn btn-cancel' diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml index fd6e4726fc5..7a6ad28f0aa 100644 --- a/app/views/import/google_code/new.html.haml +++ b/app/views/import/google_code/new.html.haml @@ -59,4 +59,4 @@ = _('Yes, let me map Google Code users to full names or GitLab users.') %li %p - = submit_tag _('Continue to the next step'), class: "btn btn-create" + = submit_tag _('Continue to the next step'), class: "btn btn-success" diff --git a/app/views/import/google_code/new_user_map.html.haml b/app/views/import/google_code/new_user_map.html.haml index baaaf6bdc63..f523b993aa7 100644 --- a/app/views/import/google_code/new_user_map.html.haml +++ b/app/views/import/google_code/new_user_map.html.haml @@ -33,4 +33,4 @@ = text_area_tag :user_map, JSON.pretty_generate(@user_map), class: 'form-control', rows: 15 .form-actions - = submit_tag _('Continue to the next step'), class: "btn btn-create" + = submit_tag _('Continue to the next step'), class: "btn btn-success" diff --git a/app/views/instance_statistics/cohorts/index.html.haml b/app/views/instance_statistics/cohorts/index.html.haml index 5e9a8c083af..e135bab10d8 100644 --- a/app/views/instance_statistics/cohorts/index.html.haml +++ b/app/views/instance_statistics/cohorts/index.html.haml @@ -1,16 +1,16 @@ -- breadcrumb_title "Cohorts" +- breadcrumb_title _("Cohorts") - @no_container = true %div{ class: container_class } - if @cohorts = render 'cohorts_table' - = render 'usage_ping' - else .bs-callout.bs-callout-warning.clearfix %p - User cohorts are only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping'), target: '_blank' - is enabled. To enable it and see user cohorts, - visit - = succeed '.' do - = link_to 'application settings', admin_application_settings_path(anchor: 'usage-statistics') + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('User Cohorts are only shown when the %{usage_ping_link_start}usage ping%{usage_ping_link_end} is enabled.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + - application_settings_path = admin_application_settings_path(anchor: 'usage-statistics') + - application_settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: application_settings_path } + = s_('To enable it and see User Cohorts, visit %{application_settings_link_start}application settings%{application_settings_link_end}.').html_safe % { application_settings_link_start: application_settings_link_start, application_settings_link_end: '</a>'.html_safe } diff --git a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml index 0a741b50960..0a5717f75e1 100644 --- a/app/views/instance_statistics/conversational_development_index/_disabled.html.haml +++ b/app/views/instance_statistics/conversational_development_index/_disabled.html.haml @@ -1,9 +1,14 @@ .container.convdev-empty .col-sm-12.justify-content-center.text-center = custom_icon('convdev_no_index') - %h4 Usage ping is not enabled - %p - ConvDev is only shown when the - = link_to 'usage ping', help_page_path('user/admin_area/settings/usage_statistics'), target: '_blank' - is enabled. Enable usage ping to get an overview of how you are using GitLab from a feature perspective - = link_to 'Enable usage ping', admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' + %h4= _('Usage ping is not enabled') + - if !current_user.admin? + %p + - usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping') + - usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path } + = s_('In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe } + - if current_user.admin? + %p + = _('Enable usage ping to get an overview of how you are using GitLab from a feature perspective.') + - if current_user.admin? + = link_to _('Enable usage ping'), admin_application_settings_path(anchor: 'usage-statistics'), class: 'btn btn-primary' diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml index dd63b98376f..1e7db4982d6 100644 --- a/app/views/instance_statistics/conversational_development_index/index.html.haml +++ b/app/views/instance_statistics/conversational_development_index/index.html.haml @@ -1,12 +1,13 @@ - @no_container = true -- page_title 'ConvDev Index' +- page_title _('ConvDev Index') +- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled .container - - if show_callout?('convdev_intro_callout_dismissed') + - if usage_ping_enabled && show_callout?('convdev_intro_callout_dismissed') = render 'callout' .prepend-top-default - - if !Gitlab::CurrentSettings.usage_ping_enabled + - if !usage_ping_enabled = render 'disabled' - elsif @metric.blank? = render 'no_data' diff --git a/app/views/issues/_issues_calendar.ics.ruby b/app/views/issues/_issues_calendar.ics.ruby index 3563635d33d..73ab8489e0c 100644 --- a/app/views/issues/_issues_calendar.ics.ruby +++ b/app/views/issues/_issues_calendar.ics.ruby @@ -2,6 +2,7 @@ cal = Icalendar::Calendar.new cal.prodid = '-//GitLab//NONSGML GitLab//EN' cal.x_wr_calname = 'GitLab Issues' +# rubocop: disable CodeReuse/ActiveRecord @issues.includes(project: :namespace).each do |issue| cal.event do |event| event.dtstart = Icalendar::Values::Date.new(issue.due_date) @@ -11,5 +12,6 @@ cal.x_wr_calname = 'GitLab Issues' event.transp = 'TRANSPARENT' end end +# rubocop: enable CodeReuse/ActiveRecord cal.to_ical diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml index f67a8878c80..a41d30da450 100644 --- a/app/views/layouts/_page.html.haml +++ b/app/views/layouts/_page.html.haml @@ -8,6 +8,7 @@ = render "layouts/broadcast" = render 'layouts/header/read_only_banner' = yield :flash_message + = render "shared/ping_consent" - unless @hide_breadcrumbs = render "layouts/nav/breadcrumbs" = render "layouts/flash" diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index ff25b040913..f912a32ee1a 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -7,14 +7,14 @@ .sidebar-context-title = _('Admin Area') %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do + = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: {class: 'home'}) do = link_to admin_root_path, class: 'shortcuts-tree' do .nav-icon-container = sprite_icon('overview') %span.nav-item-name = _('Overview') %ul.sidebar-sub-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers), html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers), html_options: { class: "fly-out-top-item" } ) do = link_to admin_root_path do %strong.fly-out-top-item-name = _('Overview') @@ -23,7 +23,7 @@ = link_to admin_root_path, title: _('Overview') do %span = _('Dashboard') - = nav_link(controller: [:admin, :projects]) do + = nav_link(controller: [:admin, 'admin/projects']) do = link_to admin_projects_path, title: _('Projects') do %span = _('Projects') @@ -199,10 +199,54 @@ = sprite_icon('settings') %span.nav-item-name = _('Settings') - %ul.sidebar-sub-level-items.is-fly-out-only + + %ul.sidebar-sub-level-items = nav_link(controller: :application_settings, html_options: { class: "fly-out-top-item" } ) do = link_to admin_application_settings_path do %strong.fly-out-top-item-name = _('Settings') + %li.divider.fly-out-top-item + = nav_link(path: 'application_settings#show') do + = link_to admin_application_settings_path, title: _('General') do + %span + = _('General') + = nav_link(path: 'application_settings#integrations') do + = link_to integrations_admin_application_settings_path, title: _('Integrations') do + %span + = _('Integrations') + = nav_link(path: 'application_settings#repository') do + = link_to repository_admin_application_settings_path, title: _('Repository') do + %span + = _('Repository') + - if template_exists?('admin/application_settings/templates') + = nav_link(path: 'application_settings#templates') do + = link_to templates_admin_application_settings_path, title: _('Templates') do + %span + = _('Templates') + = nav_link(path: 'application_settings#ci_cd') do + = link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do + %span + = _('CI/CD') + = nav_link(path: 'application_settings#reporting') do + = link_to reporting_admin_application_settings_path, title: _('Reporting') do + %span + = _('Reporting') + = nav_link(path: 'application_settings#metrics_and_profiling') do + = link_to metrics_and_profiling_admin_application_settings_path, title: _('Metrics and profiling') do + %span + = _('Metrics and profiling') + = nav_link(path: 'application_settings#network') do + = link_to network_admin_application_settings_path, title: _('Network') do + %span + = _('Network') + - if template_exists?('admin/application_settings/geo') + = nav_link(path: 'application_settings#geo') do + = link_to geo_admin_application_settings_path, title: _('Geo') do + %span + = _('Geo') + = nav_link(path: 'application_settings#preferences') do + = link_to preferences_admin_application_settings_path, title: _('Preferences') do + %span + = _('Preferences') = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml index b8ff448f261..57180f27146 100644 --- a/app/views/layouts/nav/sidebar/_instance_statistics.html.haml +++ b/app/views/layouts/nav/sidebar/_instance_statistics.html.haml @@ -18,16 +18,17 @@ %strong.fly-out-top-item-name = _('ConvDev Index') - = nav_link(controller: :cohorts) do - = link_to instance_statistics_cohorts_path do - .nav-icon-container - = sprite_icon('users') - %span.nav-item-name - = _('Cohorts') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do - = link_to instance_statistics_cohorts_path do - %strong.fly-out-top-item-name - = _('Cohorts') + - if Gitlab::CurrentSettings.usage_ping_enabled + = nav_link(controller: :cohorts) do + = link_to instance_statistics_cohorts_path do + .nav-icon-container + = sprite_icon('users') + %span.nav-item-name + = _('Cohorts') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: :cohorts, html_options: { class: "fly-out-top-item" } ) do + = link_to instance_statistics_cohorts_path do + %strong.fly-out-top-item-name + = _('Cohorts') = render 'shared/sidebar_toggle_button' diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml index d65f153b451..69167edb1df 100644 --- a/app/views/layouts/nav/sidebar/_profile.html.haml +++ b/app/views/layouts/nav/sidebar/_profile.html.haml @@ -28,18 +28,17 @@ = link_to profile_account_path do %strong.fly-out-top-item-name = _('Account') - - if Gitlab::CurrentSettings.user_oauth_applications? - = nav_link(controller: 'oauth/applications') do - = link_to applications_profile_path do - .nav-icon-container - = sprite_icon('applications') - %span.nav-item-name - = _('Applications') - %ul.sidebar-sub-level-items.is-fly-out-only - = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do - = link_to applications_profile_path do - %strong.fly-out-top-item-name - = _('Applications') + = nav_link(controller: 'oauth/applications') do + = link_to applications_profile_path do + .nav-icon-container + = sprite_icon('applications') + %span.nav-item-name + = _('Applications') + %ul.sidebar-sub-level-items.is-fly-out-only + = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do + = link_to applications_profile_path do + %strong.fly-out-top-item-name + = _('Applications') = nav_link(controller: :chat_names) do = link_to profile_chat_names_path do .nav-icon-container diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 30e0e9fca27..25cd53b378a 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -158,7 +158,7 @@ - if project_nav_tab? :pipelines = nav_link(controller: [:pipelines, :builds, :jobs, :pipeline_schedules, :artifacts]) do - = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines' do + = link_to project_pipelines_path(@project), class: 'shortcuts-pipelines qa-link-pipelines' do .nav-icon-container = sprite_icon('rocket') %span.nav-item-name @@ -245,7 +245,7 @@ = link_to _('Auto DevOps'), help_page_path('topics/autodevops/index.md') %span= _('uses Kubernetes clusters to deploy your code!') %hr - %button.btn.btn-create.btn-sm.dismiss-feature-highlight{ type: 'button' } + %button.btn.btn-success.btn-sm.dismiss-feature-highlight{ type: 'button' } %span= _("Got it!") = sprite_icon('thumb-up') diff --git a/app/views/notify/_failed_builds.html.haml b/app/views/notify/_failed_builds.html.haml new file mode 100644 index 00000000000..7c563bb016c --- /dev/null +++ b/app/views/notify/_failed_builds.html.haml @@ -0,0 +1,32 @@ +%tr + %td{ colspan: 2, style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 0 8px 16px; text-align: center;" } + had + = failed.size + failed + #{'build'.pluralize(failed.size)}. +%tr.table-warning + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; border: 1px solid #ededed; border-bottom: 0; border-radius: 4px 4px 0 0; overflow: hidden; background-color: #fdf4f6; color: #d22852; font-size: 14px; line-height: 1.4; text-align: center; padding: 8px 16px;" } + Logs may contain sensitive data. Please consider before forwarding this email. +%tr.section + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 16px; border: 1px solid #ededed; border-radius: 4px; overflow: hidden; border-top: 0; border-radius: 0 0 4px 4px;" } + %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width: 100%; border-collapse: collapse;" } + %tbody + - failed.each do |build| + %tr.build-state + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse;" } + %tbody + %tr + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #d22f57; font-weight: 500; font-size: 16px; vertical-align: middle; padding-right: 8px; line-height: 10px" } + %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display: block;", width: "10" }/ + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #8c8c8c; font-weight: 500; font-size: 14px; vertical-align: middle;" } + = build.stage + %td{ align: "right", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 16px 0; color: #8c8c8c; font-weight: 500; font-size: 14px;" } + = render "notify/links/#{build.to_partial_path}", pipeline: pipeline, build: build + %tr.build-log + - if build.has_trace? + %td{ colspan: "2", style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 0 0 16px;" } + %pre{ style: "font-family: Monaco,'Lucida Console','Courier New',Courier,monospace; background-color: #fafafa; border-radius: 4px; overflow: hidden; white-space: pre-wrap; word-break: break-all; font-size:13px; line-height: 1.4; padding: 16px 8px; color: #333333; margin: 0;" } + = build.trace.html(last_lines: 10).html_safe + - else + %td{ colspan: "2" } diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml new file mode 100644 index 00000000000..65a2f75a3e2 --- /dev/null +++ b/app/views/notify/autodevops_disabled_email.html.haml @@ -0,0 +1,49 @@ +%tr.alert + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; padding: 8px 16px; border-radius: 4px; font-size: 14px; line-height: 1.3; text-align: center; overflow: hidden; background-color: #d22f57; color: #ffffff;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse: collapse; margin: 0 auto;" } + %tbody + %tr + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; vertical-align: middle; color: #ffffff; text-align: center;" } + Auto DevOps pipeline was disabled for #{@project.name} + +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.7; padding: 16px 8px 0;" } + The Auto DevOps pipeline failed for pipeline + %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration:none;" } + = "\##{@pipeline.iid}" + and has been disabled for + %a{ href: project_url(@project), style: "color: #1b69b6; text-decoration: none;" } + = @project.name + "." + In order to use the Auto DevOps pipeline with your project, please review the + %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: "color:#1b69b6;text-decoration:none;" } currently supported languages, + adjust your project accordingly, and turn on the Auto DevOps pipeline within your + %a{ href: project_settings_ci_cd_url(@project), style: "color: #1b69b6; text-decoration: none;" } + CI/CD project settings. + +%tr.pre-section + %td{ style: 'text-align: center;border-bottom:1px solid #ededed' } + %a{ href: 'https://docs.gitlab.com/ee/topics/autodevops/', style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;" } + %button{ type: 'button', style: 'border-color: #dfdfdf; border-style: solid; border-width: 1px; border-radius: 4px; font-size: 14px; padding: 8px 16px; background-color:#fff; margin: 8px 0; cursor: pointer;' } + Learn more about Auto DevOps + +%tr.pre-section + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color: #333333; font-size: 14px; font-weight: 400; line-height: 1.4; padding: 16px 8px; text-align: center;" } + %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;margin:0 auto;" } + %tbody + %tr + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size:14px; font-weight:500;line-height: 1.4; vertical-align: baseline;" } + Pipeline + %a{ href: pipeline_url(@pipeline), style: "color: #1b69b6; text-decoration: none;" } + = "\##{@pipeline.id}" + triggered by + - if @pipeline.user + %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 15px; line-height: 1.4; vertical-align: middle; padding-right: 8px; padding-left:8px", width: "24" } + %img.avatar{ height: "24", src: avatar_icon_for_user(@pipeline.user, 24, only_path: false), style: "display: block; border-radius: 12px; margin: -2px 0;", width: "24", alt: "" }/ + %td{ style: "font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; font-size: 14px; font-weight: 500; line-height: 1.4; vertical-align: baseline;" } + %a.muted{ href: user_url(@pipeline.user), style: "color: #333333; text-decoration: none;" } + = @pipeline.user.name + - else + %td{ style: "font-family: 'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace; font-size: 14px; line-height: 1.4; vertical-align: baseline; padding:0 8px;" } + API + += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed diff --git a/app/views/notify/autodevops_disabled_email.text.erb b/app/views/notify/autodevops_disabled_email.text.erb new file mode 100644 index 00000000000..695780c3145 --- /dev/null +++ b/app/views/notify/autodevops_disabled_email.text.erb @@ -0,0 +1,20 @@ +Auto DevOps pipeline was disabled for <%= @project.name %> + +The Auto DevOps pipeline failed for pipeline <%= @pipeline.iid %> (<%= pipeline_url(@pipeline) %>) and has been disabled for <%= @project.name %>. In order to use the Auto DevOps pipeline with your project, please review the currently supported languagues (https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages), adjust your project accordingly, and turn on the Auto DevOps pipeline within your CI/CD project settings (<%= project_settings_ci_cd_url(@project) %>). + +<% if @pipeline.user -%> + Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by <%= @pipeline.user.name %> ( <%= user_url(@pipeline.user) %> ) +<% else -%> + Pipeline #<%= @pipeline.id %> ( <%= pipeline_url(@pipeline) %> ) triggered by API +<% end -%> +<% failed = @pipeline.statuses.latest.failed -%> +had <%= failed.size %> failed <%= 'build'.pluralize(failed.size) %>. + +<% failed.each do |build| -%> + <%= render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build %> + Stage: <%= build.stage %> + Name: <%= build.name %> + <% if build.has_trace? -%> + Trace: <%= build.trace.raw(last_lines: 10) %> + <% end -%> +<% end -%> diff --git a/app/views/notify/pipeline_failed_email.html.haml b/app/views/notify/pipeline_failed_email.html.haml index baafaa6e3a0..86dcca4a447 100644 --- a/app/views/notify/pipeline_failed_email.html.haml +++ b/app/views/notify/pipeline_failed_email.html.haml @@ -107,36 +107,5 @@ - else %td{ style: "font-family:'Menlo','Liberation Mono','Consolas','DejaVu Sans Mono','Ubuntu Mono','Courier New','andale mono','lucida console',monospace;font-size:14px;line-height:1.4;vertical-align:baseline;padding:0 5px;" } API -- failed = @pipeline.statuses.latest.failed -%tr - %td{ colspan: 2, style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#333333;font-size:15px;font-weight:300;line-height:1.4;padding:15px 5px;text-align:center;" } - had - = failed.size - failed - #{'build'.pluralize(failed.size)}. -%tr.table-warning - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;border:1px solid #ededed;border-bottom:0;border-radius:3px 3px 0 0;overflow:hidden;background-color:#fdf4f6;color:#d22852;font-size:14px;line-height:1.4;text-align:center;padding:8px 15px;" } - Logs may contain sensitive data. Please consider before forwarding this email. -%tr.section - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 15px;border:1px solid #ededed;border-radius:3px;overflow:hidden;border-top:0;border-radius:0 0 3px 3px;" } - %table.builds{ border: "0", cellpadding: "0", cellspacing: "0", style: "width:100%;border-collapse:collapse;" } - %tbody - - failed.each do |build| - %tr.build-state - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - %table.img{ border: "0", cellpadding: "0", cellspacing: "0", style: "border-collapse:collapse;" } - %tbody - %tr - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#d22f57;font-weight:500;font-size:15px;vertical-align:middle;padding-right:5px;line-height:10px" } - %img{ alt: "✖", height: "10", src: image_url('mailers/ci_pipeline_notif_v1/icon-x-red.gif'), style: "display:block;", width: "10" }/ - %td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;color:#8c8c8c;font-weight:500;font-size:15px;vertical-align:middle;" } - = build.stage - %td{ align: "right", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:20px 0;color:#8c8c8c;font-weight:500;font-size:15px;" } - = render "notify/links/#{build.to_partial_path}", pipeline: @pipeline, build: build - %tr.build-log - - if build.has_trace? - %td{ colspan: "2", style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:0 0 15px;" } - %pre{ style: "font-family:Monaco,'Lucida Console','Courier New',Courier,monospace;background-color:#fafafa;border-radius:3px;overflow:hidden;white-space:pre-wrap;word-break:break-all;font-size:13px;line-height:1.4;padding:12px;color:#333333;margin:0;" } - = build.trace.html(last_lines: 10).html_safe - - else - %td{ colspan: "2" } + += render 'notify/failed_builds', pipeline: @pipeline, failed: @pipeline.statuses.latest.failed diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml index 04a19ab14dd..1823f191fb3 100644 --- a/app/views/profiles/emails/index.html.haml +++ b/app/views/profiles/emails/index.html.haml @@ -15,14 +15,16 @@ = f.label :email, class: 'label-bold' = f.text_field :email, class: 'form-control' .prepend-top-default - = f.submit 'Add email address', class: 'btn btn-create' + = f.submit 'Add email address', class: 'btn btn-success' %hr %h4.prepend-top-0 Linked emails (#{@emails.count + 1}) .account-well.append-bottom-default %ul %li - Your Primary Email will be used for avatar detection and web based operations, such as edits and merges. + Your Primary Email will be used for avatar detection. + %li + Your Commit Email will be used for web based operations, such as edits and merges. %li Your Notification Email will be used for account notifications. %li @@ -34,6 +36,8 @@ = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } %span.float-right %span.badge.badge-success Primary email + - if @primary_email === current_user.commit_email + %span.badge.badge-info Commit email - if @primary_email === current_user.public_email %span.badge.badge-info Public email - if @primary_email === current_user.notification_email @@ -42,6 +46,8 @@ %li = render partial: 'shared/email_with_badge', locals: { email: email.email, verified: email.confirmed? } %span.float-right + - if email.email === current_user.commit_email + %span.badge.badge-info Commit email - if email.email === current_user.public_email %span.badge.badge-info Public email - if email.email === current_user.notification_email diff --git a/app/views/profiles/gpg_keys/_form.html.haml b/app/views/profiles/gpg_keys/_form.html.haml index aa9b0aad034..6c4cb614a2b 100644 --- a/app/views/profiles/gpg_keys/_form.html.haml +++ b/app/views/profiles/gpg_keys/_form.html.haml @@ -7,4 +7,4 @@ = f.text_area :key, class: "form-control", rows: 8, required: true, placeholder: "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'." .prepend-top-default - = f.submit 'Add key', class: "btn btn-create" + = f.submit 'Add key', class: "btn btn-success" diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml index 5207921d6fe..21eef08983c 100644 --- a/app/views/profiles/keys/_form.html.haml +++ b/app/views/profiles/keys/_form.html.haml @@ -5,10 +5,10 @@ .form-group = f.label :key, class: 'label-bold' %p= _("Paste your public SSH key, which is usually contained in the file '~/.ssh/id_rsa.pub' and begins with 'ssh-rsa'. Don't use your private SSH key.") - = f.text_area :key, class: "form-control js-add-ssh-key-validation-input", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"') + = f.text_area :key, class: "form-control js-add-ssh-key-validation-input qa-key-public-key-field", rows: 8, required: true, placeholder: s_('Profiles|Typically starts with "ssh-rsa …"') .form-group = f.label :title, class: 'label-bold' - = f.text_field :title, class: "form-control input-lg", required: true, placeholder: s_('Profiles|e.g. My MacBook key') + = f.text_field :title, class: "form-control input-lg qa-key-title-field", required: true, placeholder: s_('Profiles|e.g. My MacBook key') %p.form-text.text-muted= _('Name your individual key via a title') .js-add-ssh-key-validation-warning.hide @@ -16,7 +16,7 @@ %strong= _('Oops, are you sure?') %p= s_("Profiles|This doesn't look like a public SSH key, are you sure you want to add it?") - %button.btn.btn-create.js-add-ssh-key-validation-confirm-submit= _("Yes, add it") + %button.btn.btn-success.js-add-ssh-key-validation-confirm-submit= _("Yes, add it") .prepend-top-default - = f.submit s_('Profiles|Add key'), class: "btn btn-create js-add-ssh-key-validation-original-submit" + = f.submit s_('Profiles|Add key'), class: "btn btn-success js-add-ssh-key-validation-original-submit qa-add-key-button" diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml index 2ac514d3f6f..88473c7f72d 100644 --- a/app/views/profiles/keys/_key_details.html.haml +++ b/app/views/profiles/keys/_key_details.html.haml @@ -24,4 +24,4 @@ = @key.key .col-md-12 .float-right - = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key" + = link_to 'Remove', path_to_key(@key, is_admin), data: {confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove delete-key qa-delete-key-button" diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml index 9c8cc9c059b..0b4b9841ea1 100644 --- a/app/views/profiles/passwords/edit.html.haml +++ b/app/views/profiles/passwords/edit.html.haml @@ -29,6 +29,6 @@ = f.label :password_confirmation, class: 'label-bold' = f.password_field :password_confirmation, required: true, class: 'form-control' .prepend-top-default.append-bottom-default - = f.submit 'Save password', class: "btn btn-create append-right-10" + = f.submit 'Save password', class: "btn btn-success append-right-10" - unless @user.password_automatically_set? = link_to "I forgot my password", reset_profile_password_path, method: :put, class: "account-btn-link" diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml index 2176d7f8a31..d265f3c44ba 100644 --- a/app/views/profiles/passwords/new.html.haml +++ b/app/views/profiles/passwords/new.html.haml @@ -1,6 +1,6 @@ - page_title "New Password" - header_title "New Password" -%h3.page-title Setup new password +%h3.page-title Set up new password %hr = form_for @user, url: profile_password_path, method: :post do |f| %p.slead @@ -22,4 +22,4 @@ .col-sm-10 = f.password_field :password_confirmation, required: true, class: 'form-control' .form-actions - = f.submit 'Set new password', class: "btn btn-create" + = f.submit 'Set new password', class: "btn btn-success" diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index fd6dd74e1c5..156c0d05b02 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -58,4 +58,4 @@ .form-text.text-muted Choose what content you want to see on a project’s overview page .form-group - = f.submit 'Save changes', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-success' diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml index 6f08a294c5d..51f5ecf2166 100644 --- a/app/views/profiles/show.html.haml +++ b/app/views/profiles/show.html.haml @@ -1,5 +1,6 @@ -- breadcrumb_title "Edit Profile" +- breadcrumb_title s_("Profiles|Edit Profile") - @content_class = "limit-container-width" unless fluid_layout +- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f| = form_errors(@user) @@ -7,34 +8,36 @@ .row .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Public Avatar + = s_("Profiles|Public Avatar") %p - if @user.avatar? - You can change your avatar here - if gravatar_enabled? - or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} + = s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link } + - else + = s_("Profiles|You can change your avatar here") - else - You can upload an avatar here - if gravatar_enabled? - or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host} + = s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link } + - else + = s_("Profiles|You can upload your avatar here") .col-lg-8 .clearfix.avatar-image.append-bottom-default = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do = image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160' - %h5.prepend-top-0= _("Upload new avatar") + %h5.prepend-top-0= s_("Profiles|Upload new avatar") .prepend-top-5.append-bottom-10 - %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...") - %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen") + %button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...") + %span.avatar-file-name.prepend-left-default.js-avatar-filename= s_("Profiles|No file chosen") = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*' - .form-text.text-muted= _("The maximum file size allowed is 200KB.") + .form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.") - if @user.avatar? %hr - = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted' + = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'btn btn-danger btn-inverted' %hr .row .col-lg-4.profile-settings-sidebar - %h4.prepend-top-0= s_("User|Current status") + %h4.prepend-top-0= s_("Profiles|Current status") %p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.") .col-lg-8 = f.fields_for :status, @user.status do |status_form| @@ -66,62 +69,69 @@ .row .col-lg-4.profile-settings-sidebar %h4.prepend-top-0 - Main settings + = s_("Profiles|Main settings") %p - This information will appear on your profile. + = s_("Profiles|This information will appear on your profile.") - if current_user.ldap_user? - Some options are unavailable for LDAP accounts + = s_("Profiles|Some options are unavailable for LDAP accounts") .col-lg-8 .row - if @user.read_only_attribute?(:name) = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' }, - help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you." + help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) } - else - = f.text_field :name, required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." + = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you." = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' } - if @user.read_only_attribute?(:email) - = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account." + = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) } - else = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?), help: user_email_help_text(@user) = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), - { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' }, + { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") }, + control_class: 'select2' + = f.select :commit_email, options_for_select(@user.verified_emails, selected: @user.commit_email), + { help: 'This email will be used for web based operations, such as edits and merges.' }, control_class: 'select2' = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] }, - { help: 'This feature is experimental and translations are not complete yet.' }, + { help: s_("Profiles|This feature is experimental and translations are not complete yet.") }, control_class: 'select2' = f.text_field :skype = f.text_field :linkedin = f.text_field :twitter - = f.text_field :website_url, label: 'Website' + = f.text_field :website_url, label: s_("Profiles|Website") - if @user.read_only_attribute?(:location) - = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account." + = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) } - else = f.text_field :location = f.text_field :organization - = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.' + = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.") %hr - %h5 Private profile + %h5= ("Private profile") - private_profile_label = capture do - Don't display activity-related personal information on your profile + = s_("Profiles|Don't display activity-related personal information on your profiles") = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile') = f.check_box :private_profile, label: private_profile_label + %h5= s_("Profiles|Private contributions") + = f.check_box :include_private_contributions, label: 'Include private contributions on my profile' + .help-block + = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.") .prepend-top-default.append-bottom-default - = f.submit 'Update profile settings', class: 'btn btn-success' - = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel' + = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success' + = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel' .modal.modal-profile-crop .modal-dialog .modal-content .modal-header %h4.modal-title - Position and size your new avatar - %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') } + = s_("Profiles|Position and size your new avatar") + %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") } %span{ "aria-hidden": true } × .modal-body .profile-crop-image-container - %img.modal-profile-crop-image{ alt: 'Avatar cropper' } + %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") } .crop-controls .btn-group %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } } @@ -130,4 +140,4 @@ %span.fa.fa-search-minus .modal-footer %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' } - Set new profile picture + = s_("Profiles|Set new profile picture") diff --git a/app/views/profiles/two_factor_auths/_codes.html.haml b/app/views/profiles/two_factor_auths/_codes.html.haml index 93722d7b034..fb4fff12027 100644 --- a/app/views/profiles/two_factor_auths/_codes.html.haml +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -10,4 +10,6 @@ %li %span.monospace= code -= link_to 'Proceed', profile_account_path, class: 'btn btn-success' +.d-flex + = link_to 'Proceed', profile_account_path, class: 'btn btn-success append-right-10' + = link_to 'Download codes', "data:text/plain;charset=utf-8,#{CGI.escape(@codes.join("\n"))}", download: "gitlab-recovery-codes.txt", class: 'btn btn-default' diff --git a/app/views/projects/_commit_button.html.haml b/app/views/projects/_commit_button.html.haml index b387e38c1a6..1e27c71d20d 100644 --- a/app/views/projects/_commit_button.html.haml +++ b/app/views/projects/_commit_button.html.haml @@ -1,5 +1,5 @@ .form-actions - = button_tag 'Commit changes', class: 'btn commit-btn js-commit-button btn-create' + = button_tag 'Commit changes', class: 'btn commit-btn js-commit-button btn-success' = link_to 'Cancel', cancel_path, class: 'btn btn-cancel', data: {confirm: leave_edit_message} diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml index 0175b519867..7a5fff96676 100644 --- a/app/views/projects/_flash_messages.html.haml +++ b/app/views/projects/_flash_messages.html.haml @@ -5,3 +5,4 @@ - if current_user && can?(current_user, :download_code, project) = render 'shared/no_ssh' = render 'shared/no_password' + = render 'shared/auto_devops_implicitly_enabled_banner', project: project diff --git a/app/views/projects/_fork_suggestion.html.haml b/app/views/projects/_fork_suggestion.html.haml index c855bfaf067..0b616a0c1ce 100644 --- a/app/views/projects/_fork_suggestion.html.haml +++ b/app/views/projects/_fork_suggestion.html.haml @@ -6,6 +6,6 @@ edit files in this project directly. Please fork this project, make your changes there, and submit a merge request. - = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-new' + = link_to 'Fork', nil, method: :post, class: 'js-fork-suggestion-button btn btn-grouped btn-inverted btn-success' %button.js-cancel-fork-suggestion-button.btn.btn-grouped{ type: 'button' } Cancel diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 1b6c4193c4d..ced6a2a0399 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,16 +1,35 @@ - empty_repo = @project.empty_repo? -.project-home-panel.text-center{ class: ("empty-project" if empty_repo) } +- license = @project.license_anchor_data +.project-home-panel{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } - .avatar-container.s70.project-avatar - = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile', width: 70, height: 70) - %h1.project-title.qa-project-name - = @project.name - %span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } - = visibility_level_icon(@project.visibility_level, fw: false) + .project-header.d-flex.flex-row.flex-wrap.align-items-center.append-bottom-8 + .project-title-row.d-flex.align-items-center + .avatar-container.project-avatar.float-none + = project_icon(@project, alt: @project.name, class: 'avatar avatar-tile') + %h1.project-title.d-flex.align-items-baseline.qa-project-name + = @project.name + .project-metadata.d-flex.flex-row.flex-wrap.align-items-baseline + .project-visibility.d-inline-flex.align-items-baseline.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) } + = visibility_level_icon(@project.visibility_level, fw: false, options: {class: 'icon'}) + = visibility_level_label(@project.visibility_level) + - if license.present? + .project-license.d-inline-flex.align-items-baseline + = link_to_if license.link, sprite_icon('scale', size: 16, css_class: 'icon') + license.label, license.link, class: license.enabled ? 'btn btn-link btn-secondary-hover-link' : 'btn btn-link' + - if @project.tag_list.present? + .project-tag-list.d-inline-flex.align-items-baseline.has-tooltip{ data: { container: 'body' }, title: @project.has_extra_tags? ? @project.tag_list.join(', ') : nil } + = sprite_icon('tag', size: 16, css_class: 'icon') + = @project.tags_to_show + - if @project.has_extra_tags? + = _("+ %{count} more") % { count: @project.count_of_extra_tags_not_shown } .project-home-desc - if @project.description.present? - = markdown_field(@project, :description) + .project-description + .project-description-markdown.read-more-container + = markdown_field(@project, :description) + %button.btn.btn-blank.btn-link.text-secondary.js-read-more-trigger.text-secondary.d-lg-none{ type: "button" } + = _("Read more") + - if can?(current_user, :read_project, @project) .text-secondary.prepend-top-8 = s_('ProjectPage|Project ID: %{project_id}') % { project_id: @project.id } @@ -25,34 +44,42 @@ - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') = deleted_message % { project_name: fork_source_name(@project) } - .project-badges.prepend-top-default.append-bottom-default - - @project.badges.each do |badge| - %a.append-right-8{ href: badge.rendered_link_url(@project), - target: '_blank', - rel: 'noopener noreferrer' }> - %img.project-badge{ src: badge.rendered_image_url(@project), - 'aria-hidden': true, - alt: '' }> - - .project-repo-buttons - .count-buttons + - if @project.badges.present? + .project-badges.prepend-top-default.append-bottom-default + - @project.badges.each do |badge| + %a.append-right-8{ href: badge.rendered_link_url(@project), + target: '_blank', + rel: 'noopener noreferrer' }> + %img.project-badge{ src: badge.rendered_image_url(@project), + 'aria-hidden': true, + alt: 'Project badge' }> + + .project-repo-buttons.d-inline-flex.flex-wrap + .count-buttons.d-inline-flex = render 'projects/buttons/star' = render 'projects/buttons/fork' - %span.d-none.d-sm-inline - - if can?(current_user, :download_code, @project) - .project-clone-holder - = render "shared/clone_panel" + - if can?(current_user, :download_code, @project) + .project-clone-holder.d-inline-flex.d-sm-none + = render "shared/mobile_clone_panel" - - if show_xcode_link?(@project) - .project-action-button.project-xcode.inline - = render "projects/buttons/xcode_link" + .project-clone-holder.d-none.d-sm-inline-flex + = render "shared/clone_panel" - - if current_user - - if can?(current_user, :download_code, @project) + - if show_xcode_link?(@project) + .project-action-button.project-xcode.inline + = render "projects/buttons/xcode_link" + + - if current_user + - if can?(current_user, :download_code, @project) + .d-none.d-sm-inline-flex = render 'projects/buttons/download', project: @project, ref: @ref + .d-none.d-sm-inline-flex = render 'projects/buttons/dropdown' + .d-none.d-sm-inline-flex = render 'projects/buttons/koding' + .d-none.d-sm-inline-flex = render 'shared/notifications/button', notification_setting: @notification_setting + .d-none.d-sm-inline-flex = render 'shared/members/access_request_buttons', source: @project diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml index 70e1c557547..32da38f14b9 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -63,4 +63,4 @@ = form_for @project, html: { class: 'new_project' } do |f| %hr = render "shared/import_form", f: f - = render 'new_project_fields', f: f, project_name_id: "import-url-name" + = render 'new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index ad8c7911fad..db07c475866 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,12 +1,16 @@ - visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - ci_cd_only = local_assigns.fetch(:ci_cd_only, false) +- hide_init_with_readme = local_assigns.fetch(:hide_init_with_readme, false) .row{ id: project_name_id } = f.hidden_field :ci_cd_only, value: ci_cd_only + .form-group.project-name.col-sm-12 + = f.label :name, class: 'label-bold' do + %span= _("Project name") + = f.text_field :name, placeholder: "My awesome project", class: "form-control input-lg", autofocus: true, required: true .form-group.project-path.col-sm-6 = f.label :namespace_id, class: 'label-bold' do - %span - Project path + %span= s_("Project URL") .input-group - if current_user.can_select_namespace? .input-group-prepend.has-tooltip{ title: root_url } @@ -27,13 +31,12 @@ = f.hidden_field :namespace_id, value: current_user.namespace_id .form-group.project-path.col-sm-6 = f.label :path, class: 'label-bold' do - %span - Project name - = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, autofocus: true, required: true + %span= _("Project slug") + = f.text_field :path, placeholder: "my-awesome-project", class: "form-control", tabindex: 2, required: true - if current_user.can_create_group? .form-text.text-muted Want to house several dependent projects under the same namespace? - = link_to "Create a group", new_group_path + = link_to "Create a group.", new_group_path .form-group = f.label :description, class: 'label-bold' do @@ -46,15 +49,16 @@ = link_to icon('question-circle'), help_page_path("public_access/public_access"), aria: { label: 'Documentation for Visibility Level' }, target: '_blank', rel: 'noopener noreferrer' = render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false -.form-group.row.initialize-with-readme-setting - %div{ :class => "col-sm-12" } - .form-check - = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input' - = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do - .option-title - %strong Initialize repository with a README - .option-description - Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository. +- if !hide_init_with_readme + .form-group.row.initialize-with-readme-setting + %div{ :class => "col-sm-12" } + .form-check + = check_box_tag 'project[initialize_with_readme]', '1', false, class: 'form-check-input' + = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do + .option-title + %strong Initialize repository with a README + .option-description + Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository. -= f.submit 'Create project', class: "btn btn-create project-submit", tabindex: 4 += f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4 = link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index e90a6355214..0f6f3ad6d5e 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -5,4 +5,4 @@ .project-fields-form = render 'projects/project_templates/project_fields_form' - = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name" + = render 'projects/new_project_fields', f: f, project_name_id: "template-project-name", hide_init_with_readme: true diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml index 705338c083e..32624ac225b 100644 --- a/app/views/projects/_readme.html.haml +++ b/app/views/projects/_readme.html.haml @@ -20,4 +20,4 @@ distributed with computer software, forming part of its documentation. GitLab will render it here instead of this message. %p - = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-new' + = link_to "Add Readme", @project.add_readme_path, class: 'btn btn-success' diff --git a/app/views/projects/_stat_anchor_list.html.haml b/app/views/projects/_stat_anchor_list.html.haml index 15ec58289e3..4cf49f3cf62 100644 --- a/app/views/projects/_stat_anchor_list.html.haml +++ b/app/views/projects/_stat_anchor_list.html.haml @@ -1,7 +1,7 @@ - anchors = local_assigns.fetch(:anchors, []) - return unless anchors.any? -%ul.nav.justify-content-center +%ul.nav - anchors.each do |anchor| %li.nav-item = link_to_if anchor.link, anchor.label, anchor.link, class: anchor.enabled ? 'nav-link stat-link' : "nav-link btn btn-#{anchor.class_modifier || 'missing'}" do diff --git a/app/views/projects/_wiki.html.haml b/app/views/projects/_wiki.html.haml index 5646dc464f8..5adca007f7e 100644 --- a/app/views/projects/_wiki.html.haml +++ b/app/views/projects/_wiki.html.haml @@ -2,7 +2,7 @@ %div{ class: container_class } .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@wiki_home) + = render_wiki_content(@wiki_home, legacy_render_context(params)) - else - can_create_wiki = can?(current_user, :create_wiki, @project) .project-home-empty{ class: [('row-content-block' if can_create_wiki), ('content-block' unless can_create_wiki)] } diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml index a4b1b496b69..cf273aab108 100644 --- a/app/views/projects/blob/_blob.html.haml +++ b/app/views/projects/blob/_blob.html.haml @@ -5,6 +5,7 @@ %ul.blob-commit-info = render 'projects/commits/commit', commit: @last_commit, project: @project, ref: @ref + = render_if_exists 'projects/blob/owners', blob: blob = render "projects/blob/auxiliary_viewer", blob: blob #blob-content-holder.blob-content-holder diff --git a/app/views/projects/blob/_new_dir.html.haml b/app/views/projects/blob/_new_dir.html.haml index 6f3a691518b..e9010dc63fc 100644 --- a/app/views/projects/blob/_new_dir.html.haml +++ b/app/views/projects/blob/_new_dir.html.haml @@ -15,7 +15,7 @@ = render 'shared/new_commit_form', placeholder: _("Add new directory") .form-actions - = submit_tag _("Create directory"), class: 'btn btn-create' + = submit_tag _("Create directory"), class: 'btn btn-success' = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal" = render 'shared/projects/edit_information' diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml index 0a5c73c9037..d2b3c8ef96b 100644 --- a/app/views/projects/blob/_upload.html.haml +++ b/app/views/projects/blob/_upload.html.haml @@ -20,7 +20,7 @@ = render 'shared/new_commit_form', placeholder: placeholder .form-actions - = button_tag class: 'btn btn-create btn-upload-file', id: 'submit-all', type: 'button' do + = button_tag class: 'btn btn-success btn-upload-file', id: 'submit-all', type: 'button' do = icon('spin spinner', class: 'js-loading-icon hidden' ) = button_title = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml index 27cf040da7c..fdab8a53b41 100644 --- a/app/views/projects/blob/edit.html.haml +++ b/app/views/projects/blob/edit.html.haml @@ -21,7 +21,7 @@ Write %li - = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id) do + = link_to '#preview', 'data-preview-url' => project_preview_blob_path(@project, @id, legacy_render: params[:legacy_render]) do = editing_preview_title(@blob.name) = form_tag(project_update_blob_path(@project, @id), method: :put, class: 'js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do diff --git a/app/views/projects/blob/preview.html.haml b/app/views/projects/blob/preview.html.haml index da2cef17e8a..eb65cd90ea8 100644 --- a/app/views/projects/blob/preview.html.haml +++ b/app/views/projects/blob/preview.html.haml @@ -2,7 +2,7 @@ .diff-content - if markup?(@blob.name) .file-content.wiki - = markup(@blob.name, @content) + = markup(@blob.name, @content, legacy_render_context(params)) - else .file-content.code.js-syntax-highlight - unless @diff_lines.empty? diff --git a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml index 28c5be6ebf3..5be7cc7f25a 100644 --- a/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml +++ b/app/views/projects/blob/viewers/_gitlab_ci_yml.html.haml @@ -1,9 +1,9 @@ -- if viewer.valid? +- if viewer.valid?(@project, @commit.sha) = icon('check fw') This GitLab CI configuration is valid. - else = icon('warning fw') This GitLab CI configuration is invalid: - = viewer.validation_message + = viewer.validation_message(@project, @commit.sha) = link_to 'Learn more', help_page_path('ci/yaml/README') diff --git a/app/views/projects/blob/viewers/_markup.html.haml b/app/views/projects/blob/viewers/_markup.html.haml index 230305b488d..bd12cadf240 100644 --- a/app/views/projects/blob/viewers/_markup.html.haml +++ b/app/views/projects/blob/viewers/_markup.html.haml @@ -1,4 +1,6 @@ - blob = viewer.blob -- rendered_markup = blob.rendered_markup if blob.respond_to?(:rendered_markup) +- context = legacy_render_context(params) +- unless context[:markdown_engine] == :redcarpet + - context[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup) .file-content.wiki - = markup(blob.name, blob.data, rendered: rendered_markup) + = markup(blob.name, blob.data, context) diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index d6568c9f64a..ca867961f6b 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -41,7 +41,7 @@ data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'), container: 'body' } do = s_('Branches|Delete merged branches') - = link_to new_project_branch_path(@project), class: 'btn btn-create' do + = link_to new_project_branch_path(@project), class: 'btn btn-success' do = s_('Branches|New branch') - if can?(current_user, :admin_project, @project) diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml index 65b414c8af2..500536a5dbc 100644 --- a/app/views/projects/branches/new.html.haml +++ b/app/views/projects/branches/new.html.haml @@ -26,7 +26,7 @@ = render 'shared/ref_dropdown', dropdown_class: 'wide' .form-text.text-muted Existing branch name, tag, or commit SHA .form-actions - = button_tag 'Create branch', class: 'btn btn-create', tabindex: 3 + = button_tag 'Create branch', class: 'btn btn-success', tabindex: 3 = link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel' -# haml-lint:disable InlineJavaScript %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml index f880556a9f7..8da27ca7cb3 100644 --- a/app/views/projects/buttons/_fork.html.haml +++ b/app/views/projects/buttons/_fork.html.haml @@ -1,17 +1,17 @@ - unless @project.empty_repo? - if current_user && can?(current_user, :fork_project, @project) - - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: _('Go to your fork'), class: 'btn has-tooltip' do - = custom_icon('icon_fork') - %span= s_('GoToYourFork|Fork') - - else - - can_create_fork = current_user.can?(:create_fork) - = link_to new_project_fork_path(@project), - class: "btn btn-default #{'has-tooltip disabled' unless can_create_fork}", - title: (_('You have reached your project limit') unless can_create_fork) do - = custom_icon('icon_fork') - %span= s_('CreateNewFork|Fork') - .count-with-arrow - %span.arrow - = link_to project_forks_path(@project), title: n_('Fork', 'Forks', @project.forks_count), class: 'count' do - = @project.forks_count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.fork-count.count-badge-count.d-flex.align-items-center + = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do + = @project.forks_count + - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') + - else + - can_create_fork = current_user.can?(:create_fork) + = link_to new_project_fork_path(@project), + class: "btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", + title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do + = sprite_icon('fork', { css_class: 'icon' }) + %span= s_('ProjectOverview|Fork') diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index a2dc2730ecc..0d04ecb3a58 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -1,21 +1,19 @@ - if current_user - %button.btn.btn-default.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } }> - - if current_user.starred?(@project) - = sprite_icon('star') - %span.starred= _('Unstar') - - else - = sprite_icon('star-o') - %span= s_('StarProject|Star') - .count-with-arrow - %span.arrow - %span.count.star-count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + %button.count-badge-button.btn.btn-default.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } } + - if current_user.starred?(@project) + = sprite_icon('star', { css_class: 'icon' }) + %span.starred= s_('ProjectOverview|Unstar') + - else + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') - else - = link_to new_user_session_path, class: 'btn has-tooltip star-btn', title: _('You must sign in to star a project') do - = sprite_icon('star') - #{ s_('StarProject|Star') } - .count-with-arrow - %span.arrow - %span.count + .count-badge.d-inline-flex.align-item-stretch.append-right-8 + %span.star-count.count-badge-count.d-flex.align-items-center = @project.star_count + = link_to new_user_session_path, class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do + = sprite_icon('star-o', { css_class: 'icon' }) + %span= s_('ProjectOverview|Star') diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index 84362580a90..4616d1359b4 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,12 +1,12 @@ -%h4= s_('ClusterIntegration|Kubernetes cluster integration') +.hidden.js-cluster-error.bs-callout.bs-callout-danger{ role: 'alert' } + = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine') + %p.js-error-reason -.settings-content - .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine') - %p.js-error-reason +.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' } + = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') - .hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } - = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...') +.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } + = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - if show_cluster_security_warning? .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning diff --git a/app/views/projects/clusters/_integration_form.html.haml b/app/views/projects/clusters/_integration_form.html.haml index b46b45fea49..d0a553e3414 100644 --- a/app/views/projects/clusters/_integration_form.html.haml +++ b/app/views/projects/clusters/_integration_form.html.haml @@ -2,14 +2,6 @@ = form_errors(@cluster) .form-group %h5= s_('ClusterIntegration|Integration status') - %p - - if @cluster.enabled? - - if can?(current_user, :update_cluster, @cluster) - = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.') - - else - = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.') - - else - = s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.') %label.append-bottom-0.js-cluster-enable-toggle-area %button{ type: 'button', class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}", @@ -19,14 +11,13 @@ %span.toggle-icon = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .form-text.text-muted= s_('ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.') - if has_multiple_clusters?(@project) .form-group %h5= s_('ClusterIntegration|Environment scope') - %p - = s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.") - = link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments') - = field.text_field :environment_scope, class: 'form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') + = field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope') + .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") - if can?(current_user, :update_cluster, @cluster) .form-group @@ -38,8 +29,3 @@ %code * is the default environment scope for this cluster. This means that all jobs, regardless of their environment, will use this cluster. = link_to 'More information', ('https://docs.gitlab.com/ee/user/project/clusters/#setting-the-environment-scope') - - %h5= s_('ClusterIntegration|Security') - %p - = s_("ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application.") - = link_to s_("ClusterIntegration|Learn more about security configuration"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml index 9133de6559d..0222bbf7338 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -61,5 +61,15 @@ %p.form-text.text-muted = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' + .form-group = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml index 877e0cc876c..be84f2ae67c 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -37,5 +37,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)') = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index 08d2deff6f8..eddd3613c5f 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -23,7 +23,8 @@ .js-cluster-application-notice .flash-container - %section.settings.no-animate.expanded#cluster-integration + %section#cluster-integration + %h4= @cluster.name = render 'banner' = render 'integration_form' diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml index e8ef0008802..f497f5b606c 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -25,5 +25,15 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input' }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + = link_to _('More information'), help_page_path('user/project/clusters/index.md', anchor: 'role-based-access-control-rbac-experimental-support'), target: '_blank' + .form-group = field.submit s_('ClusterIntegration|Add Kubernetes cluster'), class: 'btn btn-success' diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml index 20a07d6695e..56b597d295a 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -26,5 +26,14 @@ = platform_kubernetes_field.label :namespace, s_('ClusterIntegration|Project namespace (optional, unique)'), class: 'label-bold' = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') + - if rbac_clusters_feature_enabled? + .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + .form-text.text-muted + = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') + = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') + .form-group = field.submit s_('ClusterIntegration|Save changes'), class: 'btn btn-success' diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml index afd70ef5774..e71615dd1c5 100644 --- a/app/views/projects/commit/_change.html.haml +++ b/app/views/projects/commit/_change.html.haml @@ -33,7 +33,7 @@ - else = hidden_field_tag 'create_merge_request', 1, id: nil .form-actions - = submit_tag label, class: 'btn btn-create' + = submit_tag label, class: 'btn btn-success' = link_to _("Cancel"), '#', class: "btn btn-cancel", "data-dismiss" => "modal" = render 'shared/projects/edit_information' diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml index 07112c98804..d24ee4a3251 100644 --- a/app/views/projects/compare/_form.html.haml +++ b/app/views/projects/compare/_form.html.haml @@ -22,7 +22,7 @@ .dropdown-toggle-text.str-truncated= params[:from] || _("Select branch/tag") = render 'shared/ref_dropdown' - = button_tag s_("CompareBranches|Compare"), class: "btn btn-create commits-compare-btn" + = button_tag s_("CompareBranches|Compare"), class: "btn btn-success commits-compare-btn" - if @merge_request.present? = link_to _("View open merge request"), project_merge_request_path(@project, @merge_request), class: 'prepend-left-10 btn' - elsif create_mr_button? diff --git a/app/views/projects/deploy_keys/_form.html.haml b/app/views/projects/deploy_keys/_form.html.haml index f8ab0c1ec54..568930595a2 100644 --- a/app/views/projects/deploy_keys/_form.html.haml +++ b/app/views/projects/deploy_keys/_form.html.haml @@ -21,4 +21,4 @@ Allow this key to push to repository as well? (Default only allows pull access.) .form-group.row - = f.submit "Add key", class: "btn-create btn" + = f.submit "Add key", class: "btn-success btn" diff --git a/app/views/projects/deploy_keys/edit.html.haml b/app/views/projects/deploy_keys/edit.html.haml index e009b6fef0e..3e7872ebc1c 100644 --- a/app/views/projects/deploy_keys/edit.html.haml +++ b/app/views/projects/deploy_keys/edit.html.haml @@ -6,5 +6,5 @@ = form_for [@project.namespace.becomes(Namespace), @project, @deploy_key], html: { class: 'js-requires-input' } do |f| = render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key } .form-actions - = f.submit 'Save changes', class: 'btn-save btn' + = f.submit 'Save changes', class: 'btn-success btn' = link_to 'Cancel', project_settings_repository_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml index aa1112c3313..229a4574eeb 100644 --- a/app/views/projects/diffs/_stats.html.haml +++ b/app/views/projects/diffs/_stats.html.haml @@ -1,5 +1,5 @@ -- sum_added_lines = diff_files.sum(&:added_lines) -- sum_removed_lines = diff_files.sum(&:removed_lines) +- sum_added_lines = diff_files.sum(&:added_lines) # rubocop: disable CodeReuse/ActiveRecord +- sum_removed_lines = diff_files.sum(&:removed_lines) # rubocop: disable CodeReuse/ActiveRecord .commit-stat-summary.dropdown Showing %button.diff-stats-summary-toggler.js-diff-stats-dropdown{ type: "button", data: { toggle: "dropdown", display: "static" } }< diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index acdde9e0f70..96ab582b050 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -59,7 +59,7 @@ - if @project.avatar? %hr = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" - = f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings" + = f.submit 'Save changes', class: "btn btn-success js-btn-success-general-project-settings" %section.settings.sharing-permissions.no-animate#js-shared-permissions{ class: ('expanded' if expanded) } .settings-header @@ -75,7 +75,7 @@ -# haml-lint:disable InlineJavaScript %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data_json(@project) .js-project-permissions-form - = f.submit 'Save changes', class: "btn btn-save" + = f.submit 'Save changes', class: "btn btn-success" = render_if_exists 'projects/issues_settings' @@ -93,7 +93,7 @@ = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f| %input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' } = render 'projects/merge_request_settings', form: f - = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes" + = f.submit 'Save changes', class: "btn btn-success qa-save-merge-request-changes" = render_if_exists 'projects/service_desk_settings' diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d47dc3d8143..d104608b2fe 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -32,9 +32,13 @@ = _('Otherwise it is recommended you start with one of the options below.') .prepend-top-20 -%nav.project-stats{ class: container_class } - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors - = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons +%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors + = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons - if can?(current_user, :push_code, @project) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } @@ -42,7 +46,7 @@ .empty_wrapper %h3#repo-command-line-instructions.page-title-empty Command line instructions - .git-empty + .git-empty.js-git-empty %fieldset %h5 Git global setup %pre.bg-light @@ -54,7 +58,7 @@ %h5 Create a new repository %pre.bg-light :preserve - git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git clone #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} cd #{h @project.path} touch README.md git add README.md @@ -69,7 +73,7 @@ :preserve cd existing_folder git init - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} git add . git commit -m "Initial commit" - if @project.can_current_user_push_to_default_branch? @@ -82,7 +86,7 @@ :preserve cd existing_repo git remote rename origin old-origin - git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} + git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'js-clone')} - if @project.can_current_user_push_to_default_branch? %span>< git push -u origin --all diff --git a/app/views/projects/environments/_form.html.haml b/app/views/projects/environments/_form.html.haml index 0586dbdf0e2..f942b936037 100644 --- a/app/views/projects/environments/_form.html.haml +++ b/app/views/projects/environments/_form.html.haml @@ -18,5 +18,5 @@ = f.url_field :external_url, class: 'form-control' .form-actions - = f.submit 'Save', class: 'btn btn-save' + = f.submit 'Save', class: 'btn btn-success' = link_to 'Cancel', project_environments_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml index 5b680189bc8..e40d631a1a1 100644 --- a/app/views/projects/environments/terminal.html.haml +++ b/app/views/projects/environments/terminal.html.haml @@ -2,7 +2,7 @@ - page_title "Terminal for environment", @environment.name - content_for :page_specific_javascripts do - = stylesheet_link_tag "xterm/xterm" + = stylesheet_link_tag "xterm.css" %div{ class: container_class } .top-area diff --git a/app/views/projects/find_file/show.html.haml b/app/views/projects/find_file/show.html.haml index a966bfb2dd9..996c7b1b960 100644 --- a/app/views/projects/find_file/show.html.haml +++ b/app/views/projects/find_file/show.html.haml @@ -13,6 +13,6 @@ .tree-content-holder .table-holder - %table.table.files-slider{ class: "table_#{@hex_path} tree-table table-striped" } + %table.table.files-slider{ class: "table_#{@hex_path} tree-table" } %tbody = spinner nil, true diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml index 57afc7ac9c3..b44ea89510b 100644 --- a/app/views/projects/forks/index.html.haml +++ b/app/views/projects/forks/index.html.haml @@ -30,11 +30,11 @@ - if current_user && can?(current_user, :fork_project, @project) - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 - = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do + = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-success' do = sprite_icon('fork', size: 12) %span Fork - else - = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-new' do + = link_to new_project_fork_path(@project), title: "Fork project", class: 'btn btn-success' do = sprite_icon('fork', size: 12) %span Fork diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml index 5990582fd55..0ab7863b77c 100644 --- a/app/views/projects/hooks/_index.html.haml +++ b/app/views/projects/hooks/_index.html.haml @@ -9,7 +9,7 @@ .col-lg-8.append-bottom-default = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f| = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } - = f.submit 'Add webhook', class: 'btn btn-create' + = f.submit 'Add webhook', class: 'btn btn-success' %hr %h5.prepend-top-default diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml index c31aef60453..57311284e11 100644 --- a/app/views/projects/hooks/edit.html.haml +++ b/app/views/projects/hooks/edit.html.haml @@ -11,7 +11,7 @@ = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: project_hook_path(@project, @hook) do |f| = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook } - = f.submit 'Save changes', class: 'btn btn-create' + = f.submit 'Save changes', class: 'btn btn-success' = render 'shared/web_hooks/test_button', triggers: ProjectHook.triggers, hook: @hook = link_to 'Remove', project_hook_path(@project, @hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: 'Are you sure?' } diff --git a/app/views/projects/imports/new.html.haml b/app/views/projects/imports/new.html.haml index 8ce822c43b7..1c50cfbde85 100644 --- a/app/views/projects/imports/new.html.haml +++ b/app/views/projects/imports/new.html.haml @@ -16,4 +16,4 @@ = render "shared/import_form", f: f .form-actions - = f.submit 'Start import', class: "btn btn-create", tabindex: 4 + = f.submit 'Start import', class: "btn btn-success", tabindex: 4 diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml index 8a14146cb87..31c72f2f759 100644 --- a/app/views/projects/issues/_issue.html.haml +++ b/app/views/projects/issues/_issue.html.haml @@ -2,9 +2,9 @@ .issue-box - if @can_bulk_update .issue-check.hidden - = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue" - .issue-info-container - .issue-main-info + = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected-issuable" + .issuable-info-container + .issuable-main-info .issue-title.title %span.issue-title-text - if issue.confidential? diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml index 0dd2d2e6c5d..e4a0d4b8479 100644 --- a/app/views/projects/issues/_nav_btns.html.haml +++ b/app/views/projects/issues/_nav_btns.html.haml @@ -6,6 +6,6 @@ = link_to "New issue", new_project_issue_path(@project, issue: { assignee_id: finder.assignee.try(:id), milestone_id: finder.milestones.first.try(:id) }), - class: "btn btn-new", + class: "btn btn-success", title: "New issue", id: "new_issue_link" diff --git a/app/views/projects/issues/index.atom.builder b/app/views/projects/issues/index.atom.builder index 6330245954e..6566866be82 100644 --- a/app/views/projects/issues/index.atom.builder +++ b/app/views/projects/issues/index.atom.builder @@ -1,3 +1,4 @@ +# rubocop: disable CodeReuse/ActiveRecord xml.title "#{@project.name} issues" xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml" xml.link href: project_issues_url(@project), rel: "alternate", type: "text/html" @@ -5,3 +6,4 @@ xml.id project_issues_url(@project) xml.updated @issues.first.updated_at.xmlschema if @issues.reorder(nil).any? xml << render(partial: 'issues/issue', collection: @issues) if @issues.reorder(nil).any? +# rubocop: enable CodeReuse/ActiveRecord diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index b81d1a188f0..c39fd0063be 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -55,7 +55,7 @@ - if can_report_spam = link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'd-none d-sm-none d-md-block btn btn-grouped btn-spam', title: 'Submit as spam' - if can_create_issue - = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do + = link_to new_project_issue_path(@project), class: 'd-none d-sm-none d-md-block btn btn-grouped new-issue-link btn-success btn-inverted', title: 'New issue', id: 'new_issue_link' do New issue .issue-details.issuable-details diff --git a/app/views/projects/jobs/_header.html.haml b/app/views/projects/jobs/_header.html.haml index b83e8dddccb..e7245622b80 100644 --- a/app/views/projects/jobs/_header.html.haml +++ b/app/views/projects/jobs/_header.html.haml @@ -24,7 +24,7 @@ - if show_controls .nav-controls - if can?(current_user, :create_issue, @project) && @build.failed? - = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-new btn-inverted' + = link_to "New issue", new_project_issue_path(@project, issue: build_failed_issue_options), class: 'btn btn-success btn-inverted' - if can?(current_user, :update_build, @build) && @build.retryable? = link_to "Retry job", retry_project_job_path(@project, @build), class: 'btn btn-inverted-secondary', method: :post %button.btn.btn-default.float-right.d-block.d-sm-none.d-md-none.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" } diff --git a/app/views/projects/jobs/index.html.haml b/app/views/projects/jobs/index.html.haml index fe1c338b634..59592abcf6a 100644 --- a/app/views/projects/jobs/index.html.haml +++ b/app/views/projects/jobs/index.html.haml @@ -8,7 +8,7 @@ .nav-controls - if can?(current_user, :update_build, @project) - - if @all_builds.running_or_pending.limit(1).any? + - if @all_builds.running_or_pending.limit(1).any? # rubocop: disable CodeReuse/ActiveRecord = link_to 'Cancel running', cancel_all_project_jobs_path(@project), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml index 078f40c4477..5321bc46e73 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -3,6 +3,9 @@ - breadcrumb_title "##{@build.id}" - page_title "#{@build.name} (##{@build.id})", "Jobs" +- content_for :page_specific_javascripts do + = stylesheet_link_tag 'page_bundles/xterm' + %div{ class: container_class } .build-page.js-build-page #js-build-header-vue @@ -10,7 +13,7 @@ - unless @build.any_runners_online? .bs-callout.bs-callout-warning.js-build-stuck %p - - if no_runners_for_project?(@build.project) + - if @project.any_runners? This job is stuck, because the project doesn't have any runners online assigned to it. - elsif @build.tags.any? This job is stuck, because you don't have any active runners online with any of these tags assigned to them: diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index dfac62e7985..683dda4f166 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -6,7 +6,7 @@ - if can_admin_label - content_for(:header_content) do .nav-controls - = link_to _('New label'), new_project_label_path(@project), class: "btn btn-new" + = link_to _('New label'), new_project_label_path(@project), class: "btn btn-success" - if @labels.exists? || @prioritized_labels.exists? || search.present? #promote-label-modal @@ -22,6 +22,7 @@ %span.input-group-append %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } = icon("search") + = render 'shared/labels/sort_dropdown' .labels-container.prepend-top-10 - if can_admin_label diff --git a/app/views/projects/mattermosts/_team_selection.html.haml b/app/views/projects/mattermosts/_team_selection.html.haml index 37c09f12f63..d0a7f89df31 100644 --- a/app/views/projects/mattermosts/_team_selection.html.haml +++ b/app/views/projects/mattermosts/_team_selection.html.haml @@ -43,4 +43,4 @@ .clearfix .float-right = link_to 'Cancel', edit_project_service_path(@project, @service), class: 'btn btn-lg' - = f.submit 'Install', class: 'btn btn-save btn-lg' + = f.submit 'Install', class: 'btn btn-success btn-lg' diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml index cd3d896fff2..faa070d0389 100644 --- a/app/views/projects/merge_requests/_merge_request.html.haml +++ b/app/views/projects/merge_requests/_merge_request.html.haml @@ -1,10 +1,10 @@ %li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } } - if @can_bulk_update .issue-check.hidden - = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue" + = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected-issuable" - .issue-info-container - .issue-main-info + .issuable-info-container + .issuable-main-info .merge-request-title.title %span.merge-request-title-text = link_to merge_request.title, merge_request_path(merge_request) diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml index e73dab8ad4a..b7498216334 100644 --- a/app/views/projects/merge_requests/_nav_btns.html.haml +++ b/app/views/projects/merge_requests/_nav_btns.html.haml @@ -1,5 +1,5 @@ - if @can_bulk_update = button_tag "Edit merge requests", class: "btn append-right-10 js-bulk-update-toggle" - if merge_project - = link_to new_merge_request_path, class: "btn btn-new", title: "New merge request" do + = link_to new_merge_request_path, class: "btn btn-success", title: "New merge request" do New merge request diff --git a/app/views/projects/merge_requests/creations/_new_compare.html.haml b/app/views/projects/merge_requests/creations/_new_compare.html.haml index afa7eb06cb4..1fd71a38472 100644 --- a/app/views/projects/merge_requests/creations/_new_compare.html.haml +++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml @@ -61,4 +61,4 @@ - if @merge_request.errors.any? = form_errors(@merge_request) - = f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn" + = f.submit 'Compare branches and continue', class: "btn btn-success mr-compare-btn" diff --git a/app/views/projects/merge_requests/diffs/_diffs.html.haml b/app/views/projects/merge_requests/diffs/_diffs.html.haml index bf3df0abf86..9ebd91dea0b 100644 --- a/app/views/projects/merge_requests/diffs/_diffs.html.haml +++ b/app/views/projects/merge_requests/diffs/_diffs.html.haml @@ -14,7 +14,7 @@ %span.ref-name= @merge_request.source_branch and %span.ref-name= @merge_request.target_branch - .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-save' + .text-center= link_to 'Create commit', project_new_blob_path(@project, @merge_request.source_branch), class: 'btn btn-success' - else - diff_viewable = @merge_request_diff ? @merge_request_diff.viewable? : true - if diff_viewable diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index 28f0a167128..ebd3229e42b 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -20,8 +20,8 @@ .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-create btn qa-milestone-create-button" + = f.submit 'Create milestone', class: "btn-success btn qa-milestone-create-button" = link_to "Cancel", project_milestones_path(@project), class: "btn btn-cancel" - else - = f.submit 'Save changes', class: "btn-save btn" + = f.submit 'Save changes', class: "btn-success btn" = link_to "Cancel", project_milestone_path(@project, @milestone), class: "btn btn-cancel" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 26d2ea8447b..57f3c640696 100644 --- a/app/views/projects/milestones/index.html.haml +++ b/app/views/projects/milestones/index.html.haml @@ -8,7 +8,7 @@ .nav-controls = render 'shared/milestones_sort_dropdown' - if can?(current_user, :admin_milestone, @project) - = link_to new_project_milestone_path(@project), class: "btn btn-new qa-new-project-milestone", title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-success qa-new-project-milestone", title: 'New milestone' do New milestone .milestones diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index c6764c7607a..d523df1cd90 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -32,7 +32,7 @@ = link_to icon('question-circle'), help_page_path('user/project/protected_branches') .panel-footer - = f.submit _('Mirror repository'), class: 'btn btn-create', name: :update_remote_mirror + = f.submit _('Mirror repository'), class: 'btn btn-success', name: :update_remote_mirror .panel.panel-default .table-responsive diff --git a/app/views/projects/pages/show.html.haml b/app/views/projects/pages/show.html.haml index 7e1a3b9bea6..88ab486a248 100644 --- a/app/views/projects/pages/show.html.haml +++ b/app/views/projects/pages/show.html.haml @@ -4,7 +4,7 @@ Pages - if can?(current_user, :update_pages, @project) && (Gitlab.config.pages.external_http || Gitlab.config.pages.external_https) - = link_to new_project_pages_domain_path(@project), class: 'btn btn-new float-right', title: 'New Domain' do + = link_to new_project_pages_domain_path(@project), class: 'btn btn-success float-right', title: 'New Domain' do New Domain %p.light diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml index ee70de22f13..342b1482df7 100644 --- a/app/views/projects/pages_domains/edit.html.haml +++ b/app/views/projects/pages_domains/edit.html.haml @@ -8,4 +8,4 @@ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions - = f.submit 'Save Changes', class: "btn btn-save" + = f.submit 'Save Changes', class: "btn btn-success" diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml index 376ce3f68aa..94ad1470052 100644 --- a/app/views/projects/pages_domains/new.html.haml +++ b/app/views/projects/pages_domains/new.html.haml @@ -7,6 +7,6 @@ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'fieldset-form' } do |f| = render 'form', { f: f } .form-actions - = f.submit 'Create New Domain', class: "btn btn-save" + = f.submit 'Create New Domain', class: "btn btn-success" .float-right = link_to _('Cancel'), project_pages_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index 9a981d53ab6..259979417e0 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -39,5 +39,5 @@ = f.check_box :active, required: false, value: @schedule.active? = _('Active') .footer-block.row-content-block - = f.submit _('Save pipeline schedule'), class: 'btn btn-create', tabindex: 3 + = f.submit _('Save pipeline schedule'), class: 'btn btn-success', tabindex: 3 = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel' diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 3677666070e..0580c15ad15 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -11,7 +11,7 @@ - if can?(current_user, :create_pipeline_schedule, @project) .nav-controls - = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-create' do + = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-success' do %span= _('New schedule') - if @schedules.present? diff --git a/app/views/projects/project_members/_new_shared_group.html.haml b/app/views/projects/project_members/_new_project_group.html.haml index d7227c32833..74570769117 100644 --- a/app/views/projects/project_members/_new_shared_group.html.haml +++ b/app/views/projects/project_members/_new_project_group.html.haml @@ -2,19 +2,19 @@ .col-sm-12 = form_tag project_group_links_path(@project), class: 'js-requires-input', method: :post do .form-group - = label_tag :link_group_id, "Select a group to share with", class: "label-bold" + = label_tag :link_group_id, _("Select a group to invite"), class: "label-bold" = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, class: "input-clamp", required: true) .form-group - = label_tag :link_group_access, "Max access level", class: "label-bold" + = label_tag :link_group_access, _("Max access level"), class: "label-bold" .select-wrapper = select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control" = icon('chevron-down') .form-text.text-muted.append-bottom-10 - = link_to "Read more", help_page_path("user/permissions"), class: "vlink" + = link_to _("Read more"), help_page_path("user/permissions"), class: "vlink" about role permissions .form-group - = label_tag :expires_at, 'Access expiration date', class: 'label-bold' + = label_tag :expires_at, _('Access expiration date'), class: 'label-bold' .clearable-input - = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: 'Expiration date', id: 'expires_at_groups' + = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date-groups', placeholder: _('Expiration date'), id: 'expires_at_groups' %i.clear-icon.js-clear-input - = submit_tag "Share", class: "btn btn-create" + = submit_tag _("Invite"), class: "btn btn-success" diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index 6272687be1c..517fd249f6e 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -17,5 +17,5 @@ = label_tag :expires_at, 'Access expiration date', class: 'label-bold' = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date' %i.clear-icon.js-clear-input - = f.submit "Add to project", class: "btn btn-create" + = f.submit "Add to project", class: "btn btn-success" = link_to "Import", import_project_project_members_path(@project), class: "btn btn-default", title: "Import members from another project" diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml index 6a52e72bfd8..8b93e81cd31 100644 --- a/app/views/projects/project_members/import.html.haml +++ b/app/views/projects/project_members/import.html.haml @@ -11,5 +11,5 @@ .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true) .form-actions - = button_tag 'Import project members', class: "btn btn-create" + = button_tag 'Import project members', class: "btn btn-success" = link_to "Cancel", project_project_members_path(@project), class: "btn btn-cancel" diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml index 9716322f8a1..14ed3345765 100644 --- a/app/views/projects/project_members/index.html.haml +++ b/app/views/projects/project_members/index.html.haml @@ -6,9 +6,9 @@ Project members - if can?(current_user, :admin_project_member, @project) %p - You can add a new member to + You can invite a new member to %strong= @project.name - or share it with another group. + or invite another group. - else %p Members can be added by project @@ -19,16 +19,16 @@ - if can?(current_user, :admin_project_member, @project) %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' } %li.nav-tab{ role: 'presentation' } - %a.nav-link.active{ href: '#add-member-pane', id: 'add-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member + %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member - if @project.allowed_to_share_with_group? %li.nav-tab{ role: 'presentation' } - %a.nav-link{ href: '#share-with-group-pane', id: 'share-with-group-tab', data: { toggle: 'tab' }, role: 'tab' } Share with group + %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group .tab-content.gitlab-tab-content - .tab-pane.active{ id: 'add-member-pane', role: 'tabpanel' } - = render 'projects/project_members/new_project_member', tab_title: 'Add member' - .tab-pane{ id: 'share-with-group-pane', role: 'tabpanel' } - = render 'projects/project_members/new_shared_group', tab_title: 'Share with group' + .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' } + = render 'projects/project_members/new_project_member', tab_title: 'Invite member' + .tab-pane{ id: 'invite-group-pane', role: 'tabpanel' } + = render 'projects/project_members/new_project_group', tab_title: 'Invite group' = render 'shared/members/requests', membership_source: @project, requesters: @requesters .clearfix diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index df2dcf19ed4..c3b8f2f8964 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -30,4 +30,4 @@ = yield :push_access_levels .card-footer - = f.submit 'Protect', class: 'btn-create btn', disabled: true + = f.submit 'Protect', class: 'btn-success btn', disabled: true diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml index f98781b77f4..b274c73d035 100644 --- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml @@ -26,4 +26,4 @@ = yield :create_access_levels .card-footer - = f.submit 'Protect', class: 'btn-create btn', disabled: true + = f.submit 'Protect', class: 'btn-success btn', disabled: true diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml index 8093cc2c2d7..52c6c7ec424 100644 --- a/app/views/projects/releases/edit.html.haml +++ b/app/views/projects/releases/edit.html.haml @@ -19,5 +19,5 @@ = render 'shared/notes/hints' .error-alert .prepend-top-default - = f.submit 'Save changes', class: 'btn btn-save' + = f.submit 'Save changes', class: 'btn btn-success' = link_to "Cancel", project_tag_path(@project, @tag.name), class: "btn btn-default btn-cancel" diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml index 86de71c732b..a6c16c70313 100644 --- a/app/views/projects/runners/_group_runners.html.haml +++ b/app/views/projects/runners/_group_runners.html.haml @@ -28,7 +28,7 @@ - group_link = link_to _('Group CI/CD settings'), group_settings_ci_cd_path(@project.group) = _('Group maintainers can register group runners in the %{link}').html_safe % { link: group_link } - else - = _('Ask your group maintainer to setup a group Runner.') + = _('Ask your group maintainer to set up a group Runner.') - else %h4.underlined-title diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml index 6ee83fae25e..548977d6a80 100644 --- a/app/views/projects/runners/_runner.html.haml +++ b/app/views/projects/runners/_runner.html.haml @@ -24,7 +24,7 @@ - if runner.belongs_to_one_project? = link_to _('Remove Runner'), project_runner_path(@project, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - else - - runner_project = @project.runner_projects.find_by(runner_id: runner) + - runner_project = @project.runner_projects.find_by(runner_id: runner) # rubocop: disable CodeReuse/ActiveRecord = link_to _('Disable for this project'), project_runner_project_path(@project, runner_project), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm' - elsif runner.project_type? = form_for [@project.namespace.becomes(Namespace), @project, @project.runner_projects.new] do |f| diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml index 9314804c5dd..9409418bbcc 100644 --- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml +++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml @@ -1,6 +1,6 @@ - run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}" -%p To setup this service: +%p To set up this service: %ul.list-unstyled.indent-list %li 1. diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml index f25d2ecdfb1..9a7004f89c0 100644 --- a/app/views/projects/services/slack_slash_commands/_help.html.haml +++ b/app/views/projects/services/slack_slash_commands/_help.html.haml @@ -14,7 +14,7 @@ by entering %kbd.inline /<command> help - unless @service.template? - %p To setup this service: + %p To set up this service: %ul.list-unstyled.indent-list %li 1. diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 9134257b631..ae923d8e6dc 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -121,7 +121,7 @@ go test -cover (Go) %code coverage: \d+.\d+% of statements - = f.submit _('Save changes'), class: "btn btn-save" + = f.submit _('Save changes'), class: "btn btn-success" %hr diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml index df8a5742450..aba289c790f 100644 --- a/app/views/projects/show.html.haml +++ b/app/views/projects/show.html.haml @@ -19,8 +19,13 @@ - if can?(current_user, :download_code, @project) %nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] } - = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) - = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + .scrolling-tabs-container.inner-page-scroll-tabs.is-smaller + .fade-left= icon('angle-left') + .fade-right= icon('angle-right') + .nav-links.scrolling-tabs + = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout) + = render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout) + = repository_languages_bar(@project.repository_languages) %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml index 4a3aa3dc626..ea963510a68 100644 --- a/app/views/projects/snippets/_actions.html.haml +++ b/app/views/projects/snippets/_actions.html.haml @@ -8,7 +8,7 @@ = link_to project_snippet_path(@project, @snippet), method: :delete, data: { confirm: _("Are you sure?") }, class: "btn btn-grouped btn-inverted btn-remove", title: _('Delete Snippet') do = _('Delete') - if can?(current_user, :create_project_snippet, @project) - = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-create', title: _("New snippet") do + = link_to new_project_snippet_path(@project), class: 'btn btn-grouped btn-inverted btn-success', title: _("New snippet") do = _('New snippet') - if @snippet.submittable_as_spam_by?(current_user) = link_to _('Submit as spam'), mark_as_spam_project_snippet_path(@project, @snippet), method: :post, class: 'btn btn-grouped btn-spam', title: _('Submit as spam') diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml index 1c4c73dc776..a4974d89c1a 100644 --- a/app/views/projects/snippets/index.html.haml +++ b/app/views/projects/snippets/index.html.haml @@ -7,6 +7,6 @@ .nav-controls - if can?(current_user, :create_project_snippet, @project) - = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-new", title: _("New snippet") + = link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-success", title: _("New snippet") = render 'snippets/snippets' diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index 20b4705521c..37535370940 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -25,7 +25,7 @@ %li = link_to title, filter_tags_path(sort: value), class: ("is-active" if @sort == value) - if can?(current_user, :push_code, @project) - = link_to new_project_tag_path(@project), class: 'btn btn-create new-tag-btn' do + = link_to new_project_tag_path(@project), class: 'btn btn-success new-tag-btn' do = s_('TagsPage|New tag') = link_to project_tags_path(@project, rss_url_options), title: _("Tags feed"), class: 'btn rss-btn has-tooltip' do = icon("rss") diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml index da822ac5675..24724394259 100644 --- a/app/views/projects/tags/new.html.haml +++ b/app/views/projects/tags/new.html.haml @@ -41,7 +41,7 @@ .form-text.text-muted = s_('TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page.') .form-actions - = button_tag s_('TagsPage|Create tag'), class: 'btn btn-create' + = button_tag s_('TagsPage|Create tag'), class: 'btn btn-success' = link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel' -# haml-lint:disable InlineJavaScript %script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe diff --git a/app/views/projects/tree/_tree_commit_column.html.haml b/app/views/projects/tree/_tree_commit_column.html.haml index abb3e918e87..406dccb74fb 100644 --- a/app/views/projects/tree/_tree_commit_column.html.haml +++ b/app/views/projects/tree/_tree_commit_column.html.haml @@ -1,2 +1,2 @@ %span.str-truncated - = link_to_markdown commit.full_title, project_commit_path(@project, commit.id), class: "tree-commit-link" + = link_to_html commit.redacted_full_title_html, project_commit_path(@project, commit.id), class: 'tree-commit-link' diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml index 1a5fc56f429..a9abfac239c 100644 --- a/app/views/projects/triggers/_form.html.haml +++ b/app/views/projects/triggers/_form.html.haml @@ -8,4 +8,4 @@ .form-group = f.label :key, "Description", class: "label-bold" = f.text_field :description, class: "form-control", required: true, title: 'Trigger description is required.', placeholder: "Trigger description" - = f.submit btn_text, class: "btn btn-save" + = f.submit btn_text, class: "btn btn-success" diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index e8681da6528..70f1bf8ef46 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -7,4 +7,4 @@ $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); - $('.edit-project .js-btn-save-general-project-settings').enable(); + $('.edit-project .js-btn-success-general-project-settings').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index de692466fe5..7d8826e540c 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -1,9 +1,13 @@ - commit_message = @page.persisted? ? s_("WikiPageEdit|Update %{page_title}") : s_("WikiPageCreate|Create %{page_title}") - commit_message = commit_message % { page_title: @page.title } +- if params[:legacy_render] || !commonmark_for_repositories_enabled? + - markdown_version = CacheMarkdownField::CACHE_REDCARPET_VERSION +- else + - markdown_version = 0 = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'wiki-form common-note-form prepend-top-default js-quick-submit' }, - data: { markdown_version: CacheMarkdownField::CACHE_REDCARPET_VERSION } do |f| + data: { markdown_version: markdown_version, uploads_path: uploads_path } do |f| = form_errors(@page) - if @page.persisted? @@ -47,10 +51,10 @@ .form-actions - if @page && @page.persisted? - = f.submit _("Save changes"), class: 'btn-save btn' + = f.submit _("Save changes"), class: 'btn-success btn' .float-right = link_to _("Cancel"), project_wiki_path(@project, @page), class: 'btn btn-cancel btn-grouped' - else - = f.submit s_("Wiki|Create page"), class: 'btn-create btn' + = f.submit s_("Wiki|Create page"), class: 'btn-success btn' .float-right = link_to _("Cancel"), project_wiki_path(@project, :home), class: 'btn btn-cancel' diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml index 8d91f411f89..643b51e01d1 100644 --- a/app/views/projects/wikis/_main_links.html.haml +++ b/app/views/projects/wikis/_main_links.html.haml @@ -1,6 +1,6 @@ - if (@page && @page.persisted?) - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do = s_("Wiki|New page") = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index 38382aae67c..dc12e368b35 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -15,4 +15,4 @@ = icon('lightbulb-o') = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-actions - = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-create" + = button_tag s_("Wiki|Create page"), class: "build-new-wiki btn btn-success" diff --git a/app/views/projects/wikis/_sidebar.html.haml b/app/views/projects/wikis/_sidebar.html.haml index 28353927135..02c5a6ea55c 100644 --- a/app/views/projects/wikis/_sidebar.html.haml +++ b/app/views/projects/wikis/_sidebar.html.haml @@ -12,7 +12,7 @@ .blocks-container .block.block-first - if @sidebar_page - = render_wiki_content(@sidebar_page) + = render_wiki_content(@sidebar_page, legacy_render_context(params)) - else %ul.wiki-pages = render @sidebar_wiki_entries, context: 'sidebar' diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 71359708022..80aa1500d53 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -22,27 +22,14 @@ .nav-controls - if can?(current_user, :create_wiki, @project) - = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do + = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-success", "data-toggle" => "modal" do = s_("Wiki|New page") - if @page.persisted? = link_to project_wiki_history_path(@project, @page), class: "btn" do = s_("Wiki|Page history") - if can?(current_user, :admin_wiki, @project) - %button.btn.btn-danger{ data: { toggle: 'modal', - target: '#delete-wiki-modal', - delete_wiki_url: project_wiki_path(@project, @page), - page_title: @page.title.capitalize }, - id: 'delete-wiki-button', - type: 'button' } - = _('Delete') + #delete-wiki-modal-wrapper{ data: { delete_wiki_url: project_wiki_path(@project, @page), page_title: @page.title.capitalize } } -= render 'form' += render 'form', uploads_path: wiki_attachment_upload_url = render 'sidebar' - -#delete-wiki-modal.modal.fade - -- content_for :scripts_body do - -# haml-lint:disable InlineJavaScript - :javascript - window.uploads_path = "#{wiki_attachment_upload_url}"; diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index a08973c7f32..19b9744b508 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -26,6 +26,6 @@ .prepend-top-default.append-bottom-default .wiki - = render_wiki_content(@page) + = render_wiki_content(@page, legacy_render_context(params)) = render 'sidebar' diff --git a/app/views/search/results/_snippet_blob.html.haml b/app/views/search/results/_snippet_blob.html.haml index 57a0b64bfd5..8b95bdf9747 100644 --- a/app/views/search/results/_snippet_blob.html.haml +++ b/app/views/search/results/_snippet_blob.html.haml @@ -21,7 +21,7 @@ .file-content.wiki - snippet_chunks.each do |chunk| - unless chunk[:data].empty? - = markup(snippet.file_name, chunk[:data]) + = markup(snippet.file_name, chunk[:data], legacy_render_context(params)) - else .file-content.code .nothing-here-block Empty file diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml new file mode 100644 index 00000000000..6c4607b2f16 --- /dev/null +++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml @@ -0,0 +1,9 @@ +- if show_auto_devops_implicitly_enabled_banner?(project) + .auto-devops-implicitly-enabled-banner.alert.alert-warning + - more_information_link = link_to _('More information'), 'https://docs.gitlab.com/ee/topics/autodevops/', class: 'alert-link' + - auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link } + = auto_devops_message.html_safe + .alert-link-group + = link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link' + | + = link_to _('Dismiss'), '#', class: 'hide-auto-devops-implicitly-enabled-banner alert-link', data: { project_id: project.id } diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml index 3655c2a1d42..a2df0347fd6 100644 --- a/app/views/shared/_clone_panel.html.haml +++ b/app/views/shared/_clone_panel.html.haml @@ -1,14 +1,14 @@ - project = project || @project -.git-clone-holder.input-group +.git-clone-holder.js-git-clone-holder.input-group .input-group-prepend - if allowed_protocols_present? .input-group-text.clone-dropdown-btn.btn - %span + %span.js-clone-dropdown-label = enabled_project_button(project, enabled_protocol) - else %a#clone-dropdown.input-group-text.btn.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } } - %span + %span.js-clone-dropdown-label = default_clone_protocol.upcase = icon('caret-down') %ul.dropdown-menu.dropdown-menu-selectable.clone-options-dropdown diff --git a/app/views/shared/_mobile_clone_panel.html.haml b/app/views/shared/_mobile_clone_panel.html.haml new file mode 100644 index 00000000000..998985cabe1 --- /dev/null +++ b/app/views/shared/_mobile_clone_panel.html.haml @@ -0,0 +1,13 @@ +- project = project || @project +- ssh_copy_label = _("Copy SSH clone URL") +- http_copy_label = _("Copy HTTPS clone URL") + +.btn-group.mobile-git-clone.js-mobile-git-clone + = clipboard_button(button_text: default_clone_label, target: '#project_clone', hide_button_icon: true, class: "input-group-text clone-dropdown-btn js-clone-dropdown-label btn btn-default") + %button.btn.btn-default.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { toggle: "dropdown" } } + = icon("caret-down", class: "dropdown-btn-icon") + %ul.dropdown-menu.dropdown-menu-selectable.dropdown-menu-right.clone-options-dropdown{ data: { dropdown: true } } + %li + = dropdown_item_with_description(ssh_copy_label, project.ssh_url_to_repo, href: project.ssh_url_to_repo, data: { clone_type: 'ssh' }) + %li + = dropdown_item_with_description(http_copy_label, project.http_url_to_repo, href: project.http_url_to_repo, data: { clone_type: 'http' }) diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index d38d161047b..9bc67a7c715 100644 --- a/app/views/shared/_new_project_item_select.html.haml +++ b/app/views/shared/_new_project_item_select.html.haml @@ -1,7 +1,7 @@ - if any_projects?(@projects) .project-item-select-holder.btn-group - %a.btn.btn-new.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } } + %a.btn.btn-success.new-project-item-link.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } } = icon('spinner spin') = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled] - %button.btn.btn-new.new-project-item-select-button.qa-new-project-item-select-button + %button.btn.btn-success.new-project-item-select-button.qa-new-project-item-select-button = icon('caret-down') diff --git a/app/views/shared/_personal_access_tokens_form.html.haml b/app/views/shared/_personal_access_tokens_form.html.haml index 58d310fac16..f4df7bdcd83 100644 --- a/app/views/shared/_personal_access_tokens_form.html.haml +++ b/app/views/shared/_personal_access_tokens_form.html.haml @@ -26,4 +26,4 @@ = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: token, scopes: scopes .prepend-top-default - = f.submit "Create #{type} token", class: "btn btn-create" + = f.submit "Create #{type} token", class: "btn btn-success" diff --git a/app/views/shared/_ping_consent.html.haml b/app/views/shared/_ping_consent.html.haml new file mode 100644 index 00000000000..f8eb2b2833b --- /dev/null +++ b/app/views/shared/_ping_consent.html.haml @@ -0,0 +1,12 @@ +- if session[:ask_for_usage_stats_consent] + .ping-consent-message.alert.alert-warning.flex-alert + - settings_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: admin_application_settings_path(anchor: 'js-usage-settings') } + - info_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" class="alert-link">'.html_safe % { url: help_page_path('user/admin_area/settings/usage_statistics.md') } + .alert-message + = s_('To help improve GitLab, we would like to periodically collect usage information. This can be changed at any time in %{settings_link_start}Settings%{link_end}. %{info_link_start}More Information%{link_end}').html_safe % { settings_link_start: settings_link_start, info_link_start: info_link_start, link_end: '</a>'.html_safe } + .alert-link-group + - send_usage_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 }) + - not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 }) + = link_to _("Send usage data"), send_usage_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-ping-enabled': true, class: 'alert-link js-usage-consent-action' + | + = link_to _('Not now'), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-ping-enabled': false, class: 'hide-ping-consent-message alert-link js-usage-consent-action' diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml index a0ba1afc284..10f358402c1 100644 --- a/app/views/shared/_recaptcha_form.html.haml +++ b/app/views/shared/_recaptcha_form.html.haml @@ -17,4 +17,4 @@ - if has_submit .row-content-block.footer-block - = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-create' + = f.submit "Submit #{humanized_resource_name}", class: 'btn btn-success' diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml index 01ce1225b8d..ba37b37a3b1 100644 --- a/app/views/shared/_visibility_level.html.haml +++ b/app/views/shared/_visibility_level.html.haml @@ -2,7 +2,7 @@ .form-group.row.visibility-level-setting - if with_label - = f.label :visibility_level, class: 'col-form-label col-sm-2' do + = f.label :visibility_level, class: 'col-form-label col-sm-2 pt-0' do Visibility Level = link_to icon('question-circle'), help_page_path("public_access/public_access") %div{ :class => (with_label ? "col-sm-10" : "col-sm-12") } diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml index e8749ee3956..b629ceafeb3 100644 --- a/app/views/shared/empty_states/_labels.html.haml +++ b/app/views/shared/empty_states/_labels.html.haml @@ -7,5 +7,5 @@ %h4= _("Labels can be applied to issues and merge requests to categorize them.") %p= _("You can also star a label to make it a priority label.") - if can?(current_user, :admin_label, @project) - = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-new', title: _('New label'), id: 'new_label_link' + = link_to _('New label'), new_project_label_path(@project), class: 'btn btn-success', title: _('New label'), id: 'new_label_link' = link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn btn-success btn-inverted', title: _('Generate a default set of labels'), id: 'generate_labels_link' diff --git a/app/views/shared/empty_states/_merge_requests.html.haml b/app/views/shared/empty_states/_merge_requests.html.haml index 186139f3526..421a1b2415b 100644 --- a/app/views/shared/empty_states/_merge_requests.html.haml +++ b/app/views/shared/empty_states/_merge_requests.html.haml @@ -17,7 +17,7 @@ - if project_select_button = render 'shared/new_project_item_select', path: 'merge_requests/new', label: _('New merge request'), type: :merge_requests, with_feature_enabled: 'merge_requests' - else - = link_to _('New merge request'), button_path, class: 'btn btn-new', title: _('New merge request'), id: 'new_merge_request_link' + = link_to _('New merge request'), button_path, class: 'btn btn-success', title: _('New merge request'), id: 'new_merge_request_link' - else %h4.text-center = _("There are no merge requests to show") diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml index f1a41074c28..5351c9ce6a4 100644 --- a/app/views/shared/empty_states/_wikis.html.haml +++ b/app/views/shared/empty_states/_wikis.html.haml @@ -2,7 +2,7 @@ - if can?(current_user, :create_wiki, @project) - create_path = project_wiki_path(@project, params[:id], { view: 'create' }) - - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-new', title: s_('WikiEmpty|Create your first page') + - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-success', title: s_('WikiEmpty|Create your first page') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do %h4 @@ -13,7 +13,7 @@ - elsif can?(current_user, :read_issue, @project) - issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project) - - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-new', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement') + - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-success', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement') = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do %h4 diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml index a9c78547eae..c35f6f5a3c1 100644 --- a/app/views/shared/groups/_empty_state.html.haml +++ b/app/views/shared/groups/_empty_state.html.haml @@ -1,7 +1,8 @@ -.groups-empty-state.qa-groups-empty-state - = custom_icon("icon_empty_groups") +.group-empty-state.row.align-items-center.justify-content-center.qa-groups-empty-state + .icon.text-center.order-md-2 + = custom_icon("icon_empty_groups") - .text-content + .text-content.m-0.order-md-1 %h4= s_("GroupsEmptyState|A group is a collection of several projects.") %p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.") %p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.") diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml index 3f91263089a..67e1cd0d67b 100644 --- a/app/views/shared/groups/_search_form.html.haml +++ b/app/views/shared/groups/_search_form.html.haml @@ -1,2 +1,2 @@ -= form_tag request.path, method: :get, class: 'group-filter-form append-right-10', id: 'group-filter-form' do |f| - = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Filter by name...'), class: 'group-filter-form-field form-control input-short js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" += form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f| + = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" diff --git a/app/views/shared/issuable/_assignees.html.haml b/app/views/shared/issuable/_assignees.html.haml index fc86f855865..ef3d44a9241 100644 --- a/app/views/shared/issuable/_assignees.html.haml +++ b/app/views/shared/issuable/_assignees.html.haml @@ -3,7 +3,7 @@ - render_count = assignees_rendering_overflow ? max_render - 1 : max_render - more_assignees_count = issue.assignees.size - render_count -- issue.assignees.take(render_count).each do |assignee| +- issue.assignees.take(render_count).each do |assignee| # rubocop: disable CodeReuse/ActiveRecord = link_to_member(@project, assignee, name: false, title: "Assigned to :name") - if more_assignees_count.positive? diff --git a/app/views/shared/issuable/_board_create_list_dropdown.html.haml b/app/views/shared/issuable/_board_create_list_dropdown.html.haml index 23b2e1b91e5..4597d9439fa 100644 --- a/app/views/shared/issuable/_board_create_list_dropdown.html.haml +++ b/app/views/shared/issuable/_board_create_list_dropdown.html.haml @@ -1,5 +1,5 @@ .dropdown.prepend-left-10#js-add-list - %button.btn.btn-create.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } + %button.btn.btn-success.btn-inverted.js-new-board-list{ type: "button", data: board_list_data } Add list .dropdown-menu.dropdown-menu-paging.dropdown-menu-right.dropdown-menu-issues-board-new.dropdown-menu-selectable.js-tab-container-labels = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Add list" } diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b49e47a7266..5b28a43a361 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -69,9 +69,9 @@ %span.append-right-10 - if issuable.new_record? - = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-create qa-issuable-create-button' + = form.submit "Submit #{issuable.class.model_name.human.downcase}", class: 'btn btn-success qa-issuable-create-button' - else - = form.submit 'Save changes', class: 'btn btn-save' + = form.submit 'Save changes', class: 'btn btn-success' - if !issuable.persisted? && !issuable.project.empty_repo? && (guide_url = issuable.project.present.contribution_guide_path) .inline.prepend-top-10 diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 9ce7f6fe269..659e03fd67d 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -34,7 +34,7 @@ %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } %button.btn.btn-link - = icon('search') + = sprite_icon('search') %span Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } @@ -42,7 +42,8 @@ %button.btn.btn-link -# Encapsulate static class name `{{icon}}` inside #{} to bypass -# haml lint's ClassAttributeWithStaticValue - %i.fa{ class: "#{'{{icon}}'}" } + %svg + %use{ 'xlink:href': "#{'{{icon}}'}" } %span.js-filter-hint {{hint}} %span.js-filter-tag.dropdown-light-content diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0ca35ea1298..32b609eed0d 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -159,7 +159,7 @@ = dropdown_content = dropdown_loading = dropdown_footer add_content_class: true do - %button.btn.btn-new.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true } + %button.btn.btn-success.sidebar-move-issue-confirmation-button.js-move-issue-confirmation-button{ disabled: true } = _('Move') = icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon') diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index d8580ad8ab4..ac8d58c0bfe 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -34,4 +34,4 @@ = form.label :due_date, "Due date", class: "col-form-label col-md-2 col-lg-4" .col-8 .issuable-form-select-holder - = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date" + = form.text_field :due_date, id: "issuable-due-date", class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off' diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index e49bdec386a..56c4b021eab 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -9,7 +9,7 @@ autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', placeholder: _('Title') - if issuable.respond_to?(:work_in_progress?) - %p.form-text.text-muted + .form-text.text-muted .js-wip-explanation %a.js-toggle-wip{ href: '', tabindex: -1 } Remove the diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml index 2bf5efae1e6..335c34a4632 100644 --- a/app/views/shared/labels/_form.html.haml +++ b/app/views/shared/labels/_form.html.haml @@ -28,7 +28,7 @@ .form-actions - if @label.persisted? - = f.submit 'Save changes', class: 'btn btn-save js-save-button' + = f.submit 'Save changes', class: 'btn btn-success js-save-button' - else - = f.submit 'Create label', class: 'btn btn-create js-save-button' + = f.submit 'Create label', class: 'btn btn-success js-save-button' = link_to 'Cancel', back_path, class: 'btn btn-cancel' diff --git a/app/views/shared/labels/_sort_dropdown.html.haml b/app/views/shared/labels/_sort_dropdown.html.haml new file mode 100644 index 00000000000..ff6e2947ffd --- /dev/null +++ b/app/views/shared/labels/_sort_dropdown.html.haml @@ -0,0 +1,9 @@ +- sort_title = label_sort_options_hash[@sort] || sort_title_name_desc +.dropdown.inline + %button.dropdown-toggle{ type: 'button', data: { toggle: 'dropdown' } } + = sort_title + = icon('chevron-down') + %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-sort + %li + - label_sort_options_hash.each do |value, title| + = sortable_item(title, page_filter_path(sort: value, label: true), sort_title) diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml index 40224cec9e8..ebae58f28ba 100644 --- a/app/views/shared/members/_access_request_buttons.html.haml +++ b/app/views/shared/members/_access_request_buttons.html.haml @@ -1,13 +1,13 @@ - model_name = source.model_name.to_s.downcase -- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) +- if can?(current_user, :"destroy_#{model_name}_member", source.members.find_by(user_id: current_user.id)) # rubocop: disable CodeReuse/ActiveRecord .project-action-button.inline - link_text = source.is_a?(Group) ? _('Leave group') : _('Leave project') = link_to link_text, polymorphic_path([:leave, source, :members]), method: :delete, data: { confirm: leave_confirmation_message(source) }, class: 'btn' -- elsif requester = source.requesters.find_by(user_id: current_user.id) +- elsif requester = source.requesters.find_by(user_id: current_user.id) # rubocop: disable CodeReuse/ActiveRecord .project-action-button.inline = link_to _('Withdraw Access Request'), polymorphic_path([:leave, source, :members]), method: :delete, diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml index ed336df4e9d..0674c822d63 100644 --- a/app/views/shared/notes/_comment_button.html.haml +++ b/app/views/shared/notes/_comment_button.html.haml @@ -1,7 +1,7 @@ - noteable_name = @note.noteable.human_class_name .float-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown - %input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' } + %input.btn.btn-nr.btn-success.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' } - if @note.can_be_discussion_note? = button_tag type: 'button', class: 'btn btn-nr dropdown-toggle comment-btn js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => 'Open comment type dropdown' do diff --git a/app/views/shared/notes/_edit_form.html.haml b/app/views/shared/notes/_edit_form.html.haml index 71a5b94e958..fec966069b9 100644 --- a/app/views/shared/notes/_edit_form.html.haml +++ b/app/views/shared/notes/_edit_form.html.haml @@ -9,6 +9,6 @@ .note-form-actions.clearfix .settings-message.note-edit-warning.js-finish-edit-warning Finish editing this message first! - = submit_tag 'Save comment', class: 'btn btn-nr btn-save js-comment-save-button' + = submit_tag 'Save comment', class: 'btn btn-nr btn-success js-comment-save-button' %button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' } Cancel diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index fa93307be31..daf08d9bb2c 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -51,6 +51,6 @@ = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.') + .form-text.text-muted= _('You can set up jobs to only use Runners with specific tags. Separate tags with commas.') .form-actions = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/shared/runners/_runner_description.html.haml b/app/views/shared/runners/_runner_description.html.haml index da5c032add5..5935750ca06 100644 --- a/app/views/shared/runners/_runner_description.html.haml +++ b/app/views/shared/runners/_runner_description.html.haml @@ -1,6 +1,6 @@ .light.prepend-top-default %p - = _("A 'Runner' is a process which runs a job. You can setup as many Runners as you need.") + = _("A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") %br = _('Runners can be placed on separate users, servers, and even on your local machine.') diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml index 5e5c050d5c3..1b66d3acd40 100644 --- a/app/views/shared/snippets/_form.html.haml +++ b/app/views/shared/snippets/_form.html.haml @@ -32,9 +32,9 @@ .form-actions - if @snippet.new_record? - = f.submit 'Create snippet', class: "btn-create btn" + = f.submit 'Create snippet', class: "btn-success btn" - else - = f.submit 'Save changes', class: "btn-save btn" + = f.submit 'Save changes', class: "btn-success btn" - if @snippet.project_id = link_to "Cancel", project_snippets_path(@project), class: "btn btn-cancel" diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml index ae69d0d07c7..0ce13ee7a53 100644 --- a/app/views/snippets/_actions.html.haml +++ b/app/views/snippets/_actions.html.haml @@ -7,7 +7,7 @@ - if can?(current_user, :admin_personal_snippet, @snippet) = link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-inverted btn-remove", title: 'Delete Snippet' do Delete - = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-create", title: "New snippet" do + = link_to new_snippet_path, class: "btn btn-grouped btn-inverted btn-success", title: "New snippet" do New snippet - if @snippet.submittable_as_spam_by?(current_user) = link_to 'Submit as spam', mark_as_spam_snippet_path(@snippet), method: :post, class: 'btn btn-grouped btn-spam', title: 'Submit as spam' diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml index cc0e93c0755..39d4d82a77d 100644 --- a/app/views/u2f/_register.html.haml +++ b/app/views/u2f/_register.html.haml @@ -8,13 +8,13 @@ - if current_user.two_factor_otp_enabled? .row.append-bottom-10 .col-md-4 - %button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device + %button#js-setup-u2f-device.btn.btn-info.btn-block Set up new U2F device .col-md-8 %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. - else .row.append-bottom-10 .col-md-4 - %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device + %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Set up new U2F device .col-md-8 %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device. diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 2d4656e8608..938cb579e9f 100644 --- a/app/views/users/calendar_activities.html.haml +++ b/app/views/users/calendar_activities.html.haml @@ -1,6 +1,5 @@ %h4.prepend-top-20 - Contributions for - %strong= @calendar_date.to_s(:medium) + = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) } - if @events.any? %ul.bordered-list @@ -9,25 +8,28 @@ %span.light %i.fa.fa-clock-o = event.created_at.strftime('%-I:%M%P') - - if event.push? - #{event.action_name} #{event.ref_type} + - if event.visible_to_user?(current_user) + - if event.push? + #{event.action_name} #{event.ref_type} + %strong + - commits_path = project_commits_path(event.project, event.ref_name) + = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path + - else + = event_action_name(event) + %strong + - if event.note? + = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title + - elsif event.target + = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title + + at %strong - - commits_path = project_commits_path(event.project, event.ref_name) - = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path + - if event.project + = link_to_project(event.project) + - else + = event.project_name - else - = event_action_name(event) - %strong - - if event.note? - = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title - - elsif event.target - = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title - - at - %strong - - if event.project - = link_to_project event.project - - else - = event.project_name + made a private contribution - else %p - No contributions found for #{@calendar_date.to_s(:medium)} + = _('No contributions were found') diff --git a/app/workers/admin_email_worker.rb b/app/workers/admin_email_worker.rb index 06324575ffc..f69e74b2674 100644 --- a/app/workers/admin_email_worker.rb +++ b/app/workers/admin_email_worker.rb @@ -10,10 +10,12 @@ class AdminEmailWorker private + # rubocop: disable CodeReuse/ActiveRecord def send_repository_check_mail repository_check_failed_count = Project.where(last_repository_check_failed: true).count return if repository_check_failed_count.zero? RepositoryCheckMailer.notify(repository_check_failed_count).deliver_now end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index f95df7ecf03..1eeb972cee9 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1,4 +1,6 @@ --- +- auto_devops:auto_devops_disable + - cronjob:admin_email - cronjob:expire_build_artifacts - cronjob:gitlab_usage_ping @@ -85,6 +87,7 @@ - authorized_projects - background_migration - create_gpg_signature +- delete_container_repository - delete_merged_branches - delete_user - email_receiver diff --git a/app/workers/archive_trace_worker.rb b/app/workers/archive_trace_worker.rb index c6f89a17729..c1283e9b2fc 100644 --- a/app/workers/archive_trace_worker.rb +++ b/app/workers/archive_trace_worker.rb @@ -4,9 +4,11 @@ class ArchiveTraceWorker include ApplicationWorker include PipelineBackgroundQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(job_id) Ci::Build.without_archived_trace.find_by(id: job_id).try do |job| job.trace.archive! end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index dd62bb0f33d..c9ddeb08613 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -12,9 +12,11 @@ class AuthorizedProjectsWorker end end + # rubocop: disable CodeReuse/ActiveRecord def perform(user_id) user = User.find_by(id: user_id) user&.refresh_authorized_projects end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/auto_devops/disable_worker.rb b/app/workers/auto_devops/disable_worker.rb new file mode 100644 index 00000000000..73ddc591505 --- /dev/null +++ b/app/workers/auto_devops/disable_worker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module AutoDevops + class DisableWorker + include ApplicationWorker + include AutoDevopsQueue + + def perform(pipeline_id) + pipeline = Ci::Pipeline.find(pipeline_id) + project = pipeline.project + + send_notification_email(pipeline, project) if disable_service(project).execute + end + + private + + def disable_service(project) + Projects::AutoDevops::DisableService.new(project) + end + + def send_notification_email(pipeline, project) + recipients = email_receivers_for(pipeline, project) + + return unless recipients.any? + + NotificationService.new.autodevops_disabled(pipeline, recipients) + end + + def email_receivers_for(pipeline, project) + recipients = [pipeline.user&.email] + recipients << project.owner.email unless project.group + recipients.uniq.compact + end + end +end diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb index 53d77dc4524..912c53e11f8 100644 --- a/app/workers/build_coverage_worker.rb +++ b/app/workers/build_coverage_worker.rb @@ -4,7 +4,9 @@ class BuildCoverageWorker include ApplicationWorker include PipelineQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id)&.update_coverage end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb index 9dc2c7f3601..51cbbe8882e 100644 --- a/app/workers/build_finished_worker.rb +++ b/app/workers/build_finished_worker.rb @@ -6,6 +6,7 @@ class BuildFinishedWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| # We execute that in sync as this access the files in order to access local file, and reduce IO @@ -17,4 +18,5 @@ class BuildFinishedWorker ArchiveTraceWorker.perform_async(build.id) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/build_hooks_worker.rb b/app/workers/build_hooks_worker.rb index f1f71dc589c..b0c3676714c 100644 --- a/app/workers/build_hooks_worker.rb +++ b/app/workers/build_hooks_worker.rb @@ -6,8 +6,10 @@ class BuildHooksWorker queue_namespace :pipeline_hooks + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id) .try(:execute_hooks) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/build_queue_worker.rb b/app/workers/build_queue_worker.rb index 1b3f1fd3c2a..67d5b0f5f5b 100644 --- a/app/workers/build_queue_worker.rb +++ b/app/workers/build_queue_worker.rb @@ -6,9 +6,11 @@ class BuildQueueWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| Ci::UpdateBuildQueueService.new.execute(build) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index e1c1cc24a94..c17608f7378 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -6,11 +6,13 @@ class BuildSuccessWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| create_deployment(build) if build.has_environment? end end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/build_trace_sections_worker.rb b/app/workers/build_trace_sections_worker.rb index f4114b3353c..0641130fd64 100644 --- a/app/workers/build_trace_sections_worker.rb +++ b/app/workers/build_trace_sections_worker.rb @@ -4,7 +4,9 @@ class BuildTraceSectionsWorker include ApplicationWorker include PipelineQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) Ci::Build.find_by(id: build_id)&.parse_trace_sections! end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/ci/archive_traces_cron_worker.rb b/app/workers/ci/archive_traces_cron_worker.rb index 7d4e9660a4e..7443aad1380 100644 --- a/app/workers/ci/archive_traces_cron_worker.rb +++ b/app/workers/ci/archive_traces_cron_worker.rb @@ -5,6 +5,7 @@ module Ci include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform # Archive stale live traces which still resides in redis or database # This could happen when ArchiveTraceWorker sidekiq jobs were lost by receiving SIGKILL @@ -19,6 +20,7 @@ module Ci end end end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/ci/build_trace_chunk_flush_worker.rb b/app/workers/ci/build_trace_chunk_flush_worker.rb index 9dbf2e5e1ac..23a11c28f9b 100644 --- a/app/workers/ci/build_trace_chunk_flush_worker.rb +++ b/app/workers/ci/build_trace_chunk_flush_worker.rb @@ -5,10 +5,12 @@ module Ci include ApplicationWorker include PipelineBackgroundQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(build_trace_chunk_id) ::Ci::BuildTraceChunk.find_by(id: build_trace_chunk_id).try do |build_trace_chunk| build_trace_chunk.persist_data! end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/workers/concerns/auto_devops_queue.rb b/app/workers/concerns/auto_devops_queue.rb new file mode 100644 index 00000000000..aba928ccaab --- /dev/null +++ b/app/workers/concerns/auto_devops_queue.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +# +module AutoDevopsQueue + extend ActiveSupport::Concern + + included do + queue_namespace :auto_devops + end +end diff --git a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb index 692ca6b7f42..1c6413674a0 100644 --- a/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb +++ b/app/workers/concerns/gitlab/github_import/rescheduling_methods.rb @@ -8,6 +8,7 @@ module Gitlab # project_id - The ID of the GitLab project to import the note into. # hash - A Hash containing the details of the GitHub object to imoprt. # notify_key - The Redis key to notify upon completion, if any. + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, hash, notify_key = nil) project = Project.find_by(id: project_id) @@ -24,6 +25,7 @@ module Gitlab .perform_in(client.rate_limit_resets_in, project.id, hash, notify_key) end end + # rubocop: enable CodeReuse/ActiveRecord def try_import(*args) import(*args) diff --git a/app/workers/concerns/gitlab/github_import/stage_methods.rb b/app/workers/concerns/gitlab/github_import/stage_methods.rb index 147c8c8d683..59e6bc2c97d 100644 --- a/app/workers/concerns/gitlab/github_import/stage_methods.rb +++ b/app/workers/concerns/gitlab/github_import/stage_methods.rb @@ -20,11 +20,13 @@ module Gitlab self.class.perform_in(client.rate_limit_resets_in, project.id) end + # rubocop: disable CodeReuse/ActiveRecord def find_project(id) # If the project has been marked as failed we want to bail out # automatically. Project.import_started.find_by(id: id) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/workers/concerns/new_issuable.rb b/app/workers/concerns/new_issuable.rb index 7735dec5e6b..a89451a4475 100644 --- a/app/workers/concerns/new_issuable.rb +++ b/app/workers/concerns/new_issuable.rb @@ -10,17 +10,21 @@ module NewIssuable user && issuable end + # rubocop: disable CodeReuse/ActiveRecord def set_user(user_id) @user = User.find_by(id: user_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables log_error(User, user_id) unless @user # rubocop:disable Gitlab/ModuleWithInstanceVariables end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def set_issuable(issuable_id) @issuable = issuable_class.find_by(id: issuable_id) # rubocop:disable Gitlab/ModuleWithInstanceVariables log_error(issuable_class, issuable_id) unless @issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables end + # rubocop: enable CodeReuse/ActiveRecord def log_error(record_class, record_id) Rails.logger.error("#{self.class}: couldn't find #{record_class} with ID=#{record_id}, skipping job") diff --git a/app/workers/create_gpg_signature_worker.rb b/app/workers/create_gpg_signature_worker.rb index a1aeeb7c4fc..49c7a403838 100644 --- a/app/workers/create_gpg_signature_worker.rb +++ b/app/workers/create_gpg_signature_worker.rb @@ -3,6 +3,7 @@ class CreateGpgSignatureWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(commit_shas, project_id) # Older versions of GitPushService may push a single commit ID on the stack. # We need this to be backwards compatible. @@ -26,4 +27,5 @@ class CreateGpgSignatureWorker end end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb new file mode 100644 index 00000000000..e8fe9d82797 --- /dev/null +++ b/app/workers/delete_container_repository_worker.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +class DeleteContainerRepositoryWorker + include ApplicationWorker + include ExclusiveLeaseGuard + + LEASE_TIMEOUT = 1.hour + + attr_reader :container_repository + + # rubocop: disable CodeReuse/ActiveRecord + def perform(current_user_id, container_repository_id) + current_user = User.find_by(id: current_user_id) + @container_repository = ContainerRepository.find_by(id: container_repository_id) + project = container_repository&.project + + return unless current_user && container_repository && project + + # If a user accidentally attempts to delete the same container registry in quick succession, + # this can lead to orphaned tags. + try_obtain_lease do + Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository) + end + end + # rubocop: enable CodeReuse/ActiveRecord + + # For ExclusiveLeaseGuard concern + def lease_key + @lease_key ||= "container_repository:delete:#{container_repository.id}" + end + + # For ExclusiveLeaseGuard concern + def lease_timeout + LEASE_TIMEOUT + end +end diff --git a/app/workers/delete_diff_files_worker.rb b/app/workers/delete_diff_files_worker.rb index 0874a0b75e8..f518dfe871c 100644 --- a/app/workers/delete_diff_files_worker.rb +++ b/app/workers/delete_diff_files_worker.rb @@ -3,6 +3,7 @@ class DeleteDiffFilesWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(merge_request_diff_id) merge_request_diff = MergeRequestDiff.find(merge_request_diff_id) @@ -16,4 +17,5 @@ class DeleteDiffFilesWorker .delete_all end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb index 854b74b884a..64bc9776d48 100644 --- a/app/workers/detect_repository_languages_worker.rb +++ b/app/workers/detect_repository_languages_worker.rb @@ -11,6 +11,7 @@ class DetectRepositoryLanguagesWorker attr_reader :project + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, user_id) @project = Project.find_by(id: project_id) user = User.find_by(id: user_id) @@ -20,6 +21,7 @@ class DetectRepositoryLanguagesWorker ::Projects::DetectRepositoryLanguagesService.new(project, user).execute end end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb index 5d3a9a39b93..dce812d1ae2 100644 --- a/app/workers/expire_build_artifacts_worker.rb +++ b/app/workers/expire_build_artifacts_worker.rb @@ -4,6 +4,7 @@ class ExpireBuildArtifactsWorker include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform Rails.logger.info 'Scheduling removal of build artifacts' @@ -12,4 +13,5 @@ class ExpireBuildArtifactsWorker ExpireBuildInstanceArtifactsWorker.bulk_perform_async(build_ids) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb index 3b57ecb36e3..4fcd1e5bd24 100644 --- a/app/workers/expire_build_instance_artifacts_worker.rb +++ b/app/workers/expire_build_instance_artifacts_worker.rb @@ -3,6 +3,7 @@ class ExpireBuildInstanceArtifactsWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(build_id) build = Ci::Build .with_expired_artifacts @@ -14,4 +15,5 @@ class ExpireBuildInstanceArtifactsWorker Rails.logger.info "Removing artifacts for build #{build.id}..." build.erase_artifacts! end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb index 14a57b90114..b09d0a5d121 100644 --- a/app/workers/expire_job_cache_worker.rb +++ b/app/workers/expire_job_cache_worker.rb @@ -6,6 +6,7 @@ class ExpireJobCacheWorker queue_namespace :pipeline_cache + # rubocop: disable CodeReuse/ActiveRecord def perform(job_id) job = CommitStatus.joins(:pipeline, :project).find_by(id: job_id) return unless job @@ -18,6 +19,7 @@ class ExpireJobCacheWorker store.touch(project_job_path(project, job)) end end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb index 992fc63c451..c96e8a0379b 100644 --- a/app/workers/expire_pipeline_cache_worker.rb +++ b/app/workers/expire_pipeline_cache_worker.rb @@ -6,6 +6,7 @@ class ExpirePipelineCacheWorker queue_namespace :pipeline_cache + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) pipeline = Ci::Pipeline.find_by(id: pipeline_id) return unless pipeline @@ -23,6 +24,7 @@ class ExpirePipelineCacheWorker Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline) end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/gitlab/github_import/advance_stage_worker.rb b/app/workers/gitlab/github_import/advance_stage_worker.rb index be0b6c180b0..cd2ceb8dcdf 100644 --- a/app/workers/gitlab/github_import/advance_stage_worker.rb +++ b/app/workers/gitlab/github_import/advance_stage_worker.rb @@ -63,12 +63,14 @@ module Gitlab end end + # rubocop: disable CodeReuse/ActiveRecord def find_project(id) # TODO: Only select the JID # This is due to the fact that the JID could be present in either the project record or # its associated import_state record Project.import_started.find_by(id: id) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb index 68d2c5c4331..65473026b4c 100644 --- a/app/workers/gitlab/github_import/refresh_import_jid_worker.rb +++ b/app/workers/gitlab/github_import/refresh_import_jid_worker.rb @@ -30,12 +30,14 @@ module Gitlab # stage, if it died there's nothing we can do anyway. end + # rubocop: disable CodeReuse/ActiveRecord def find_project(id) # TODO: Only select the JID # This is due to the fact that the JID could be present in either the project record or # its associated import_state record Project.import_started.find_by(id: id) end + # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/app/workers/invalid_gpg_signature_update_worker.rb b/app/workers/invalid_gpg_signature_update_worker.rb index 4724ab7ad98..fc8a731b427 100644 --- a/app/workers/invalid_gpg_signature_update_worker.rb +++ b/app/workers/invalid_gpg_signature_update_worker.rb @@ -3,6 +3,7 @@ class InvalidGpgSignatureUpdateWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(gpg_key_id) gpg_key = GpgKey.find_by(id: gpg_key_id) @@ -10,4 +11,5 @@ class InvalidGpgSignatureUpdateWorker Gitlab::Gpg::InvalidGpgSignatureUpdater.new(gpg_key).run end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/issue_due_scheduler_worker.rb b/app/workers/issue_due_scheduler_worker.rb index c04a2d75e0b..476cba47ad7 100644 --- a/app/workers/issue_due_scheduler_worker.rb +++ b/app/workers/issue_due_scheduler_worker.rb @@ -4,9 +4,11 @@ class IssueDueSchedulerWorker include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform project_ids = Issue.opened.due_tomorrow.group(:project_id).pluck(:project_id).map { |id| [id] } MailScheduler::IssueDueWorker.bulk_perform_async(project_ids) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/mail_scheduler/issue_due_worker.rb b/app/workers/mail_scheduler/issue_due_worker.rb index 8794ad7a82c..1e1dde1e829 100644 --- a/app/workers/mail_scheduler/issue_due_worker.rb +++ b/app/workers/mail_scheduler/issue_due_worker.rb @@ -5,10 +5,12 @@ module MailScheduler include ApplicationWorker include MailSchedulerQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id) Issue.opened.due_tomorrow.in_projects(project_id).preload(:project).find_each do |issue| notification_service.issue_due(issue) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb index 5d8b8904502..fa48c1b29a8 100644 --- a/app/workers/new_merge_request_worker.rb +++ b/app/workers/new_merge_request_worker.rb @@ -9,6 +9,8 @@ class NewMergeRequestWorker EventCreateService.new.open_mr(issuable, user) NotificationService.new.new_merge_request(issuable, user) + + issuable.diffs(include_stats: false).write_cache issuable.create_cross_references!(user) end diff --git a/app/workers/new_note_worker.rb b/app/workers/new_note_worker.rb index 74f34dcf9aa..42f5b945a75 100644 --- a/app/workers/new_note_worker.rb +++ b/app/workers/new_note_worker.rb @@ -5,6 +5,7 @@ class NewNoteWorker # Keep extra parameter to preserve backwards compatibility with # old `NewNoteWorker` jobs (can remove later) + # rubocop: disable CodeReuse/ActiveRecord def perform(note_id, _params = {}) if note = Note.find_by(id: note_id) NotificationService.new.new_note(note) @@ -13,4 +14,5 @@ class NewNoteWorker Rails.logger.error("NewNoteWorker: couldn't find note with ID=#{note_id}, skipping job") end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb index 01d03ec7888..fe5d27b087d 100644 --- a/app/workers/object_storage/migrate_uploads_worker.rb +++ b/app/workers/object_storage/migrate_uploads_worker.rb @@ -57,11 +57,13 @@ module ObjectStorage include Report + # rubocop: disable CodeReuse/ActiveRecord def self.enqueue!(uploads, model_class, mounted_as, to_store) sanity_check!(uploads, model_class, mounted_as) perform_async(uploads.ids, model_class.to_s, mounted_as, to_store) end + # rubocop: enable CodeReuse/ActiveRecord # We need to be sure all the uploads are for the same uploader and model type # and that the mount point exists if provided. @@ -78,6 +80,7 @@ module ObjectStorage raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount end + # rubocop: disable CodeReuse/ActiveRecord def perform(*args) args_check!(args) @@ -97,6 +100,7 @@ module ObjectStorage # do not retry: the job is insane Rails.logger.warn "#{self.class}: Sanity check error (#{e.message})" end + # rubocop: enable CodeReuse/ActiveRecord def sanity_check!(uploads) self.class.sanity_check!(uploads, @model_class, @mounted_as) diff --git a/app/workers/pages_domain_verification_worker.rb b/app/workers/pages_domain_verification_worker.rb index 4610b688189..b3319ff5a13 100644 --- a/app/workers/pages_domain_verification_worker.rb +++ b/app/workers/pages_domain_verification_worker.rb @@ -3,6 +3,7 @@ class PagesDomainVerificationWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(domain_id) domain = PagesDomain.find_by(id: domain_id) @@ -10,4 +11,5 @@ class PagesDomainVerificationWorker VerifyPagesDomainService.new(domain).execute end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb index 13a6576a301..fa0dfa2ff4b 100644 --- a/app/workers/pages_worker.rb +++ b/app/workers/pages_worker.rb @@ -9,6 +9,7 @@ class PagesWorker send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend end + # rubocop: disable CodeReuse/ActiveRecord def deploy(build_id) build = Ci::Build.find_by(id: build_id) result = Projects::UpdatePagesService.new(build.project, build).execute @@ -18,6 +19,7 @@ class PagesWorker result end + # rubocop: enable CodeReuse/ActiveRecord def remove(namespace_path, project_path) full_path = File.join(Settings.pages.path, namespace_path, project_path) diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb index 58023e0af1b..eae1115e60c 100644 --- a/app/workers/pipeline_hooks_worker.rb +++ b/app/workers/pipeline_hooks_worker.rb @@ -6,8 +6,10 @@ class PipelineHooksWorker queue_namespace :pipeline_hooks + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) .try(:execute_hooks) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_metrics_worker.rb b/app/workers/pipeline_metrics_worker.rb index a97019b100a..c2fbfd2b3a5 100644 --- a/app/workers/pipeline_metrics_worker.rb +++ b/app/workers/pipeline_metrics_worker.rb @@ -4,12 +4,14 @@ class PipelineMetricsWorker include ApplicationWorker include PipelineQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| update_metrics_for_active_pipeline(pipeline) if pipeline.active? update_metrics_for_succeeded_pipeline(pipeline) if pipeline.success? end end + # rubocop: enable CodeReuse/ActiveRecord private @@ -21,9 +23,11 @@ class PipelineMetricsWorker metrics(pipeline).update_all(latest_build_started_at: pipeline.started_at, latest_build_finished_at: pipeline.finished_at, pipeline_id: pipeline.id) end + # rubocop: disable CodeReuse/ActiveRecord def metrics(pipeline) MergeRequest::Metrics.where(merge_request_id: merge_requests(pipeline)) end + # rubocop: enable CodeReuse/ActiveRecord def merge_requests(pipeline) pipeline.merge_requests.map(&:id) diff --git a/app/workers/pipeline_notification_worker.rb b/app/workers/pipeline_notification_worker.rb index 3a8846b3747..e4a18573d20 100644 --- a/app/workers/pipeline_notification_worker.rb +++ b/app/workers/pipeline_notification_worker.rb @@ -4,6 +4,7 @@ class PipelineNotificationWorker include ApplicationWorker include PipelineQueue + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id, recipients = nil) pipeline = Ci::Pipeline.find_by(id: pipeline_id) @@ -11,4 +12,5 @@ class PipelineNotificationWorker NotificationService.new.pipeline_finished(pipeline, recipients) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb index 83744c5338a..f2aa17acb51 100644 --- a/app/workers/pipeline_process_worker.rb +++ b/app/workers/pipeline_process_worker.rb @@ -6,8 +6,10 @@ class PipelineProcessWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) .try(:process!) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb index a1815757735..85d1ffe0fa9 100644 --- a/app/workers/pipeline_schedule_worker.rb +++ b/app/workers/pipeline_schedule_worker.rb @@ -4,6 +4,7 @@ class PipelineScheduleWorker include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now) .preload(:owner, :project).find_each do |schedule| @@ -21,4 +22,5 @@ class PipelineScheduleWorker end end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_success_worker.rb b/app/workers/pipeline_success_worker.rb index 68e9af6a619..4f349ed922c 100644 --- a/app/workers/pipeline_success_worker.rb +++ b/app/workers/pipeline_success_worker.rb @@ -6,6 +6,7 @@ class PipelineSuccessWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline| MergeRequests::MergeWhenPipelineSucceedsService @@ -13,4 +14,5 @@ class PipelineSuccessWorker .trigger(pipeline) end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/pipeline_update_worker.rb b/app/workers/pipeline_update_worker.rb index c33468c1f14..13a748e1551 100644 --- a/app/workers/pipeline_update_worker.rb +++ b/app/workers/pipeline_update_worker.rb @@ -6,8 +6,10 @@ class PipelineUpdateWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(pipeline_id) Ci::Pipeline.find_by(id: pipeline_id) .try(:update_status) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index c9f6df9b56d..7b167c95c29 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -14,6 +14,7 @@ class ProcessCommitWorker # commit_hash - Hash containing commit details to use for constructing a # Commit object without having to use the Git repository. # default - The data was pushed to the default branch. + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, user_id, commit_hash, default = false) project = Project.find_by(id: project_id) @@ -30,6 +31,7 @@ class ProcessCommitWorker process_commit_message(project, commit, user, author, default) update_issue_metrics(commit, author) end + # rubocop: enable CodeReuse/ActiveRecord def process_commit_message(project, commit, user, author, default = false) # Ignore closing references from GitLab-generated commit messages. @@ -50,6 +52,7 @@ class ProcessCommitWorker end end + # rubocop: disable CodeReuse/ActiveRecord def update_issue_metrics(commit, author) mentioned_issues = commit.all_references(author).issues @@ -58,6 +61,7 @@ class ProcessCommitWorker Issue::Metrics.where(issue_id: mentioned_issues.map(&:id), first_mentioned_in_commit_at: nil) .update_all(first_mentioned_in_commit_at: commit.committed_date) end + # rubocop: enable CodeReuse/ActiveRecord def build_commit(project, hash) date_suffix = '_date' diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index b0e1d8837d9..d27b5e62574 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -12,6 +12,7 @@ class ProjectCacheWorker # CHANGELOG. # statistics - An Array containing columns from ProjectStatistics to # refresh, if empty all columns will be refreshed + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, files = [], statistics = []) project = Project.find_by(id: project_id) @@ -23,6 +24,7 @@ class ProjectCacheWorker project.cleanup end + # rubocop: enable CodeReuse/ActiveRecord def update_statistics(project, statistics = []) return unless try_obtain_lease_for(project.id, :update_statistics) diff --git a/app/workers/project_migrate_hashed_storage_worker.rb b/app/workers/project_migrate_hashed_storage_worker.rb index ad0003e7bff..4c6339f7701 100644 --- a/app/workers/project_migrate_hashed_storage_worker.rb +++ b/app/workers/project_migrate_hashed_storage_worker.rb @@ -5,6 +5,7 @@ class ProjectMigrateHashedStorageWorker LEASE_TIMEOUT = 30.seconds.to_i + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, old_disk_path = nil) project = Project.find_by(id: project_id) return if project.nil? || project.pending_delete? @@ -19,6 +20,7 @@ class ProjectMigrateHashedStorageWorker cancel_lease_for(project_id, uuid) if uuid raise ex end + # rubocop: enable CodeReuse/ActiveRecord def lease_for(project_id) Gitlab::ExclusiveLease.new(lease_key(project_id), timeout: LEASE_TIMEOUT) diff --git a/app/workers/project_service_worker.rb b/app/workers/project_service_worker.rb index a0bc9288cf0..25567cec08b 100644 --- a/app/workers/project_service_worker.rb +++ b/app/workers/project_service_worker.rb @@ -7,6 +7,10 @@ class ProjectServiceWorker def perform(hook_id, data) data = data.with_indifferent_access - Service.find(hook_id).execute(data) + service = Service.find(hook_id) + service.execute(data) + rescue => error + service_class = service&.class&.name || "Not Found" + logger.error class: self.class.name, service_class: service_class, message: error.message end end diff --git a/app/workers/propagate_service_template_worker.rb b/app/workers/propagate_service_template_worker.rb index c9da1cae255..3ccd7615697 100644 --- a/app/workers/propagate_service_template_worker.rb +++ b/app/workers/propagate_service_template_worker.rb @@ -6,11 +6,13 @@ class PropagateServiceTemplateWorker LEASE_TIMEOUT = 4.hours.to_i + # rubocop: disable CodeReuse/ActiveRecord def perform(template_id) return unless try_obtain_lease_for(template_id) Projects::PropagateServiceTemplate.propagate(Service.find_by(id: template_id)) end + # rubocop: enable CodeReuse/ActiveRecord private diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb index c1d05ebbcfd..d44ad0d8030 100644 --- a/app/workers/prune_old_events_worker.rb +++ b/app/workers/prune_old_events_worker.rb @@ -4,6 +4,7 @@ class PruneOldEventsWorker include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform # Contribution calendar shows maximum 12 months of events. # Double nested query is used because MySQL doesn't allow DELETE subqueries @@ -17,4 +18,5 @@ class PruneOldEventsWorker .limit(10_000)) .delete_all end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/prune_web_hook_logs_worker.rb b/app/workers/prune_web_hook_logs_worker.rb index 45c7d32f7eb..38054069f4e 100644 --- a/app/workers/prune_web_hook_logs_worker.rb +++ b/app/workers/prune_web_hook_logs_worker.rb @@ -9,6 +9,7 @@ class PruneWebHookLogsWorker # The maximum number of rows to remove in a single job. DELETE_LIMIT = 50_000 + # rubocop: disable CodeReuse/ActiveRecord def perform # MySQL doesn't allow "DELETE FROM ... WHERE id IN ( ... )" if the inner # query refers to the same table. To work around this we wrap the IN body in @@ -23,4 +24,5 @@ class PruneWebHookLogsWorker ) .delete_all end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/reactive_caching_worker.rb b/app/workers/reactive_caching_worker.rb index 9b331f15dc5..96ff8cd6222 100644 --- a/app/workers/reactive_caching_worker.rb +++ b/app/workers/reactive_caching_worker.rb @@ -3,6 +3,7 @@ class ReactiveCachingWorker include ApplicationWorker + # rubocop: disable CodeReuse/ActiveRecord def perform(class_name, id, *args) klass = begin Kernel.const_get(class_name) @@ -13,4 +14,5 @@ class ReactiveCachingWorker klass.find_by(id: id).try(:exclusively_update_reactive_cache!, *args) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/repository_check/batch_worker.rb b/app/workers/repository_check/batch_worker.rb index 07559ea479b..c1bb1adc9cc 100644 --- a/app/workers/repository_check/batch_worker.rb +++ b/app/workers/repository_check/batch_worker.rb @@ -59,22 +59,28 @@ module RepositoryCheck never_checked_project_ids(BATCH_SIZE) + old_checked_project_ids(BATCH_SIZE) end + # rubocop: disable CodeReuse/ActiveRecord def never_checked_project_ids(batch_size) projects_on_shard.where(last_repository_check_at: nil) .where('created_at < ?', 24.hours.ago) .limit(batch_size).pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def old_checked_project_ids(batch_size) projects_on_shard.where.not(last_repository_check_at: nil) .where('last_repository_check_at < ?', 1.month.ago) .reorder(last_repository_check_at: :asc) .limit(batch_size).pluck(:id) end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def projects_on_shard Project.where(repository_storage: shard_name) end + # rubocop: enable CodeReuse/ActiveRecord def try_obtain_lease_for_project(id) # Use a 24-hour timeout because on servers/projects where 'git fsck' is diff --git a/app/workers/repository_check/clear_worker.rb b/app/workers/repository_check/clear_worker.rb index 81e1a4b63bb..01964c69fb2 100644 --- a/app/workers/repository_check/clear_worker.rb +++ b/app/workers/repository_check/clear_worker.rb @@ -5,6 +5,7 @@ module RepositoryCheck include ApplicationWorker include RepositoryCheckQueue + # rubocop: disable CodeReuse/ActiveRecord def perform # Do small batched updates because these updates will be slow and locking Project.select(:id).find_in_batches(batch_size: 100) do |batch| @@ -14,5 +15,6 @@ module RepositoryCheck ) end end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/app/workers/repository_check/single_repository_worker.rb b/app/workers/repository_check/single_repository_worker.rb index f44e5693b25..a8097af321f 100644 --- a/app/workers/repository_check/single_repository_worker.rb +++ b/app/workers/repository_check/single_repository_worker.rb @@ -48,9 +48,11 @@ module RepositoryCheck false end + # rubocop: disable CodeReuse/ActiveRecord def has_changes?(project) Project.with_push.exists?(project.id) end + # rubocop: enable CodeReuse/ActiveRecord def has_wiki_changes?(project) return false unless project.wiki_enabled? diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb index 1f6cb18c812..f72331c003a 100644 --- a/app/workers/run_pipeline_schedule_worker.rb +++ b/app/workers/run_pipeline_schedule_worker.rb @@ -6,6 +6,7 @@ class RunPipelineScheduleWorker queue_namespace :pipeline_creation + # rubocop: disable CodeReuse/ActiveRecord def perform(schedule_id, user_id) schedule = Ci::PipelineSchedule.find_by(id: schedule_id) user = User.find_by(id: user_id) @@ -14,6 +15,7 @@ class RunPipelineScheduleWorker run_pipeline_schedule(schedule, user) end + # rubocop: enable CodeReuse/ActiveRecord def run_pipeline_schedule(schedule, user) Ci::CreatePipelineService.new(schedule.project, diff --git a/app/workers/stage_update_worker.rb b/app/workers/stage_update_worker.rb index ec8c8e3689f..ea587789d03 100644 --- a/app/workers/stage_update_worker.rb +++ b/app/workers/stage_update_worker.rb @@ -6,9 +6,11 @@ class StageUpdateWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(stage_id) Ci::Stage.find_by(id: stage_id).try do |stage| stage.update_status end end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb index c78b7fac589..f6bca1176d1 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -46,6 +46,7 @@ class StuckCiJobsWorker end end + # rubocop: disable CodeReuse/ActiveRecord def search(status, timeout) loop do jobs = Ci::Build.where(status: status) @@ -60,6 +61,7 @@ class StuckCiJobsWorker end end end + # rubocop: enable CodeReuse/ActiveRecord def drop_build(type, build, status, timeout) Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})" diff --git a/app/workers/stuck_import_jobs_worker.rb b/app/workers/stuck_import_jobs_worker.rb index 79ce06dd66e..de92f3eca6a 100644 --- a/app/workers/stuck_import_jobs_worker.rb +++ b/app/workers/stuck_import_jobs_worker.rb @@ -23,6 +23,7 @@ class StuckImportJobsWorker end.count end + # rubocop: disable CodeReuse/ActiveRecord def mark_projects_with_jid_as_failed! # TODO: Rollback this change to use SQL through #pluck jids_and_ids = enqueued_projects_with_jid.map { |project| [project.import_jid, project.id] }.to_h @@ -43,18 +44,25 @@ class StuckImportJobsWorker project.mark_import_as_failed(error_message) end.count end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def enqueued_projects Project.joins_import_state.where("(import_state.status = 'scheduled' OR import_state.status = 'started') OR (projects.import_status = 'scheduled' OR projects.import_status = 'started')") end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def enqueued_projects_with_jid enqueued_projects.where.not("import_state.jid IS NULL AND projects.import_jid IS NULL") end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def enqueued_projects_without_jid enqueued_projects.where("import_state.jid IS NULL AND projects.import_jid IS NULL") end + # rubocop: enable CodeReuse/ActiveRecord def error_message "Import timed out. Import took longer than #{IMPORT_JOBS_EXPIRATION} seconds" diff --git a/app/workers/stuck_merge_jobs_worker.rb b/app/workers/stuck_merge_jobs_worker.rb index b0a62f76e94..98c81956cba 100644 --- a/app/workers/stuck_merge_jobs_worker.rb +++ b/app/workers/stuck_merge_jobs_worker.rb @@ -4,6 +4,7 @@ class StuckMergeJobsWorker include ApplicationWorker include CronjobQueue + # rubocop: disable CodeReuse/ActiveRecord def perform stuck_merge_requests.find_in_batches(batch_size: 100) do |group| jids = group.map(&:merge_jid) @@ -18,9 +19,11 @@ class StuckMergeJobsWorker end end end + # rubocop: enable CodeReuse/ActiveRecord private + # rubocop: disable CodeReuse/ActiveRecord def apply_current_state!(completed_jids, completed_ids) merge_requests = MergeRequest.where(id: completed_ids) @@ -34,8 +37,11 @@ class StuckMergeJobsWorker Rails.logger.info("Updated state of locked merge jobs. JIDs: #{completed_jids.join(', ')}") end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def stuck_merge_requests MergeRequest.select('id, merge_jid').with_state(:locked).where.not(merge_jid: nil).reorder(nil) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/workers/update_head_pipeline_for_merge_request_worker.rb b/app/workers/update_head_pipeline_for_merge_request_worker.rb index 0487a393566..9ce51662969 100644 --- a/app/workers/update_head_pipeline_for_merge_request_worker.rb +++ b/app/workers/update_head_pipeline_for_merge_request_worker.rb @@ -6,6 +6,7 @@ class UpdateHeadPipelineForMergeRequestWorker queue_namespace :pipeline_processing + # rubocop: disable CodeReuse/ActiveRecord def perform(merge_request_id) merge_request = MergeRequest.find(merge_request_id) pipeline = Ci::Pipeline.where(project: merge_request.source_project, ref: merge_request.source_branch).last @@ -20,6 +21,7 @@ class UpdateHeadPipelineForMergeRequestWorker merge_request.update_attribute(:head_pipeline_id, pipeline.id) end + # rubocop: enable CodeReuse/ActiveRecord def log_error_message_for(merge_request) Rails.logger.error( diff --git a/app/workers/update_merge_requests_worker.rb b/app/workers/update_merge_requests_worker.rb index 742841219b3..c7213df652a 100644 --- a/app/workers/update_merge_requests_worker.rb +++ b/app/workers/update_merge_requests_worker.rb @@ -5,6 +5,7 @@ class UpdateMergeRequestsWorker LOG_TIME_THRESHOLD = 90 # seconds + # rubocop: disable CodeReuse/ActiveRecord def perform(project_id, user_id, oldrev, newrev, ref) project = Project.find_by(id: project_id) return unless project @@ -28,4 +29,5 @@ class UpdateMergeRequestsWorker Rails.logger.info("UpdateMergeRequestsWorker#perform #{args_log}") if time.real > LOG_TIME_THRESHOLD end + # rubocop: enable CodeReuse/ActiveRecord end |