diff options
author | Filipa Lacerda <filipa@gitlab.com> | 2018-10-08 10:40:10 +0100 |
---|---|---|
committer | Filipa Lacerda <filipa@gitlab.com> | 2018-10-08 10:40:10 +0100 |
commit | fa875ba7a9441df6827ef1d6b05405c66ee0c579 (patch) | |
tree | 23d0cf911c9bf6a73fec9bb1f3de1bf61bedeacd /app | |
parent | ecefe090460687a078e3d1aacf621fd5bff07fb5 (diff) | |
parent | 838c1076694d24d180e19625d663749c8b5c1a1c (diff) | |
download | gitlab-ce-fa875ba7a9441df6827ef1d6b05405c66ee0c579.tar.gz |
Merge branch 'master' into 42611-removed-branch-link
* master: (1252 commits)
Render log artifact files in GitLab
Check disabled_services when finding a service
Fix invalid parent path on group settings page
Backport CE changes for: [Frontend only] Batch comments on merge requests
Add button to insert table in markdown editor
Update GITALY_SERVER_VERSION
Updates Laravel.gitlab-ci.yml template
Update operations metrics empty state
Fix LFS uploaded images not being rendered
Prepare admin/projects/show view to allow EE specific feature
Add timed incremental rollout to Auto DevOps
Update spec comment to point to correct issue
Fix documentation for variables
Document Security and Licence Management features permissions
Fix time dependent jobs spec
Use a CTE to remove the query timeout
Backport changes from gitlab-ee!7538
Fix CE to EE merge (backport)
Add changelog entry
Refactor Feature.flipper method
...
Diffstat (limited to 'app')
1538 files changed, 15183 insertions, 7517 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/ci_favicons/canary/favicon_status_scheduled.ico b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico Binary files differnew file mode 100644 index 00000000000..5444b8e41dc --- /dev/null +++ b/app/assets/images/ci_favicons/canary/favicon_status_scheduled.ico diff --git a/app/assets/images/ci_favicons/favicon_status_scheduled.png b/app/assets/images/ci_favicons/favicon_status_scheduled.png Binary files differnew file mode 100644 index 00000000000..d198c255fdd --- /dev/null +++ b/app/assets/images/ci_favicons/favicon_status_scheduled.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/api.js b/app/assets/javascripts/api.js index cd800d75f7a..3f7a1ef1bfc 100644 --- a/app/assets/javascripts/api.js +++ b/app/assets/javascripts/api.js @@ -15,13 +15,11 @@ const Api = { mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes', mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions', groupLabelsPath: '/groups/:namespace_path/-/labels', - templatesPath: '/api/:version/templates/:key', - licensePath: '/api/:version/templates/licenses/:key', - gitignorePath: '/api/:version/templates/gitignores/:key', - gitlabCiYmlPath: '/api/:version/templates/gitlab_ci_ymls/:key', - dockerfilePath: '/api/:version/templates/dockerfiles/:key', issuableTemplatePath: '/:namespace_path/:project_path/templates/:type/:key', + projectTemplatePath: '/api/:version/projects/:id/templates/:type/:key', + projectTemplatesPath: '/api/:version/projects/:id/templates/:type', usersPath: '/api/:version/users.json', + userStatusPath: '/api/:version/user/status', commitPath: '/api/:version/projects/:id/repository/commits', commitPipelinesPath: '/:project_id/commit/:sha/pipelines', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', @@ -195,29 +193,29 @@ const Api = { return axios.get(url); }, - // Return text for a specific license - licenseText(key, data, callback) { - const url = Api.buildUrl(Api.licensePath).replace(':key', key); - return axios - .get(url, { - params: data, - }) - .then(res => callback(res.data)); - }, + projectTemplate(id, type, key, options, callback) { + const url = Api.buildUrl(this.projectTemplatePath) + .replace(':id', encodeURIComponent(id)) + .replace(':type', type) + .replace(':key', encodeURIComponent(key)); - gitignoreText(key, callback) { - const url = Api.buildUrl(Api.gitignorePath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); - }, + return axios.get(url, { params: options }).then(res => { + if (callback) callback(res.data); - gitlabCiYml(key, callback) { - const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); + return res; + }); }, - dockerfileYml(key, callback) { - const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key); - return axios.get(url).then(({ data }) => callback(data)); + projectTemplates(id, type, params = {}, callback) { + const url = Api.buildUrl(this.projectTemplatesPath) + .replace(':id', encodeURIComponent(id)) + .replace(':type', type); + + return axios.get(url, { params }).then(res => { + if (callback) callback(res.data); + + return res; + }); }, issueTemplate(namespacePath, projectPath, key, type, callback) { @@ -266,10 +264,13 @@ const Api = { }); }, - templates(key, params = {}) { - const url = Api.buildUrl(this.templatesPath).replace(':key', key); + postUserStatus({ emoji, message }) { + const url = Api.buildUrl(this.userStatusPath); - return axios.get(url, { params }); + return axios.put(url, { + emoji, + message, + }); }, buildUrl(url) { diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index 5b0c4285339..cace8bb9dba 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -42,10 +42,11 @@ export class AwardsHandler { } bindEvents() { + const $parentEl = this.targetContainerEl ? $(this.targetContainerEl) : $(document); // If the user shows intent let's pre-build the menu this.registerEventListener( 'one', - $(document), + $parentEl, 'mouseenter focus', this.toggleButtonSelector, 'mouseenter focus', @@ -58,7 +59,7 @@ export class AwardsHandler { } }, ); - this.registerEventListener('on', $(document), 'click', this.toggleButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', this.toggleButtonSelector, e => { e.stopPropagation(); e.preventDefault(); this.showEmojiMenu($(e.currentTarget)); @@ -76,7 +77,7 @@ export class AwardsHandler { }); const emojiButtonSelector = `.js-awards-block .js-emoji-btn, .${this.menuClass} .js-emoji-btn`; - this.registerEventListener('on', $(document), 'click', emojiButtonSelector, e => { + this.registerEventListener('on', $parentEl, 'click', emojiButtonSelector, e => { e.preventDefault(); const $target = $(e.currentTarget); const $glEmojiElement = $target.find('gl-emoji'); @@ -168,7 +169,8 @@ export class AwardsHandler { </div> `; - document.body.insertAdjacentHTML('beforeend', emojiMenuMarkup); + const targetEl = this.targetContainerEl ? this.targetContainerEl : document.body; + targetEl.insertAdjacentHTML('beforeend', emojiMenuMarkup); this.addRemainingEmojiMenuCategories(); this.setupSearch(); @@ -250,6 +252,12 @@ export class AwardsHandler { } positionMenu($menu, $addBtn) { + if (this.targetContainerEl) { + return $menu.css({ + top: `${$addBtn.outerHeight()}px`, + }); + } + const position = $addBtn.data('position'); // The menu could potentially be off-screen or in a hidden overflow element // So we position the element absolute in the body @@ -424,9 +432,7 @@ export class AwardsHandler { users = origTitle.trim().split(FROM_SENTENCE_REGEX); } users.unshift('You'); - return awardBlock - .attr('title', this.toSentence(users)) - .tooltip('_fixTitle'); + return awardBlock.attr('title', this.toSentence(users)).tooltip('_fixTitle'); } createAwardButtonForVotesBlock(votesBlock, emojiName) { @@ -609,13 +615,11 @@ export class AwardsHandler { let awardsHandlerPromise = null; export default function loadAwardsHandler(reload = false) { if (!awardsHandlerPromise || reload) { - awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then( - Emoji => { - const awardsHandler = new AwardsHandler(Emoji); - awardsHandler.bindEvents(); - return awardsHandler; - }, - ); + awardsHandlerPromise = import(/* webpackChunkName: 'emoji' */ './emoji').then(Emoji => { + const awardsHandler = new AwardsHandler(Emoji); + awardsHandler.bindEvents(); + return awardsHandler; + }); } return awardsHandlerPromise; } 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 7a13f74c570..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: { @@ -23,6 +21,11 @@ export default { required: true, }, }, + data() { + return { + wasValidated: false, + }; + }, computed: { ...mapState([ 'badgeInAddForm', @@ -39,16 +42,6 @@ export default { return this.badgeInAddForm; }, - canSubmit() { - return ( - this.badge !== null && - this.badge.imageUrl && - this.badge.imageUrl.trim() !== '' && - this.badge.linkUrl && - this.badge.linkUrl.trim() !== '' && - !this.isSaving - ); - }, helpText() { const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha'] .map(placeholder => `<code>%{${placeholder}}</code>`) @@ -93,11 +86,18 @@ export default { }); }, }, - submitButtonLabel() { - if (this.isEditing) { - return s__('Badges|Save changes'); - } - return s__('Badges|Add badge'); + badgeImageUrlExample() { + const exampleUrl = + 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/badge.svg'; + return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + exampleUrl, + }); + }, + badgeLinkUrlExample() { + const exampleUrl = 'https://example.gitlab.com/%{project_path}'; + return sprintf(s__('Badges|e.g. %{exampleUrl}'), { + exampleUrl, + }); }, }, methods: { @@ -109,7 +109,9 @@ export default { this.stopEditing(); }, onSubmit() { - if (!this.canSubmit) { + const form = this.$el; + if (!form.checkValidity()) { + this.wasValidated = true; return Promise.resolve(); } @@ -117,6 +119,7 @@ export default { return this.saveBadge() .then(() => { createFlash(s__('Badges|The badge was saved.'), 'notice'); + this.wasValidated = false; }) .catch(error => { createFlash( @@ -129,6 +132,7 @@ export default { return this.addBadge() .then(() => { createFlash(s__('Badges|A new badge was added.'), 'notice'); + this.wasValidated = false; }) .catch(error => { createFlash( @@ -138,47 +142,58 @@ export default { }); }, }, - badgeImageUrlPlaceholder: - 'https://example.gitlab.com/%{project_path}/badges/%{default_branch}/<badge>.svg', - badgeLinkUrlPlaceholder: 'https://example.gitlab.com/%{project_path}', }; </script> <template> <form - class="prepend-top-default append-bottom-default" + :class="{ 'was-validated': wasValidated }" + class="prepend-top-default append-bottom-default needs-validation" + novalidate @submit.prevent.stop="onSubmit" > <div class="form-group"> - <label for="badge-link-url">{{ s__('Badges|Link') }}</label> + <label + for="badge-link-url" + class="label-bold" + >{{ s__('Badges|Link') }}</label> + <p v-html="helpText"></p> <input id="badge-link-url" v-model="linkUrl" - :placeholder="$options.badgeLinkUrlPlaceholder" - type="text" + type="URL" class="form-control" + required @input="debouncedPreview" /> - <span - class="form-text text-muted" - v-html="helpText" - ></span> + <div class="invalid-feedback"> + {{ s__('Badges|Please fill in a valid URL') }} + </div> + <span class="form-text text-muted"> + {{ badgeLinkUrlExample }} + </span> </div> <div class="form-group"> - <label for="badge-image-url">{{ s__('Badges|Badge image URL') }}</label> + <label + for="badge-image-url" + class="label-bold" + >{{ s__('Badges|Badge image URL') }}</label> + <p v-html="helpText"></p> <input id="badge-image-url" v-model="imageUrl" - :placeholder="$options.badgeImageUrlPlaceholder" - type="text" + type="URL" class="form-control" + required @input="debouncedPreview" /> - <span - class="form-text text-muted" - v-html="helpText" - ></span> + <div class="invalid-feedback"> + {{ s__('Badges|Please fill in a valid URL') }} + </div> + <span class="form-text text-muted"> + {{ badgeImageUrlExample }} + </span> </div> <div class="form-group"> @@ -190,7 +205,7 @@ export default { :link-url="renderedLinkUrl" /> <p v-show="isRendering"> - <loading-icon + <gl-loading-icon :inline="true" /> </p> @@ -200,20 +215,32 @@ export default { >{{ s__('Badges|No image to preview') }}</p> </div> - <div class="row-content-block"> + <div + v-if="isEditing" + class="row-content-block" + > <loading-button - :disabled="!canSubmit" :loading="isSaving" - :label="submitButtonLabel" + :label="s__('Badges|Save changes')" type="submit" container-class="btn btn-success" /> <button - v-if="isEditing" class="btn btn-cancel" type="button" @click="onCancel" >{{ __('Cancel') }}</button> </div> + <div + v-else + class="form-group" + > + <loading-button + :loading="isSaving" + :label="s__('Badges|Add badge')" + type="submit" + container-class="btn btn-success" + /> + </div> </form> </template> diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue index 268968b63b3..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']), @@ -28,13 +26,13 @@ export default { {{ s__('Badges|Your badges') }} <span v-show="!isLoading" - class="badge" + 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 98aa00af0d7..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: { @@ -43,13 +41,13 @@ export default { <badge :image-url="badge.renderedImageUrl" :link-url="badge.renderedLinkUrl" - class="table-section section-30" + class="table-section section-40" /> - <span class="table-section section-50 str-truncated">{{ badge.linkUrl }}</span> - <div class="table-section section-10"> - <span class="badge">{{ badgeKindText }}</span> + <span class="table-section section-30 str-truncated">{{ badge.linkUrl }}</span> + <div class="table-section section-15"> + <span class="badge badge-pill">{{ badgeKindText }}</span> </div> - <div class="table-section section-10 table-button-footer"> + <div class="table-section section-15 table-button-footer"> <div v-if="canEditBadge" class="table-action-buttons"> @@ -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/highlight_current_user.js b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js new file mode 100644 index 00000000000..6208b3f0032 --- /dev/null +++ b/app/assets/javascripts/behaviors/markdown/highlight_current_user.js @@ -0,0 +1,17 @@ +/** + * Highlights the current user in existing elements with a user ID data attribute. + * + * @param elements DOM elements that represent user mentions + */ +export default function highlightCurrentUser(elements) { + const currentUserId = gon && gon.current_user_id; + if (!currentUserId) { + return; + } + + elements.forEach(element => { + if (parseInt(element.dataset.user, 10) === currentUserId) { + element.classList.add('current-user'); + } + }); +} diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index dbff2bd4b10..a2d4331b6d1 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -2,8 +2,9 @@ import $ from 'jquery'; import syntaxHighlight from '~/syntax_highlight'; import renderMath from './render_math'; import renderMermaid from './render_mermaid'; +import highlightCurrentUser from './highlight_current_user'; -// Render Gitlab flavoured Markdown +// Render GitLab flavoured Markdown // // Delegates to syntax highlight and render math & mermaid diagrams. // @@ -11,6 +12,7 @@ $.fn.renderGFM = function renderGFM() { syntaxHighlight(this.find('.js-syntax-highlight')); renderMath(this.find('.js-render-math')); renderMermaid(this.find('.js-render-mermaid')); + highlightCurrentUser(this.find('.gfm-project_member').get()); return this; }; 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/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js index ff1cbcad145..addacf29f1e 100644 --- a/app/assets/javascripts/blob/file_template_mediator.js +++ b/app/assets/javascripts/blob/file_template_mediator.js @@ -1,4 +1,4 @@ -/* eslint-disable class-methods-use-this */ +import Api from '~/api'; import $ from 'jquery'; import Flash from '../flash'; @@ -9,9 +9,10 @@ import GitignoreSelector from './template_selectors/gitignore_selector'; import LicenseSelector from './template_selectors/license_selector'; export default class FileTemplateMediator { - constructor({ editor, currentAction }) { + constructor({ editor, currentAction, projectId }) { this.editor = editor; this.currentAction = currentAction; + this.projectId = projectId; this.initTemplateSelectors(); this.initTemplateTypeSelector(); @@ -33,15 +34,14 @@ export default class FileTemplateMediator { initTemplateTypeSelector() { this.typeSelector = new FileTemplateTypeSelector({ mediator: this, - dropdownData: this.templateSelectors - .map((templateSelector) => { - const cfg = templateSelector.config; - - return { - name: cfg.name, - key: cfg.key, - }; - }), + dropdownData: this.templateSelectors.map(templateSelector => { + const cfg = templateSelector.config; + + return { + name: cfg.name, + key: cfg.key, + }; + }), }); } @@ -89,7 +89,7 @@ export default class FileTemplateMediator { } listenForPreviewMode() { - this.$navLinks.on('click', 'a', (e) => { + this.$navLinks.on('click', 'a', e => { const urlPieces = e.target.href.split('#'); const hash = urlPieces[1]; if (hash === 'preview') { @@ -105,7 +105,7 @@ export default class FileTemplateMediator { e.preventDefault(); } - this.templateSelectors.forEach((selector) => { + this.templateSelectors.forEach(selector => { if (selector.config.key === item.key) { selector.show(); } else { @@ -126,8 +126,8 @@ export default class FileTemplateMediator { selector.renderLoading(); // in case undo menu is already already there this.destroyUndoMenu(); - this.fetchFileTemplate(selector.config.endpoint, query, data) - .then((file) => { + this.fetchFileTemplate(selector.config.type, query, data) + .then(file => { this.showUndoMenu(); this.setEditorContent(file); this.setFilename(selector.config.name); @@ -138,7 +138,7 @@ export default class FileTemplateMediator { displayMatchedTemplateSelector() { const currentInput = this.getFilename(); - this.templateSelectors.forEach((selector) => { + this.templateSelectors.forEach(selector => { const match = selector.config.pattern.test(currentInput); if (match) { @@ -149,15 +149,11 @@ export default class FileTemplateMediator { }); } - fetchFileTemplate(apiCall, query, data) { - return new Promise((resolve) => { + fetchFileTemplate(type, query, data = {}) { + return new Promise(resolve => { const resolveFile = file => resolve(file); - if (!data) { - apiCall(query, resolveFile); - } else { - apiCall(query, data, resolveFile); - } + Api.projectTemplate(this.projectId, type, query, data, resolveFile); }); } diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js index 9dfdb06007d..9db1fa70ffb 100644 --- a/app/assets/javascripts/blob/template_selector.js +++ b/app/assets/javascripts/blob/template_selector.js @@ -66,9 +66,6 @@ export default class TemplateSelector { // be added by all subclasses. } - // To be implemented on the extending class - // e.g. Api.gitlabCiYml(query.name, file => this.setEditorContent(file)); - setEditorContent(file, { skipFocus } = {}) { if (!file) return; diff --git a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js index 9c41e429c8d..43f7aead8b9 100644 --- a/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js +++ b/app/assets/javascripts/blob/template_selectors/ci_yaml_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobCiYamlSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobCiYamlSelector extends FileTemplateSelector { key: 'gitlab-ci-yaml', name: '.gitlab-ci.yml', pattern: /(.gitlab-ci.yml)/, - endpoint: Api.gitlabCiYml, + type: 'gitlab_ci_ymls', dropdown: '.js-gitlab-ci-yml-selector', wrapper: '.js-gitlab-ci-yml-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js index 45fb614fe00..4718b642617 100644 --- a/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js +++ b/app/assets/javascripts/blob/template_selectors/dockerfile_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class DockerfileSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class DockerfileSelector extends FileTemplateSelector { key: 'dockerfile', name: 'Dockerfile', pattern: /(Dockerfile)/, - endpoint: Api.dockerfileYml, + type: 'dockerfiles', dropdown: '.js-dockerfile-selector', wrapper: '.js-dockerfile-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js index a894953cc86..a8067ec5c84 100644 --- a/app/assets/javascripts/blob/template_selectors/gitignore_selector.js +++ b/app/assets/javascripts/blob/template_selectors/gitignore_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobGitignoreSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobGitignoreSelector extends FileTemplateSelector { key: 'gitignore', name: '.gitignore', pattern: /(.gitignore)/, - endpoint: Api.gitignoreText, + type: 'gitignores', dropdown: '.js-gitignore-selector', wrapper: '.js-gitignore-selector-wrap', }; diff --git a/app/assets/javascripts/blob/template_selectors/license_selector.js b/app/assets/javascripts/blob/template_selectors/license_selector.js index b7c4da0f62e..ac1fe95eee5 100644 --- a/app/assets/javascripts/blob/template_selectors/license_selector.js +++ b/app/assets/javascripts/blob/template_selectors/license_selector.js @@ -1,5 +1,3 @@ -import Api from '../../api'; - import FileTemplateSelector from '../file_template_selector'; export default class BlobLicenseSelector extends FileTemplateSelector { @@ -9,7 +7,7 @@ export default class BlobLicenseSelector extends FileTemplateSelector { key: 'license', name: 'LICENSE', pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i, - endpoint: Api.licenseText, + type: 'licenses', dropdown: '.js-license-selector', wrapper: '.js-license-selector-wrap', }; diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js index a603d89b84a..4e4598870fa 100644 --- a/app/assets/javascripts/blob_edit/blob_bundle.js +++ b/app/assets/javascripts/blob_edit/blob_bundle.js @@ -15,8 +15,9 @@ export default () => { const assetsPath = editBlobForm.data('assetsPrefix'); const blobLanguage = editBlobForm.data('blobLanguage'); const currentAction = $('.js-file-title').data('currentAction'); + const projectId = editBlobForm.data('project-id'); - new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction); + new EditBlob(`${urlRoot}${assetsPath}`, blobLanguage, currentAction, projectId); new NewCommitForm(editBlobForm); } diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js index 82a3d494b67..ec2b130ab7d 100644 --- a/app/assets/javascripts/blob_edit/edit_blob.js +++ b/app/assets/javascripts/blob_edit/edit_blob.js @@ -7,11 +7,11 @@ import { __ } from '~/locale'; import TemplateSelectorMediator from '../blob/file_template_mediator'; export default class EditBlob { - constructor(assetsPath, aceMode, currentAction) { + constructor(assetsPath, aceMode, currentAction, projectId) { this.configureAceEditor(aceMode, assetsPath); this.initModePanesAndLinks(); this.initSoftWrap(); - this.initFileSelectors(currentAction); + this.initFileSelectors(currentAction, projectId); } configureAceEditor(aceMode, assetsPath) { @@ -30,10 +30,11 @@ export default class EditBlob { } } - initFileSelectors(currentAction) { + initFileSelectors(currentAction, projectId) { this.fileTemplateMediator = new TemplateSelectorMediator({ currentAction, editor: this.editor, + projectId, }); } @@ -60,14 +61,15 @@ export default class EditBlob { if (paneId === '#preview') { this.$toggleButton.hide(); - axios.post(currentLink.data('previewUrl'), { - content: this.editor.getValue(), - }) - .then(({ data }) => { - currentPane.empty().append(data); - currentPane.renderGFM(); - }) - .catch(() => createFlash(__('An error occurred previewing the blob'))); + axios + .post(currentLink.data('previewUrl'), { + content: this.editor.getValue(), + }) + .then(({ data }) => { + currentPane.empty().append(data); + currentPane.renderGFM(); + }) + .catch(() => createFlash(__('An error occurred previewing the blob'))); } this.$toggleButton.show(); 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..f7ce5128964 100644 --- a/app/assets/javascripts/boards/components/board_new_issue.vue +++ b/app/assets/javascripts/boards/components/board_new_issue.vue @@ -1,5 +1,6 @@ <script> import $ from 'jquery'; +import { Button } from '@gitlab-org/gitlab-ui'; import eventHub from '../eventhub'; import ProjectSelect from './project_select.vue'; import ListIssue from '../models/issue'; @@ -10,6 +11,7 @@ export default { name: 'BoardNewIssue', components: { ProjectSelect, + 'gl-button': Button, }, props: { groupId: { @@ -110,9 +112,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" @@ -123,21 +125,23 @@ export default { :group-id="groupId" /> <div class="clearfix prepend-top-10"> - <button + <gl-button ref="submit-button" :disabled="disabled" - class="btn btn-success float-left" + class="float-left" + variant="success" type="submit" > Submit issue - </button> - <button - class="btn btn-default float-right" + </gl-button> + <gl-button + class="float-right" type="button" + variant="default" @click="cancel" > Cancel - </button> + </gl-button> </div> </form> </div> diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue index d50641dc3a9..8b5536200e1 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.vue +++ b/app/assets/javascripts/boards/components/issue_card_inner.vue @@ -149,10 +149,11 @@ <a :href="issue.path" :title="issue.title" - class="js-no-trigger">{{ issue.title }}</a> + class="js-no-trigger" + @mousemove.stop>{{ issue.title }}</a> <span v-if="issueId" - class="board-card-number" + class="board-card-number append-right-5" > {{ issue.referencePath }} </span> @@ -170,8 +171,8 @@ tooltip-placement="bottom" /> <span - v-tooltip v-if="shouldRenderCounter" + v-tooltip :title="assigneeCounterTooltip" class="avatar-counter" > @@ -184,10 +185,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/modal/lists_dropdown.vue b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue index 6a5a39099bd..4f23e5db35c 100644 --- a/app/assets/javascripts/boards/components/modal/lists_dropdown.vue +++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.vue @@ -1,7 +1,11 @@ <script> +import { Link } from '@gitlab-org/gitlab-ui'; import ModalStore from '../../stores/modal_store'; export default { + components: { + 'gl-link': Link, + }, data() { return { modal: ModalStore.store, @@ -38,7 +42,7 @@ export default { v-for="(list, i) in state.lists" v-if="list.type == 'label'" :key="i"> - <a + <gl-link :class="{ 'is-active': list.id == selected.id }" href="#" role="button" @@ -48,7 +52,7 @@ export default { class="dropdown-label-box"> </span> {{ list.title }} - </a> + </gl-link> </li> </ul> </div> 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..caa6ce84335 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,8 @@ 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'; +import { NavigationType } from '~/lib/utils/common_utils'; export default () => { const $boardApp = document.getElementById('board-app'); @@ -32,6 +33,16 @@ export default () => { window.gl = window.gl || {}; + // check for browser back and trigger a hard reload to circumvent browser caching. + window.addEventListener('pageshow', (event) => { + const isNavTypeBackForward = window.performance && + window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD; + + if (event.persisted || isNavTypeBackForward) { + window.location.reload(); + } + }); + if (gl.IssueBoardsApp) { gl.IssueBoardsApp.$destroy(true); } @@ -229,7 +240,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/build_variables.js b/app/assets/javascripts/build_variables.js deleted file mode 100644 index d398e4a4c83..00000000000 --- a/app/assets/javascripts/build_variables.js +++ /dev/null @@ -1,10 +0,0 @@ -import $ from 'jquery'; - -export default function handleRevealVariables() { - $('.js-reveal-variables') - .off('click') - .on('click', function click() { - $('.js-build-variables').toggle(); - $(this).hide(); - }); -} diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 0fdf0c7a389..65e7cee7039 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 PersistentUserCallout from '../persistent_user_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'); + Clusters.initDismissableCallout(); initSettingsPanels(); setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area')); this.initApplications(); @@ -108,6 +105,12 @@ export default class Clusters { }); } + static initDismissableCallout() { + const callout = document.querySelector('.js-cluster-security-warning'); + + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new + } + addListeners() { if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken); eventHub.$on('installApplication', this.installApplication); @@ -129,7 +132,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 +181,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 +228,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..e32d507d1f7 100644 --- a/app/assets/javascripts/clusters/clusters_index.js +++ b/app/assets/javascripts/clusters/clusters_index.js @@ -1,14 +1,15 @@ import createFlash from '~/flash'; import { __ } from '~/locale'; import setupToggleButtons from '~/toggle_buttons'; -import gcpSignupOffer from '~/clusters/components/gcp_signup_offer'; +import PersistentUserCallout from '../persistent_user_callout'; import ClustersService from './services/clusters_service'; export default () => { const clusterList = document.querySelector('.js-clusters-list'); - gcpSignupOffer(); + const callout = document.querySelector('.gcp-signup-offer'); + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new // 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 8bc20a1c09f..00000000000 --- a/app/assets/javascripts/clusters/components/gcp_signup_offer.js +++ /dev/null @@ -1,27 +0,0 @@ -import $ from 'jquery'; -import axios from '~/lib/utils/axios_utils'; -import { __ } from '~/locale'; -import Flash from '~/flash'; - -export default function gcpSignupOffer() { - const alertEl = document.querySelector('.gcp-signup-offer'); - if (!alertEl) { - return; - } - - const closeButtonEl = alertEl.getElementsByClassName('close')[0]; - const { dismissEndpoint, featureId } = closeButtonEl.dataset; - - closeButtonEl.addEventListener('click', () => { - axios - .post(dismissEndpoint, { - feature_name: featureId, - }) - .then(() => { - $(alertEl).alert('close'); - }) - .catch(() => { - Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); - }); - }); -} 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..1411f7ffd5e 100644 --- a/app/assets/javascripts/commons/gitlab_ui.js +++ b/app/assets/javascripts/commons/gitlab_ui.js @@ -1,4 +1,17 @@ import Vue from 'vue'; -import progressBar from '@gitlab-org/gitlab-ui/dist/base/progress_bar'; +import { + Pagination, + ProgressBar, + Modal, + LoadingIcon, + ModalDirective, + TooltipDirective, +} from '@gitlab-org/gitlab-ui'; -Vue.component('gl-progress-bar', progressBar); +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', ModalDirective); +Vue.directive('gl-tooltip', TooltipDirective); 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..edca45f22f9 100644 --- a/app/assets/javascripts/diffs/components/app.vue +++ b/app/assets/javascripts/diffs/components/app.vue @@ -4,23 +4,23 @@ 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'; import NoChanges from './no_changes.vue'; import HiddenFilesWarning from './hidden_files_warning.vue'; +import CommitWidget from './commit_widget.vue'; +import TreeList from './tree_list.vue'; export default { name: 'DiffsApp', components: { Icon, - LoadingIcon, CompareVersions, - ChangedFiles, DiffFile, NoChanges, HiddenFilesWarning, + CommitWidget, + TreeList, }, props: { endpoint: { @@ -58,8 +58,9 @@ export default { plainDiffPath: state => state.diffs.plainDiffPath, emailPatchPath: state => state.diffs.emailPatchPath, }), + ...mapState('diffs', ['showTreeList']), ...mapGetters('diffs', ['isParallelView']), - ...mapGetters(['isNotesFetched']), + ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), targetBranch() { return { branchName: this.targetBranchName, @@ -88,6 +89,9 @@ export default { canCurrentUserFork() { return this.currentUser.canFork === true && this.currentUser.canCreateMergeRequest; }, + showCompareVersions() { + return this.mergeRequestDiffs && this.mergeRequestDiff; + }, }, watch: { diffViewType() { @@ -102,6 +106,8 @@ export default { this.adjustView(); }, + isLoading: 'adjustView', + showTreeList: 'adjustView', }, mounted() { this.setBaseConfig({ endpoint: this.endpoint, projectPath: this.projectPath }); @@ -112,13 +118,25 @@ 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,11 +146,22 @@ 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(); - } else { - window.mrTabs.resetViewContainer(); + if (this.shouldShow) { + this.$nextTick(() => { + window.mrTabs.resetViewContainer(); + window.mrTabs.expandViewContainer(this.showTreeList); + }); } }, }, @@ -145,7 +174,7 @@ export default { v-if="isLoading" class="loading" > - <loading-icon /> + <gl-loading-icon /> </div> <div v-else @@ -154,7 +183,7 @@ export default { class="diffs tab-pane" > <compare-versions - v-if="!commit && mergeRequestDiffs.length > 1" + v-if="showCompareVersions" :merge-request-diffs="mergeRequestDiffs" :merge-request-diff="mergeRequestDiff" :start-version="startVersion" @@ -187,22 +216,31 @@ export default { </div> </div> - <changed-files - :diff-files="diffFiles" + <commit-widget + v-if="commit" + :commit="commit" /> - <div - v-if="diffFiles.length > 0" - class="files" - > - <diff-file - v-for="file in diffFiles" - :key="file.newPath" - :file="file" - :can-current-user-fork="canCurrentUserFork" - /> + <div class="files d-flex prepend-top-default"> + <div + v-show="showTreeList" + class="diff-tree-list" + > + <tree-list /> + </div> + <div + v-if="diffFiles.length > 0" + class="diff-files-holder" + > + <diff-file + v-for="file in diffFiles" + :key="file.newPath" + :file="file" + :can-current-user-fork="canCurrentUserFork" + /> + </div> + <no-changes v-else /> </div> - <no-changes v-else /> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/changed_files.vue b/app/assets/javascripts/diffs/components/changed_files.vue deleted file mode 100644 index 97751db1254..00000000000 --- a/app/assets/javascripts/diffs/components/changed_files.vue +++ /dev/null @@ -1,171 +0,0 @@ -<script> -import { mapGetters, mapActions } from 'vuex'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; -import { pluralize } from '~/lib/utils/text_utility'; -import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; -import { contentTop } from '~/lib/utils/common_utils'; -import { __ } from '~/locale'; -import ChangedFilesDropdown from './changed_files_dropdown.vue'; -import changedFilesMixin from '../mixins/changed_files'; - -export default { - components: { - Icon, - ChangedFilesDropdown, - ClipboardButton, - }, - mixins: [changedFilesMixin], - data() { - return { - isStuck: false, - maxWidth: 'auto', - offsetTop: 0, - }; - }, - computed: { - ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']), - sumAddedLines() { - return this.sumValues('addedLines'); - }, - sumRemovedLines() { - return this.sumValues('removedLines'); - }, - whitespaceVisible() { - return !getParameterValues('w')[0]; - }, - toggleWhitespaceText() { - if (this.whitespaceVisible) { - return __('Hide whitespace changes'); - } - return __('Show whitespace changes'); - }, - toggleWhitespacePath() { - if (this.whitespaceVisible) { - return mergeUrlParams({ w: 1 }, window.location.href); - } - - return mergeUrlParams({ w: 0 }, window.location.href); - }, - top() { - return `${this.offsetTop}px`; - }, - }, - created() { - document.addEventListener('scroll', this.handleScroll); - this.offsetTop = contentTop(); - }, - beforeDestroy() { - document.removeEventListener('scroll', this.handleScroll); - }, - methods: { - ...mapActions('diffs', ['setInlineDiffViewType', 'setParallelDiffViewType', 'expandAllFiles']), - pluralize, - handleScroll() { - if (!this.updating) { - this.$nextTick(this.updateIsStuck); - this.updating = true; - } - }, - updateIsStuck() { - if (!this.$refs.wrapper) { - return; - } - - const scrollPosition = window.scrollY; - - this.isStuck = scrollPosition + this.offsetTop >= this.$refs.placeholder.offsetTop; - this.updating = false; - }, - sumValues(key) { - return this.diffFiles.reduce((total, file) => total + file[key], 0); - }, - }, -}; -</script> - -<template> - <span> - <div ref="placeholder"></div> - <div - ref="wrapper" - :style="{ top }" - :class="{'is-stuck': isStuck}" - class="content-block oneline-block diff-files-changed diff-files-changed-merge-request - files-changed js-diff-files-changed" - > - <div class="files-changed-inner"> - <div - class="inline-parallel-buttons d-none d-md-block" - > - <a - v-if="areAllFilesCollapsed" - class="btn btn-default" - @click="expandAllFiles" - > - {{ __('Expand all') }} - </a> - <a - :href="toggleWhitespacePath" - class="btn btn-default" - > - {{ toggleWhitespaceText }} - </a> - <div class="btn-group"> - <button - id="inline-diff-btn" - :class="{ active: isInlineView }" - type="button" - class="btn js-inline-diff-button" - data-view-type="inline" - @click="setInlineDiffViewType" - > - {{ __('Inline') }} - </button> - <button - id="parallel-diff-btn" - :class="{ active: isParallelView }" - type="button" - class="btn js-parallel-diff-button" - data-view-type="parallel" - @click="setParallelDiffViewType" - > - {{ __('Side-by-side') }} - </button> - </div> - </div> - - <div class="commit-stat-summary dropdown"> - <changed-files-dropdown - :diff-files="diffFiles" - /> - - <span - class="js-diff-stats-additions-deletions-expanded - diff-stats-additions-deletions-expanded" - > - with - <strong class="cgreen"> - {{ pluralize(`${sumAddedLines} addition`, sumAddedLines) }} - </strong> - and - <strong class="cred"> - {{ pluralize(`${sumRemovedLines} deletion`, sumRemovedLines) }} - </strong> - </span> - <div - class="js-diff-stats-additions-deletions-collapsed - diff-stats-additions-deletions-collapsed float-right d-sm-none" - > - <strong class="cgreen"> - +{{ sumAddedLines }} - </strong> - <strong class="cred"> - -{{ sumRemovedLines }} - </strong> - </div> - </div> - </div> - </div> - </span> -</template> diff --git a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue b/app/assets/javascripts/diffs/components/changed_files_dropdown.vue deleted file mode 100644 index 045688a32bf..00000000000 --- a/app/assets/javascripts/diffs/components/changed_files_dropdown.vue +++ /dev/null @@ -1,126 +0,0 @@ -<script> -import Icon from '~/vue_shared/components/icon.vue'; -import changedFilesMixin from '../mixins/changed_files'; - -export default { - components: { - Icon, - }, - mixins: [changedFilesMixin], - data() { - return { - searchText: '', - }; - }, - computed: { - filteredDiffFiles() { - return this.diffFiles.filter(file => - file.filePath.toLowerCase().includes(this.searchText.toLowerCase()), - ); - }, - }, - methods: { - clearSearch() { - this.searchText = ''; - }, - }, -}; -</script> - -<template> - <span> - Showing - <button - class="diff-stats-summary-toggler" - data-toggle="dropdown" - type="button" - aria-expanded="false" - > - <span> - {{ n__('%d changed file', '%d changed files', diffFiles.length) }} - </span> - <icon - class="caret-icon" - name="chevron-down" - /> - </button> - <div class="dropdown-menu diff-file-changes"> - <div class="dropdown-input"> - <input - v-model="searchText" - type="search" - class="dropdown-input-field" - placeholder="Search files" - autocomplete="off" - /> - <i - v-if="searchText.length === 0" - aria-hidden="true" - data-hidden="true" - class="fa fa-search dropdown-input-search"> - </i> - <i - v-else - role="button" - class="fa fa-times dropdown-input-search" - @click="clearSearch" - ></i> - </div> - <div class="dropdown-content"> - <ul> - <li - v-for="diffFile in filteredDiffFiles" - :key="diffFile.name" - > - <a - :href="`#${diffFile.fileHash}`" - :title="diffFile.newPath" - class="diff-changed-file" - > - <icon - :name="fileChangedIcon(diffFile)" - :size="16" - :class="fileChangedClass(diffFile)" - class="diff-file-changed-icon append-right-8" - /> - <span class="diff-changed-file-content append-right-8"> - <strong - v-if="diffFile.blob && diffFile.blob.name" - class="diff-changed-file-name" - > - {{ diffFile.blob.name }} - </strong> - <strong - v-else - class="diff-changed-blank-file-name" - > - {{ s__('Diffs|No file name available') }} - </strong> - <span class="diff-changed-file-path prepend-top-5"> - {{ truncatedDiffPath(diffFile.blob.path) }} - </span> - </span> - <span class="diff-changed-stats"> - <span class="cgreen"> - +{{ diffFile.addedLines }} - </span> - <span class="cred"> - -{{ diffFile.removedLines }} - </span> - </span> - </a> - </li> - - <li - v-show="filteredDiffFiles.length === 0" - class="dropdown-menu-empty-item" - > - <a> - {{ __('No files found') }} - </a> - </li> - </ul> - </div> - </div> - </span> -</template> diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue new file mode 100644 index 00000000000..993206b2e73 --- /dev/null +++ b/app/assets/javascripts/diffs/components/commit_item.vue @@ -0,0 +1,129 @@ +<script> +import tooltip from '~/vue_shared/directives/tooltip'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import CIIcon from '~/vue_shared/components/ci_icon.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue'; + +/** + * CommitItem + * + * ----------------------------------------------------------------- + * WARNING: Please keep changes up-to-date with the following files: + * - `views/projects/commits/_commit.html.haml` + * ----------------------------------------------------------------- + * + * This Component was cloned from a HAML view. For the time being they + * coexist, but there is an issue to remove the duplication. + * https://gitlab.com/gitlab-org/gitlab-ce/issues/51613 + * + */ +export default { + directives: { + tooltip, + }, + components: { + UserAvatarLink, + Icon, + ClipboardButton, + CIIcon, + TimeAgoTooltip, + CommitPipelineStatus, + }, + props: { + commit: { + type: Object, + required: true, + }, + }, + computed: { + authorName() { + return (this.commit.author && this.commit.author.name) || this.commit.authorName; + }, + authorUrl() { + return (this.commit.author && this.commit.author.webUrl) || `mailto:${this.commit.authorEmail}`; + }, + authorAvatar() { + return (this.commit.author && this.commit.author.avatarUrl) || this.commit.authorGravatarUrl; + }, + }, +}; +</script> + +<template> + <li class="commit flex-row js-toggle-container"> + <user-avatar-link + :link-href="authorUrl" + :img-src="authorAvatar" + :img-alt="authorName" + :img-size="36" + class="avatar-cell d-none d-sm-block" + /> + <div class="commit-detail flex-list"> + <div class="commit-content qa-commit-content"> + <a + :href="commit.commitUrl" + class="commit-row-message item-title" + v-html="commit.titleHtml" + ></a> + + <span class="commit-row-message d-block d-sm-none"> + · + {{ commit.shortId }} + </span> + + <button + v-if="commit.descriptionHtml" + class="text-expander js-toggle-button" + type="button" + :aria-label="__('Toggle commit description')" + > + <icon + :size="12" + name="ellipsis_h" + /> + </button> + + <div class="commiter"> + <a + :href="authorUrl" + v-text="authorName" + ></a> + {{ s__('CommitWidget|authored') }} + <time-ago-tooltip + :time="commit.authoredDate" + /> + </div> + + <pre + v-if="commit.descriptionHtml" + class="commit-row-description js-toggle-content append-bottom-8" + v-html="commit.descriptionHtml" + ></pre> + </div> + <div class="commit-actions flex-row d-none d-sm-flex"> + <div + v-if="commit.signatureHtml" + v-html="commit.signatureHtml" + ></div> + <commit-pipeline-status + v-if="commit.pipelineStatusPath" + :endpoint="commit.pipelineStatusPath" + /> + <div class="commit-sha-group"> + <div + class="label label-monospace" + v-text="commit.shortId" + ></div> + <clipboard-button + :text="commit.id" + :title="__('Copy commit SHA to clipboard')" + class="btn btn-default" + /> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/diffs/components/commit_widget.vue b/app/assets/javascripts/diffs/components/commit_widget.vue new file mode 100644 index 00000000000..cc8e72eb1c8 --- /dev/null +++ b/app/assets/javascripts/diffs/components/commit_widget.vue @@ -0,0 +1,40 @@ +<script> +import CommitItem from './commit_item.vue'; + +/** + * CommitWidget + * + * ----------------------------------------------------------------- + * WARNING: Please keep changes up-to-date with the following files: + * - `views/projects/merge_requests/diffs/_commit_widget.html.haml` + * ----------------------------------------------------------------- + * + * This Component was cloned from a HAML view. For the time being, + * they coexist, but there is an issue to remove the duplication. + * https://gitlab.com/gitlab-org/gitlab-ce/issues/51613 + * + */ +export default { + components: { + CommitItem, + }, + props: { + commit: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div class="info-well prepend-top-default"> + <div class="well-segment"> + <ul class="blob-commit-info"> + <commit-item + :commit="commit" + /> + </ul> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue index 1c9ad8e77f1..9bbf62c0eb6 100644 --- a/app/assets/javascripts/diffs/components/compare_versions.vue +++ b/app/assets/javascripts/diffs/components/compare_versions.vue @@ -1,9 +1,18 @@ <script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Tooltip from '@gitlab-org/gitlab-ui/dist/directives/tooltip'; +import { __ } from '~/locale'; +import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; +import Icon from '~/vue_shared/components/icon.vue'; import CompareVersionsDropdown from './compare_versions_dropdown.vue'; export default { components: { CompareVersionsDropdown, + Icon, + }, + directives: { + Tooltip, }, props: { mergeRequestDiffs: { @@ -26,30 +35,119 @@ export default { }, }, computed: { + ...mapState('diffs', ['commit', 'showTreeList']), + ...mapGetters('diffs', ['isInlineView', 'isParallelView', 'areAllFilesCollapsed']), comparableDiffs() { return this.mergeRequestDiffs.slice(1); }, + isWhitespaceVisible() { + return !getParameterValues('w')[0]; + }, + toggleWhitespaceText() { + if (this.isWhitespaceVisible) { + return __('Hide whitespace changes'); + } + return __('Show whitespace changes'); + }, + toggleWhitespacePath() { + if (this.isWhitespaceVisible) { + return mergeUrlParams({ w: 1 }, window.location.href); + } + + return mergeUrlParams({ w: 0 }, window.location.href); + }, + showDropdowns() { + return !this.commit && this.mergeRequestDiffs.length; + }, + }, + methods: { + ...mapActions('diffs', [ + 'setInlineDiffViewType', + 'setParallelDiffViewType', + 'expandAllFiles', + 'toggleShowTreeList', + ]), }, }; </script> <template> <div class="mr-version-controls"> - <div class="mr-version-menus-container content-block"> - Changes between - <compare-versions-dropdown - :other-versions="mergeRequestDiffs" - :merge-request-version="mergeRequestDiff" - :show-commit-count="true" - class="mr-version-dropdown" - /> - and - <compare-versions-dropdown - :other-versions="comparableDiffs" - :start-version="startVersion" - :target-branch="targetBranch" - class="mr-version-compare-dropdown" - /> + <div + class="mr-version-menus-container content-block" + > + <button + v-tooltip.hover + type="button" + class="btn btn-default append-right-8 js-toggle-tree-list" + :class="{ + active: showTreeList + }" + :title="__('Toggle file browser')" + @click="toggleShowTreeList" + > + <icon + name="hamburger" + /> + </button> + <div + v-if="showDropdowns" + class="d-flex align-items-center compare-versions-container" + > + Changes between + <compare-versions-dropdown + :other-versions="mergeRequestDiffs" + :merge-request-version="mergeRequestDiff" + :show-commit-count="true" + class="mr-version-dropdown" + /> + and + <compare-versions-dropdown + :other-versions="comparableDiffs" + :start-version="startVersion" + :target-branch="targetBranch" + class="mr-version-compare-dropdown" + /> + </div> + <div + class="inline-parallel-buttons d-none d-md-flex ml-auto" + > + <a + v-if="areAllFilesCollapsed" + class="btn btn-default" + @click="expandAllFiles" + > + {{ __('Expand all') }} + </a> + <a + :href="toggleWhitespacePath" + class="btn btn-default" + > + {{ toggleWhitespaceText }} + </a> + <div class="btn-group prepend-left-8"> + <button + id="inline-diff-btn" + :class="{ active: isInlineView }" + type="button" + class="btn js-inline-diff-button" + data-view-type="inline" + @click="setInlineDiffViewType" + > + {{ __('Inline') }} + </button> + <button + id="parallel-diff-btn" + :class="{ active: isParallelView }" + type="button" + class="btn js-parallel-diff-button" + data-view-type="parallel" + @click="setParallelDiffViewType" + > + {{ __('Side-by-side') }} + </button> + </div> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue index 96cccb49378..c3acc352d5e 100644 --- a/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue +++ b/app/assets/javascripts/diffs/components/compare_versions_dropdown.vue @@ -108,7 +108,7 @@ export default { <template> <span class="dropdown inline"> <a - class="dropdown-toggle btn btn-default" + class="dropdown-menu-toggle btn btn-default w-100" data-toggle="dropdown" aria-expanded="false" > @@ -118,6 +118,7 @@ export default { <Icon :size="12" name="angle-down" + class="position-absolute" /> </a> <div class="dropdown-menu dropdown-select dropdown-menu-selectable"> @@ -163,3 +164,10 @@ export default { </div> </span> </template> + +<style> +.dropdown { + min-width: 0; + max-height: 170px; +} +</style> diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue index 02d5be1821b..fb5556e3cd7 100644 --- a/app/assets/javascripts/diffs/components/diff_content.vue +++ b/app/assets/javascripts/diffs/components/diff_content.vue @@ -28,7 +28,7 @@ export default { return diffModes[diffModeKey] || diffModes.replaced; }, isTextFile() { - return this.diffFile.text; + return this.diffFile.viewer.name === 'text'; }, }, }; 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..4e04e50c52a 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, mapState } 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,8 @@ export default { }; }, computed: { + ...mapState('diffs', ['currentDiffFileId']), + ...mapGetters(['isNotesFetched', 'discussionsStructuredByLineCode']), isCollapsed() { return this.file.collapsed || false; }, @@ -44,23 +44,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 +76,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!')); @@ -94,6 +102,9 @@ export default { <template> <div :id="file.fileHash" + :class="{ + 'is-active': currentDiffFileId === file.fileHash + }" class="diff-file file-holder" > <diff-file-header @@ -135,12 +146,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.') }} @@ -161,3 +172,20 @@ export default { </div> </div> </template> + +<style> +@keyframes shadow-fade { + from { + box-shadow: 0 0 4px #919191; + } + + to { + box-shadow: 0 0 0 #dfdfdf; + } +} + +.diff-file.is-active { + box-shadow: 0 0 0 #dfdfdf; + animation: shadow-fade 1.2s 0.1s 1; +} +</style> diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index d3ffbe0415a..15b37243030 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -166,23 +166,21 @@ export default { :title="diffFile.oldPath" class="file-title-name" data-container="body" - > - {{ diffFile.oldPath }} - </strong> + v-html="diffFile.oldPathHtml" + ></strong> → <strong v-tooltip :title="diffFile.newPath" class="file-title-name" data-container="body" - > - {{ diffFile.newPath }} - </strong> + v-html="diffFile.newPathHtml" + ></strong> </span> <strong - v-tooltip v-else + v-tooltip :title="filePath" class="file-title-name" data-container="body" @@ -255,8 +253,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/file_row_stats.vue b/app/assets/javascripts/diffs/components/file_row_stats.vue new file mode 100644 index 00000000000..105f7ebdbed --- /dev/null +++ b/app/assets/javascripts/diffs/components/file_row_stats.vue @@ -0,0 +1,30 @@ +<script> +export default { + props: { + file: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <span + v-once + class="file-row-stats" + > + <span class="cgreen"> + +{{ file.addedLines }} + </span> + <span class="cred"> + -{{ file.removedLines }} + </span> + </span> +</template> + +<style> +.file-row-stats { + font-size: 12px; +} +</style> 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/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue new file mode 100644 index 00000000000..cfe4273742f --- /dev/null +++ b/app/assets/javascripts/diffs/components/tree_list.vue @@ -0,0 +1,101 @@ +<script> +import { mapActions, mapGetters, mapState } from 'vuex'; +import Icon from '~/vue_shared/components/icon.vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; +import FileRowStats from './file_row_stats.vue'; + +export default { + components: { + Icon, + FileRow, + }, + data() { + return { + search: '', + }; + }, + computed: { + ...mapState('diffs', ['tree', 'addedLines', 'removedLines']), + ...mapGetters('diffs', ['allBlobs', 'diffFilesLength']), + filteredTreeList() { + const search = this.search.toLowerCase().trim(); + + if (search === '') return this.tree; + + return this.allBlobs.filter(f => f.name.toLowerCase().indexOf(search) >= 0); + }, + }, + methods: { + ...mapActions('diffs', ['toggleTreeOpen', 'scrollToFile']), + clearSearch() { + this.search = ''; + }, + }, + FileRowStats, +}; +</script> + +<template> + <div class="tree-list-holder d-flex flex-column"> + <div class="append-bottom-8 position-relative tree-list-search"> + <icon + name="search" + class="position-absolute tree-list-icon" + /> + <input + v-model="search" + :placeholder="s__('MergeRequest|Filter files')" + type="search" + class="form-control" + /> + <button + v-show="search" + :aria-label="__('Clear search')" + type="button" + class="position-absolute tree-list-icon tree-list-clear-icon border-0 p-0" + @click="clearSearch" + > + <icon + name="close" + /> + </button> + </div> + <div + class="tree-list-scroll" + > + <template v-if="filteredTreeList.length"> + <file-row + v-for="file in filteredTreeList" + :key="file.key" + :file="file" + :level="0" + :hide-extra-on-tree="true" + :extra-component="$options.FileRowStats" + :show-changed-icon="true" + @toggleTreeOpen="toggleTreeOpen" + @clickFile="scrollToFile" + /> + </template> + <p + v-else + class="prepend-top-20 append-bottom-20 text-center" + > + {{ s__('MergeRequest|No files found') }} + </p> + </div> + <div + v-once + class="pt-3 pb-3 text-center" + > + {{ n__('%d changed file', '%d changed files', diffFilesLength) }} + <div> + <span class="cgreen"> + {{ n__('%d addition', '%d additions', addedLines) }} + </span> + <span class="cred"> + {{ n__('%d deleted', '%d deletions', removedLines) }} + </span> + </div> + </div> + </div> +</template> diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js index f68afa44837..6a50d2c1426 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'; @@ -28,3 +29,5 @@ export const LENGTH_OF_AVATAR_TOOLTIP = 17; export const LINES_TO_BE_RENDERED_DIRECTLY = 100; export const MAX_LINES_TO_BE_RENDERED = 2000; + +export const MR_TREE_SHOW_KEY = 'mr_tree_show'; diff --git a/app/assets/javascripts/diffs/mixins/changed_files.js b/app/assets/javascripts/diffs/mixins/changed_files.js deleted file mode 100644 index da1339f0ffa..00000000000 --- a/app/assets/javascripts/diffs/mixins/changed_files.js +++ /dev/null @@ -1,38 +0,0 @@ -export default { - props: { - diffFiles: { - type: Array, - required: true, - }, - }, - methods: { - fileChangedIcon(diffFile) { - if (diffFile.deletedFile) { - return 'file-deletion'; - } else if (diffFile.newFile) { - return 'file-addition'; - } - return 'file-modified'; - }, - fileChangedClass(diffFile) { - if (diffFile.deletedFile) { - return 'cred'; - } else if (diffFile.newFile) { - return 'cgreen'; - } - - return ''; - }, - truncatedDiffPath(path) { - const maxLength = 60; - - if (path.length > maxLength) { - const start = path.length - maxLength; - const end = start + maxLength; - return `...${path.slice(start, end)}`; - } - - return path; - }, - }, -}; diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 4ab6ceb249a..1e0b27b538d 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -1,13 +1,18 @@ 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, INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, + MR_TREE_SHOW_KEY, } from '../constants'; export const setBaseConfig = ({ commit }, options) => { @@ -29,25 +34,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 }, - ); } - }; + }); +}; - checkItem(); +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(); + } + }); + + return checkItem(); }; export const setInlineDiffViewType = ({ commit }) => { @@ -91,6 +124,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 +182,37 @@ 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'))); +}; + +export const toggleTreeOpen = ({ commit }, path) => { + commit(types.TOGGLE_FOLDER_OPEN, path); +}; + +export const scrollToFile = ({ state, commit }, path) => { + const { fileHash } = state.treeEntries[path]; + document.location.hash = fileHash; + + commit(types.UPDATE_CURRENT_DIFF_FILE_ID, fileHash); + + setTimeout(() => commit(types.UPDATE_CURRENT_DIFF_FILE_ID, ''), 1000); +}; + +export const toggleShowTreeList = ({ commit, state }) => { + commit(types.TOGGLE_SHOW_TREE_LIST); + localStorage.setItem(MR_TREE_SHOW_KEY, state.showTreeList); +}; + // 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..d4c205882ff 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,50 +72,47 @@ 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 export const getDiffFileByHash = state => fileHash => state.diffFiles.find(file => file.fileHash === fileHash); +export const allBlobs = state => Object.values(state.treeEntries).filter(f => f.type === 'blob'); + +export const diffFilesLength = state => state.diffFiles.length; + // prevent babel-plugin-rewire from generating an invalid default during karma tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js index 39d90a64aab..ae8930c8968 100644 --- a/app/assets/javascripts/diffs/store/modules/diff_state.js +++ b/app/assets/javascripts/diffs/store/modules/diff_state.js @@ -1,18 +1,25 @@ import Cookies from 'js-cookie'; import { getParameterValues } from '~/lib/utils/url_utility'; -import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME } from '../../constants'; +import { INLINE_DIFF_VIEW_TYPE, DIFF_VIEW_COOKIE_NAME, MR_TREE_SHOW_KEY } from '../../constants'; const viewTypeFromQueryString = getParameterValues('view')[0]; const viewTypeFromCookie = Cookies.get(DIFF_VIEW_COOKIE_NAME); const defaultViewType = INLINE_DIFF_VIEW_TYPE; +const storedTreeShow = localStorage.getItem(MR_TREE_SHOW_KEY); export default () => ({ isLoading: true, endpoint: '', basePath: '', commit: null, + startVersion: null, diffFiles: [], mergeRequestDiffs: [], + mergeRequestDiff: null, diffLineCommentForms: {}, diffViewType: viewTypeFromQueryString || viewTypeFromCookie || defaultViewType, + tree: [], + treeEntries: {}, + showTreeList: storedTreeShow === null ? true : storedTreeShow === 'true', + currentDiffFileId: '', }); 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..6474ee628e2 100644 --- a/app/assets/javascripts/diffs/store/mutation_types.js +++ b/app/assets/javascripts/diffs/store/mutation_types.js @@ -9,3 +9,8 @@ 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'; +export const TOGGLE_FOLDER_OPEN = 'TOGGLE_FOLDER_OPEN'; +export const TOGGLE_SHOW_TREE_LIST = 'TOGGLE_SHOW_TREE_LIST'; +export const UPDATE_CURRENT_DIFF_FILE_ID = 'UPDATE_CURRENT_DIFF_FILE_ID'; diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js index 0522e32c410..0b4485ecdb5 100644 --- a/app/assets/javascripts/diffs/store/mutations.js +++ b/app/assets/javascripts/diffs/store/mutations.js @@ -1,8 +1,15 @@ 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 { sortTree } from '~/ide/stores/utils'; +import { + findDiffFile, + addLineReferences, + removeMatchLine, + addContextLines, + prepareDiffData, + isDiscussionApplicableToLine, + generateTreeList, +} from './utils'; import * as types from './mutation_types'; export default { @@ -17,41 +24,13 @@ 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); + const { tree, treeEntries } = generateTreeList(diffData.diffFiles); Object.assign(state, { ...diffData, + tree: sortTree(tree), + treeEntries, }); }, @@ -98,19 +77,104 @@ 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: [], + }); + } + } + } + }, + [types.TOGGLE_FOLDER_OPEN](state, path) { + state.treeEntries[path].opened = !state.treeEntries[path].opened; + }, + [types.TOGGLE_SHOW_TREE_LIST](state) { + state.showTreeList = !state.showTreeList; + }, + [types.UPDATE_CURRENT_DIFF_FILE_ID](state, fileId) { + state.currentDiffFileId = fileId; + }, }; diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js index 82082ac508a..a482a2b82c0 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) { @@ -22,7 +25,7 @@ export const getReversePosition = linePosition => { return LINE_POSITION_RIGHT; }; -export function getNoteFormData(params) { +export function getFormData(params) { const { note, noteableType, @@ -52,20 +55,30 @@ 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, }, }; + return postData; +} + +export function getNoteFormData(params) { + const data = getFormData(params); + return { - endpoint: noteableData.create_note_path, - data: postData, + endpoint: params.noteableData.create_note_path, + data, }; } @@ -161,6 +174,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 +192,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 +241,17 @@ 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, + positionType: 'text', + }; } }); } @@ -194,3 +259,64 @@ 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); + const refs = convertObjectPropsToCamelCase(discussion.position); + + return _.isEqual(refs, diffPositionCopy) || _.isEqual(originalRefs, diffPositionCopy); + } + + return latestDiff && discussion.active && lineCode === discussion.line_code; +} + +export const generateTreeList = files => + files.reduce( + (acc, file) => { + const { fileHash, addedLines, removedLines, newFile, deletedFile, newPath } = file; + const split = newPath.split('/'); + + split.forEach((name, i) => { + const parent = acc.treeEntries[split.slice(0, i).join('/')]; + const path = `${parent ? `${parent.path}/` : ''}${name}`; + + if (!acc.treeEntries[path]) { + const type = path === newPath ? 'blob' : 'tree'; + acc.treeEntries[path] = { + key: path, + path, + name, + type, + tree: [], + }; + + const entry = acc.treeEntries[path]; + + if (type === 'blob') { + Object.assign(entry, { + changed: true, + tempFile: newFile, + deleted: deletedFile, + fileHash, + addedLines, + removedLines, + }); + } else { + Object.assign(entry, { + opened: true, + }); + } + + (parent ? parent.tree : acc.tree).push(entry); + } + }); + + return acc; + }, + { treeEntries: {}, tree: [] }, + ); 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_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 11e3b781e5a..a1d8e531940 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -466,7 +466,9 @@ export default { class="gl-responsive-table-row" role="row"> <div - class="table-section section-wrap section-15" + v-tooltip + :title="model.name" + class="table-section section-wrap section-15 text-truncate" role="gridcell" > <div @@ -480,9 +482,7 @@ export default { v-if="!model.isFolder" class="environment-name table-mobile-content"> <a - v-tooltip :href="environmentPath" - :title="model.name" > {{ model.name }} </a> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index ccc8419ca6d..a0797b594cb 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -2,12 +2,14 @@ /** * Renders the Monitoring (Metrics) link in environments table. */ +import { Button } from '@gitlab-org/gitlab-ui'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '../../vue_shared/directives/tooltip'; export default { components: { Icon, + 'gl-button': Button, }, directives: { tooltip, @@ -26,15 +28,16 @@ export default { }; </script> <template> - <a + <gl-button v-tooltip :href="monitoringUrl" :title="title" :aria-label="title" - class="btn monitoring-url d-none d-sm-none d-md-block" + class="monitoring-url d-none d-sm-none d-md-block" data-container="body" rel="noopener noreferrer nofollow" + variant="default" > <icon name="chart" /> - </a> + </gl-button> </template> 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..d7aa4ce597f --- /dev/null +++ b/app/assets/javascripts/filtered_search/admin_runners_filtered_search_token_keys.js @@ -0,0 +1,21 @@ +import FilteredSearchTokenKeys from './filtered_search_token_keys'; + +const tokenKeys = [{ + key: 'status', + type: 'string', + param: 'status', + symbol: '', + icon: 'messages', + tag: 'status', +}, { + key: 'type', + type: 'string', + param: 'type', + symbol: '', + icon: 'cube', + tag: 'type', +}]; + +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..c568f4e4ebf 100644 --- a/app/assets/javascripts/filtered_search/dropdown_hint.js +++ b/app/assets/javascripts/filtered_search/dropdown_hint.js @@ -51,7 +51,11 @@ export default class DropdownHint extends FilteredSearchDropdown { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms.join(' ')); } - FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''), '', false, this.container); + const key = token.replace(':', ''); + const { uppercaseTokenName } = this.tokenKeys.searchByKey(key); + FilteredSearchDropdownManager.addWordToInput(key, '', false, { + uppercaseTokenName, + }); } this.dismissDropdown(); this.dispatchInputEvent(); @@ -62,7 +66,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/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js index 27fff488603..6da6ca10008 100644 --- a/app/assets/javascripts/filtered_search/dropdown_utils.js +++ b/app/assets/javascripts/filtered_search/dropdown_utils.js @@ -143,7 +143,9 @@ export default class DropdownUtils { const dataValue = selected.getAttribute('data-value'); if (dataValue) { - FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true); + FilteredSearchDropdownManager.addWordToInput(filter, dataValue, true, { + capitalizeTokenValue: selected.hasAttribute('data-capitalize'), + }); } // Return boolean based on whether it was set 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..cd3d532c958 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,21 @@ export default class FilteredSearchDropdownManager { gl: DropdownEmoji, element: this.container.querySelector('#js-dropdown-my-reaction'), }, + wip: { + reference: null, + gl: DropdownNonUser, + element: this.container.querySelector('#js-dropdown-wip'), + }, + status: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-status'), + }, + type: { + reference: null, + gl: NullDropdown, + element: this.container.querySelector('#js-dropdown-admin-runner-type'), + }, }; supportedTokens.forEach((type) => { @@ -125,10 +141,16 @@ export default class FilteredSearchDropdownManager { return endpoint; } - static addWordToInput(tokenName, tokenValue = '', clicked = false) { + static addWordToInput(tokenName, tokenValue = '', clicked = false, options = {}) { + const { + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; const input = FilteredSearchContainer.container.querySelector('.filtered-search'); - - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, tokenValue, { + uppercaseTokenName, + capitalizeTokenValue, + }); input.value = ''; if (clicked) { diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js index 81286c54c4c..54533ebb70d 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; @@ -405,7 +405,10 @@ export default class FilteredSearchManager { if (isLastVisualTokenValid) { tokens.forEach((t) => { input.value = input.value.replace(`${t.key}:${t.symbol}${t.value}`, ''); - FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`); + FilteredSearchVisualTokens.addFilterVisualToken(t.key, `${t.symbol}${t.value}`, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(t.key), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(t.key), + }); }); const fragments = searchToken.split(':'); @@ -421,7 +424,10 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addSearchVisualToken(searchTerms); } - FilteredSearchVisualTokens.addFilterVisualToken(tokenKey); + FilteredSearchVisualTokens.addFilterVisualToken(tokenKey, null, { + uppercaseTokenName: this.filteredSearchTokenKeys.shouldUppercaseTokenName(tokenKey), + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); input.value = input.value.replace(`${tokenKey}:`, ''); } } else { @@ -429,7 +435,10 @@ export default class FilteredSearchManager { const valueCompletedRegex = /([~%@]{0,1}".+")|([~%@]{0,1}'.+')|^((?![~%@]')(?![~%@]")(?!')(?!")).*/g; if (searchToken.match(valueCompletedRegex) && input.value[input.value.length - 1] === ' ') { - FilteredSearchVisualTokens.addFilterVisualToken(searchToken); + const tokenKey = FilteredSearchVisualTokens.getLastTokenPartial(); + FilteredSearchVisualTokens.addFilterVisualToken(searchToken, null, { + capitalizeTokenValue: this.filteredSearchTokenKeys.shouldCapitalizeTokenValue(tokenKey), + }); // Trim the last space as seen in the if statement above input.value = input.value.replace(searchToken, '').trim(); @@ -480,7 +489,7 @@ export default class FilteredSearchManager { FilteredSearchVisualTokens.addFilterVisualToken( condition.tokenKey, condition.value, - canEdit, + { canEdit }, ); } else { // Sanitize value since URL converts spaces into + @@ -506,10 +515,15 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const canEdit = this.canEdit && this.canEdit(sanitizedKey, sanitizedValue); + const { uppercaseTokenName, capitalizeTokenValue } = match; FilteredSearchVisualTokens.addFilterVisualToken( sanitizedKey, `${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`, - canEdit, + { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }, ); } else if (!match && keyParam === 'assignee_id') { const id = parseInt(value, 10); @@ -517,7 +531,7 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'assignee'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); } } else if (!match && keyParam === 'author_id') { const id = parseInt(value, 10); @@ -525,7 +539,7 @@ export default class FilteredSearchManager { hasFilteredSearch = true; const tokenName = 'author'; const canEdit = this.canEdit && this.canEdit(tokenName); - FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, canEdit); + FilteredSearchVisualTokens.addFilterVisualToken(tokenName, `@${usernameParams[id]}`, { canEdit }); } } else if (!match && keyParam === 'search') { hasFilteredSearch = true; @@ -561,15 +575,17 @@ export default class FilteredSearchManager { this.saveCurrentSearchQuery(); - const { tokens, searchToken } - = this.tokenizer.processTokens(searchQuery, this.filteredSearchTokenKeys.getKeys()); + const tokenKeys = this.filteredSearchTokenKeys.getKeys(); + const { tokens, searchToken } = this.tokenizer.processTokens(searchQuery, tokenKeys); const currentState = state || getParameterByName('state') || 'opened'; paths.push(`state=${currentState}`); tokens.forEach((token) => { const condition = this.filteredSearchTokenKeys .searchByConditionKeyValue(token.key, token.value.toLowerCase()); - const { param } = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const tokenConfig = this.filteredSearchTokenKeys.searchByKey(token.key) || {}; + const { param } = tokenConfig; + // Replace hyphen with underscore to use as request parameter // e.g. 'my-reaction' => 'my_reaction' const underscoredKey = token.key.replace('-', '_'); @@ -581,6 +597,10 @@ export default class FilteredSearchManager { } else { let tokenValue = token.value; + if (tokenConfig.lowercaseValueOnSubmit) { + tokenValue = tokenValue.toLowerCase(); + } + if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') || (tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) { tokenValue = tokenValue.slice(1, tokenValue.length - 1); 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..a09ad3e4758 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,48 @@ -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', - }); -} +export default class FilteredSearchTokenKeys { + constructor(tokenKeys = [], alternativeTokenKeys = [], conditions = []) { + this.tokenKeys = tokenKeys; + this.alternativeTokenKeys = alternativeTokenKeys; + this.conditions = conditions; -const alternativeTokenKeys = [{ - key: 'label', - type: 'string', - param: 'name', - symbol: '~', -}]; - -const tokenKeysWithAlternative = tokenKeys.concat(alternativeTokenKeys); - -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; + } + + getKeys() { + return this.tokenKeys.map(i => i.key); + } + + getAlternatives() { + return this.alternativeTokenKeys; } - static getKeys() { - return tokenKeys.map(i => i.key); + getConditions() { + return this.conditions; } - static getAlternatives() { - return alternativeTokenKeys; + shouldUppercaseTokenName(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.uppercaseTokenName; } - static getConditions() { - return conditions; + shouldCapitalizeTokenValue(tokenKey) { + const token = this.searchByKey(tokenKey.toLowerCase()); + return token && token.capitalizeTokenValue; } - 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 +57,29 @@ 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; } + + addExtraTokensForMergeRequests() { + const wipToken = { + key: 'wip', + type: 'string', + param: '', + symbol: '', + icon: 'admin', + tag: 'Yes or No', + lowercaseValueOnSubmit: true, + uppercaseTokenName: true, + capitalizeTokenValue: true, + }; + + this.tokenKeys.push(wipToken); + this.tokenKeysWithAlternative.push(wipToken); + } } diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js index 56fe1ab4e90..0854c1822fb 100644 --- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js +++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js @@ -55,12 +55,18 @@ export default class FilteredSearchVisualTokens { } } - static createVisualTokenElementHTML(canEdit = true) { + static createVisualTokenElementHTML(options = {}) { + const { + canEdit = true, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = options; + return ` <div class="${canEdit ? 'selectable' : 'hidden'}" role="button"> - <div class="name"></div> + <div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div> <div class="value-container"> - <div class="value"></div> + <div class="${capitalizeTokenValue ? 'text-capitalize' : ''} value"></div> <div class="remove-token" role="button"> <i class="fa fa-close"></i> </div> @@ -182,16 +188,26 @@ export default class FilteredSearchVisualTokens { } } - static addVisualTokenElement(name, value, isSearchTerm, canEdit) { + static addVisualTokenElement(name, value, options = {}) { + const { + isSearchTerm = false, + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + } = options; const li = document.createElement('li'); li.classList.add('js-visual-token'); li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token'); if (value) { - li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML(canEdit); + li.innerHTML = FilteredSearchVisualTokens.createVisualTokenElementHTML({ + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); FilteredSearchVisualTokens.renderVisualTokenValue(li, name, value); } else { - li.innerHTML = '<div class="name"></div>'; + li.innerHTML = `<div class="${uppercaseTokenName ? 'text-uppercase' : ''} name"></div>`; } li.querySelector('.name').innerText = name; @@ -212,20 +228,32 @@ export default class FilteredSearchVisualTokens { } } - static addFilterVisualToken(tokenName, tokenValue, canEdit) { + static addFilterVisualToken(tokenName, tokenValue, { + canEdit, + uppercaseTokenName = false, + capitalizeTokenValue = false, + } = {}) { const { lastVisualToken, isLastVisualTokenValid } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput(); const { addVisualTokenElement } = FilteredSearchVisualTokens; if (isLastVisualTokenValid) { - addVisualTokenElement(tokenName, tokenValue, false, canEdit); + addVisualTokenElement(tokenName, tokenValue, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } else { const previousTokenName = lastVisualToken.querySelector('.name').innerText; const tokensContainer = FilteredSearchContainer.container.querySelector('.tokens-container'); tokensContainer.removeChild(lastVisualToken); const value = tokenValue || tokenName; - addVisualTokenElement(previousTokenName, value, false, canEdit); + addVisualTokenElement(previousTokenName, value, { + canEdit, + uppercaseTokenName, + capitalizeTokenValue, + }); } } @@ -235,7 +263,9 @@ export default class FilteredSearchVisualTokens { if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) { lastVisualToken.querySelector('.name').innerText += ` ${searchTerm}`; } else { - FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, true); + FilteredSearchVisualTokens.addVisualTokenElement(searchTerm, null, { + isSearchTerm: true, + }); } } @@ -306,7 +336,9 @@ export default class FilteredSearchVisualTokens { let value; if (token.classList.contains('filtered-search-token')) { - FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText); + FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText, null, { + uppercaseTokenName: nameElement.classList.contains('text-uppercase'), + }); const valueContainerElement = token.querySelector('.value-container'); value = valueContainerElement.dataset.originalValue; 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/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js index 73b2cd0b2c7..95636a9ccdd 100644 --- a/app/assets/javascripts/gfm_auto_complete.js +++ b/app/assets/javascripts/gfm_auto_complete.js @@ -15,6 +15,7 @@ export const defaultAutocompleteConfig = { epics: true, milestones: true, labels: true, + snippets: true, }; class GfmAutoComplete { @@ -50,6 +51,7 @@ class GfmAutoComplete { if (this.enableMap.milestones) this.setupMilestones($input); if (this.enableMap.mergeRequests) this.setupMergeRequests($input); if (this.enableMap.labels) this.setupLabels($input); + if (this.enableMap.snippets) this.setupSnippets($input); // We don't instantiate the quick actions autocomplete for note and issue/MR edit forms $input.filter('[data-supports-quick-actions="true"]').atwho({ @@ -360,6 +362,39 @@ class GfmAutoComplete { }); } + setupSnippets($input) { + $input.atwho({ + at: '$', + alias: 'snippets', + searchKey: 'search', + displayTpl(value) { + let tmpl = GfmAutoComplete.Loading.template; + if (value.title != null) { + tmpl = GfmAutoComplete.Issues.template; + } + return tmpl; + }, + data: GfmAutoComplete.defaultLoadingData, + // eslint-disable-next-line no-template-curly-in-string + insertTpl: '${atwho-at}${id}', + callbacks: { + ...this.getDefaultCallbacks(), + beforeSave(snippets) { + return $.map(snippets, (m) => { + if (m.title == null) { + return m; + } + return { + id: m.id, + title: sanitize(m.title), + search: `${m.id} ${m.title}`, + }; + }); + }, + }, + }); + } + getDefaultCallbacks() { const fetchData = this.fetchData.bind(this); @@ -470,7 +505,7 @@ class GfmAutoComplete { // The below is taken from At.js source // Tweaked to commands to start without a space only if char before is a non-word character // https://github.com/ichord/At.js - const atSymbolsWithBar = Object.keys(controllers).join('|'); + const atSymbolsWithBar = Object.keys(controllers).join('|').replace(/[$]/, '\\$&'); const atSymbolsWithoutBar = Object.keys(controllers).join(''); const targetSubtext = subtext.split(GfmAutoComplete.regexSubtext).pop(); const resultantFlag = flag.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&'); @@ -497,6 +532,7 @@ GfmAutoComplete.atTypeMap = { '~': 'labels', '%': 'milestones', '/': 'commands', + $: 'snippets', }; // Emoji @@ -519,7 +555,7 @@ GfmAutoComplete.Labels = { // eslint-disable-next-line no-template-curly-in-string template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>', }; -// Issues and MergeRequests +// Issues, MergeRequests and Snippets GfmAutoComplete.Issues = { // eslint-disable-next-line no-template-curly-in-string template: '<li><small>${id}</small> ${title}</li>', 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 adde8c8cdb3..81b2e5ea37b 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,57 +1,66 @@ <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> - <div class="groups-list-tree-container"> + <div class="groups-list-tree-container qa-groups-list-tree-container"> <div v-if="searchEmpty" class="has-no-search-results" > {{ 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/header.js b/app/assets/javascripts/header.js index 4ae3a714bee..175d0b8498b 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -1,5 +1,9 @@ import $ from 'jquery'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; import { highCountTrim } from '~/lib/utils/text_utility'; +import SetStatusModalTrigger from './set_status_modal/set_status_modal_trigger.vue'; +import SetStatusModalWrapper from './set_status_modal/set_status_modal_wrapper.vue'; /** * Updates todo counter when todos are toggled. @@ -17,3 +21,54 @@ export default function initTodoToggle() { $todoPendingCount.toggleClass('hidden', parsedCount === 0); }); } + +document.addEventListener('DOMContentLoaded', () => { + const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger'); + const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper'); + + if (setStatusModalTriggerEl || setStatusModalWrapperEl) { + Vue.use(Translate); + + // eslint-disable-next-line no-new + new Vue({ + el: setStatusModalTriggerEl, + data() { + const { hasStatus } = this.$options.el.dataset; + + return { + hasStatus: hasStatus === 'true', + }; + }, + render(createElement) { + return createElement(SetStatusModalTrigger, { + props: { + hasStatus: this.hasStatus, + }, + }); + }, + }); + + // eslint-disable-next-line no-new + new Vue({ + el: setStatusModalWrapperEl, + data() { + const { currentEmoji, currentMessage } = this.$options.el.dataset; + + return { + currentEmoji, + currentMessage, + }; + }, + render(createElement) { + const { currentEmoji, currentMessage } = this; + + return createElement(SetStatusModalWrapper, { + props: { + currentEmoji, + currentMessage, + }, + }); + }, + }); + } +}); 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..b0e60edcbe5 --- /dev/null +++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue @@ -0,0 +1,79 @@ +<script> +import $ from 'jquery'; +import { mapActions } from 'vuex'; +import { __ } from '~/locale'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/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" + class="ml-0" + /> + <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..ee0e72cd05f 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,20 @@ 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> + </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..72ce37be63a 100644 --- a/app/assets/javascripts/ide/components/file_finder/item.vue +++ b/app/assets/javascripts/ide/components/file_finder/item.vue @@ -1,7 +1,7 @@ <script> import fuzzaldrinPlus from 'fuzzaldrin-plus'; import FileIcon from '../../../vue_shared/components/file_icon.vue'; -import ChangedFileIcon from '../changed_file_icon.vue'; +import ChangedFileIcon from '../../../vue_shared/components/changed_file_icon.vue'; const MAX_PATH_LENGTH = 60; @@ -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..2ad14b88410 --- /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 ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; +import NewDropdown from './new_dropdown/index.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..ad6151e3bf6 100644 --- a/app/assets/javascripts/ide/components/ide.vue +++ b/app/assets/javascripts/ide/components/ide.vue @@ -1,4 +1,5 @@ <script> +import Vue from 'vue'; import Mousetrap from 'mousetrap'; import { mapActions, mapState, mapGetters } from 'vuex'; import { __ } from '~/locale'; @@ -10,6 +11,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; @@ -21,8 +23,15 @@ export default { IdeStatusBar, RepoEditor, FindFile, - RightPane, ErrorMessage, + CommitEditorHeader, + }, + props: { + rightPaneComponent: { + type: Vue.Component, + required: false, + default: () => RightPane, + }, }, computed: { ...mapState([ @@ -34,7 +43,7 @@ export default { 'currentProjectId', 'errorMessage', ]), - ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']), + ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']), }, mounted() { window.onbeforeunload = e => this.onBeforeUnload(e); @@ -96,7 +105,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" @@ -136,7 +150,8 @@ export default { </div> </template> </div> - <right-pane + <component + :is="rightPaneComponent" v-if="currentProjectId" /> </div> diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue index 4771c58a11d..f99ff6d6da8 100644 --- a/app/assets/javascripts/ide/components/ide_side_bar.vue +++ b/app/assets/javascripts/ide/components/ide_side_bar.vue @@ -1,6 +1,6 @@ <script> import { mapState, mapGetters } from 'vuex'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; import IdeTree from './ide_tree.vue'; import ResizablePanel from './resizable_panel.vue'; import ActivityBar from './activity_bar.vue'; @@ -13,7 +13,7 @@ import { activityBarViews } from '../constants'; export default { components: { - SkeletonLoadingContainer, + SkeletonLoading, ResizablePanel, ActivityBar, CommitSection, @@ -56,7 +56,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading-container /> + <skeleton-loading /> </div> </div> </template> diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue index 715dc1bfb42..a04d09ef374 100644 --- a/app/assets/javascripts/ide/components/ide_status_bar.vue +++ b/app/assets/javascripts/ide/components/ide_status_bar.vue @@ -50,7 +50,9 @@ export default { this.stopPipelinePolling(); }, methods: { - ...mapActions(['setRightPane']), + ...mapActions('rightPane', { + openRightPane: 'open', + }), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']), startTimer() { this.intervalId = setInterval(() => { @@ -88,7 +90,7 @@ export default { <button type="button" class="p-0 border-0 h-50" - @click="setRightPane($options.rightSidebarViews.pipelines)" + @click="openRightPane($options.rightSidebarViews.pipelines)" > <ci-icon v-tooltip diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue index 00ae5ea2c15..cfe25084b42 100644 --- a/app/assets/javascripts/ide/components/ide_tree_list.vue +++ b/app/assets/javascripts/ide/components/ide_tree_list.vue @@ -1,16 +1,17 @@ <script> 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 { SkeletonLoading } from '@gitlab-org/gitlab-ui'; +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, + SkeletonLoading, NavDropdown, + FileRow, }, props: { viewerType: { @@ -34,8 +35,9 @@ export default { this.updateViewer(this.viewerType); }, methods: { - ...mapActions(['updateViewer']), + ...mapActions(['updateViewer', 'toggleTreeOpen']), }, + FileRowExtra, }; </script> @@ -49,7 +51,7 @@ export default { :key="n" class="multi-file-loading-container" > - <skeleton-loading-container /> + <skeleton-loading /> </div> </template> <template v-else> @@ -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/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue index 79df225c432..bd07f372177 100644 --- a/app/assets/javascripts/ide/components/panes/right.vue +++ b/app/assets/javascripts/ide/components/panes/right.vue @@ -1,5 +1,7 @@ <script> import { mapActions, mapState, mapGetters } from 'vuex'; +import _ from 'underscore'; +import { __ } from '~/locale'; import tooltip from '../../../vue_shared/directives/tooltip'; import Icon from '../../../vue_shared/components/icon.vue'; import { rightSidebarViews } from '../../constants'; @@ -21,28 +23,77 @@ export default { MergeRequestInfo, Clientside, }, + props: { + extensionTabs: { + type: Array, + required: false, + default: () => [], + }, + }, computed: { - ...mapState(['rightPane', 'currentMergeRequestId', 'clientsidePreviewEnabled']), + ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']), + ...mapState('rightPane', ['isOpen', 'currentView']), ...mapGetters(['packageJson']), - pipelinesActive() { - return ( - this.rightPane === rightSidebarViews.pipelines || - this.rightPane === rightSidebarViews.jobsDetail - ); - }, + ...mapGetters('rightPane', ['isActiveView', 'isAliveView']), showLivePreview() { return this.packageJson && this.clientsidePreviewEnabled; }, + defaultTabs() { + return [ + { + show: this.currentMergeRequestId, + title: __('Merge Request'), + views: [ + rightSidebarViews.mergeRequestInfo, + ], + icon: 'text-description', + }, + { + show: true, + title: __('Pipelines'), + views: [ + rightSidebarViews.pipelines, + rightSidebarViews.jobsDetail, + ], + icon: 'rocket', + }, + { + show: this.showLivePreview, + title: __('Live preview'), + views: [ + rightSidebarViews.clientSidePreview, + ], + icon: 'live-preview', + }, + ]; + }, + tabs() { + return this.defaultTabs + .concat(this.extensionTabs) + .filter(tab => tab.show); + }, + tabViews() { + return _.flatten(this.tabs.map(tab => tab.views)); + }, + aliveTabViews() { + return this.tabViews.filter(view => this.isAliveView(view.name)); + }, }, methods: { - ...mapActions(['setRightPane']), - clickTab(e, view) { + ...mapActions('rightPane', ['toggleOpen', 'open']), + clickTab(e, tab) { e.target.blur(); - this.setRightPane(view); + if (this.isActiveTab(tab)) { + this.toggleOpen(); + } else { + this.open(tab.views[0]); + } + }, + isActiveTab(tab) { + return tab.views.some(view => this.isActiveView(view.name)); }, }, - rightSidebarViews, }; </script> @@ -51,77 +102,45 @@ export default { class="multi-file-commit-panel ide-right-sidebar" > <resizable-panel - v-if="rightPane" + v-show="isOpen" :collapsible="false" :initial-width="350" :min-size="350" - :class="`ide-right-sidebar-${rightPane}`" + :class="`ide-right-sidebar-${currentView}`" side="right" class="multi-file-commit-panel-inner" > - <component :is="rightPane" /> + <div + v-for="tabView in aliveTabViews" + v-show="isActiveView(tabView.name)" + :key="tabView.name" + class="h-100" + > + <component :is="tabView.name" /> + </div> </resizable-panel> <nav class="ide-activity-bar"> <ul class="list-unstyled"> <li - v-if="currentMergeRequestId" + v-for="tab of tabs" + :key="tab.title" > <button v-tooltip - :title="__('Merge Request')" - :aria-label="__('Merge Request')" - :class="{ - active: rightPane === $options.rightSidebarViews.mergeRequestInfo - }" - data-container="body" - data-placement="left" - class="ide-sidebar-link is-right" - type="button" - @click="clickTab($event, $options.rightSidebarViews.mergeRequestInfo)" - > - <icon - :size="16" - name="text-description" - /> - </button> - </li> - <li> - <button - v-tooltip - :title="__('Pipelines')" - :aria-label="__('Pipelines')" - :class="{ - active: pipelinesActive - }" - data-container="body" - data-placement="left" - class="ide-sidebar-link is-right" - type="button" - @click="clickTab($event, $options.rightSidebarViews.pipelines)" - > - <icon - :size="16" - name="rocket" - /> - </button> - </li> - <li v-if="showLivePreview"> - <button - v-tooltip - :title="__('Live preview')" - :aria-label="__('Live preview')" + :title="tab.title" + :aria-label="tab.title" :class="{ - active: rightPane === $options.rightSidebarViews.clientSidePreview + active: isActiveTab(tab) && isOpen }" data-container="body" data-placement="left" class="ide-sidebar-link is-right" type="button" - @click="clickTab($event, $options.rightSidebarViews.clientSidePreview)" + @click="clickTab($event, tab)" > <icon :size="16" - name="live-preview" + :name="tab.icon" /> </button> </li> 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..b2599128213 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: { @@ -20,12 +22,14 @@ export default { }, }, computed: { + ...mapState('rightPane', { + rightPaneIsOpen: 'isOpen', + }), ...mapState([ 'rightPanelCollapsed', 'viewer', 'panelResizing', 'currentActivityView', - 'rightPane', ]), ...mapGetters([ 'currentMergeRequest', @@ -34,6 +38,7 @@ export default { 'isCommitModeActive', 'isReviewModeActive', ]), + ...mapGetters('fileTemplates', ['showFileTemplatesBar']), shouldHideEditor() { return this.file && this.file.binary && !this.file.content; }, @@ -96,7 +101,7 @@ export default { this.editor.updateDimensions(); } }, - rightPane() { + rightPaneIsOpen() { this.editor.updateDimensions(); }, }, @@ -216,7 +221,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 +254,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/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue deleted file mode 100644 index 7a5ede82253..00000000000 --- a/app/assets/javascripts/ide/components/repo_loading_file.vue +++ /dev/null @@ -1,42 +0,0 @@ -<script> - import { mapState } from 'vuex'; - import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; - - export default { - components: { - skeletonLoadingContainer, - }, - computed: { - ...mapState([ - 'leftPanelCollapsed', - ]), - }, - }; -</script> - -<template> - <tr - class="loading-file" - aria-label="Loading files" - > - <td class="multi-file-table-col-name"> - <skeleton-loading-container - :small="true" - /> - </td> - <template v-if="!leftPanelCollapsed"> - <td class="d-none d-sm-none d-md-block"> - <skeleton-loading-container - :small="true" - /> - </td> - - <td class="d-none d-sm-block"> - <skeleton-loading-container - :small="true" - class="animation-container-right" - /> - </td> - </template> - </tr> -</template> diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue index db47b75ec5c..d621653d6fd 100644 --- a/app/assets/javascripts/ide/components/repo_tab.vue +++ b/app/assets/javascripts/ide/components/repo_tab.vue @@ -3,8 +3,8 @@ import { mapActions } from 'vuex'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; import FileStatusIcon from './repo_file_status_icon.vue'; -import ChangedFileIcon from './changed_file_icon.vue'; export default { components: { diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js index 8caa5b86a9b..3b201f006aa 100644 --- a/app/assets/javascripts/ide/constants.js +++ b/app/assets/javascripts/ide/constants.js @@ -29,10 +29,10 @@ export const diffModes = { }; export const rightSidebarViews = { - pipelines: 'pipelines-list', - jobsDetail: 'jobs-detail', - mergeRequestInfo: 'merge-request-info', - clientSidePreview: 'clientside', + pipelines: { name: 'pipelines-list', keepAlive: true }, + jobsDetail: { name: 'jobs-detail', keepAlive: false }, + mergeRequestInfo: { name: 'merge-request-info', keepAlive: true }, + clientSidePreview: { name: 'clientside', keepAlive: false }, }; export const stageKeys = { diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js index 79e38ae911e..c0550116633 100644 --- a/app/assets/javascripts/ide/index.js +++ b/app/assets/javascripts/ide/index.js @@ -8,16 +8,28 @@ import { convertPermissionToBoolean } from '../lib/utils/common_utils'; Vue.use(Translate); -export function initIde(el) { +/** + * Initialize the IDE on the given element. + * + * @param {Element} el - The element that will contain the IDE. + * @param {Object} options - Extra options for the IDE (Used by EE). + * @param {(e:Element) => Object} options.extraInitialData - + * Function that returns extra properties to seed initial data. + * @param {Component} options.rootComponent - + * Component that overrides the root component. + */ +export function initIde(el, options = {}) { if (!el) return null; + const { + extraInitialData = () => ({}), + rootComponent = ide, + } = options; + return new Vue({ el, store, router, - components: { - ide, - }, created() { this.setEmptyStateSvgs({ emptyStateSvgPath: el.dataset.emptyStateSvgPath, @@ -32,13 +44,14 @@ export function initIde(el) { }); this.setInitialData({ clientsidePreviewEnabled: convertPermissionToBoolean(el.dataset.clientsidePreviewEnabled), + ...extraInitialData(el), }); }, methods: { ...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']), }, render(createElement) { - return createElement('ide'); + return createElement(rootComponent); }, }); } @@ -52,3 +65,18 @@ export function resetServiceWorkersPublicPath() { const webpackAssetPath = `${relativeRootPath}/assets/webpack/`; __webpack_public_path__ = webpackAssetPath; // eslint-disable-line camelcase } + +/** + * Start the IDE. + * + * @param {Objects} options - Extra options for the IDE (Used by EE). + */ +export function startIde(options) { + document.addEventListener('DOMContentLoaded', () => { + const ideElement = document.getElementById('ide'); + if (ideElement) { + resetServiceWorkersPublicPath(); + initIde(ideElement, options); + } + }); +} diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js index aa02dfbddc4..e10a132ab4b 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) => { @@ -169,10 +184,6 @@ export const burstUnusedSeal = ({ state, commit }) => { } }; -export const setRightPane = ({ commit }, view) => { - commit(types.SET_RIGHT_PANE, view); -}; - export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links); export const setErrorMessage = ({ commit }, errorMessage) => @@ -206,6 +217,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 +226,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..f1f544b52b2 100644 --- a/app/assets/javascripts/ide/stores/index.js +++ b/app/assets/javascripts/ide/stores/index.js @@ -8,6 +8,8 @@ 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'; +import paneModule from './modules/pane'; Vue.use(Vuex); @@ -22,6 +24,8 @@ export const createStore = () => pipelines, mergeRequests, branches, + fileTemplates: fileTemplates(), + rightPane: paneModule(), }, }); 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..b7090e09daf 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,8 @@ import Api from '~/api'; import { __ } from '~/locale'; +import { normalizeHeaders } from '~/lib/utils/common_utils'; import * as types from './mutation_types'; +import eventHub from '../../../eventhub'; export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES); export const receiveTemplateTypesError = ({ commit, dispatch }) => { @@ -21,19 +23,41 @@ export const receiveTemplateTypesError = ({ commit, dispatch }) => { export const receiveTemplateTypesSuccess = ({ commit }, templates) => commit(types.RECEIVE_TEMPLATE_TYPES_SUCCESS, templates); -export const fetchTemplateTypes = ({ dispatch, state }) => { +export const fetchTemplateTypes = ({ dispatch, state, rootState }, page = 1) => { if (!Object.keys(state.selectedTemplateType).length) return Promise.reject(); dispatch('requestTemplateTypes'); - return Api.templates(state.selectedTemplateType.key) - .then(({ data }) => dispatch('receiveTemplateTypesSuccess', data)) + return Api.projectTemplates(rootState.currentProjectId, state.selectedTemplateType.key, { page }) + .then(({ data, headers }) => { + const nextPage = parseInt(normalizeHeaders(headers)['X-NEXT-PAGE'], 10); + + dispatch('receiveTemplateTypesSuccess', data); + + if (nextPage) { + dispatch('fetchTemplateTypes', nextPage); + } + }) .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', @@ -50,12 +74,16 @@ export const receiveTemplateError = ({ dispatch }, template) => { ); }; -export const fetchTemplate = ({ dispatch, state }, template) => { +export const fetchTemplate = ({ dispatch, state, rootState }, template) => { if (template.content) { return dispatch('setFileTemplate', template); } - return Api.templates(`${state.selectedTemplateType.key}/${template.key || template.name}`) + return Api.projectTemplate( + rootState.currentProjectId, + state.selectedTemplateType.key, + template.key || template.name, + ) .then(({ data }) => { dispatch('setFileTemplate', data); }) @@ -69,6 +97,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 +105,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..d519c033769 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 { @@ -10,7 +9,7 @@ export default { }, [types.RECEIVE_TEMPLATE_TYPES_SUCCESS](state, templates) { state.isLoading = false; - state.templates = templates; + state.templates = state.templates.concat(templates); }, [types.SET_SELECTED_TEMPLATE_TYPE](state, type) { state.selectedTemplateType = type; 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/pane/actions.js b/app/assets/javascripts/ide/stores/modules/pane/actions.js new file mode 100644 index 00000000000..7f5d167a14f --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/actions.js @@ -0,0 +1,30 @@ +import * as types from './mutation_types'; + +export const toggleOpen = ({ dispatch, state }, view) => { + if (state.isOpen) { + dispatch('close'); + } else { + dispatch('open', view); + } +}; + +export const open = ({ commit }, view) => { + commit(types.SET_OPEN, true); + + if (view) { + const { name, keepAlive } = view; + + commit(types.SET_CURRENT_VIEW, name); + + if (keepAlive) { + commit(types.KEEP_ALIVE_VIEW, name); + } + } +}; + +export const close = ({ commit }) => { + commit(types.SET_OPEN, false); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/getters.js b/app/assets/javascripts/ide/stores/modules/pane/getters.js new file mode 100644 index 00000000000..c346cf13689 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/getters.js @@ -0,0 +1,4 @@ +export const isActiveView = state => view => state.currentView === view; + +export const isAliveView = (state, getters) => view => + state.keepAliveViews[view] || (state.isOpen && getters.isActiveView(view)); diff --git a/app/assets/javascripts/ide/stores/modules/pane/index.js b/app/assets/javascripts/ide/stores/modules/pane/index.js new file mode 100644 index 00000000000..5f61cb732c8 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/index.js @@ -0,0 +1,12 @@ +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; +import state from './state'; + +export default () => ({ + namespaced: true, + state: state(), + actions, + getters, + mutations, +}); diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js new file mode 100644 index 00000000000..abdebc4d913 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_OPEN = 'SET_OPEN'; +export const SET_CURRENT_VIEW = 'SET_CURRENT_VIEW'; +export const KEEP_ALIVE_VIEW = 'KEEP_ALIVE_VIEW'; diff --git a/app/assets/javascripts/ide/stores/modules/pane/mutations.js b/app/assets/javascripts/ide/stores/modules/pane/mutations.js new file mode 100644 index 00000000000..c16484b4402 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/mutations.js @@ -0,0 +1,19 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_OPEN](state, isOpen) { + Object.assign(state, { + isOpen, + }); + }, + [types.SET_CURRENT_VIEW](state, currentView) { + Object.assign(state, { + currentView, + }); + }, + [types.KEEP_ALIVE_VIEW](state, viewName) { + Object.assign(state.keepAliveViews, { + [viewName]: true, + }); + }, +}; diff --git a/app/assets/javascripts/ide/stores/modules/pane/state.js b/app/assets/javascripts/ide/stores/modules/pane/state.js new file mode 100644 index 00000000000..353065b5735 --- /dev/null +++ b/app/assets/javascripts/ide/stores/modules/pane/state.js @@ -0,0 +1,5 @@ +export default () => ({ + isOpen: false, + currentView: null, + keepAliveViews: {}, +}); diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js index 3e67b222e66..8fa86995ef0 100644 --- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js +++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js @@ -113,7 +113,7 @@ export const toggleStageCollapsed = ({ commit }, stageId) => export const setDetailJob = ({ commit, dispatch }, job) => { commit(types.SET_DETAIL_JOB, job); - dispatch('setRightPane', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { + dispatch('rightPane/open', job ? rightSidebarViews.jobsDetail : rightSidebarViews.pipelines, { root: true, }); }; 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/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js index 5a7991d2fa7..a5f8098dc17 100644 --- a/app/assets/javascripts/ide/stores/mutation_types.js +++ b/app/assets/javascripts/ide/stores/mutation_types.js @@ -68,8 +68,6 @@ export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; -export const SET_RIGHT_PANE = 'SET_RIGHT_PANE'; - export const CLEAR_PROJECTS = 'CLEAR_PROJECTS'; export const RESET_OPEN_FILES = 'RESET_OPEN_FILES'; diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js index 0347f803757..78cdfda74f0 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'; @@ -166,11 +166,6 @@ export default { unusedSeal: false, }); }, - [types.SET_RIGHT_PANE](state, view) { - Object.assign(state, { - rightPane: state.rightPane === view ? null : view, - }); - }, [types.SET_LINKS](state, links) { Object.assign(state, { links }); }, @@ -227,7 +222,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 +241,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/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js index 46b52fa00fc..d400b9831a9 100644 --- a/app/assets/javascripts/ide/stores/state.js +++ b/app/assets/javascripts/ide/stores/state.js @@ -23,7 +23,6 @@ export default () => ({ currentActivityView: activityBarViews.edit, unusedSeal: true, fileFindVisible: false, - rightPane: null, links: {}, errorMessage: null, entryModal: { 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/app.vue b/app/assets/javascripts/issue_show/components/app.vue index ad928484952..c6ad3aa3e0d 100644 --- a/app/assets/javascripts/issue_show/components/app.vue +++ b/app/assets/javascripts/issue_show/components/app.vue @@ -293,6 +293,7 @@ :show-delete-button="showDeleteButton" :can-attach-file="canAttachFile" :enable-autocomplete="enableAutocomplete" + :issuable-type="issuableType" /> <recaptcha-modal diff --git a/app/assets/javascripts/issue_show/components/edit_actions.vue b/app/assets/javascripts/issue_show/components/edit_actions.vue index 597c6d69a81..c3d39082714 100644 --- a/app/assets/javascripts/issue_show/components/edit_actions.vue +++ b/app/assets/javascripts/issue_show/components/edit_actions.vue @@ -1,7 +1,13 @@ <script> + import { __, sprintf } from '~/locale'; import updateMixin from '../mixins/update'; import eventHub from '../event_hub'; + const issuableTypes = { + issue: __('Issue'), + epic: __('Epic'), + }; + export default { mixins: [updateMixin], props: { @@ -18,6 +24,10 @@ required: false, default: true, }, + issuableType: { + type: String, + required: true, + }, }, data() { return { @@ -37,8 +47,11 @@ eventHub.$emit('close.form'); }, deleteIssuable() { + const confirmMessage = sprintf(__('%{issuableType} will be removed! Are you sure?'), { + issuableType: issuableTypes[this.issuableType], + }); // eslint-disable-next-line no-alert - if (window.confirm('Issue will be removed! Are you sure?')) { + if (window.confirm(confirmMessage)) { this.deleteLoading = true; eventHub.$emit('delete.issuable'); @@ -53,7 +66,7 @@ <button :class="{ disabled: formState.updateLoading || !isSubmitEnabled }" :disabled="formState.updateLoading || !isSubmitEnabled" - class="btn btn-save float-left" + class="btn btn-success float-left qa-save-button" type="submit" @click.prevent="updateIssuable"> Save changes @@ -73,7 +86,7 @@ v-if="shouldShowDeleteButton" :class="{ disabled: deleteLoading }" :disabled="deleteLoading" - class="btn btn-danger float-right append-right-default" + class="btn btn-danger float-right append-right-default qa-delete-button" type="button" @click="deleteIssuable"> Delete diff --git a/app/assets/javascripts/issue_show/components/fields/description.vue b/app/assets/javascripts/issue_show/components/fields/description.vue index 97acc5ba385..1a78c59d715 100644 --- a/app/assets/javascripts/issue_show/components/fields/description.vue +++ b/app/assets/javascripts/issue_show/components/fields/description.vue @@ -61,7 +61,8 @@ ref="textarea" slot="textarea" v-model="formState.description" - class="note-textarea js-gfm-input js-autosize markdown-area" + class="note-textarea js-gfm-input js-autosize markdown-area + qa-description-textarea" data-supports-quick-actions="false" aria-label="Description" placeholder="Write a comment or drag your files here…" diff --git a/app/assets/javascripts/issue_show/components/fields/title.vue b/app/assets/javascripts/issue_show/components/fields/title.vue index 7d1526a64b4..b7f2b1a6050 100644 --- a/app/assets/javascripts/issue_show/components/fields/title.vue +++ b/app/assets/javascripts/issue_show/components/fields/title.vue @@ -22,7 +22,7 @@ <input id="issuable-title" v-model="formState.title" - class="form-control" + class="form-control qa-title-input" type="text" placeholder="Title" aria-label="Title" diff --git a/app/assets/javascripts/issue_show/components/form.vue b/app/assets/javascripts/issue_show/components/form.vue index e509bb52f7d..03d8d0ec67c 100644 --- a/app/assets/javascripts/issue_show/components/form.vue +++ b/app/assets/javascripts/issue_show/components/form.vue @@ -27,6 +27,10 @@ required: false, default: () => [], }, + issuableType: { + type: String, + required: true, + }, markdownPreviewPath: { type: String, required: true, @@ -110,6 +114,7 @@ :form-state="formState" :can-destroy="canDestroy" :show-delete-button="showDeleteButton" + :issuable-type="issuableType" /> </form> </template> diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue index b5e8e0ea44b..ed26e53ac0e 100644 --- a/app/assets/javascripts/issue_show/components/title.vue +++ b/app/assets/javascripts/issue_show/components/title.vue @@ -76,10 +76,11 @@ 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" + class="btn btn-default btn-edit btn-svg js-issuable-edit + qa-edit-button" title="Edit title and description" data-placement="bottom" data-container="body" diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js index 75dfdedcf1b..d08e8ba0c4b 100644 --- a/app/assets/javascripts/issue_show/index.js +++ b/app/assets/javascripts/issue_show/index.js @@ -1,10 +1,11 @@ import Vue from 'vue'; +import sanitize from 'sanitize-html'; import issuableApp from './components/app.vue'; import '../vue_shared/vue_resource_interceptor'; -document.addEventListener('DOMContentLoaded', () => { +export default function initIssueableApp() { const initialDataEl = document.getElementById('js-issuable-app-initial-data'); - const props = JSON.parse(initialDataEl.innerHTML.replace(/"/g, '"')); + const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"')); return new Vue({ el: document.getElementById('js-issuable-app'), @@ -17,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => { }); }, }); -}); +} diff --git a/app/assets/javascripts/job.js b/app/assets/javascripts/job.js index d4f2a3ef7d3..854445bd2a4 100644 --- a/app/assets/javascripts/job.js +++ b/app/assets/javascripts/job.js @@ -6,7 +6,7 @@ import { visitUrl } from './lib/utils/url_utility'; import bp from './breakpoints'; import { numberToHumanSize } from './lib/utils/number_utils'; import { setCiStatusFavicon } from './lib/utils/common_utils'; -import { isScrolledToBottom, scrollDown } from './lib/utils/scroll_utils'; +import { isScrolledToBottom, scrollDown, scrollUp } from './lib/utils/scroll_utils'; import LogOutputBehaviours from './lib/utils/logoutput_behaviours'; export default class Job extends LogOutputBehaviours { @@ -24,7 +24,6 @@ export default class Job extends LogOutputBehaviours { this.$document = $(document); this.$window = $(window); this.logBytes = 0; - this.updateDropdown = this.updateDropdown.bind(this); this.$buildTrace = $('#build-trace'); this.$buildRefreshAnimation = $('.js-build-refresh'); @@ -35,18 +34,12 @@ export default class Job extends LogOutputBehaviours { clearTimeout(this.timeout); this.initSidebar(); - this.populateJobs(this.buildStage); - this.updateStageDropdownText(this.buildStage); this.sidebarOnResize(); this.$document .off('click', '.js-sidebar-build-toggle') .on('click', '.js-sidebar-build-toggle', this.sidebarOnClick.bind(this)); - this.$document - .off('click', '.stage-item') - .on('click', '.stage-item', this.updateDropdown); - this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.$window @@ -80,7 +73,7 @@ export default class Job extends LogOutputBehaviours { } scrollToTop() { - $(document).scrollTop(0); + scrollUp(); this.hasBeenScrolled = true; this.toggleScroll(); } @@ -194,20 +187,4 @@ export default class Job extends LogOutputBehaviours { if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); } - // eslint-disable-next-line class-methods-use-this - populateJobs(stage) { - $('.build-job').hide(); - $(`.build-job[data-stage="${stage}"]`).show(); - } - // eslint-disable-next-line class-methods-use-this - updateStageDropdownText(stage) { - $('.stage-selection').text(stage); - } - - updateDropdown(e) { - e.preventDefault(); - const stage = e.currentTarget.text; - this.updateStageDropdownText(stage); - this.populateJobs(stage); - } } diff --git a/app/assets/javascripts/jobs/components/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue index 525c5eec91a..d5866f9b9f1 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/artifacts_block.vue @@ -1,40 +1,27 @@ <script> - import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue'; + import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import timeagoMixin from '~/vue_shared/mixins/timeago'; export default { components: { - TimeagoTooltiop, + TimeagoTooltip, }, + mixins: [ + timeagoMixin, + ], props: { - // @build.artifacts_expired? - haveArtifactsExpired: { - type: Boolean, + artifact: { + type: Object, required: true, }, - // @build.has_expiring_artifacts? - willArtifactsExpire: { - type: Boolean, - required: true, - }, - expireAt: { - type: String, - required: false, - default: null, - }, - keepArtifactsPath: { - type: String, - required: false, - default: null, - }, - downloadArtifactsPath: { - type: String, - required: false, - default: null, + }, + computed: { + isExpired() { + return this.artifact.expired; }, - browseArtifactsPath: { - type: String, - required: false, - default: null, + // Only when the key is `false` we can render this block + willExpire() { + return this.artifact.expired === false; }, }, }; @@ -46,21 +33,22 @@ </div> <p - v-if="haveArtifactsExpired" + v-if="isExpired" class="js-artifacts-removed build-detail-row" > {{ s__('Job|The artifacts were removed') }} </p> + <p - v-else-if="willArtifactsExpire" + v-else-if="willExpire" class="js-artifacts-will-be-removed build-detail-row" > - {{ s__('Job|The artifacts will be removed') }} + {{ s__('Job|The artifacts will be removed in') }} </p> - <timeago-tooltiop - v-if="expireAt" - :time="expireAt" + <timeago-tooltip + v-if="artifact.expire_at" + :time="artifact.expire_at" /> <div @@ -68,8 +56,8 @@ role="group" > <a - v-if="keepArtifactsPath" - :href="keepArtifactsPath" + v-if="artifact.keep_path" + :href="artifact.keep_path" class="js-keep-artifacts btn btn-sm btn-default" data-method="post" > @@ -77,8 +65,8 @@ </a> <a - v-if="downloadArtifactsPath" - :href="downloadArtifactsPath" + v-if="artifact.download_path" + :href="artifact.download_path" class="js-download-artifacts btn btn-sm btn-default" download rel="nofollow" @@ -87,8 +75,8 @@ </a> <a - v-if="browseArtifactsPath" - :href="browseArtifactsPath" + v-if="artifact.browse_path" + :href="artifact.browse_path" class="js-browse-artifacts btn btn-sm btn-default" > {{ s__('Job|Browse') }} diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 7f485295513..4b1788a1c16 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -1,64 +1,56 @@ <script> -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -export default { - components: { - ClipboardButton, - }, - props: { - pipelineShortSha: { - type: String, - required: true, + export default { + components: { + ClipboardButton, }, - pipelineShaPath: { - type: String, - required: true, + props: { + commit: { + type: Object, + required: true, + }, + mergeRequest: { + type: Object, + required: false, + default: null, + }, + isLastBlock: { + type: Boolean, + required: true, + }, }, - mergeRequestReference: { - type: String, - required: false, - default: null, - }, - mergeRequestPath: { - type: String, - required: false, - default: null, - }, - gitCommitTitlte: { - type: String, - required: true, - }, - }, -}; + }; </script> <template> - <div class="block"> + <div + :class="{ + 'block-last': isLastBlock, + block: !isLastBlock + }"> <p> {{ __('Commit') }} <a - :href="pipelineShaPath" + :href="commit.commit_path" class="js-commit-sha commit-sha link-commit" - > - {{ pipelineShortSha }} - </a> + >{{ commit.short_id }}</a> <clipboard-button - :text="pipelineShortSha" + :text="commit.short_id" :title="__('Copy commit SHA to clipboard')" + css-class="btn btn-clipboard btn-transparent" /> <a - v-if="mergeRequestPath && mergeRequestReference" - :href="mergeRequestPath" + v-if="mergeRequest" + :href="mergeRequest.path" class="js-link-commit link-commit" - > - {{ mergeRequestReference }} - </a> + >!{{ mergeRequest.iid }}</a> </p> <p class="build-light-text append-bottom-0"> - {{ gitCommitTitlte }} + {{ commit.title }} </p> </div> </template> diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue index 4faf08387fb..ff500eddddb 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/empty_state.vue @@ -25,9 +25,9 @@ validator(value) { return ( value === null || - (Object.prototype.hasOwnProperty.call(value, 'link') && + (Object.prototype.hasOwnProperty.call(value, 'path') && Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'title')) + Object.prototype.hasOwnProperty.call(value, 'button_title')) ); }, }, @@ -63,11 +63,11 @@ class="text-center" > <a - :href="action.link" + :href="action.path" :data-method="action.method" class="js-job-empty-state-action btn btn-primary" > - {{ action.title }} + {{ action.button_title }} </a> </div> </div> diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/environments_block.vue index ca6386595c7..e6e1d418194 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/environments_block.vue @@ -12,12 +12,16 @@ type: Object, required: true, }, + iconStatus: { + type: Object, + required: true, + }, }, computed: { environment() { let environmentText; switch (this.deploymentStatus.status) { - case 'latest': + case 'last': environmentText = sprintf( __('This job is the most recent deployment to %{link}.'), { link: this.environmentLink }, @@ -32,7 +36,7 @@ ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(`#${this.lastDeployment.iid}`), }, false, ); @@ -56,11 +60,11 @@ if (this.hasLastDeployment) { environmentText = sprintf( __( - 'This job is creating a deployment to %{environmentLink} and will overwrite the last %{deploymentLink}.', + 'This job is creating a deployment to %{environmentLink} and will overwrite the %{deploymentLink}.', ), { environmentLink: this.environmentLink, - deploymentLink: this.deploymentLink, + deploymentLink: this.deploymentLink(__('latest deployment')), }, false, ); @@ -78,41 +82,57 @@ return environmentText; }, environmentLink() { - return sprintf( - '%{startLink}%{name}%{endLink}', - { - startLink: `<a href="${this.deploymentStatus.environment.path}">`, - name: _.escape(this.deploymentStatus.environment.name), - endLink: '</a>', - }, - false, - ); + if (this.hasEnvironment) { + return sprintf( + '%{startLink}%{name}%{endLink}', + { + startLink: `<a href="${ + this.deploymentStatus.environment.environment_path + }" class="js-environment-link">`, + name: _.escape(this.deploymentStatus.environment.name), + endLink: '</a>', + }, + false, + ); + } + return ''; }, - deploymentLink() { + hasLastDeployment() { + return this.hasEnvironment && this.deploymentStatus.environment.last_deployment; + }, + lastDeployment() { + return this.hasLastDeployment ? this.deploymentStatus.environment.last_deployment : {}; + }, + hasEnvironment() { + return !_.isEmpty(this.deploymentStatus.environment); + }, + lastDeploymentPath() { + return !_.isEmpty(this.lastDeployment.deployable) ? this.lastDeployment.deployable.build_path : ''; + }, + }, + methods: { + deploymentLink(name) { return sprintf( '%{startLink}%{name}%{endLink}', { - startLink: `<a href="${this.lastDeployment.path}">`, - name: _.escape(this.lastDeployment.name), + startLink: `<a href="${this.lastDeploymentPath}" class="js-job-deployment-link">`, + name, endLink: '</a>', }, false, ); }, - hasLastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, - lastDeployment() { - return this.deploymentStatus.environment.last_deployment; - }, }, }; </script> <template> <div class="prepend-top-default js-environment-container"> <div class="environment-information"> - <ci-icon :status="deploymentStatus.icon" /> - <p v-html="environment"></p> + <ci-icon :status="iconStatus"/> + <p + class="inline append-bottom-0" + v-html="environment" + ></p> </div> </div> </template> diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/erased_block.vue index d688eebfa95..3d6d9ba4387 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/erased_block.vue @@ -1,39 +1,36 @@ <script> -import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + import _ from 'underscore'; + import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -export default { - components: { - TimeagoTooltip, - }, - props: { - erasedByUser: { - type: Boolean, - required: true, + export default { + components: { + TimeagoTooltip, }, - username: { - type: String, - required: false, - default: null, + props: { + user: { + type: Object, + required: false, + default: () => ({}), + }, + erasedAt: { + type: String, + required: true, + }, }, - linkToUser: { - type: String, - required: false, - default: null, + computed: { + isErasedByUser() { + return !_.isEmpty(this.user); + }, }, - erasedAt: { - type: String, - required: true, - }, - }, -}; + }; </script> <template> <div class="prepend-top-default js-build-erased"> <div class="erased alert alert-warning"> - <template v-if="erasedByUser"> + <template v-if="isErasedByUser"> {{ s__("Job|Job has been erased by") }} - <a :href="linkToUser"> - {{ username }} + <a :href="user.web_url"> + {{ user.username }} </a> </template> <template v-else> diff --git a/app/assets/javascripts/jobs/components/header.vue b/app/assets/javascripts/jobs/components/header.vue deleted file mode 100644 index 1e7f4b2c3f7..00000000000 --- a/app/assets/javascripts/jobs/components/header.vue +++ /dev/null @@ -1,97 +0,0 @@ -<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: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - }, - data() { - return { - actions: this.getActions(), - }; - }, - computed: { - status() { - return this.job && this.job.status; - }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length; - }, - shouldRenderReason() { - return !!(this.job.status && this.job.callout_message); - }, - /** - * When job has not started the key will be `false` - * When job started the key will be a string with a date. - */ - jobStarted() { - return !this.job.started === false; - }, - headerTime() { - return this.jobStarted ? this.job.started : this.job.created_at; - }, - }, - watch: { - job() { - this.actions = this.getActions(); - }, - }, - methods: { - getActions() { - const actions = []; - - if (this.job.new_issue_path) { - 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', - type: 'link', - }); - } - return actions; - }, - }, -}; -</script> -<template> - <header> - <div class="js-build-header build-header top-area"> - <ci-header - v-if="shouldRenderContent" - :status="status" - :item-id="job.id" - :time="headerTime" - :user="job.user" - :actions="actions" - :has-sidebar-button="true" - :should-render-triggered-label="jobStarted" - item-name="Job" - /> - <loading-icon - v-if="isLoading" - size="2" - class="prepend-top-default append-bottom-default" - /> - </div> - - <callout - v-if="shouldRenderReason" - :message="job.callout_message" - /> - </header> -</template> diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue new file mode 100644 index 00000000000..047e55866ce --- /dev/null +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -0,0 +1,114 @@ +<script> + import { mapGetters, mapState } from 'vuex'; + import CiHeader from '~/vue_shared/components/header_ci_component.vue'; + import Callout from '~/vue_shared/components/callout.vue'; + import EmptyState from './empty_state.vue'; + import EnvironmentsBlock from './environments_block.vue'; + import ErasedBlock from './erased_block.vue'; + import StuckBlock from './stuck_block.vue'; + + export default { + name: 'JobPageApp', + components: { + CiHeader, + Callout, + EmptyState, + EnvironmentsBlock, + ErasedBlock, + StuckBlock, + }, + props: { + runnerHelpUrl: { + type: String, + required: false, + default: null, + }, + }, + computed: { + ...mapState(['isLoading', 'job']), + ...mapGetters([ + 'headerActions', + 'headerTime', + 'shouldRenderCalloutMessage', + 'jobHasStarted', + 'hasEnvironment', + 'isJobStuck', + 'hasTrace', + 'emptyStateIllustration', + ]), + }, + }; +</script> +<template> + <div> + <gl-loading-icon + v-if="isLoading" + :size="2" + class="prepend-top-20" + /> + + <template v-else> + <!-- Header Section --> + <header> + <div class="js-build-header build-header top-area"> + <ci-header + :status="job.status" + :item-id="job.id" + :time="headerTime" + :user="job.user" + :actions="headerActions" + :has-sidebar-button="true" + :should-render-triggered-label="jobHasStarted" + :item-name="__('Job')" + /> + </div> + + <callout + v-if="shouldRenderCalloutMessage" + :message="job.callout_message" + /> + </header> + <!-- EO Header Section --> + + <!-- Body Section --> + <stuck-block + v-if="isJobStuck" + class="js-job-stuck" + :has-no-runners-for-project="job.runners.available" + :tags="job.tags" + :runners-path="runnerHelpUrl" + /> + + <environments-block + v-if="hasEnvironment" + class="js-job-environment" + :deployment-status="job.deployment_status" + :icon-status="job.status" + /> + + <erased-block + v-if="job.erased" + class="js-job-erased" + :user="job.erased_by" + :erased-at="job.erased_at" + /> + + <!--job log --> + <!-- EO job log --> + + <!--empty state --> + <empty-state + v-if="!hasTrace" + class="js-job-empty-state" + :illustration-path="emptyStateIllustration.image" + :illustration-size-class="emptyStateIllustration.size" + :title="emptyStateIllustration.title" + :content="emptyStateIllustration.content" + :action="job.status.action" + /> + <!-- EO empty state --> + + <!-- EO Body Section --> + </template> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/job_log.vue b/app/assets/javascripts/jobs/components/job_log.vue index 3c4749d996b..b12e963b60c 100644 --- a/app/assets/javascripts/jobs/components/job_log.vue +++ b/app/assets/javascripts/jobs/components/job_log.vue @@ -6,7 +6,7 @@ type: String, required: true, }, - isReceivingBuildTrace: { + isComplete: { type: Boolean, required: true, }, @@ -22,7 +22,7 @@ </code> <div - v-if="isReceivingBuildTrace" + v-if="isComplete" class="js-log-animation build-loader-animation" > <div class="dot"></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..3e62ababea3 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,8 +1,9 @@ <script> + import { polyfillSticky } from '~/lib/utils/sticky'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; import { numberToHumanSize } from '~/lib/utils/number_utils'; - import { s__, sprintf } from '~/locale'; + import { sprintf } from '~/locale'; export default { components: { @@ -12,44 +13,48 @@ tooltip, }, props: { - canEraseJob: { - type: Boolean, - required: true, + erasePath: { + type: String, + required: false, + default: null, }, size: { type: Number, required: true, }, - rawTracePath: { + rawPath: { type: String, required: false, default: null, }, - canScrollToTop: { + isScrollTopDisabled: { + type: Boolean, + required: true, + }, + isScrollBottomDisabled: { + type: Boolean, + required: true, + }, + isScrollingDown: { type: Boolean, required: true, }, - canScrollToBottom: { + isTraceSizeVisible: { type: Boolean, required: true, }, }, computed: { jobLogSize() { - return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', { - startSpanTag: '<span class="s-truncated-info-size truncated-info-size">', - endSpanTag: '</span>', + return sprintf('Showing last %{size} of log -', { size: numberToHumanSize(this.size), }); }, }, + mounted() { + polyfillSticky(this.$el); + }, methods: { - handleEraseJobClick() { - // eslint-disable-next-line no-alert - if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) { - this.$emit('eraseJob'); - } - }, handleScrollToTop() { this.$emit('scrollJobLogTop'); }, @@ -57,48 +62,52 @@ this.$emit('scrollJobLogBottom'); }, }, + }; </script> <template> <div class="top-bar"> <!-- truncate information --> <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> - <p v-html="jobLogSize"></p> + <template v-if="isTraceSizeVisible"> + {{ jobLogSize }} - <a - v-if="rawTracePath" - :href="rawTracePath" - class="js-raw-link raw-link" - > - {{ s__("Job|Complete Raw") }} - </a> + <a + v-if="rawPath" + :href="rawPath" + class="js-raw-link raw-link" + > + {{ s__("Job|Complete Raw") }} + </a> + </template> </div> <!-- eo truncate information --> <div class="controllers float-right"> <!-- links --> <a + v-if="rawPath" v-tooltip - v-if="rawTracePath" :title="s__('Job|Show complete raw')" - :href="rawTracePath" + :href="rawPath" class="js-raw-link-controller controllers-buttons" data-container="body" > <icon name="doc-text" /> </a> - <button + <a + v-if="erasePath" v-tooltip - v-if="canEraseJob" :title="s__('Job|Erase job log')" - type="button" + :href="erasePath" + data-confirm="__('Are you sure you want to erase this build?')" class="js-erase-link controllers-buttons" data-container="body" - @click="handleEraseJobClick" + data-method="post" > <icon name="remove" /> - </button> + </a> <!-- eo links --> <!-- scroll buttons --> @@ -109,7 +118,7 @@ data-container="body" > <button - :disabled="!canScrollToTop" + :disabled="isScrollTopDisabled" type="button" class="js-scroll-top btn-scroll btn-transparent btn-blank" @click="handleScrollToTop" @@ -125,9 +134,10 @@ data-container="body" > <button - :disabled="!canScrollToBottom" + :disabled="isScrollBottomDisabled" type="button" class="js-scroll-bottom btn-scroll btn-transparent btn-blank" + :class="{ animate: isScrollingDown }" @click="handleScrollToBottom" > <icon name="scroll_down"/> diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue index b81109bdd06..271b7790d75 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/jobs_container.vue @@ -1,4 +1,5 @@ <script> + import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; @@ -16,26 +17,39 @@ type: Array, required: true, }, + jobId: { + type: Number, + required: true, + }, + }, + methods: { + isJobActive(currentJobId) { + return this.jobId === currentJobId; + }, + tooltipText(job) { + return `${_.escape(job.name)} - ${job.status.tooltip}`; + }, }, }; </script> <template> - <div class="builds-container"> + <div class="js-jobs-container builds-container"> <div + v-for="job in jobs" + :key="job.id" class="build-job" + :class="{ retried: job.retried, active: isJobActive(job.id) }" > <a v-tooltip - v-for="job in jobs" - :key="job.id" - :href="job.path" - :title="job.tooltip" - :class="{ active: job.active, retried: job.retried }" + :href="job.status.details_path" + :title="tooltipText(job)" + data-container="body" > <icon - v-if="job.active" + v-if="isJobActive(job.id)" name="arrow-right" - class="js-arrow-right" + class="js-arrow-right icon-arrow-right" /> <ci-icon :status="job.status" /> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue new file mode 100644 index 00000000000..22bcd402e72 --- /dev/null +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -0,0 +1,297 @@ +<script> + import _ from 'underscore'; + import { mapActions, mapState } from 'vuex'; + import timeagoMixin from '~/vue_shared/mixins/timeago'; + import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; + import Icon from '~/vue_shared/components/icon.vue'; + import DetailRow from './sidebar_detail_row.vue'; + import ArtifactsBlock from './artifacts_block.vue'; + import TriggerBlock from './trigger_block.vue'; + import CommitBlock from './commit_block.vue'; + import StagesDropdown from './stages_dropdown.vue'; + import JobsContainer from './jobs_container.vue'; + + export default { + name: 'JobSidebar', + components: { + ArtifactsBlock, + CommitBlock, + DetailRow, + Icon, + TriggerBlock, + StagesDropdown, + JobsContainer, + }, + mixins: [timeagoMixin], + props: { + runnerHelpUrl: { + type: String, + required: false, + default: '', + }, + terminalPath: { + type: String, + required: false, + default: null, + }, + }, + computed: { + ...mapState(['job', 'isLoading', 'stages', 'jobs']), + coverage() { + return `${this.job.coverage}%`; + }, + duration() { + return timeIntervalInWords(this.job.duration); + }, + queued() { + return timeIntervalInWords(this.job.queued); + }, + runnerId() { + return `${this.job.runner.description} (#${this.job.runner.id})`; + }, + retryButtonClass() { + let className = + 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; + className += + this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; + return className; + }, + hasTimeout() { + return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; + }, + timeout() { + if (this.job.metadata == null) { + return ''; + } + + let t = this.job.metadata.timeout_human_readable; + if (this.job.metadata.timeout_source !== '') { + t += ` (from ${this.job.metadata.timeout_source})`; + } + + return t; + }, + renderBlock() { + return ( + this.job.merge_request || + this.job.duration || + this.job.finished_data || + this.job.erased_at || + this.job.queued || + this.job.runner || + this.job.coverage || + this.job.tags.length || + this.job.cancel_path + ); + }, + hasArtifact() { + return !_.isEmpty(this.job.artifact); + }, + hasTriggers() { + return !_.isEmpty(this.job.trigger); + }, + hasStages() { + return ( + (this.job && + this.job.pipeline && + this.job.pipeline.stages && + this.job.pipeline.stages.length > 0) || + false + ); + }, + commit() { + return this.job.pipeline.commit || {}; + }, + }, + methods: { + ...mapActions(['fetchJobsForStage']), + }, + }; +</script> +<template> + <aside + class="right-sidebar right-sidebar-expanded build-sidebar" + data-offset-top="101" + data-spy="affix" + > + <div class="sidebar-container"> + <div class="blocks-container"> + <template v-if="!isLoading"> + <div class="block"> + <strong class="inline prepend-top-8"> + {{ job.name }} + </strong> + <a + v-if="job.retry_path" + :class="retryButtonClass" + :href="job.retry_path" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + <a + v-if="terminalPath" + :href="terminalPath" + class="js-terminal-link pull-right btn btn-primary + btn-inverted visible-md-block visible-lg-block" + target="_blank" + > + {{ __('Debug') }} + <icon name="external-link" /> + </a> + <button + :aria-label="__('Toggle Sidebar')" + type="button" + class="btn btn-blank gutter-toggle + float-right d-block d-md-none js-sidebar-build-toggle" + > + <i + aria-hidden="true" + data-hidden="true" + class="fa fa-angle-double-right" + ></i> + </button> + </div> + <div + v-if="job.retry_path || job.new_issue_path" + class="block retry-link" + > + <a + v-if="job.new_issue_path" + :href="job.new_issue_path" + class="js-new-issue btn btn-success btn-inverted" + > + {{ __('New issue') }} + </a> + <a + v-if="job.retry_path" + :href="job.retry_path" + class="js-retry-job btn btn-inverted-secondary" + data-method="post" + rel="nofollow" + > + {{ __('Retry') }} + </a> + </div> + <div :class="{ block : renderBlock }"> + <p + v-if="job.merge_request" + class="build-detail-row js-job-mr" + > + <span class="build-light-text"> + {{ __('Merge Request:') }} + </span> + <a :href="job.merge_request.path"> + !{{ job.merge_request.iid }} + </a> + </p> + + <detail-row + v-if="job.duration" + :value="duration" + class="js-job-duration" + title="Duration" + /> + <detail-row + v-if="job.finished_at" + :value="timeFormated(job.finished_at)" + class="js-job-finished" + title="Finished" + /> + <detail-row + v-if="job.erased_at" + :value="timeFormated(job.erased_at)" + class="js-job-erased" + title="Erased" + /> + <detail-row + v-if="job.queued" + :value="queued" + class="js-job-queued" + title="Queued" + /> + <detail-row + v-if="hasTimeout" + :help-url="runnerHelpUrl" + :value="timeout" + class="js-job-timeout" + title="Timeout" + /> + <detail-row + v-if="job.runner" + :value="runnerId" + class="js-job-runner" + title="Runner" + /> + <detail-row + v-if="job.coverage" + :value="coverage" + class="js-job-coverage" + title="Coverage" + /> + <p + v-if="job.tags.length" + class="build-detail-row js-job-tags" + > + <span class="build-light-text"> + {{ __('Tags:') }} + </span> + <span + v-for="(tag, i) in job.tags" + :key="i" + class="label label-primary"> + {{ tag }} + </span> + </p> + + <div + v-if="job.cancel_path" + class="btn-group prepend-top-5" + role="group"> + <a + :href="job.cancel_path" + class="js-cancel-job btn btn-sm btn-default" + data-method="post" + rel="nofollow" + > + {{ __('Cancel') }} + </a> + </div> + </div> + <artifacts-block + v-if="hasArtifact" + :artifact="job.artifact" + /> + <trigger-block + v-if="hasTriggers" + :trigger="job.trigger" + /> + <commit-block + :is-last-block="hasStages" + :commit="commit" + :merge-request="job.merge_request" + /> + + <stages-dropdown + :stages="stages" + :pipeline="job.pipeline" + @requestSidebarStageDropdown="fetchJobsForStage" + /> + + </template> + <gl-loading-icon + v-else + :size="2" + class="prepend-top-10" + /> + </div> + + <jobs-container + v-if="!isLoading && jobs.length" + :jobs="jobs" + :job-id="job.id" + /> + </div> + </aside> +</template> diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue deleted file mode 100644 index 36d4a3e2bc9..00000000000 --- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue +++ /dev/null @@ -1,241 +0,0 @@ -<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'; -import DetailRow from './sidebar_detail_row.vue'; - -export default { - name: 'SidebarDetailsBlock', - components: { - DetailRow, - LoadingIcon, - Icon, - }, - mixins: [timeagoMixin], - props: { - job: { - type: Object, - required: true, - }, - isLoading: { - type: Boolean, - required: true, - }, - runnerHelpUrl: { - type: String, - required: false, - default: '', - }, - terminalPath: { - type: String, - required: false, - default: null, - }, - }, - computed: { - shouldRenderContent() { - return !this.isLoading && Object.keys(this.job).length > 0; - }, - coverage() { - return `${this.job.coverage}%`; - }, - duration() { - return timeIntervalInWords(this.job.duration); - }, - queued() { - return timeIntervalInWords(this.job.queued); - }, - runnerId() { - return `${this.job.runner.description} (#${this.job.runner.id})`; - }, - retryButtonClass() { - let className = - 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block'; - className += - this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary'; - return className; - }, - hasTimeout() { - return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null; - }, - timeout() { - if (this.job.metadata == null) { - return ''; - } - - let t = this.job.metadata.timeout_human_readable; - if (this.job.metadata.timeout_source !== '') { - t += ` (from ${this.job.metadata.timeout_source})`; - } - - return t; - }, - renderBlock() { - return ( - this.job.merge_request || - this.job.duration || - this.job.finished_data || - this.job.erased_at || - this.job.queued || - this.job.runner || - this.job.coverage || - this.job.tags.length || - this.job.cancel_path - ); - }, - }, -}; -</script> -<template> - <div> - <div class="block"> - <strong class="inline prepend-top-8"> - {{ job.name }} - </strong> - <a - v-if="job.retry_path" - :class="retryButtonClass" - :href="job.retry_path" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - <a - v-if="terminalPath" - :href="terminalPath" - class="js-terminal-link pull-right btn btn-primary - btn-inverted visible-md-block visible-lg-block" - target="_blank" - > - {{ __('Debug') }} - <icon name="external-link" /> - </a> - <button - :aria-label="__('Toggle Sidebar')" - type="button" - class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle" - > - <i - aria-hidden="true" - data-hidden="true" - class="fa fa-angle-double-right" - ></i> - </button> - </div> - <template v-if="shouldRenderContent"> - <div - v-if="job.retry_path || job.new_issue_path" - class="block retry-link" - > - <a - v-if="job.new_issue_path" - :href="job.new_issue_path" - class="js-new-issue btn btn-new btn-inverted" - > - {{ __('New issue') }} - </a> - <a - v-if="job.retry_path" - :href="job.retry_path" - class="js-retry-job btn btn-inverted-secondary" - data-method="post" - rel="nofollow" - > - {{ __('Retry') }} - </a> - </div> - <div :class="{block : renderBlock }"> - <p - v-if="job.merge_request" - class="build-detail-row js-job-mr" - > - <span class="build-light-text"> - {{ __('Merge Request:') }} - </span> - <a :href="job.merge_request.path"> - !{{ job.merge_request.iid }} - </a> - </p> - - <detail-row - v-if="job.duration" - :value="duration" - class="js-job-duration" - title="Duration" - /> - <detail-row - v-if="job.finished_at" - :value="timeFormated(job.finished_at)" - class="js-job-finished" - title="Finished" - /> - <detail-row - v-if="job.erased_at" - :value="timeFormated(job.erased_at)" - class="js-job-erased" - title="Erased" - /> - <detail-row - v-if="job.queued" - :value="queued" - class="js-job-queued" - title="Queued" - /> - <detail-row - v-if="hasTimeout" - :help-url="runnerHelpUrl" - :value="timeout" - class="js-job-timeout" - title="Timeout" - /> - <detail-row - v-if="job.runner" - :value="runnerId" - class="js-job-runner" - title="Runner" - /> - <detail-row - v-if="job.coverage" - :value="coverage" - class="js-job-coverage" - title="Coverage" - /> - <p - v-if="job.tags.length" - class="build-detail-row js-job-tags" - > - <span class="build-light-text"> - {{ __('Tags:') }} - </span> - <span - v-for="(tag, i) in job.tags" - :key="i" - class="label label-primary"> - {{ tag }} - </span> - </p> - - <div - v-if="job.cancel_path" - class="btn-group prepend-top-5" - role="group"> - <a - :href="job.cancel_path" - class="js-cancel-job btn btn-sm btn-default" - data-method="post" - rel="nofollow" - > - {{ __('Cancel') }} - </a> - </div> - </div> - </template> - <loading-icon - v-if="isLoading" - class="prepend-top-10" - size="2" - /> - </div> -</template> diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index d6d64fa32f7..1c15af55a8b 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -1,8 +1,8 @@ <script> + import _ from 'underscore'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; import Icon from '~/vue_shared/components/icon.vue'; - - import { sprintf, __ } from '~/locale'; + import { __ } from '~/locale'; export default { components: { @@ -10,30 +10,14 @@ Icon, }, props: { - pipelineId: { - type: Number, - required: true, - }, - pipelinePath: { - type: String, - required: true, - }, - pipelineRef: { - type: String, - required: true, - }, - pipelineRefPath: { - type: String, + pipeline: { + type: Object, required: true, }, stages: { type: Array, required: true, }, - pipelineStatus: { - type: Object, - required: true, - }, }, data() { return { @@ -41,57 +25,73 @@ }; }, computed: { - pipelineLink() { - return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), { - pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`, - pipelineId: this.pipelineId, - pipelineLinkEnd: '</a>', - pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`, - pipelineRef: this.pipelineRef, - pipelineLinkRefEnd: '</a>', - }, false); + hasRef() { + return !_.isEmpty(this.pipeline.ref); + }, + }, + watch: { + // When the component is initially mounted it may start with an empty stages array. + // Once the prop is updated, we set the first stage as the selected one + stages(newVal) { + if (newVal.length) { + this.selectedStage = newVal[0].name; + } }, }, methods: { onStageClick(stage) { - // todo: consider moving into store - this.selectedStage = stage.name; - - // update dropdown with jobs - // jobs container is a new component. this.$emit('requestSidebarStageDropdown', stage); + this.selectedStage = stage.name; }, }, }; </script> <template> - <div class="block-last"> - <ci-icon :status="pipelineStatus" /> + <div class="block-last dropdown"> + <ci-icon + :status="pipeline.details.status" + class="vertical-align-middle" + /> + + {{ __('Pipeline') }} + <a + :href="pipeline.path" + class="js-pipeline-path link-commit" + > + #{{ pipeline.id }} + </a> + <template v-if="hasRef"> + {{ __('from') }} + <a + :href="pipeline.ref.path" + class="link-commit ref-name" + > + {{ pipeline.ref.name }} + </a> + </template> - <p v-html="pipelineLink"></p> + <button + type="button" + data-toggle="dropdown" + class="js-selected-stage dropdown-menu-toggle prepend-top-8" + > + {{ selectedStage }} + <i class="fa fa-chevron-down" ></i> + </button> - <div class="dropdown"> - <button - type="button" - data-toggle="dropdown" + <ul class="dropdown-menu"> + <li + v-for="stage in stages" + :key="stage.name" > - {{ selectedStage }} - <icon name="chevron-down" /> - </button> - <ul class="dropdown-menu"> - <li - v-for="(stage, index) in stages" - :key="index" + <button + type="button" + class="js-stage-item stage-item" + @click="onStageClick(stage)" > - <button - type="button" - class="stage-item" - @click="onStageClick(stage)" - > - {{ stage.name }} - </button> - </li> - </ul> - </div> + {{ stage.name }} + </button> + </li> + </ul> </div> </template> diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/stuck_block.vue index 18883fea950..a60643b2c65 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/stuck_block.vue @@ -24,14 +24,14 @@ export default { <div class="bs-callout bs-callout-warning"> <p v-if="hasNoRunnersForProject" - class="js-stuck-no-runners" + class="js-stuck-no-runners append-bottom-0" > {{ s__(`Job|This job is stuck, because the project doesn't have any runners online assigned to it.`) }} </p> <p v-else-if="tags.length" - class="js-stuck-with-tags" + class="js-stuck-with-tags append-bottom-0" > {{ s__(`This job is stuck, because you don't have any active runners online with any of these tags assigned to them:`) }} @@ -45,7 +45,7 @@ export default { </p> <p v-else - class="js-stuck-no-active-runner" + class="js-stuck-no-active-runner append-bottom-0" > {{ s__(`This job is stuck, because you don't have any active runners that can run this job.`) }} diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue index 8a88e5da6aa..d7b3c4fcb5b 100644 --- a/app/assets/javascripts/jobs/components/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/trigger_block.vue @@ -1,16 +1,9 @@ <script> export default { props: { - shortToken: { - type: String, - required: false, - default: null, - }, - - variables: { + trigger: { type: Object, - required: false, - default: () => ({}), + required: true, }, }, data() { @@ -20,7 +13,7 @@ }, computed: { hasVariables() { - return Object.keys(this.variables).length > 0; + return this.trigger.variables && this.trigger.variables.length > 0; }, }, methods: { @@ -38,17 +31,18 @@ </h4> <p - v-if="shortToken" + v-if="trigger.short_token" class="js-short-token" > <span class="build-light-text"> {{ __('Token') }} </span> - {{ shortToken }} + {{ trigger.short_token }} </p> <p v-if="hasVariables"> <button + v-if="!areVariablesVisible" type="button" class="btn btn-default group js-reveal-variables" @click="revealVariables" @@ -63,20 +57,20 @@ class="js-build-variables trigger-build-variables" > <template - v-for="(value, key) in variables" + v-for="variable in trigger.variables" > <dt - :key="`${key}-variable`" + :key="`${variable.key}-variable`" class="js-build-variable trigger-build-variable" > - {{ key }} + {{ variable.key }} </dt> <dd - :key="`${key}-value`" + :key="`${variable.key}-value`" class="js-build-value trigger-build-value" > - {{ value }} + {{ variable.value }} </dd> </template> </dl> diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js index a84324f14b2..3eb75e72506 100644 --- a/app/assets/javascripts/jobs/job_details_bundle.js +++ b/app/assets/javascripts/jobs/job_details_bundle.js @@ -1,34 +1,39 @@ +import _ from 'underscore'; +import { mapState, mapActions } from 'vuex'; import Vue from 'vue'; -import JobMediator from './job_details_mediator'; -import jobHeader from './components/header.vue'; -import detailsBlock from './components/sidebar_details_block.vue'; +import Job from '../job'; +import JobApp from './components/job_app.vue'; +import Sidebar from './components/sidebar.vue'; +import createStore from './store'; export default () => { const { dataset } = document.getElementById('js-job-details-vue'); - const mediator = new JobMediator({ endpoint: dataset.endpoint }); - mediator.fetchJob(); + // eslint-disable-next-line no-new + new Job(); + + const store = createStore(); + store.dispatch('setJobEndpoint', dataset.endpoint); + + store.dispatch('fetchJob'); // Header // eslint-disable-next-line no-new new Vue({ el: '#js-build-header-vue', components: { - jobHeader, - }, - data() { - return { - mediator, - }; + JobApp, }, - mounted() { - this.mediator.initBuildClass(); + store, + computed: { + ...mapState(['job', 'isLoading']), }, render(createElement) { - return createElement('job-header', { + return createElement('job-app', { props: { - isLoading: this.mediator.state.isLoading, - job: this.mediator.store.state.job, + isLoading: this.isLoading, + job: this.job, + runnerHelpUrl: dataset.runnerHelpUrl, }, }); }, @@ -41,18 +46,25 @@ export default () => { new Vue({ el: detailsBlockElement, components: { - detailsBlock, + Sidebar, + }, + computed: { + ...mapState(['job']), + }, + watch: { + job(newVal, oldVal) { + if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { + this.fetchStages(); + } + }, }, - data() { - return { - mediator, - }; + methods: { + ...mapActions(['fetchStages']), }, + store, render(createElement) { - return createElement('details-block', { + return createElement('sidebar', { props: { - isLoading: this.mediator.state.isLoading, - job: this.mediator.store.state.job, runnerHelpUrl: dataset.runnerHelpUrl, terminalPath: detailsBlockDataset.terminalPath, }, diff --git a/app/assets/javascripts/jobs/job_details_mediator.js b/app/assets/javascripts/jobs/job_details_mediator.js deleted file mode 100644 index 89019da9d1e..00000000000 --- a/app/assets/javascripts/jobs/job_details_mediator.js +++ /dev/null @@ -1,67 +0,0 @@ -import Visibility from 'visibilityjs'; -import Flash from '../flash'; -import Poll from '../lib/utils/poll'; -import JobStore from './stores/job_store'; -import JobService from './services/job_service'; -import Job from '../job'; -import handleRevealVariables from '../build_variables'; - -export default class JobMediator { - constructor(options = {}) { - this.options = options; - - this.store = new JobStore(); - this.service = new JobService(options.endpoint); - - this.state = { - isLoading: false, - }; - } - - initBuildClass() { - this.build = new Job(); - handleRevealVariables(); - } - - fetchJob() { - this.poll = new Poll({ - resource: this.service, - method: 'getJob', - successCallback: response => this.successCallback(response), - errorCallback: () => this.errorCallback(), - }); - - if (!Visibility.hidden()) { - this.state.isLoading = true; - this.poll.makeRequest(); - } else { - this.getJob(); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - } - - getJob() { - return this.service - .getJob() - .then(response => this.successCallback(response)) - .catch(() => this.errorCallback()); - } - - successCallback(response) { - this.state.isLoading = false; - return this.store.storeJob(response.data); - } - - errorCallback() { - this.state.isLoading = false; - - return new Flash('An error occurred while fetching the job.'); - } -} diff --git a/app/assets/javascripts/jobs/services/job_service.js b/app/assets/javascripts/jobs/services/job_service.js deleted file mode 100644 index b746489c45c..00000000000 --- a/app/assets/javascripts/jobs/services/job_service.js +++ /dev/null @@ -1,11 +0,0 @@ -import axios from '../../lib/utils/axios_utils'; - -export default class JobService { - constructor(endpoint) { - this.job = endpoint; - } - - getJob() { - return axios.get(this.job); - } -} diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js new file mode 100644 index 00000000000..298367c9342 --- /dev/null +++ b/app/assets/javascripts/jobs/store/actions.js @@ -0,0 +1,187 @@ +import Visibility from 'visibilityjs'; +import * as types from './mutation_types'; +import axios from '../../lib/utils/axios_utils'; +import Poll from '../../lib/utils/poll'; +import { setCiStatusFavicon } from '../../lib/utils/common_utils'; +import flash from '../../flash'; +import { __ } from '../../locale'; + +export const setJobEndpoint = ({ commit }, endpoint) => commit(types.SET_JOB_ENDPOINT, endpoint); +export const setTraceEndpoint = ({ commit }, endpoint) => + commit(types.SET_TRACE_ENDPOINT, endpoint); +export const setStagesEndpoint = ({ commit }, endpoint) => + commit(types.SET_STAGES_ENDPOINT, endpoint); +export const setJobsEndpoint = ({ commit }, endpoint) => commit(types.SET_JOBS_ENDPOINT, endpoint); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +export const requestJob = ({ commit }) => commit(types.REQUEST_JOB); + +export const fetchJob = ({ state, dispatch }) => { + dispatch('requestJob'); + + eTagPoll = new Poll({ + resource: { + getJob(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.jobEndpoint, + method: 'getJob', + successCallback: ({ data }) => dispatch('receiveJobSuccess', data), + errorCallback: () => dispatch('receiveJobError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } else { + axios + .get(state.jobEndpoint) + .then(({ data }) => dispatch('receiveJobSuccess', data)) + .catch(() => dispatch('receiveJobError')); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveJobSuccess = ({ commit }, data) => { + commit(types.RECEIVE_JOB_SUCCESS, data); +}; +export const receiveJobError = ({ commit }) => { + commit(types.RECEIVE_JOB_ERROR); + flash(__('An error occurred while fetching the job.')); +}; + +/** + * Job's Trace + */ +export const scrollTop = ({ commit }) => { + commit(types.SCROLL_TO_TOP); + window.scrollTo({ top: 0 }); +}; + +export const scrollBottom = ({ commit }) => { + commit(types.SCROLL_TO_BOTTOM); + window.scrollTo({ top: document.height }); +}; + +export const requestTrace = ({ commit }) => commit(types.REQUEST_TRACE); + +let traceTimeout; +export const fetchTrace = ({ dispatch, state }) => { + dispatch('requestTrace'); + + axios + .get(`${state.traceEndpoint}/trace.json`, { + params: { state: state.traceState }, + }) + .then(({ data }) => { + if (!state.fetchingStatusFavicon) { + dispatch('fetchFavicon'); + } + dispatch('receiveTraceSuccess', data); + + if (!data.complete) { + traceTimeout = setTimeout(() => { + dispatch('fetchTrace'); + }, 4000); + } else { + dispatch('stopPollingTrace'); + } + }) + .catch(() => dispatch('receiveTraceError')); +}; +export const stopPollingTrace = ({ commit }) => { + commit(types.STOP_POLLING_TRACE); + clearTimeout(traceTimeout); +}; +export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log); +export const receiveTraceError = ({ commit }) => { + commit(types.RECEIVE_TRACE_ERROR); + clearTimeout(traceTimeout); + flash(__('An error occurred while fetching the job log.')); +}; + +export const fetchFavicon = ({ state, dispatch }) => { + dispatch('requestStatusFavicon'); + setCiStatusFavicon(`${state.pagePath}/status.json`) + .then(() => dispatch('receiveStatusFaviconSuccess')) + .catch(() => dispatch('requestStatusFaviconError')); +}; +export const requestStatusFavicon = ({ commit }) => commit(types.REQUEST_STATUS_FAVICON); +export const receiveStatusFaviconSuccess = ({ commit }) => + commit(types.RECEIVE_STATUS_FAVICON_SUCCESS); +export const requestStatusFaviconError = ({ commit }) => commit(types.RECEIVE_STATUS_FAVICON_ERROR); + +/** + * Stages dropdown on sidebar + */ +export const requestStages = ({ commit }) => commit(types.REQUEST_STAGES); +export const fetchStages = ({ state, dispatch }) => { + dispatch('requestStages'); + + axios + .get(state.job.pipeline.path) + .then(({ data }) => { + dispatch('receiveStagesSuccess', data.details.stages); + dispatch('fetchJobsForStage', data.details.stages[0]); + }) + .catch(() => dispatch('receiveStagesError')); +}; +export const receiveStagesSuccess = ({ commit }, data) => + commit(types.RECEIVE_STAGES_SUCCESS, data); +export const receiveStagesError = ({ commit }) => { + commit(types.RECEIVE_STAGES_ERROR); + flash(__('An error occurred while fetching stages.')); +}; + +/** + * Jobs list on sidebar - depend on stages dropdown + */ +export const requestJobsForStage = ({ commit }) => commit(types.REQUEST_JOBS_FOR_STAGE); + +// On stage click, set selected stage + fetch job +export const fetchJobsForStage = ({ dispatch }, stage) => { + dispatch('requestJobsForStage'); + + axios + .get(stage.dropdown_path, { + params: { + retried: 1, + }, + }) + .then(({ data }) => { + const retriedJobs = data.retried.map(job => Object.assign({}, job, { retried: true })); + const jobs = data.latest_statuses.concat(retriedJobs); + + dispatch('receiveJobsForStageSuccess', jobs); + }) + .catch(() => dispatch('receiveJobsForStageError')); +}; +export const receiveJobsForStageSuccess = ({ commit }, data) => + commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data); +export const receiveJobsForStageError = ({ commit }) => { + commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR); + flash(__('An error occurred while fetching the jobs.')); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/getters.js b/app/assets/javascripts/jobs/store/getters.js new file mode 100644 index 00000000000..afe5f88b292 --- /dev/null +++ b/app/assets/javascripts/jobs/store/getters.js @@ -0,0 +1,53 @@ +import _ from 'underscore'; +import { __ } from '~/locale'; + +export const headerActions = state => { + if (state.job.new_issue_path) { + return [ + { + label: __('New issue'), + path: state.job.new_issue_path, + cssClass: + 'js-new-issue btn btn-success btn-inverted d-none d-md-block d-lg-block d-xl-block', + type: 'link', + }, + ]; + } + return []; +}; + +export const headerTime = state => (state.job.started ? state.job.started : state.job.created_at); + +export const shouldRenderCalloutMessage = state => + !_.isEmpty(state.job.status) && !_.isEmpty(state.job.callout_message); + +/** + * When job has not started the key will be `false` + * When job started the key will be a string with a date. + */ +export const jobHasStarted = state => !(state.job.started === false); + +export const hasEnvironment = state => !_.isEmpty(state.job.deployment_status); + +/** + * Checks if it the job has trace. + * Used to check if it should render the job log or the empty state + * @returns {Boolean} + */ +export const hasTrace = state => state.job.has_trace || state.job.status.group === 'running'; + +export const emptyStateIllustration = state => + (state.job && state.job.status && state.job.status.illustration) || {}; + +/** + * When the job is pending and there are no available runners + * we need to render the stuck block; + * + * @returns {Boolean} + */ +export const isJobStuck = state => + state.job.status.group === 'pending' && + (!_.isEmpty(state.job.runners) && state.job.runners.available === false); + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/jobs/store/index.js b/app/assets/javascripts/jobs/store/index.js new file mode 100644 index 00000000000..96e38f9a2fa --- /dev/null +++ b/app/assets/javascripts/jobs/store/index.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import state from './state'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default () => new Vuex.Store({ + actions, + mutations, + getters, + state: state(), +}); diff --git a/app/assets/javascripts/jobs/store/mutation_types.js b/app/assets/javascripts/jobs/store/mutation_types.js new file mode 100644 index 00000000000..e66e1d4f116 --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutation_types.js @@ -0,0 +1,29 @@ +export const SET_JOB_ENDPOINT = 'SET_JOB_ENDPOINT'; +export const SET_TRACE_ENDPOINT = 'SET_TRACE_ENDPOINT'; +export const SET_STAGES_ENDPOINT = 'SET_STAGES_ENDPOINT'; +export const SET_JOBS_ENDPOINT = 'SET_JOBS_ENDPOINT'; + +export const SCROLL_TO_TOP = 'SCROLL_TO_TOP'; +export const SCROLL_TO_BOTTOM = 'SCROLL_TO_BOTTOM'; + +export const REQUEST_JOB = 'REQUEST_JOB'; +export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS'; +export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR'; + +export const REQUEST_TRACE = 'REQUEST_TRACE'; +export const STOP_POLLING_TRACE = 'STOP_POLLING_TRACE'; +export const RECEIVE_TRACE_SUCCESS = 'RECEIVE_TRACE_SUCCESS'; +export const RECEIVE_TRACE_ERROR = 'RECEIVE_TRACE_ERROR'; + +export const REQUEST_STATUS_FAVICON = 'REQUEST_STATUS_FAVICON'; +export const RECEIVE_STATUS_FAVICON_SUCCESS = 'RECEIVE_STATUS_FAVICON_SUCCESS'; +export const RECEIVE_STATUS_FAVICON_ERROR = 'RECEIVE_STATUS_FAVICON_ERROR'; + +export const REQUEST_STAGES = 'REQUEST_STAGES'; +export const RECEIVE_STAGES_SUCCESS = 'RECEIVE_STAGES_SUCCESS'; +export const RECEIVE_STAGES_ERROR = 'RECEIVE_STAGES_ERROR'; + +export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; +export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE'; +export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS'; +export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR'; diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js new file mode 100644 index 00000000000..c3f2359fa4d --- /dev/null +++ b/app/assets/javascripts/jobs/store/mutations.js @@ -0,0 +1,95 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_JOB_ENDPOINT](state, endpoint) { + state.jobEndpoint = endpoint; + }, + [types.REQUEST_STATUS_FAVICON](state) { + state.fetchingStatusFavicon = true; + }, + [types.RECEIVE_STATUS_FAVICON_SUCCESS](state) { + state.fetchingStatusFavicon = false; + }, + [types.RECEIVE_STATUS_FAVICON_ERROR](state) { + state.fetchingStatusFavicon = false; + }, + + [types.RECEIVE_TRACE_SUCCESS](state, log) { + if (log.state) { + state.traceState = log.state; + } + + if (log.append) { + state.trace += log.html; + state.traceSize += log.size; + } else { + state.trace = log.html; + state.traceSize = log.size; + } + + if (state.traceSize < log.total) { + state.isTraceSizeVisible = true; + } else { + state.isTraceSizeVisible = false; + } + + state.isTraceComplete = log.complete; + state.hasTraceError = false; + }, + [types.STOP_POLLING_TRACE](state) { + state.isTraceComplete = true; + }, + // todo_fl: check this. + [types.RECEIVE_TRACE_ERROR](state) { + state.isLoadingTrace = false; + state.isTraceComplete = true; + state.hasTraceError = true; + }, + + [types.REQUEST_JOB](state) { + state.isLoading = true; + }, + [types.RECEIVE_JOB_SUCCESS](state, job) { + state.isLoading = false; + state.hasError = false; + state.job = job; + }, + [types.RECEIVE_JOB_ERROR](state) { + state.isLoading = false; + state.hasError = true; + state.job = {}; + }, + + [types.SCROLL_TO_TOP](state) { + state.isTraceScrolledToBottom = false; + state.hasBeenScrolled = true; + }, + [types.SCROLL_TO_BOTTOM](state) { + state.isTraceScrolledToBottom = true; + state.hasBeenScrolled = true; + }, + + [types.REQUEST_STAGES](state) { + state.isLoadingStages = true; + }, + [types.RECEIVE_STAGES_SUCCESS](state, stages) { + state.isLoadingStages = false; + state.stages = stages; + }, + [types.RECEIVE_STAGES_ERROR](state) { + state.isLoadingStages = false; + state.stages = []; + }, + + [types.REQUEST_JOBS_FOR_STAGE](state) { + state.isLoadingJobs = true; + }, + [types.RECEIVE_JOBS_FOR_STAGE_SUCCESS](state, jobs) { + state.isLoadingJobs = false; + state.jobs = jobs; + }, + [types.RECEIVE_JOBS_FOR_STAGE_ERROR](state) { + state.isLoadingJobs = false; + state.jobs = []; + }, +}; diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js new file mode 100644 index 00000000000..509cb69a5d3 --- /dev/null +++ b/app/assets/javascripts/jobs/store/state.js @@ -0,0 +1,40 @@ +export default () => ({ + jobEndpoint: null, + traceEndpoint: null, + + // dropdown options + stagesEndpoint: null, + // list of jobs on sidebard + stageJobsEndpoint: null, + + // job log + isLoading: false, + hasError: false, + job: {}, + + // trace + isLoadingTrace: false, + hasTraceError: false, + + trace: '', + + isTraceScrolledToBottom: false, + hasBeenScrolled: false, + + isTraceComplete: false, + traceSize: 0, // todo_fl: needs to be converted into human readable format in components + isTraceSizeVisible: false, + + fetchingStatusFavicon: false, + // used as a query parameter + traceState: null, + // used to check if we need to redirect the user - todo_fl: check if actually needed + traceStatus: null, + + // sidebar dropdown + isLoadingStages: false, + isLoadingJobs: false, + selectedStage: null, + stages: [], + jobs: [], +}); diff --git a/app/assets/javascripts/jobs/stores/job_store.js b/app/assets/javascripts/jobs/stores/job_store.js deleted file mode 100644 index 766194b8387..00000000000 --- a/app/assets/javascripts/jobs/stores/job_store.js +++ /dev/null @@ -1,11 +0,0 @@ -export default class JobStore { - constructor() { - this.state = { - job: {}, - }; - } - - storeJob(job = {}) { - this.state.job = job; - } -} 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/lazy_loader.js b/app/assets/javascripts/lazy_loader.js index bd2212edec7..61b4862b4e3 100644 --- a/app/assets/javascripts/lazy_loader.js +++ b/app/assets/javascripts/lazy_loader.js @@ -2,54 +2,114 @@ import _ from 'underscore'; export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='; -const SCROLL_THRESHOLD = 300; +const SCROLL_THRESHOLD = 500; export default class LazyLoader { constructor(options = {}) { + this.intersectionObserver = null; this.lazyImages = []; this.observerNode = options.observerNode || '#content-body'; - const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300); - const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300); - - window.addEventListener('scroll', throttledScrollCheck); - window.addEventListener('resize', debouncedElementsInView); - const scrollContainer = options.scrollContainer || window; - scrollContainer.addEventListener('load', () => this.loadCheck()); + scrollContainer.addEventListener('load', () => this.register()); + } + + static supportsIntersectionObserver() { + return 'IntersectionObserver' in window; } + searchLazyImages() { - const that = this; requestIdleCallback( () => { - that.lazyImages = [].slice.call(document.querySelectorAll('.lazy')); + const lazyImages = [].slice.call(document.querySelectorAll('.lazy')); - if (that.lazyImages.length) { - that.checkElementsInView(); + if (LazyLoader.supportsIntersectionObserver()) { + if (this.intersectionObserver) { + lazyImages.forEach(img => this.intersectionObserver.observe(img)); + } + } else if (lazyImages.length) { + this.lazyImages = lazyImages; + this.checkElementsInView(); } }, { timeout: 500 }, ); } + startContentObserver() { const contentNode = document.querySelector(this.observerNode) || document.querySelector('body'); - if (contentNode) { - const observer = new MutationObserver(() => this.searchLazyImages()); + this.mutationObserver = new MutationObserver(() => this.searchLazyImages()); - observer.observe(contentNode, { + this.mutationObserver.observe(contentNode, { childList: true, subtree: true, }); } } - loadCheck() { - this.searchLazyImages(); + + stopContentObserver() { + if (this.mutationObserver) { + this.mutationObserver.takeRecords(); + this.mutationObserver.disconnect(); + this.mutationObserver = null; + } + } + + unregister() { + this.stopContentObserver(); + if (this.intersectionObserver) { + this.intersectionObserver.takeRecords(); + this.intersectionObserver.disconnect(); + this.intersectionObserver = null; + } + if (this.throttledScrollCheck) { + window.removeEventListener('scroll', this.throttledScrollCheck); + } + if (this.debouncedElementsInView) { + window.removeEventListener('resize', this.debouncedElementsInView); + } + } + + register() { + if (LazyLoader.supportsIntersectionObserver()) { + this.startIntersectionObserver(); + } else { + this.startLegacyObserver(); + } this.startContentObserver(); + this.searchLazyImages(); } + + startIntersectionObserver = () => { + this.throttledElementsInView = _.throttle(() => this.checkElementsInView(), 300); + this.intersectionObserver = new IntersectionObserver(this.onIntersection, { + rootMargin: `${SCROLL_THRESHOLD}px 0px`, + thresholds: 0.1, + }); + }; + + onIntersection = entries => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.intersectionObserver.unobserve(entry.target); + this.lazyImages.push(entry.target); + } + }); + this.throttledElementsInView(); + }; + + startLegacyObserver() { + this.throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300); + this.debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300); + window.addEventListener('scroll', this.throttledScrollCheck); + window.addEventListener('resize', this.debouncedElementsInView); + } + scrollCheck() { requestAnimationFrame(() => this.checkElementsInView()); } + checkElementsInView() { const scrollTop = window.pageYOffset; const visHeight = scrollTop + window.innerHeight + SCROLL_THRESHOLD; @@ -61,18 +121,29 @@ export default class LazyLoader { const imgTop = scrollTop + imgBoundRect.top; const imgBound = imgTop + imgBoundRect.height; - if (scrollTop < imgBound && visHeight > imgTop) { + if (scrollTop <= imgBound && visHeight >= imgTop) { requestAnimationFrame(() => { LazyLoader.loadImage(selectedImage); }); return false; } + /* + If we are scrolling fast, the img we watched intersecting could have left the view port. + So we are going watch for new intersections. + */ + if (LazyLoader.supportsIntersectionObserver()) { + if (this.intersectionObserver) { + this.intersectionObserver.observe(selectedImage); + } + return false; + } return true; } return false; }); } + static loadImage(img) { if (img.getAttribute('data-src')) { let imgUrl = img.getAttribute('data-src'); diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js index 2f3dd6f6cbc..e14fff7a610 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,8 @@ 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'); + const topPadding = 8; let adjustment = 0; if (fixedNav) adjustment -= fixedNav.offsetHeight; @@ -102,6 +105,14 @@ export const handleLocationHash = () => { adjustment -= fixedDiffStats.offsetHeight; } + if (performanceBar) { + adjustment -= performanceBar.offsetHeight; + } + + if (isInMRPage()) { + adjustment -= topPadding; + } + window.scrollBy(0, adjustment); }; @@ -131,17 +142,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 +226,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') { @@ -349,8 +386,11 @@ export const objectToQueryString = (params = {}) => .map(param => `${param}=${params[param]}`) .join('&'); -export const buildUrlWithCurrentLocation = param => - (param ? `${window.location.pathname}${param}` : window.location.pathname); +export const buildUrlWithCurrentLocation = param => { + if (param) return `${window.location.pathname}${param}`; + + return window.location.pathname; +}; /** * Based on the current location and the string parameters provided @@ -426,7 +466,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 +477,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 +512,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 +551,10 @@ export const setCiStatusFavicon = pageUrl => } return resetFavicon(); }) - .catch(resetFavicon); + .catch(error => { + resetFavicon(); + throw error; + }); export const spriteIcon = (icon, className = '') => { const classAttribute = className.length > 0 ? `class="${className}"` : ''; @@ -561,6 +624,17 @@ export const roundOffFloat = (number, precision = 0) => { return Math.round(number * multiplier) / multiplier; }; +/** + * Represents navigation type constants of the Performance Navigation API. + * Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation. + */ +export const NavigationType = { + TYPE_NAVIGATE: 0, + TYPE_RELOAD: 1, + TYPE_BACK_FORWARD: 2, + TYPE_RESERVED: 255, +}; + window.gl = window.gl || {}; window.gl.utils = { ...(window.gl.utils || {}), diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js index 1f66fa811ea..833dbefd3dc 100644 --- a/app/assets/javascripts/lib/utils/datetime_utility.js +++ b/app/assets/javascripts/lib/utils/datetime_utility.js @@ -370,3 +370,24 @@ window.gl.utils = { getTimeago, localTimeAgo, }; + +/** + * Formats milliseconds as timestamp (e.g. 01:02:03). + * This takes durations longer than a day into account (e.g. two days would be 48:00:00). + * + * @param milliseconds + * @returns {string} + */ +export const formatTime = milliseconds => { + const remainingSeconds = Math.floor(milliseconds / 1000) % 60; + const remainingMinutes = Math.floor(milliseconds / 1000 / 60) % 60; + const remainingHours = Math.floor(milliseconds / 1000 / 60 / 60); + let formattedTime = ''; + if (remainingHours < 10) formattedTime += '0'; + formattedTime += `${remainingHours}:`; + if (remainingMinutes < 10) formattedTime += '0'; + formattedTime += `${remainingMinutes}:`; + if (remainingSeconds < 10) formattedTime += '0'; + formattedTime += remainingSeconds; + return formattedTime; +}; diff --git a/app/assets/javascripts/lib/utils/logoutput_behaviours.js b/app/assets/javascripts/lib/utils/logoutput_behaviours.js index 1bf99d935ef..41b57025cc9 100644 --- a/app/assets/javascripts/lib/utils/logoutput_behaviours.js +++ b/app/assets/javascripts/lib/utils/logoutput_behaviours.js @@ -1,5 +1,11 @@ import $ from 'jquery'; -import { canScroll, isScrolledToBottom, toggleDisableButton } from './scroll_utils'; +import { + canScroll, + isScrolledToBottom, + isScrolledToTop, + isScrolledToMiddle, + toggleDisableButton, +} from './scroll_utils'; export default class LogOutputBehaviours { constructor() { @@ -12,18 +18,13 @@ export default class LogOutputBehaviours { } toggleScroll() { - const $document = $(document); - const currentPosition = $document.scrollTop(); - const scrollHeight = $document.height(); - - const windowHeight = $(window).height(); if (canScroll()) { - if (currentPosition > 0 && scrollHeight - currentPosition !== windowHeight) { + if (isScrolledToMiddle()) { // User is in the middle of the log toggleDisableButton(this.$scrollTopBtn, false); toggleDisableButton(this.$scrollBottomBtn, false); - } else if (currentPosition === 0) { + } else if (isScrolledToTop()) { // User is at Top of Log toggleDisableButton(this.$scrollTopBtn, true); 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/scroll_utils.js b/app/assets/javascripts/lib/utils/scroll_utils.js index 9313b570863..b4da1e16f08 100644 --- a/app/assets/javascripts/lib/utils/scroll_utils.js +++ b/app/assets/javascripts/lib/utils/scroll_utils.js @@ -4,6 +4,7 @@ export const canScroll = () => $(document).height() > $(window).height(); /** * Checks if the entire page is scrolled down all the way to the bottom + * @returns {Boolean} */ export const isScrolledToBottom = () => { const $document = $(document); @@ -16,11 +17,34 @@ export const isScrolledToBottom = () => { return scrollHeight - currentPosition === windowHeight; }; +/** + * Checks if page is scrolled to the top + * @returns {Boolean} + */ +export const isScrolledToTop = () => $(document).scrollTop() === 0; + export const scrollDown = () => { const $document = $(document); $document.scrollTop($document.height()); }; +export const scrollUp = () => { + $(document).scrollTop(0); +}; + +/** + * Checks if scroll position is in the middle of the page + * @returns {Boolean} + */ +export const isScrolledToMiddle = () => { + const $document = $(document); + const currentPosition = $document.scrollTop(); + const scrollHeight = $document.height(); + const windowHeight = $(window).height(); + + return currentPosition > 0 && scrollHeight - currentPosition !== windowHeight; +}; + export const toggleDisableButton = ($button, disable) => { if (disable && $button.prop('disabled')) return; $button.prop('disabled', disable); diff --git a/app/assets/javascripts/lib/utils/text_markdown.js b/app/assets/javascripts/lib/utils/text_markdown.js index ce0bc4d40e9..f7429601afa 100644 --- a/app/assets/javascripts/lib/utils/text_markdown.js +++ b/app/assets/javascripts/lib/utils/text_markdown.js @@ -31,11 +31,17 @@ function blockTagText(text, textArea, blockTag, selected) { } } -function moveCursor(textArea, tag, wrapped, removedLastNewLine) { +function moveCursor({ textArea, tag, wrapped, removedLastNewLine, select }) { var pos; if (!textArea.setSelectionRange) { return; } + if (select && select.length > 0) { + // calculate the part of the text to be selected + const startPosition = textArea.selectionStart - (tag.length - tag.indexOf(select)); + const endPosition = startPosition + select.length; + return textArea.setSelectionRange(startPosition, endPosition); + } if (textArea.selectionStart === textArea.selectionEnd) { if (wrapped) { pos = textArea.selectionStart - tag.length; @@ -51,7 +57,7 @@ function moveCursor(textArea, tag, wrapped, removedLastNewLine) { } } -export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap) { +export function insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }) { var textToInsert, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine; removedLastNewLine = false; removedFirstNewLine = false; @@ -82,11 +88,16 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : ''; + const textPlaceholder = '{text}'; + if (selectedSplit.length > 1 && (!wrap || (blockTag != null && blockTag !== ''))) { if (blockTag != null && blockTag !== '') { textToInsert = blockTagText(text, textArea, blockTag, selected); } else { textToInsert = selectedSplit.map(function(val) { + if (tag.indexOf(textPlaceholder) > -1) { + return tag.replace(textPlaceholder, val); + } if (val.indexOf(tag) === 0) { return "" + (val.replace(tag, '')); } else { @@ -94,6 +105,8 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap } }).join('\n'); } + } else if (tag.indexOf(textPlaceholder) > -1) { + textToInsert = tag.replace(textPlaceholder, selected); } else { textToInsert = "" + startChar + tag + selected + (wrap ? tag : ' '); } @@ -107,17 +120,17 @@ export function insertMarkdownText(textArea, text, tag, blockTag, selected, wrap } insertText(textArea, textToInsert); - return moveCursor(textArea, tag, wrap, removedLastNewLine); + return moveCursor({ textArea, tag: tag.replace(textPlaceholder, selected), wrap, removedLastNewLine, select }); } -function updateText(textArea, tag, blockTag, wrap) { +function updateText({ textArea, tag, blockTag, wrap, select }) { var $textArea, selected, text; $textArea = $(textArea); textArea = $textArea.get(0); text = $textArea.val(); selected = selectedText(text, textArea); $textArea.focus(); - return insertMarkdownText(textArea, text, tag, blockTag, selected, wrap); + return insertMarkdownText({ textArea, text, tag, blockTag, selected, wrap, select }); } function replaceRange(s, start, end, substitute) { @@ -127,7 +140,12 @@ function replaceRange(s, start, end, substitute) { export function addMarkdownListeners(form) { return $('.js-md', form).off('click').on('click', function() { const $this = $(this); - return updateText($this.closest('.md-area').find('textarea'), $this.data('mdTag'), $this.data('mdBlock'), !$this.data('mdPrepend')); + return updateText({ + textArea: $this.closest('.md-area').find('textarea'), + tag: $this.data('mdTag'), + blockTag: $this.data('mdBlock'), + wrap: !$this.data('mdPrepend'), + select: $this.data('mdSelect') }); }); } 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..78f56ab57ff 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(); } @@ -193,9 +194,7 @@ export default class MergeRequestTabs { if (bp.getBreakpointSize() !== 'lg') { this.shrinkView(); } - if (this.diffViewType() === 'parallel') { - this.expandViewContainer(); - } + this.expandViewContainer(); this.destroyPipelinesView(); this.commitsTab.classList.remove('active'); } else if (action === 'pipelines') { @@ -354,7 +353,7 @@ export default class MergeRequestTabs { localTimeAgo($('.js-timeago', 'div#diffs')); syntaxHighlight($('#diffs .js-syntax-highlight')); - if (this.diffViewType() === 'parallel' && this.isDiffAction(this.currentAction)) { + if (this.isDiffAction(this.currentAction)) { this.expandViewContainer(); } this.diffsLoaded = true; @@ -407,19 +406,23 @@ export default class MergeRequestTabs { } diffViewType() { - return $('.inline-parallel-buttons a.active').data('viewType'); + return $('.inline-parallel-buttons button.active').data('viewType'); } isDiffAction(action) { return action === 'diffs' || action === 'new/diffs'; } - expandViewContainer() { + expandViewContainer(removeLimited = true) { const $wrapper = $('.content-wrapper .container-fluid').not('.breadcrumbs'); if (this.fixedLayoutPref === null) { this.fixedLayoutPref = $wrapper.hasClass('container-limited'); } - $wrapper.removeClass('container-limited'); + if (this.diffViewType() === 'parallel' || removeLimited) { + $wrapper.removeClass('container-limited'); + } else { + $wrapper.addClass('container-limited'); + } } resetViewContainer() { diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index ae96ac3b80c..67338aa96c3 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -97,33 +97,45 @@ export default { store: new MonitoringStore(), state: 'gettingStarted', showEmptyState: true, - updateAspectRatio: false, - updatedAspectRatios: 0, hoverData: {}, - resizeThrottled: {}, + elWidth: 0, }; }, + computed: { + forceRedraw() { + return this.elWidth; + }, + }, created() { this.service = new MonitoringService({ metricsEndpoint: this.metricsEndpoint, deploymentEndpoint: this.deploymentEndpoint, environmentsEndpoint: this.environmentsEndpoint, }); - eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); + this.mutationObserverConfig = { + attributes: true, + childList: false, + subtree: false, + }; eventHub.$on('hoverChanged', this.hoverChanged); }, beforeDestroy() { - eventHub.$off('toggleAspectRatio', this.toggleAspectRatio); eventHub.$off('hoverChanged', this.hoverChanged); window.removeEventListener('resize', this.resizeThrottled, false); + this.sidebarMutationObserver.disconnect(); }, mounted() { - this.resizeThrottled = _.throttle(this.resize, 600); + this.resizeThrottled = _.debounce(this.resize, 100); if (!this.hasMetrics) { this.state = 'gettingStarted'; } else { this.getGraphsData(); window.addEventListener('resize', this.resizeThrottled, false); + + const sidebarEl = document.querySelector('.nav-sidebar'); + // The sidebar listener + this.sidebarMutationObserver = new MutationObserver(this.resizeThrottled); + this.sidebarMutationObserver.observe(sidebarEl, this.mutationObserverConfig); } }, methods: { @@ -153,14 +165,7 @@ export default { }); }, resize() { - this.updateAspectRatio = true; - }, - toggleAspectRatio() { - this.updatedAspectRatios += 1; - if (this.store.getMetricsCount() === this.updatedAspectRatios) { - this.updateAspectRatio = !this.updateAspectRatio; - this.updatedAspectRatios = 0; - } + this.elWidth = this.$el.clientWidth; }, hoverChanged(data) { this.hoverData = data; @@ -172,6 +177,7 @@ export default { <template> <div v-if="!showEmptyState" + :key="forceRedraw" class="prometheus-graphs prepend-top-default" > <div class="environments d-flex align-items-center"> @@ -214,11 +220,10 @@ 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" :deployment-data="store.deploymentData" :project-path="projectPath" :tags-path="tagsPath" diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue index e5680a0499f..3cccaf72ed7 100644 --- a/app/assets/javascripts/monitoring/components/graph.vue +++ b/app/assets/javascripts/monitoring/components/graph.vue @@ -32,10 +32,6 @@ export default { type: Object, required: true, }, - updateAspectRatio: { - type: Boolean, - required: true, - }, deploymentData: { type: Array, required: true, @@ -82,11 +78,13 @@ export default { value: 0, }, currentXCoordinate: 0, - currentCoordinates: [], + currentCoordinates: {}, showFlag: false, showFlagContent: false, timeSeries: [], + graphDrawData: {}, realPixelRatio: 1, + seriesUnderMouse: [], }; }, computed: { @@ -109,15 +107,6 @@ export default { }, }, watch: { - updateAspectRatio() { - if (this.updateAspectRatio) { - this.graphHeight = 450; - this.graphWidth = 600; - this.measurements = measurements.large; - this.draw(); - eventHub.$emit('toggleAspectRatio'); - } - }, hoverData() { this.positionFlag(); }, @@ -126,6 +115,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 +147,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]; @@ -172,12 +181,12 @@ export default { }); }, renderAxesPaths() { - this.timeSeries = createTimeSeries( + ({ timeSeries: this.timeSeries, graphDrawData: this.graphDrawData } = createTimeSeries( this.graphData.queries, this.graphWidth, this.graphHeight, this.graphHeightOffset, - ); + )); if (_.findWhere(this.timeSeries, { renderCanary: true })) { this.timeSeries = this.timeSeries.map(series => ({ ...series, renderCanary: true })); @@ -190,6 +199,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) @@ -269,6 +289,10 @@ export default { :viewBox="innerViewBox" class="graph-data" > + <slot + name="additionalSvgContent" + :graphDrawData="graphDrawData" + /> <graph-path v-for="(path, index) in timeSeries" :key="index" @@ -277,9 +301,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 +326,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..d5971730e31 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, }; @@ -29,7 +30,7 @@ const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple']; const defaultStyleOrder = ['solid', 'dashed', 'dotted']; -function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) { +function queryTimeSeries(query, graphDrawData, lineStyle) { let usedColors = []; let renderCanary = false; const timeSeriesParsed = []; @@ -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] = graphDrawData.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 = ''; @@ -65,31 +84,6 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom renderCanary = true; } - const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]); - - const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]); - - timeSeriesScaleX.domain(xDom); - timeSeriesScaleX.ticks(d3.timeMinute, 60); - timeSeriesScaleY.domain(yDom); - - const defined = d => !Number.isNaN(d.value) && d.value != null; - - const lineFunction = d3 - .line() - .defined(defined) - .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate - .x(d => timeSeriesScaleX(d.time)) - .y(d => timeSeriesScaleY(d.value)); - - const areaFunction = d3 - .area() - .defined(defined) - .curve(d3.curveLinear) - .x(d => timeSeriesScaleX(d.time)) - .y0(graphHeight - graphHeightOffset) - .y1(d => timeSeriesScaleY(d.value)); - const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]]; const seriesCustomizationData = query.series != null && _.findWhere(query.series[0].when, { value: timeSeriesMetricLabel }); @@ -119,11 +113,16 @@ 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), - timeSeriesScaleX, - timeSeriesScaleY, + linePath: graphDrawData.lineFunction(values), + areaPath: graphDrawData.areaBelowLine(values), + timeSeriesScaleX: graphDrawData.timeSeriesScaleX, + timeSeriesScaleY: graphDrawData.timeSeriesScaleY, values: timeSeries.values, max: maximumValue, average: accum / timeSeries.values.length, @@ -140,7 +139,7 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom return timeSeriesParsed; } -export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { +function xyDomain(queries) { const allValues = queries.reduce( (allQueryResults, query) => allQueryResults.concat( @@ -152,10 +151,70 @@ export default function createTimeSeries(queries, graphWidth, graphHeight, graph const xDom = d3.extent(allValues, d => d.time); const yDom = [0, d3.max(allValues.map(d => d.value))]; - return queries.reduce((series, query, index) => { + return { + xDom, + yDom, + }; +} + +export function generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset) { + const { xDom, yDom } = xyDomain(queries); + + const timeSeriesScaleX = d3.scaleTime().range([0, graphWidth - 70]); + const timeSeriesScaleY = d3.scaleLinear().range([graphHeight - graphHeightOffset, 0]); + + timeSeriesScaleX.domain(xDom); + timeSeriesScaleX.ticks(d3.timeMinute, 60); + timeSeriesScaleY.domain(yDom); + + const defined = d => !Number.isNaN(d.value) && d.value != null; + + const lineFunction = d3 + .line() + .defined(defined) + .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate + .x(d => timeSeriesScaleX(d.time)) + .y(d => timeSeriesScaleY(d.value)); + + const areaBelowLine = d3 + .area() + .defined(defined) + .curve(d3.curveLinear) + .x(d => timeSeriesScaleX(d.time)) + .y0(graphHeight - graphHeightOffset) + .y1(d => timeSeriesScaleY(d.value)); + + const areaAboveLine = d3 + .area() + .defined(defined) + .curve(d3.curveLinear) + .x(d => timeSeriesScaleX(d.time)) + .y0(0) + .y1(d => timeSeriesScaleY(d.value)); + + return { + lineFunction, + areaBelowLine, + areaAboveLine, + xDom, + yDom, + timeSeriesScaleX, + timeSeriesScaleY, + }; +} + +export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) { + const graphDrawData = generateGraphDrawData(queries, graphWidth, graphHeight, graphHeightOffset); + + const timeSeries = queries.reduce((series, query, index) => { const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length]; return series.concat( - queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle), + queryTimeSeries(query, graphDrawData, lineStyle), ); }, []); + + return { + timeSeries, + graphDrawData, + }; } diff --git a/app/assets/javascripts/mr_notes/stores/index.js b/app/assets/javascripts/mr_notes/stores/index.js index dd2019001db..c4225c8ec08 100644 --- a/app/assets/javascripts/mr_notes/stores/index.js +++ b/app/assets/javascripts/mr_notes/stores/index.js @@ -6,10 +6,13 @@ import mrPageModule from './modules'; Vue.use(Vuex); -export default new Vuex.Store({ - modules: { - page: mrPageModule, - notes: notesModule, - diffs: diffsModule, - }, -}); +export const createStore = () => + new Vuex.Store({ + modules: { + page: mrPageModule, + notes: notesModule(), + diffs: diffsModule(), + }, + }); + +export default createStore(); 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..f301f093ef4 100644 --- a/app/assets/javascripts/notes.js +++ b/app/assets/javascripts/notes.js @@ -16,7 +16,7 @@ import 'vendor/jquery.atwho'; import AjaxCache from '~/lib/utils/ajax_cache'; import Vue from 'vue'; import syntaxHighlight from '~/syntax_highlight'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; import axios from './lib/utils/axios_utils'; import { getLocationHash } from './lib/utils/url_utility'; import Flash from './flash'; @@ -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}`; @@ -1293,10 +1293,10 @@ export default class Notes { new Vue({ el, components: { - SkeletonLoadingContainer, + SkeletonLoading, }, render(createElement) { - return createElement('skeleton-loading-container'); + return createElement('skeleton-loading'); }, }); } @@ -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..b980e43b898 100644 --- a/app/assets/javascripts/notes/components/comment_form.vue +++ b/app/assets/javascripts/notes/components/comment_form.vue @@ -7,7 +7,11 @@ import { __, sprintf } from '~/locale'; import Flash from '../../flash'; import Autosave from '../../autosave'; import TaskList from '../../task_list'; -import { capitalizeFirstCharacter, convertToCamelCase, splitCamelCase } from '../../lib/utils/text_utility'; +import { + capitalizeFirstCharacter, + convertToCamelCase, + splitCamelCase, +} from '../../lib/utils/text_utility'; import * as constants from '../constants'; import eventHub from '../event_hub'; import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; @@ -122,7 +126,9 @@ export default { return this.getNoteableData.create_note_path; }, issuableTypeTitle() { - return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE ? 'merge request' : 'issue'; + return this.noteableType === constants.MERGE_REQUEST_NOTEABLE_TYPE + ? 'merge request' + : 'issue'; }, }, watch: { @@ -359,7 +365,7 @@ Please check your network connection and try again.`; :disabled="isSubmitting" name="note[note]" class="note-textarea js-vue-comment-form js-note-text -js-gfm-input js-autosize markdown-area js-vue-textarea" +js-gfm-input js-autosize markdown-area js-vue-textarea qa-comment-input" data-supports-quick-actions="true" aria-label="Description" placeholder="Write a comment or drag your files here…" @@ -374,7 +380,8 @@ 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-create comment-btn js-comment-button js-comment-submit-button + qa-comment-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 deleted file mode 100644 index fc7b52be241..00000000000 --- a/app/assets/javascripts/notes/components/diff_file_header.vue +++ /dev/null @@ -1,94 +0,0 @@ -<script> -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Icon from '~/vue_shared/components/icon.vue'; - -export default { - components: { - ClipboardButton, - Icon, - }, - props: { - diffFile: { - type: Object, - required: true, - }, - }, - computed: { - titleTag() { - return this.diffFile.discussionPath ? 'a' : 'span'; - }, - }, -}; -</script> - -<template> - <div class="file-header-content"> - <div - v-if="diffFile.submodule" - > - <span> - <icon name="archive" /> - <strong - class="file-title-name" - v-html="diffFile.submoduleLink" - ></strong> - <clipboard-button - :text="diffFile.submoduleLink" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - </span> - </div> - <template v-else> - <component - ref="titleWrapper" - :is="titleTag" - :href="diffFile.discussionPath" - > - <span v-html="diffFile.blobIcon"></span> - <span v-if="diffFile.renamedFile"> - <strong - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.oldPath }} - </strong> - → - <strong - :title="diffFile.newPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.newPath }} - </strong> - </span> - - <strong - v-else - :title="diffFile.oldPath" - class="file-title-name has-tooltip" - data-container="body" - > - {{ diffFile.filePath }} - <span v-if="diffFile.deletedFile"> - deleted - </span> - </strong> - </component> - - <clipboard-button - :text="diffFile.filePath" - title="Copy file path to clipboard" - css-class="btn-default btn-transparent btn-clipboard" - /> - - <small - v-if="diffFile.modeChanged" - ref="fileMode" - > - {{ diffFile.aMode }} → {{ diffFile.bMode }} - </small> - </template> - </div> -</template> diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue index 27ff7dea909..353aa790743 100644 --- a/app/assets/javascripts/notes/components/diff_with_note.vue +++ b/app/assets/javascripts/notes/components/diff_with_note.vue @@ -3,13 +3,13 @@ import { mapState, mapActions } from 'vuex'; import imageDiffHelper from '~/image_diff/helpers/index'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import DiffFileHeader from '~/diffs/components/diff_file_header.vue'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; import { trimFirstCharOfLineContent } from '~/diffs/store/utils'; export default { components: { DiffFileHeader, - SkeletonLoadingContainer, + SkeletonLoading, }, props: { discussion: { @@ -142,16 +142,15 @@ export default { class="line_content js-success-lazy-load" > <span></span> - <skeleton-loading-container /> + <skeleton-loading /> <span></span> </td> </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..e075f94b82b 100644 --- a/app/assets/javascripts/notes/components/note_actions.vue +++ b/app/assets/javascripts/notes/components/note_actions.vue @@ -7,29 +7,30 @@ 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 Icon from '~/vue_shared/components/icon.vue'; import tooltip from '~/vue_shared/directives/tooltip'; export default { name: 'NoteActions', + components: { + Icon, + }, directives: { tooltip, }, - components: { - loadingIcon, - }, props: { authorId: { type: Number, required: true, }, noteId: { - type: Number, + type: [String, Number], required: true, }, noteUrl: { type: String, - required: true, + required: false, + default: '', }, accessLevel: { type: String, @@ -38,7 +39,8 @@ export default { }, reportAbusePath: { type: String, - required: true, + required: false, + default: null, }, canEdit: { type: Boolean, @@ -87,6 +89,9 @@ export default { shouldShowActionsDropdown() { return this.currentUserId && (this.canEdit || this.canReportAsAbuse); }, + showDeleteAction() { + return this.canDelete && !this.canReportAsAbuse && !this.noteUrl; + }, isAuthoredByCurrentUser() { return this.authorId === this.currentUserId; }, @@ -152,9 +157,9 @@ export default { v-else v-html="resolveDiscussionSvg"></div> </template> - <loading-icon + <gl-loading-icon v-else - :inline="true" + inline /> </button> </div> @@ -171,7 +176,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"> @@ -204,7 +209,26 @@ export default { </button> </div> <div - v-if="shouldShowActionsDropdown" + v-if="showDeleteAction" + class="note-actions-item" + > + <button + v-tooltip + type="button" + title="Delete comment" + class="note-action-button js-note-delete btn btn-transparent" + data-container="body" + data-placement="bottom" + @click="onDelete" + > + <icon + name="remove" + class="link-highlight" + /> + </button> + </div> + <div + v-else-if="shouldShowActionsDropdown" class="dropdown more-actions note-actions-item"> <button v-tooltip @@ -225,11 +249,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_body.vue b/app/assets/javascripts/notes/components/note_body.vue index 6f4a0709825..cf4c35de42c 100644 --- a/app/assets/javascripts/notes/components/note_body.vue +++ b/app/assets/javascripts/notes/components/note_body.vue @@ -109,7 +109,7 @@ export default { class="note_edited_ago" /> <note-awards-list - v-if="note.award_emoji.length" + v-if="note.award_emoji && note.award_emoji.length" :note-id="note.id" :note-author-id="note.author.id" :awards="note.award_emoji" diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index abcd4422d7c..33998394a69 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, Number], 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..7b6e7b72caf 100644 --- a/app/assets/javascripts/notes/components/note_header.vue +++ b/app/assets/javascripts/notes/components/note_header.vue @@ -9,11 +9,13 @@ export default { props: { author: { type: Object, - required: true, + required: false, + default: () => ({}), }, createdAt: { type: String, - required: true, + required: false, + default: null, }, actionText: { type: String, @@ -21,8 +23,9 @@ export default { default: '', }, noteId: { - type: Number, - required: true, + type: [String, Number], + required: false, + default: null, }, includeToggle: { type: Boolean, @@ -72,7 +75,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 +87,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"> @@ -89,18 +98,22 @@ export default { <span class="system-note-message"> <slot></slot> </span> - <span class="system-note-separator"> - · - </span> - <a - :href="noteTimestampLink" - class="note-timestamp system-note-separator" - @click="updateTargetNoteHash"> - <time-ago-tooltip - :time="createdAt" - tooltip-placement="bottom" - /> - </a> + <template + v-if="createdAt" + > + <span class="system-note-separator"> + · + </span> + <a + :href="noteTimestampLink" + class="note-timestamp system-note-separator" + @click="updateTargetNoteHash"> + <time-ago-tooltip + :time="createdAt" + tooltip-placement="bottom" + /> + </a> + </template> <i class="fa fa-spinner fa-spin editing-spinner" aria-label="Comment is being updated" diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index 0fe1c16854a..e9218723149 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; @@ -189,6 +191,7 @@ export default { if (note.placeholderType === SYSTEM_NOTE) { return placeholderSystemNote; } + return placeholderNote; } @@ -199,7 +202,7 @@ export default { return noteableNote; }, componentData(note) { - return note.isPlaceholderNote ? this.discussion.notes[0] : note; + return note.isPlaceholderNote ? note.notes[0] : note; }, toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); @@ -256,11 +259,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 +278,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 +349,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..f391ed848a4 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -52,7 +52,7 @@ export default { return this.note.resolvable && !!this.getUserData.id; }, canReportAsAbuse() { - return this.note.report_abuse_path && this.author.id !== this.getUserData.id; + return !!this.note.report_abuse_path && this.author.id !== this.getUserData.id; }, noteAnchorId() { return `note_${this.note.id}`; @@ -81,11 +81,16 @@ export default { ...mapActions(['deleteNote', 'updateNote', 'toggleResolveNote', 'scrollToNoteIfNeeded']), editHandler() { this.isEditing = true; + this.$emit('handleEdit'); }, deleteHandler() { + const typeOfComment = this.note.isDraft ? 'pending comment' : 'comment'; // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to delete this comment?')) { + if (window.confirm(`Are you sure you want to delete this ${typeOfComment}?`)) { this.isDeleting = true; + this.$emit('handleDeleteNote', this.note); + + if (this.note.isDraft) return; this.deleteNote(this.note) .then(() => { @@ -97,7 +102,20 @@ export default { }); } }, + updateSuccess() { + this.isEditing = false; + this.isRequesting = false; + this.oldContent = null; + $(this.$refs.noteBody.$el).renderGFM(); + this.$refs.noteBody.resetAutoSave(); + this.$emit('updateSuccess'); + }, formUpdateHandler(noteText, parentElement, callback) { + this.$emit('handleUpdateNote', { + note: this.note, + noteText, + callback: () => this.updateSuccess(), + }); const data = { endpoint: this.note.path, note: { @@ -112,11 +130,7 @@ export default { this.updateNote(data) .then(() => { - this.isEditing = false; - this.isRequesting = false; - this.oldContent = null; - $(this.$refs.noteBody.$el).renderGFM(); - this.$refs.noteBody.resetAutoSave(); + this.updateSuccess(); callback(); }) .catch(() => { @@ -141,6 +155,7 @@ export default { this.oldContent = null; } this.isEditing = false; + this.$emit('cancelForm'); }, recoverNoteContent(noteText) { // we need to do this to prevent noteForm inconsistent content warning diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue index 9b8713b40fb..618a1581d8f 100644 --- a/app/assets/javascripts/notes/components/notes_app.vue +++ b/app/assets/javascripts/notes/components/notes_app.vue @@ -10,8 +10,8 @@ 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'; +import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user'; export default { name: 'NotesApp', @@ -20,7 +20,6 @@ export default { noteableDiscussion, systemNote, commentForm, - loadingIcon, placeholderNote, placeholderSystemNote, }, @@ -98,6 +97,9 @@ export default { }); } }, + updated() { + this.$nextTick(() => highlightCurrentUser(this.$el.querySelectorAll('.gfm-project_member'))); + }, methods: { ...mapActions({ fetchDiscussions: 'fetchDiscussions', @@ -138,6 +140,7 @@ export default { .then(() => { this.isLoading = false; this.setNotesFetchedState(true); + eventHub.$emit('fetchedNotesData'); }) .then(() => this.$nextTick()) .then(() => this.checkLocationHash()) @@ -188,10 +191,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..7ab7e5a9abb 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); + + return utils.findNoteObjectById(state.discussions, discussion.id); +}; -export const deleteNote = ({ commit }, note) => +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 }) => { @@ -146,35 +150,50 @@ export const toggleIssueLocalState = ({ commit }, newState) => { export const saveNote = ({ commit, dispatch }, noteData) => { // For MR discussuions we need to post as `note[note]` and issue we use `note.note`. - const note = noteData.data['note[note]'] || noteData.data.note.note; + // For batch comments, we use draft_note + const note = noteData.data.draft_note || noteData.data['note[note]'] || noteData.data.note.note; let placeholderText = note; const hasQuickActions = utils.hasQuickActions(placeholderText); const replyId = noteData.data.in_reply_to_discussion_id; - const methodToDispatch = replyId ? 'replyToDiscussion' : 'createNewNote'; + let methodToDispatch; + const postData = Object.assign({}, noteData); + if (postData.isDraft === true) { + methodToDispatch = replyId + ? 'batchComments/addDraftToDiscussion' + : 'batchComments/createNewDraft'; + if (!postData.draft_note && noteData.note) { + postData.draft_note = postData.note; + delete postData.note; + } + } else { + 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 => { + return dispatch(methodToDispatch, postData, { root: true }).then(res => { const { errors } = res; const commandsChanges = res.commands_changes; @@ -211,7 +230,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 +341,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..a829149a17e 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; @@ -82,6 +74,9 @@ export const allDiscussions = (state, getters) => { return Object.values(resolved).concat(unresolved); }; +export const isDiscussionResolved = (state, getters) => discussionId => + getters.resolvedDiscussionsById[discussionId] !== undefined; + export const allResolvableDiscussions = (state, getters) => getters.allDiscussions.filter(d => !d.individual_note && d.resolvable); @@ -134,8 +129,8 @@ export const unresolvedDiscussionsIdsByDiff = (state, getters) => const filenameComparison = a.diff_file.file_path.localeCompare(b.diff_file.file_path); // Get the line numbers, to compare within the same file - const aLines = [a.position.formatter.new_line, a.position.formatter.old_line]; - const bLines = [b.position.formatter.new_line, b.position.formatter.old_line]; + const aLines = [a.position.new_line, a.position.old_line]; + const bLines = [b.position.new_line, b.position.old_line]; return filenameComparison < 0 || (filenameComparison === 0 && 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/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 9aa83ce6269..72f3f70b98f 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -79,10 +79,13 @@ export default class Todos { .then(({ data }) => { this.updateRowState(target); this.updateBadges(data); - }).catch(() => flash(__('Error updating todo status.'))); + }).catch(() => { + this.updateRowState(target, true); + return flash(__('Error updating todo status.')); + }); } - updateRowState(target) { + updateRowState(target, isInactive = false) { const row = target.closest('li'); const restoreBtn = row.querySelector('.js-undo-todo'); const doneBtn = row.querySelector('.js-done-todo'); @@ -91,7 +94,10 @@ export default class Todos { target.removeAttribute('disabled'); target.classList.remove('disabled'); - if (target === doneBtn) { + if (isInactive === true) { + restoreBtn.classList.add('hidden'); + doneBtn.classList.remove('hidden'); + } else if (target === doneBtn) { row.classList.add('done-reversible'); restoreBtn.classList.remove('hidden'); } else if (target === restoreBtn) { 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/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js index 8737f537296..002b2279fcc 100644 --- a/app/assets/javascripts/pages/groups/edit/index.js +++ b/app/assets/javascripts/pages/groups/edit/index.js @@ -2,14 +2,13 @@ import groupAvatar from '~/group_avatar'; import TransferDropdown from '~/groups/transfer_dropdown'; import initConfirmDangerModal from '~/confirm_danger_modal'; import initSettingsPanels from '~/settings_panels'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; +import { GROUP_BADGE } from '~/badges/constants'; document.addEventListener('DOMContentLoaded', () => { groupAvatar(); new TransferDropdown(); // eslint-disable-line no-new initConfirmDangerModal(); -}); - -document.addEventListener('DOMContentLoaded', () => { - // Initialize expandable settings panels initSettingsPanels(); + mountBadgeSettings(GROUP_BADGE); }); 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..339ce67438a 100644 --- a/app/assets/javascripts/pages/groups/merge_requests/index.js +++ b/app/assets/javascripts/pages/groups/merge_requests/index.js @@ -1,11 +1,15 @@ 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', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + 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/ide/index.js b/app/assets/javascripts/pages/ide/index.js index efadf6967aa..d192df3561e 100644 --- a/app/assets/javascripts/pages/ide/index.js +++ b/app/assets/javascripts/pages/ide/index.js @@ -1,9 +1,3 @@ -import { initIde, resetServiceWorkersPublicPath } from '~/ide/index'; +import { startIde } from '~/ide/index'; -document.addEventListener('DOMContentLoaded', () => { - const ideElement = document.getElementById('ide'); - if (ideElement) { - resetServiceWorkersPublicPath(); - initIde(ideElement); - } -}); +startIde(); 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/profiles/show/index.js b/app/assets/javascripts/pages/profiles/show/index.js index aea7b649c20..c7ce4675573 100644 --- a/app/assets/javascripts/pages/profiles/show/index.js +++ b/app/assets/javascripts/pages/profiles/show/index.js @@ -11,7 +11,7 @@ document.addEventListener('DOMContentLoaded', () => { const statusEmojiField = document.getElementById('js-status-emoji-field'); const statusMessageField = document.getElementById('js-status-message-field'); - const toggleNoEmojiPlaceholder = (isVisible) => { + const toggleNoEmojiPlaceholder = isVisible => { const placeholderElement = document.getElementById('js-no-emoji-placeholder'); placeholderElement.classList.toggle('hidden', !isVisible); }; @@ -69,5 +69,5 @@ document.addEventListener('DOMContentLoaded', () => { } }); }) - .catch(() => createFlash('Failed to load emoji list!')); + .catch(() => createFlash('Failed to load emoji list.')); }); 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/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 628913483c6..f5b1cf85e68 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -1,6 +1,8 @@ +import { PROJECT_BADGE } from '~/badges/constants'; import initSettingsPanels from '~/settings_panels'; import setupProjectEdit from '~/project_edit'; import initConfirmDangerModal from '~/confirm_danger_modal'; +import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import initProjectLoadingSpinner from '../shared/save_project_loader'; import projectAvatar from '../shared/project_avatar'; import initProjectPermissionsSettings from '../shared/permissions'; @@ -13,4 +15,5 @@ document.addEventListener('DOMContentLoaded', () => { projectAvatar(); initProjectPermissionsSettings(); initConfirmDangerModal(); + mountBadgeSettings(PROJECT_BADGE); }); 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..b0345b4e50d 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 initGkeDropdowns from '~/projects/gke_cluster_dropdowns'; +import PersistentUserCallout from '../../persistent_user_callout'; 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,9 @@ document.addEventListener('DOMContentLoaded', () => { ]; if (newClusterViews.indexOf(page) > -1) { - gcpSignupOffer(); + const callout = document.querySelector('.gcp-signup-offer'); + if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new + 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..ef65196872c 100644 --- a/app/assets/javascripts/pages/projects/issues/show.js +++ b/app/assets/javascripts/pages/projects/issues/show.js @@ -1,11 +1,12 @@ 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'; +import initIssueableApp from '~/issue_show'; export default function () { + initIssueableApp(); new Issue(); // eslint-disable-line no-new new ShortcutsIssuable(); // eslint-disable-line no-new new ZenMode(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index a7aa616319f..ec39db12e74 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -1,14 +1,19 @@ 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', () => { + IssuableFilteredSearchTokenKeys.addExtraTokensForMergeRequests(); + 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 new UsersSelect(); // 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/settings/badges/index/index.js b/app/assets/javascripts/pages/projects/settings/badges/index/index.js deleted file mode 100644 index 30469550866..00000000000 --- a/app/assets/javascripts/pages/projects/settings/badges/index/index.js +++ /dev/null @@ -1,10 +0,0 @@ -import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; -import { PROJECT_BADGE } from '~/badges/constants'; -import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; - -Vue.use(Translate); - -document.addEventListener('DOMContentLoaded', () => { - mountBadgeSettings(PROJECT_BADGE); -}); 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..a16f7e6b77c 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 @@ -52,6 +52,21 @@ required: false, default: '', }, + pagesAvailable: { + type: Boolean, + required: false, + default: false, + }, + pagesAccessControlEnabled: { + type: Boolean, + required: false, + default: false, + }, + pagesHelpPath: { + type: String, + required: false, + default: '', + }, }, data() { @@ -64,6 +79,7 @@ buildsAccessLevel: 20, wikiAccessLevel: 20, snippetsAccessLevel: 20, + pagesAccessLevel: 20, containerRegistryEnabled: true, lfsEnabled: true, requestAccessEnabled: true, @@ -90,6 +106,13 @@ ); }, + pagesFeatureAccessLevelOptions() { + if (this.visibilityLevel !== visibilityOptions.PUBLIC) { + return this.featureAccessLevelOptions.concat([[30, 'Everyone']]); + } + return this.featureAccessLevelOptions; + }, + repositoryEnabled() { return this.repositoryAccessLevel > 0; }, @@ -109,6 +132,10 @@ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel); this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel); this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel); + if (this.pagesAccessLevel === 20) { + // When from Internal->Private narrow access for only members + this.pagesAccessLevel = 10; + } this.highlightChanges(); } else if (oldValue === visibilityOptions.PRIVATE) { // if changing away from private, make enabled features more permissive @@ -118,6 +145,7 @@ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20; if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20; if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20; + if (this.pagesAccessLevel === 10) this.pagesAccessLevel = 20; this.highlightChanges(); } }, @@ -240,8 +268,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 +278,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 +289,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 +300,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 +336,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,11 +346,23 @@ 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> + <project-setting-row + v-if="pagesAvailable && pagesAccessControlEnabled" + :help-path="pagesHelpPath" + label="Pages" + help-text="Static website for the project." + > + <project-feature-setting + v-model="pagesAccessLevel" + :options="pagesFeatureAccessLevelOptions" + name="project[project_feature_attributes][pages_access_level]" + /> + </project-setting-row> </div> </div> </template> 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/pages/root/index.js b/app/assets/javascripts/pages/root/index.js new file mode 100644 index 00000000000..09f8185d3b5 --- /dev/null +++ b/app/assets/javascripts/pages/root/index.js @@ -0,0 +1,5 @@ +// if the "projects dashboard" is a user's default dashboard, when they visit the +// instance root index, the dashboard will be served by the root controller instead +// of a dashboard controller. The root index redirects for all other default dashboards. + +import '../dashboard/projects/index'; diff --git a/app/assets/javascripts/pages/snippets/form.js b/app/assets/javascripts/pages/snippets/form.js index f369c7ef9a6..8859557e62d 100644 --- a/app/assets/javascripts/pages/snippets/form.js +++ b/app/assets/javascripts/pages/snippets/form.js @@ -11,6 +11,7 @@ export default () => { epics: false, milestones: false, labels: false, + snippets: false, }); new ZenMode(); // eslint-disable-line no-new }; diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 9892a039941..bf592ba7a3c 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -43,7 +43,15 @@ const initColorKey = () => .domain([0, 3]); export default class ActivityCalendar { - constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0, firstDayOfWeek = 0) { + constructor( + container, + activitiesContainer, + timestamps, + calendarActivitiesPath, + utcOffset = 0, + firstDayOfWeek = 0, + monthsAgo = 12, + ) { this.calendarActivitiesPath = calendarActivitiesPath; this.clickDay = this.clickDay.bind(this); this.currentSelectedDate = ''; @@ -66,6 +74,8 @@ export default class ActivityCalendar { ]; this.months = []; this.firstDayOfWeek = firstDayOfWeek; + this.activitiesContainer = activitiesContainer; + this.container = container; // Loop through the timestamps to create a group of objects // The group of objects will be grouped based on the day of the week they are @@ -75,13 +85,13 @@ export default class ActivityCalendar { const today = getSystemDate(utcOffset); today.setHours(0, 0, 0, 0, 0); - const oneYearAgo = new Date(today); - oneYearAgo.setFullYear(today.getFullYear() - 1); + const timeAgo = new Date(today); + timeAgo.setMonth(today.getMonth() - monthsAgo); - const days = getDayDifference(oneYearAgo, today); + const days = getDayDifference(timeAgo, today); for (let i = 0; i <= days; i += 1) { - const date = new Date(oneYearAgo); + const date = new Date(timeAgo); date.setDate(date.getDate() + i); const day = date.getDay(); @@ -280,7 +290,7 @@ export default class ActivityCalendar { this.currentSelectedDate.getDate(), ].join('-'); - $('.user-calendar-activities').html(LOADING_HTML); + $(this.activitiesContainer).html(LOADING_HTML); axios .get(this.calendarActivitiesPath, { @@ -289,11 +299,11 @@ export default class ActivityCalendar { }, responseType: 'text', }) - .then(({ data }) => $('.user-calendar-activities').html(data)) + .then(({ data }) => $(this.activitiesContainer).html(data)) .catch(() => flash(__('An error occurred while retrieving calendar activity'))); } else { this.currentSelectedDate = ''; - $('.user-calendar-activities').html(''); + $(this.activitiesContainer).html(''); } } } diff --git a/app/assets/javascripts/pages/users/user_overview_block.js b/app/assets/javascripts/pages/users/user_overview_block.js new file mode 100644 index 00000000000..0009419cd0c --- /dev/null +++ b/app/assets/javascripts/pages/users/user_overview_block.js @@ -0,0 +1,42 @@ +import axios from '~/lib/utils/axios_utils'; + +export default class UserOverviewBlock { + constructor(options = {}) { + this.container = options.container; + this.url = options.url; + this.limit = options.limit || 20; + this.loadData(); + } + + loadData() { + const loadingEl = document.querySelector(`${this.container} .loading`); + + loadingEl.classList.remove('hide'); + + axios + .get(this.url, { + params: { + limit: this.limit, + }, + }) + .then(({ data }) => this.render(data)) + .catch(() => loadingEl.classList.add('hide')); + } + + render(data) { + const { html, count } = data; + const contentList = document.querySelector(`${this.container} .overview-content-list`); + + contentList.innerHTML += html; + + const loadingEl = document.querySelector(`${this.container} .loading`); + + if (count && count > 0) { + document.querySelector(`${this.container} .js-view-all`).classList.remove('hide'); + } else { + document.querySelector(`${this.container} .nothing-here-block`).classList.add('text-left', 'p-0'); + } + + loadingEl.classList.add('hide'); + } +} diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js index a2ca03536f2..23b0348a99f 100644 --- a/app/assets/javascripts/pages/users/user_tabs.js +++ b/app/assets/javascripts/pages/users/user_tabs.js @@ -2,9 +2,10 @@ import $ from 'jquery'; import axios from '~/lib/utils/axios_utils'; import Activities from '~/activities'; import { localTimeAgo } from '~/lib/utils/datetime_utility'; -import { __ } from '~/locale'; +import { __, sprintf } from '~/locale'; import flash from '~/flash'; import ActivityCalendar from './activity_calendar'; +import UserOverviewBlock from './user_overview_block'; /** * UserTabs @@ -61,19 +62,28 @@ import ActivityCalendar from './activity_calendar'; * </div> */ -const CALENDAR_TEMPLATE = ` - <div class="clearfix calendar"> - <div class="js-contrib-calendar"></div> - <div class="calendar-hint"> - Summary of issues, merge requests, push events, and comments +const CALENDAR_TEMPLATES = { + activity: ` + <div class="clearfix calendar"> + <div class="js-contrib-calendar"></div> + <div class="calendar-hint bottom-right"></div> </div> - </div> -`; + `, + overview: ` + <div class="clearfix calendar"> + <div class="calendar-hint"></div> + <div class="js-contrib-calendar prepend-top-20"></div> + </div> + `, +}; + +const CALENDAR_PERIOD_6_MONTHS = 6; +const CALENDAR_PERIOD_12_MONTHS = 12; export default class UserTabs { constructor({ defaultAction, action, parentEl }) { this.loaded = {}; - this.defaultAction = defaultAction || 'activity'; + this.defaultAction = defaultAction || 'overview'; this.action = action || this.defaultAction; this.$parentEl = $(parentEl) || $(document); this.windowLocation = window.location; @@ -124,6 +134,8 @@ export default class UserTabs { } if (action === 'activity') { this.loadActivities(); + } else if (action === 'overview') { + this.loadOverviewTab(); } const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; @@ -154,7 +166,40 @@ export default class UserTabs { if (this.loaded.activity) { return; } - const $calendarWrap = this.$parentEl.find('.user-calendar'); + + this.loadActivityCalendar('activity'); + + // eslint-disable-next-line no-new + new Activities(); + + this.loaded.activity = true; + } + + loadOverviewTab() { + if (this.loaded.overview) { + return; + } + + this.loadActivityCalendar('overview'); + + UserTabs.renderMostRecentBlocks('#js-overview .activities-block', 5); + UserTabs.renderMostRecentBlocks('#js-overview .projects-block', 10); + + this.loaded.overview = true; + } + + static renderMostRecentBlocks(container, limit) { + // eslint-disable-next-line no-new + new UserOverviewBlock({ + container, + url: $(`${container} .overview-content-list`).data('href'), + limit, + }); + } + + loadActivityCalendar(action) { + const monthsAgo = action === 'overview' ? CALENDAR_PERIOD_6_MONTHS : CALENDAR_PERIOD_12_MONTHS; + const $calendarWrap = this.$parentEl.find('.tab-pane.active .user-calendar'); const calendarPath = $calendarWrap.data('calendarPath'); const calendarActivitiesPath = $calendarWrap.data('calendarActivitiesPath'); const utcOffset = $calendarWrap.data('utcOffset'); @@ -166,17 +211,22 @@ export default class UserTabs { axios .get(calendarPath) .then(({ data }) => { - $calendarWrap.html(CALENDAR_TEMPLATE); - $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); + $calendarWrap.html(CALENDAR_TEMPLATES[action]); + + let calendarHint = ''; + + if (action === 'activity') { + calendarHint = sprintf(__('Summary of issues, merge requests, push events, and comments (Timezone: %{utcFormatted})'), { utcFormatted }); + } else if (action === 'overview') { + calendarHint = __('Issues, merge requests, pushes and comments.'); + } + + $calendarWrap.find('.calendar-hint').text(calendarHint); // eslint-disable-next-line no-new - new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset); + new ActivityCalendar('.tab-pane.active .js-contrib-calendar', '.tab-pane.active .user-calendar-activities', data, calendarActivitiesPath, utcOffset, 0, monthsAgo); }) .catch(() => flash(__('There was an error loading users activity calendar.'))); - - // eslint-disable-next-line no-new - new Activities(); - this.loaded.activity = true; } toggleLoading(status) { 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..1522e2227e4 100644 --- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue +++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue @@ -42,7 +42,7 @@ export default { keys: ['feature', 'request'], }, ], - simpleMetrics: ['redis', 'sidekiq'], + simpleMetrics: ['redis'], data() { return { currentRequestId: '' }; }, @@ -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/performance_bar/components/simple_metric.vue b/app/assets/javascripts/performance_bar/components/simple_metric.vue index b654bc66249..760ea8fe1e6 100644 --- a/app/assets/javascripts/performance_bar/components/simple_metric.vue +++ b/app/assets/javascripts/performance_bar/components/simple_metric.vue @@ -1,16 +1,29 @@ <script> -export default { - props: { - currentRequest: { - type: Object, - required: true, + export default { + props: { + currentRequest: { + type: Object, + required: true, + }, + metric: { + type: String, + required: true, + }, }, - metric: { - type: String, - required: true, + computed: { + duration() { + return ( + this.currentRequest.details[this.metric] && + this.currentRequest.details[this.metric].duration + ); + }, + calls() { + return ( + this.currentRequest.details[this.metric] && this.currentRequest.details[this.metric].calls + ); + }, }, - }, -}; + }; </script> <template> <div @@ -21,9 +34,9 @@ export default { v-if="currentRequest.details" class="bold" > - {{ currentRequest.details[metric].duration }} + {{ duration }} / - {{ currentRequest.details[metric].calls }} + {{ calls }} </span> {{ metric }} </div> diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js new file mode 100644 index 00000000000..1e34e74a152 --- /dev/null +++ b/app/assets/javascripts/persistent_user_callout.js @@ -0,0 +1,34 @@ +import axios from './lib/utils/axios_utils'; +import { __ } from './locale'; +import Flash from './flash'; + +export default class PersistentUserCallout { + constructor(container) { + const { dismissEndpoint, featureId } = container.dataset; + this.container = container; + this.dismissEndpoint = dismissEndpoint; + this.featureId = featureId; + + this.init(); + } + + init() { + const closeButton = this.container.querySelector('.js-close'); + closeButton.addEventListener('click', event => this.dismiss(event)); + } + + dismiss(event) { + event.preventDefault(); + + axios + .post(this.dismissEndpoint, { + feature_name: this.featureId, + }) + .then(() => { + this.container.remove(); + }) + .catch(() => { + Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.')); + }); + } +} 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..16e69759091 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_actions.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_actions.vue @@ -1,6 +1,7 @@ <script> +import { s__, sprintf } from '~/locale'; +import { formatTime } from '~/lib/utils/datetime_utility'; 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 +10,6 @@ export default { tooltip, }, components: { - loadingIcon, icon, }, props: { @@ -24,10 +24,24 @@ export default { }; }, methods: { - onClickAction(endpoint) { + onClickAction(action) { + if (action.scheduled_at) { + const confirmationMessage = sprintf( + s__( + "DelayedJobs|Are you sure you want to run %{jobName} immediately? This job will run automatically after it's timer finishes.", + ), + { jobName: action.name }, + ); + // https://gitlab.com/gitlab-org/gitlab-ce/issues/52156 + // eslint-disable-next-line no-alert + if (!window.confirm(confirmationMessage)) { + return; + } + } + this.isLoading = true; - eventHub.$emit('postAction', endpoint); + eventHub.$emit('postAction', action.path); }, isActionDisabled(action) { @@ -37,6 +51,11 @@ export default { return !action.playable; }, + + remainingTime(action) { + const remainingMilliseconds = new Date(action.scheduled_at).getTime() - Date.now(); + return formatTime(Math.max(0, remainingMilliseconds)); + }, }, }; </script> @@ -60,22 +79,29 @@ 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"> <li - v-for="(action, i) in actions" - :key="i" + v-for="action in actions" + :key="action.path" > <button :class="{ disabled: isActionDisabled(action) }" :disabled="isActionDisabled(action)" type="button" class="js-pipeline-action-link no-btn btn" - @click="onClickAction(action.path)" + @click="onClickAction(action)" > {{ action.name }} + <span + v-if="action.scheduled_at" + class="pull-right" + > + <icon name="clock" /> + {{ remainingTime(action) }} + </span> </button> </li> </ul> diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue index 29b347824de..09ee190b8ca 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue @@ -59,6 +59,16 @@ export default { }; }, computed: { + actions() { + if (!this.pipeline || !this.pipeline.details) { + return []; + } + const { details } = this.pipeline; + return [ + ...(details.manual_actions || []), + ...(details.scheduled_actions || []), + ]; + }, /** * If provided, returns the commit tag. * Needed to render the commit component column. @@ -132,10 +142,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; @@ -323,8 +331,8 @@ export default { > <div class="btn-group table-action-buttons"> <pipelines-actions-component - v-if="pipeline.details.manual_actions.length" - :actions="pipeline.details.manual_actions" + v-if="actions.length > 0" + :actions="actions" /> <pipelines-artifacts-component 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..ebe18b47e4e 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.on('keyup change', () => { + 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/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue index dc609d6f90e..d196f497362 100644 --- a/app/assets/javascripts/reports/components/report_section.vue +++ b/app/assets/javascripts/reports/components/report_section.vue @@ -139,7 +139,7 @@ export default { <section class="media-section"> <div class="media"> <status-icon :status="statusIconName" /> - <div class="media-body space-children d-flex flex-align-self-center"> + <div class="media-body d-flex flex-align-self-center"> <span class="js-code-text code-text"> {{ headerText }} 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/set_status_modal/emoji_menu_in_modal.js b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js new file mode 100644 index 00000000000..14a89ef9293 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/emoji_menu_in_modal.js @@ -0,0 +1,21 @@ +import { AwardsHandler } from '~/awards_handler'; + +class EmojiMenuInModal extends AwardsHandler { + constructor(emoji, toggleButtonSelector, menuClass, selectEmojiCallback, targetContainerEl) { + super(emoji); + + this.selectEmojiCallback = selectEmojiCallback; + this.toggleButtonSelector = toggleButtonSelector; + this.menuClass = menuClass; + this.targetContainerEl = targetContainerEl; + + this.bindEvents(); + } + + postEmoji($emojiButton, awardUrl, selectedEmoji, callback) { + this.selectEmojiCallback(selectedEmoji, this.emoji.glEmojiTag(selectedEmoji)); + callback(); + } +} + +export default EmojiMenuInModal; diff --git a/app/assets/javascripts/set_status_modal/event_hub.js b/app/assets/javascripts/set_status_modal/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue new file mode 100644 index 00000000000..48e5ede80f2 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_trigger.vue @@ -0,0 +1,33 @@ +<script> +import { s__ } from '~/locale'; +import eventHub from './event_hub'; + +export default { + props: { + hasStatus: { + type: Boolean, + required: true, + }, + }, + computed: { + buttonText() { + return this.hasStatus ? s__('SetStatusModal|Edit status') : s__('SetStatusModal|Set status'); + }, + }, + methods: { + openModal() { + eventHub.$emit('openModal'); + }, + }, +}; +</script> + +<template> + <button + type="button" + class="btn menu-item" + @click="openModal" + > + {{ buttonText }} + </button> +</template> diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue new file mode 100644 index 00000000000..43f0b6651b9 --- /dev/null +++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue @@ -0,0 +1,241 @@ +<script> +import $ from 'jquery'; +import createFlash from '~/flash'; +import Icon from '~/vue_shared/components/icon.vue'; +import GfmAutoComplete from '~/gfm_auto_complete'; +import { __, s__ } from '~/locale'; +import Api from '~/api'; +import eventHub from './event_hub'; +import EmojiMenuInModal from './emoji_menu_in_modal'; + +const emojiMenuClass = 'js-modal-status-emoji-menu'; + +export default { + components: { + Icon, + }, + props: { + currentEmoji: { + type: String, + required: true, + }, + currentMessage: { + type: String, + required: true, + }, + }, + data() { + return { + defaultEmojiTag: '', + emoji: this.currentEmoji, + emojiMenu: null, + emojiTag: '', + isEmojiMenuVisible: false, + message: this.currentMessage, + modalId: 'set-user-status-modal', + noEmoji: true, + }; + }, + computed: { + isDirty() { + return this.message.length || this.emoji.length; + }, + }, + mounted() { + eventHub.$on('openModal', this.openModal); + }, + beforeDestroy() { + this.emojiMenu.destroy(); + }, + methods: { + openModal() { + this.$root.$emit('bv::show::modal', this.modalId); + }, + closeModal() { + this.$root.$emit('bv::hide::modal', this.modalId); + }, + setupEmojiListAndAutocomplete() { + const toggleEmojiMenuButtonSelector = '#set-user-status-modal .js-toggle-emoji-menu'; + const emojiAutocomplete = new GfmAutoComplete(); + emojiAutocomplete.setup($(this.$refs.statusMessageField), { emojis: true }); + + import(/* webpackChunkName: 'emoji' */ '~/emoji') + .then(Emoji => { + if (this.emoji) { + this.emojiTag = Emoji.glEmojiTag(this.emoji); + } + this.noEmoji = this.emoji === ''; + this.defaultEmojiTag = Emoji.glEmojiTag('speech_balloon'); + + this.emojiMenu = new EmojiMenuInModal( + Emoji, + toggleEmojiMenuButtonSelector, + emojiMenuClass, + this.setEmoji, + this.$refs.userStatusForm, + ); + }) + .catch(() => createFlash(__('Failed to load emoji list.'))); + }, + showEmojiMenu() { + this.isEmojiMenuVisible = true; + this.emojiMenu.showEmojiMenu($(this.$refs.toggleEmojiMenuButton)); + }, + hideEmojiMenu() { + if (!this.isEmojiMenuVisible) { + return; + } + + this.isEmojiMenuVisible = false; + this.emojiMenu.hideMenuElement($(`.${emojiMenuClass}`)); + }, + setDefaultEmoji() { + const { emojiTag } = this; + const hasStatusMessage = this.message; + if (hasStatusMessage && emojiTag) { + return; + } + + if (hasStatusMessage) { + this.noEmoji = false; + this.emojiTag = this.defaultEmojiTag; + } else if (emojiTag === this.defaultEmojiTag) { + this.noEmoji = true; + this.clearEmoji(); + } + }, + setEmoji(emoji, emojiTag) { + this.emoji = emoji; + this.noEmoji = false; + this.clearEmoji(); + this.emojiTag = emojiTag; + }, + clearEmoji() { + if (this.emojiTag) { + this.emojiTag = ''; + } + }, + clearStatusInputs() { + this.emoji = ''; + this.message = ''; + this.noEmoji = true; + this.clearEmoji(); + this.hideEmojiMenu(); + }, + removeStatus() { + this.clearStatusInputs(); + this.setStatus(); + }, + setStatus() { + const { emoji, message } = this; + + Api.postUserStatus({ + emoji, + message, + }) + .then(this.onUpdateSuccess) + .catch(this.onUpdateFail); + }, + onUpdateSuccess() { + this.closeModal(); + window.location.reload(); + }, + onUpdateFail() { + createFlash( + s__("SetStatusModal|Sorry, we weren't able to set your status. Please try again later."), + ); + + this.closeModal(); + }, + }, +}; +</script> + +<template> + <gl-ui-modal + :title="s__('SetStatusModal|Set a status')" + :modal-id="modalId" + :ok-title="s__('SetStatusModal|Set status')" + :cancel-title="s__('SetStatusModal|Remove status')" + ok-variant="success" + class="set-user-status-modal" + @shown="setupEmojiListAndAutocomplete" + @hide="hideEmojiMenu" + @ok="setStatus" + @cancel="removeStatus" + > + <div> + <input + v-model="emoji" + class="js-status-emoji-field" + type="hidden" + name="user[status][emoji]" + /> + <div + ref="userStatusForm" + class="form-group position-relative m-0" + > + <div class="input-group"> + <span class="input-group-btn"> + <button + ref="toggleEmojiMenuButton" + v-gl-tooltip.bottom + :title="s__('SetStatusModal|Add status emoji')" + :aria-label="s__('SetStatusModal|Add status emoji')" + name="button" + type="button" + class="js-toggle-emoji-menu emoji-menu-toggle-button btn" + @click="showEmojiMenu" + > + <span v-html="emojiTag"></span> + <span + v-show="noEmoji" + class="js-no-emoji-placeholder no-emoji-placeholder position-relative" + > + <icon + name="emoji_slightly_smiling_face" + css-classes="award-control-icon-neutral" + /> + <icon + name="emoji_smiley" + css-classes="award-control-icon-positive" + /> + <icon + name="emoji_smile" + css-classes="award-control-icon-super-positive" + /> + </span> + </button> + </span> + <input + ref="statusMessageField" + v-model="message" + :placeholder="s__('SetStatusModal|What\'s your status?')" + type="text" + class="form-control form-control input-lg js-status-message-field" + name="user[status][message]" + @keyup="setDefaultEmoji" + @keyup.enter.prevent + @click="hideEmojiMenu" + /> + <span + v-show="isDirty" + class="input-group-btn" + > + <button + v-gl-tooltip.bottom + :title="s__('SetStatusModal|Clear status')" + :aria-label="s__('SetStatusModal|Clear status')" + name="button" + type="button" + class="js-clear-user-status-button clear-user-status btn" + @click="clearStatusInputs()" + > + <icon name="close" /> + </button> + </span> + </div> + </div> + </div> + </gl-ui-modal> +</template> diff --git a/app/assets/javascripts/shared/milestones/form.js b/app/assets/javascripts/shared/milestones/form.js index 8681a1776c6..0ff84dc4667 100644 --- a/app/assets/javascripts/shared/milestones/form.js +++ b/app/assets/javascripts/shared/milestones/form.js @@ -15,5 +15,6 @@ export default (initGFM = true) => { epics: initGFM, milestones: initGFM, labels: initGFM, + snippets: initGFM, }); }; 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/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue index 2e1d6e9643a..8660b0546cf 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue @@ -51,10 +51,10 @@ export default { <template> <div class="block"> <issuable-time-tracker - :time_estimate="store.timeEstimate" - :time_spent="store.totalTimeSpent" - :human_time_estimate="store.humanTimeEstimate" - :human_time_spent="store.humanTotalTimeSpent" + :time-estimate="store.timeEstimate" + :time-spent="store.totalTimeSpent" + :human-time-estimate="store.humanTimeEstimate" + :human-time-spent="store.humanTotalTimeSpent" :root-path="store.rootPath" /> </div> 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..ef76dc13ce9 100644 --- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue +++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue @@ -19,20 +19,20 @@ export default { TimeTrackingHelpState, }, props: { - time_estimate: { + timeEstimate: { type: Number, required: true, }, - time_spent: { + timeSpent: { type: Number, required: true, }, - human_time_estimate: { + humanTimeEstimate: { type: String, required: false, default: '', }, - human_time_spent: { + humanTimeSpent: { type: String, required: false, default: '', @@ -48,18 +48,6 @@ export default { }; }, computed: { - timeSpent() { - return this.time_spent; - }, - timeEstimate() { - return this.time_estimate; - }, - timeEstimateHumanReadable() { - return this.human_time_estimate; - }, - timeSpentHumanReadable() { - return this.human_time_spent; - }, hasTimeSpent() { return !!this.timeSpent; }, @@ -90,10 +78,12 @@ export default { this.showHelp = show; }, update(data) { - this.time_estimate = data.time_estimate; - this.time_spent = data.time_spent; - this.human_time_estimate = data.human_time_estimate; - this.human_time_spent = data.human_time_spent; + const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = data; + + this.timeEstimate = timeEstimate; + this.timeSpent = timeSpent; + this.humanTimeEstimate = humanTimeEstimate; + this.humanTimeSpent = humanTimeSpent; }, }, }; @@ -110,8 +100,8 @@ export default { :show-help-state="showHelpState" :show-spent-only-state="showSpentOnlyState" :show-estimate-only-state="showEstimateOnlyState" - :time-spent-human-readable="timeSpentHumanReadable" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-spent-human-readable="humanTimeSpent" + :time-estimate-human-readable="humanTimeEstimate" /> <div class="title hide-collapsed"> {{ __('Time tracking') }} @@ -141,11 +131,11 @@ export default { <div class="time-tracking-content hide-collapsed"> <time-tracking-estimate-only-pane v-if="showEstimateOnlyState" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-estimate-human-readable="humanTimeEstimate" /> <time-tracking-spent-only-pane v-if="showSpentOnlyState" - :time-spent-human-readable="timeSpentHumanReadable" + :time-spent-human-readable="humanTimeSpent" /> <time-tracking-no-tracking-pane v-if="showNoTimeTrackingState" @@ -154,8 +144,8 @@ export default { v-if="showComparisonState" :time-estimate="timeEstimate" :time-spent="timeSpent" - :time-spent-human-readable="timeSpentHumanReadable" - :time-estimate-human-readable="timeEstimateHumanReadable" + :time-spent-human-readable="humanTimeSpent" + :time-estimate-human-readable="humanTimeEstimate" /> <transition name="help-state-toggle"> <time-tracking-help-state 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/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js index b15ad0e5586..87da65a1b1f 100644 --- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js +++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js @@ -7,6 +7,8 @@ export default class SidebarMilestone { if (!el) return; + const { timeEstimate, timeSpent, humanTimeEstimate, humanTimeSpent } = el.dataset; + // eslint-disable-next-line no-new new Vue({ el, @@ -15,10 +17,10 @@ export default class SidebarMilestone { }, render: createElement => createElement('timeTracker', { props: { - time_estimate: parseInt(el.dataset.timeEstimate, 10), - time_spent: parseInt(el.dataset.timeSpent, 10), - human_time_estimate: el.dataset.humanTimeEstimate, - human_time_spent: el.dataset.humanTimeSpent, + timeEstimate: parseInt(timeEstimate, 10), + timeSpent: parseInt(timeSpent, 10), + humanTimeEstimate, + humanTimeSpent, rootPath: '/', }, }), 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/users_select.js b/app/assets/javascripts/users_select.js index e19bbbacf4d..b9dfa22dd49 100644 --- a/app/assets/javascripts/users_select.js +++ b/app/assets/javascripts/users_select.js @@ -592,7 +592,7 @@ function UsersSelect(currentUser, els, options = {}) { if (showEmailUser && data.results.length === 0 && query.term.match(/^[^@]+@[^@]+$/)) { var trimmed = query.term.trim(); emailUser = { - name: "Invite \"" + query.term + "\" by email", + name: "Invite \"" + trimmed + "\" by email", username: trimmed, id: trimmed, invite: true 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_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue index 72bd28ae03f..acfdab3a015 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue @@ -4,6 +4,7 @@ import { n__, s__, sprintf } from '~/locale'; import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility'; import Icon from '~/vue_shared/components/icon.vue'; import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import tooltip from '~/vue_shared/directives/tooltip'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; export default { @@ -13,6 +14,9 @@ export default { clipboardButton, TooltipOnTruncate, }, + directives: { + tooltip, + }, props: { mr: { type: Object, @@ -24,11 +28,17 @@ export default { return this.mr.divergedCommitsCount > 0; }, commitsBehindText() { - return sprintf(s__('mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch'), { - commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`, - commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount), - commitsBehindLinkEnd: '</a>', - }, false); + return sprintf( + s__( + 'mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch', + ), + { + commitsBehindLinkStart: `<a href="${_.escape(this.mr.targetBranchPath)}">`, + commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount), + commitsBehindLinkEnd: '</a>', + }, + false, + ); }, branchNameClipboardData() { // This supports code in app/assets/javascripts/copy_to_clipboard.js that @@ -40,10 +50,26 @@ export default { }); }, webIdePath() { - return mergeUrlParams({ - target_project: this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath ? - this.mr.targetProjectFullPath : '', - }, webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`)); + if (this.mr.canPushToSourceBranch) { + return mergeUrlParams( + { + target_project: + this.mr.sourceProjectFullPath !== this.mr.targetProjectFullPath + ? this.mr.targetProjectFullPath + : '', + }, + webIDEUrl(`/${this.mr.sourceProjectFullPath}/merge_requests/${this.mr.iid}`), + ); + } + + return null; + }, + ideButtonTitle() { + return !this.mr.canPushToSourceBranch + ? s__( + 'mrWidget|You are not allowed to edit this project directly. Please fork to make changes.', + ) + : ''; }, }, }; @@ -91,12 +117,18 @@ export default { <div v-if="mr.isOpen" - class="branch-actions" + class="branch-actions d-flex" > <a v-if="!mr.sourceBranchRemoved" + v-tooltip :href="webIdePath" - class="btn btn-default inline js-web-ide d-none d-md-inline-block" + :title="ideButtonTitle" + :class="{ disabled: !mr.canPushToSourceBranch }" + class="btn btn-default js-web-ide d-none d-md-inline-block append-right-8" + data-placement="bottom" + tabindex="0" + role="button" > {{ s__("mrWidget|Open in Web IDE") }} </a> @@ -104,15 +136,15 @@ export default { :disabled="mr.sourceBranchRemoved" data-target="#modal_merge_info" data-toggle="modal" - class="btn btn-default inline js-check-out-branch" + class="btn btn-default js-check-out-branch append-right-default" type="button" > {{ s__("mrWidget|Check out branch") }} </button> - <span class="dropdown prepend-left-10"> + <span class="dropdown"> <button type="button" - class="btn inline dropdown-toggle" + class="btn dropdown-toggle" data-toggle="dropdown" aria-label="Download as" aria-haspopup="true" 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.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js deleted file mode 100644 index bf8628d18a6..00000000000 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js +++ /dev/null @@ -1,15 +0,0 @@ -/* -The squash-before-merge button is EE only, but it's located right in the middle -of the readyToMerge state component template. - -If we didn't declare this component in CE, we'd need to maintain a separate copy -of the readyToMergeState template in EE, which is pretty big and likely to change. - -Instead, in CE, we declare the component, but it's hidden and is configured to do nothing. -In EE, the configuration extends this object to add a functioning squash-before-merge -button. -*/ - -export default { - template: '', -}; 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..c8ad2aa30a6 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -6,7 +6,7 @@ import MergeRequest from '../../../merge_request'; import Flash from '../../../flash'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; -import SquashBeforeMerge from './mr_widget_squash_before_merge.vue'; +import SquashBeforeMerge from './squash_before_merge.vue'; export default { name: 'ReadyToMerge', @@ -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/components/states/mr_widget_squash_before_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/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/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/dependencies.js b/app/assets/javascripts/vue_merge_request_widget/dependencies.js index 15097fa2a3f..a23496c6bf5 100644 --- a/app/assets/javascripts/vue_merge_request_widget/dependencies.js +++ b/app/assets/javascripts/vue_merge_request_widget/dependencies.js @@ -40,7 +40,7 @@ export { default as MRWidgetService } from './services/mr_widget_service'; export { default as eventHub } from './event_hub'; export { default as getStateKey } from './stores/get_state_key'; export { default as stateMaps } from './stores/state_maps'; -export { default as SquashBeforeMerge } from './components/states/mr_widget_squash_before_merge.vue'; +export { default as SquashBeforeMerge } from './components/states/squash_before_merge.vue'; export { default as notify } from '../lib/utils/notify'; export { default as SourceBranchRemovalStatus } from './components/source_branch_removal_status.vue'; 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..0e445a29de4 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,16 @@ 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); + this.pollingInterval.destroy(); + this.deploymentsInterval.destroy(); + }, 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/ide/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue index 720ae11aaa6..8684005e0fb 100644 --- a/app/assets/javascripts/ide/components/changed_file_icon.vue +++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue @@ -3,7 +3,7 @@ import tooltip from '~/vue_shared/directives/tooltip'; import Icon from '~/vue_shared/components/icon.vue'; import { pluralize } from '~/lib/utils/text_utility'; import { __, sprintf } from '~/locale'; -import { getCommitIconMap } from '../utils'; +import { getCommitIconMap } from '~/ide/utils'; export default { components: { @@ -32,6 +32,11 @@ export default { required: false, default: false, }, + size: { + type: Number, + required: false, + default: 12, + }, }, computed: { changedIcon() { @@ -42,7 +47,7 @@ export default { return `${getCommitIconMap(this.file).icon}${suffix}`; }, changedIconClass() { - return `ide-${this.changedIcon} float-left`; + return `${this.changedIcon} float-left d-block`; }, tooltipTitle() { if (!this.showTooltip) return undefined; @@ -78,13 +83,30 @@ export default { :title="tooltipTitle" data-container="body" data-placement="right" - class="ide-file-changed-icon" + class="file-changed-icon ml-auto" > <icon v-if="showIcon" :name="changedIcon" - :size="12" + :size="size" :css-classes="changedIconClass" /> </span> </template> + +<style> +.file-addition, +.file-addition-solid { + color: #1aaa55; +} + +.file-modified, +.file-modified-solid { + color: #fc9403; +} + +.file-deletion, +.file-deletion-solid { + color: #db3b21; +} +</style> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue index f1ef50d0e3d..a07d63a495d 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue @@ -1,9 +1,11 @@ <script> +import { Link } from '@gitlab-org/gitlab-ui'; import Icon from '../../icon.vue'; import { numberToHumanSize } from '../../../../lib/utils/number_utils'; export default { components: { + 'gl-link': Link, Icon, }, props: { @@ -37,7 +39,7 @@ export default { ({{ fileSizeReadable }}) </template> </p> - <a + <gl-link :href="path" class="btn btn-default" rel="nofollow" @@ -49,7 +51,7 @@ export default { css-classes="float-left append-right-8" /> {{ __('Download') }} - </a> + </gl-link> </div> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue index a10deb93f0f..807e049caf6 100644 --- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue +++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue @@ -2,14 +2,14 @@ import axios from '~/lib/utils/axios_utils'; import { __ } from '~/locale'; import $ from 'jquery'; -import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; const { CancelToken } = axios; let axiosSource; export default { components: { - SkeletonLoadingContainer, + SkeletonLoading, }, props: { content: { @@ -81,7 +81,7 @@ export default { <div ref="markdown-preview" class="md md-previewer"> - <skeleton-loading-container v-if="isLoading" /> + <skeleton-loading v-if="isLoading" /> <div v-else v-html="previewContent"> 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..36a345130c0 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/file_row.vue @@ -0,0 +1,237 @@ +<script> +import Icon from '~/vue_shared/components/icon.vue'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import ChangedFileIcon from '~/vue_shared/components/changed_file_icon.vue'; + +export default { + name: 'FileRow', + components: { + FileIcon, + Icon, + ChangedFileIcon, + }, + props: { + file: { + type: Object, + required: true, + }, + level: { + type: Number, + required: true, + }, + extraComponent: { + type: Object, + required: false, + default: null, + }, + hideExtraOnTree: { + type: Boolean, + required: false, + default: false, + }, + showChangedIcon: { + type: Boolean, + required: false, + default: false, + }, + }, + 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); + }, + clickedFile(path) { + this.$emit('clickFile', 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}`); + + if (this.isBlob) this.clickedFile(this.file.path); + }, + 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 + v-if="!showChangedIcon || file.type === 'tree'" + :file-name="file.name" + :loading="file.loading" + :folder="isTree" + :opened="file.opened" + :size="16" + /> + <changed-file-icon + v-else + :file="file" + :size="16" + class="append-right-5" + /> + {{ file.name }} + </span> + <component + :is="extraComponent" + v-if="extraComponent && !(hideExtraOnTree && file.type === 'tree')" + :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" + :hide-extra-on-tree="hideExtraOnTree" + :extra-component="extraComponent" + :show-changed-icon="showChangedIcon" + @toggleTreeOpen="toggleTreeOpen" + @clickFile="clickedFile" + /> + </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/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index d62537021ca..10e8ddad9cd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -76,6 +76,7 @@ epics: this.enableAutocomplete, milestones: this.enableAutocomplete, labels: this.enableAutocomplete, + snippets: this.enableAutocomplete, }); }, beforeDestroy() { diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 8c22f3f6536..ccd53158820 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -18,6 +18,16 @@ required: true, }, }, + computed: { + mdTable() { + return [ + '| header | header |', + '| ------ | ------ |', + '| cell | cell |', + '| cell | cell |', + ].join('\n'); + }, + }, mounted() { $(document).on('markdown-preview:show.vue', this.previewMarkdownTab); $(document).on('markdown-preview:hide.vue', this.writeMarkdownTab); @@ -106,6 +116,12 @@ icon="code" /> <toolbar-button + tag="[{text}](url)" + tag-select="url" + button-title="Add a link" + icon="link" + /> + <toolbar-button :prepend="true" tag="* " button-title="Add a bullet list" @@ -123,6 +139,12 @@ button-title="Add a task list" icon="task-done" /> + <toolbar-button + :tag="mdTable" + :prepend="true" + :button-title="__('Add a table')" + icon="table" + /> <button v-tooltip aria-label="Go full screen" diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue index d63318f3da6..c45dafa9807 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue @@ -1,5 +1,10 @@ <script> + import { Link } from '@gitlab-org/gitlab-ui'; + export default { + components: { + 'gl-link': Link, + }, props: { markdownDocsPath: { type: String, @@ -28,30 +33,30 @@ <div class="comment-toolbar clearfix"> <div class="toolbar-text"> <template v-if="!hasQuickActionsDocsPath && markdownDocsPath"> - <a + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" > Markdown is supported - </a> + </gl-link> </template> <template v-if="hasQuickActionsDocsPath && markdownDocsPath"> - <a + <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1" > Markdown - </a> + </gl-link> and - <a + <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1" > quick actions - </a> + </gl-link> are supported </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 9f1e009efdd..bda33636369 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -27,6 +27,11 @@ required: false, default: '', }, + tagSelect: { + type: String, + required: false, + default: '', + }, prepend: { type: Boolean, required: false, @@ -40,6 +45,7 @@ <button v-tooltip :data-md-tag="tag" + :data-md-select="tagSelect" :data-md-block="tagBlock" :data-md-prepend="prepend" :title="buttonTitle" diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue index 2eb6c20b2c0..1d9c9220469 100644 --- a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue +++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue @@ -1,3 +1,14 @@ +<script> +import { SkeletonLoading } from '@gitlab-org/gitlab-ui'; + +export default { + name: 'SkeletonNote', + components: { + SkeletonLoading, + }, +}; +</script> + <template> <li class="timeline-entry note"> <div class="timeline-entry-inner"> @@ -6,20 +17,9 @@ <div class="timeline-content"> <div class="note-header"></div> <div class="note-body"> - <skeleton-loading-container /> + <skeleton-loading /> </div> </div> </div> </li> </template> - -<script> -import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; - -export default { - name: 'SkeletonNote', - components: { - skeletonLoadingContainer, - }, -}; -</script> 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/skeleton_loading_container.vue b/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue deleted file mode 100644 index 4a5ffbe5d5a..00000000000 --- a/app/assets/javascripts/vue_shared/components/skeleton_loading_container.vue +++ /dev/null @@ -1,37 +0,0 @@ -<script> - export default { - props: { - small: { - type: Boolean, - required: false, - default: false, - }, - lines: { - type: Number, - required: false, - default: 3, - }, - }, - computed: { - lineClasses() { - return new Array(this.lines).fill().map((_, i) => `skeleton-line-${i + 1}`); - }, - }, - }; -</script> - -<template> - <div - :class="{ - 'animation-container-small': small, - }" - class="animation-container" - > - <div - v-for="(css, index) in lineClasses" - :key="index" - :class="css" - > - </div> - </div> -</template> 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..ee3157bcb1b 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 @@ -18,12 +18,14 @@ */ +import { Link } from '@gitlab-org/gitlab-ui'; import userAvatarImage from './user_avatar_image.vue'; import tooltip from '../../directives/tooltip'; export default { name: 'UserAvatarLink', components: { + 'gl-link': Link, userAvatarImage, }, directives: { @@ -83,7 +85,7 @@ export default { </script> <template> - <a + <gl-link :href="linkHref" class="user-avatar-link"> <user-avatar-image @@ -94,10 +96,10 @@ export default { :tooltip-text="avatarTooltipText" :tooltip-placement="tooltipPlacement" /><span - v-tooltip v-if="shouldShowUsername" + v-tooltip :title="tooltipText" :tooltip-placement="tooltipPlacement" >{{ username }}</span> - </a> + </gl-link> </template> 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/application.scss b/app/assets/stylesheets/application.scss index f2950308019..ffe65ce780e 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -5,7 +5,6 @@ *= require jquery.atwho *= require select2 *= require_self - *= require dropzone/basic *= require cropper.css */ @@ -18,6 +17,7 @@ */ @import "../../../node_modules/pikaday/scss/pikaday"; +@import "../../../node_modules/dropzone/dist/basic.css"; /* * GitLab UI framework 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..c4296c7a88a 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; } } @@ -362,10 +360,14 @@ i { color: $gl-text-color-secondary; } + + svg { + fill: $gl-text-color-secondary; + } } .clone-dropdown-btn a { - color: $dropdown-link-color; + color: $gl-gray-700; &:hover { text-decoration: none; diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss index 0b9dff64b0b..9638fee6078 100644 --- a/app/assets/stylesheets/framework/calendar.scss +++ b/app/assets/stylesheets/framework/calendar.scss @@ -1,8 +1,7 @@ -.calender-block { +.calendar-block { padding-left: 0; padding-right: 0; border-top: 0; - direction: rtl; @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { overflow-x: auto; @@ -42,10 +41,13 @@ } .calendar-hint { - margin-top: -23px; - float: right; font-size: 12px; - direction: ltr; + + &.bottom-right { + direction: ltr; + margin-top: -23px; + float: right; + } } .pika-single.gitlab-theme { diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 72e27f9ad16..3c9505a21d6 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; @@ -433,6 +324,16 @@ img.emoji { word-wrap: break-word; } +.checkbox-icon-inline-wrapper { + .checkbox { + display: inline; + + label { + display: inline; + } + } +} + /** COMMON CLASSES **/ .prepend-top-0 { margin-top: 0; } .prepend-top-2 { margin-top: 2px; } diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss index e2bbcc67a67..2e7f25d975e 100644 --- a/app/assets/stylesheets/framework/contextual_sidebar.scss +++ b/app/assets/stylesheets/framework/contextual_sidebar.scss @@ -9,8 +9,7 @@ padding-left: $contextual-sidebar-width; } - .issues-bulk-update.right-sidebar.right-sidebar-expanded - .issuable-sidebar-header { + .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header { padding: 10px 0 15px; } } @@ -75,7 +74,7 @@ .nav-sidebar { transition: width $sidebar-transition-duration, left $sidebar-transition-duration; position: fixed; - z-index: 400; + z-index: 600; width: $contextual-sidebar-width; top: $header-height; bottom: 0; @@ -86,8 +85,7 @@ &:not(.sidebar-collapsed-desktop) { @media (min-width: map-get($grid-breakpoints, sm)) and (max-width: map-get($grid-breakpoints, sm)) { - box-shadow: inset -1px 0 0 $border-color, - 2px 1px 3px $dropdown-shadow-color; + box-shadow: inset -1px 0 0 $border-color, 2px 1px 3px $dropdown-shadow-color; } } @@ -113,7 +111,7 @@ } .avatar-container { - margin-right: 0; + margin: 0 auto; } } 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/gfm.scss b/app/assets/stylesheets/framework/gfm.scss index d2ba76f5160..50d4298d418 100644 --- a/app/assets/stylesheets/framework/gfm.scss +++ b/app/assets/stylesheets/framework/gfm.scss @@ -11,6 +11,10 @@ padding: 0 2px; background-color: $blue-100; border-radius: $border-radius-default; + + &.current-user { + background-color: $orange-100; + } } .gfm-color_chip { diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 11a30d83f03..c430009bfe0 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -529,9 +529,10 @@ } .header-user { - .dropdown-menu { + &.show .dropdown-menu { width: auto; min-width: unset; + max-height: 323px; margin-top: 4px; color: $gl-text-color; left: auto; @@ -542,6 +543,18 @@ .user-name { display: block; } + + .user-status-emoji { + margin-right: 0; + display: block; + vertical-align: text-top; + max-width: 148px; + font-size: 12px; + + gl-emoji { + font-size: $gl-font-size; + } + } } svg { @@ -573,3 +586,24 @@ } } } + +.set-user-status-modal { + .modal-body { + min-height: unset; + } + + .input-lg { + max-width: unset; + } + + .no-emoji-placeholder, + .clear-user-status { + svg { + fill: $gl-text-color-secondary; + } + } + + .emoji-menu-toggle-button { + @include emoji-menu-toggle-button; + } +} diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index f002edced8a..abd26e38d18 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -64,6 +64,7 @@ } } +.ci-status-icon-scheduled, .ci-status-icon-manual { svg { fill: $gl-text-color; 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..be41dbfc61f 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -20,20 +20,24 @@ display: inline-block; overflow-x: auto; border: 0; - border-color: $md-area-border; + border-color: $gl-gray-100; @supports (width: fit-content) { display: block; width: fit-content; } + tbody { + background-color: $white-light; + } + tr { th { - border-bottom: solid 2px $md-area-border; + border-bottom: solid 2px $gl-gray-100; } td { - border-color: $md-area-border; + border-color: $gl-gray-100; } } } @@ -266,3 +270,59 @@ border-radius: 50%; } } + +@mixin emoji-menu-toggle-button { + line-height: 1; + padding: 0; + min-width: 16px; + color: $gray-darkest; + fill: $gray-darkest; + + .fa { + position: relative; + font-size: 16px; + } + + svg { + @include btn-svg; + margin: 0; + } + + .award-control-icon-positive, + .award-control-icon-super-positive { + position: absolute; + top: 0; + left: 0; + opacity: 0; + } + + &:hover, + &.is-active { + .danger-highlight { + color: $red-500; + } + + .link-highlight { + color: $blue-600; + fill: $blue-600; + } + + .award-control-icon-neutral { + opacity: 0; + } + + .award-control-icon-positive { + opacity: 1; + } + } + + &.is-active { + .award-control-icon-positive { + opacity: 0; + } + + .award-control-icon-super-positive { + opacity: 1; + } + } +} diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss index 6244fb86fea..6d20c46b99d 100644 --- a/app/assets/stylesheets/framework/mobile.scss +++ b/app/assets/stylesheets/framework/mobile.scss @@ -4,11 +4,6 @@ margin-top: 20px; } - .container-fluid { - padding-left: 5px; - padding-right: 5px; - } - .nav-links > li > a { padding: 10px; font-size: 12px; @@ -49,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/modal.scss b/app/assets/stylesheets/framework/modal.scss index 7d53a631cdf..7e30747963a 100644 --- a/app/assets/stylesheets/framework/modal.scss +++ b/app/assets/stylesheets/framework/modal.scss @@ -19,6 +19,17 @@ } } + // leave enough space for the close icon + .modal-title { + &.mw-100, + &.w-100 { + // after upgrading to Bootstrap 4.2 we can use $modal-header-padding-x here + // https://github.com/twbs/bootstrap/pull/26976 + margin-right: -2rem; + padding-right: 2rem; + } + } + .page-title { margin-top: 0; } @@ -59,7 +70,7 @@ } @include media-breakpoint-up(sm) { - .btn:first-of-type { + .btn:nth-child(1) { margin-left: auto; } } 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..29a9c076cdf 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 60 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..6d891e21556 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; @@ -286,7 +286,7 @@ body { } .page-title { - margin-top: $gl-padding; + margin: #{2 * $grid-size} 0; line-height: 1.3; font-size: 1.25em; font-weight: $gl-font-weight-bold; diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d76f5cbd9ff..b7a95f604b8 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; @@ -47,7 +55,7 @@ $blue-50: #f6fafe; $blue-100: #e4f0fb; $blue-200: #b8d6f4; $blue-300: #73afea; -$blue-400: #2e87e0; +$blue-400: #418cd8; $blue-500: #1f78d1; $blue-600: #1b69b6; $blue-700: #17599c; @@ -59,7 +67,7 @@ $orange-50: #fffaf4; $orange-100: #fff1de; $orange-200: #fed69f; $orange-300: #fdbc60; -$orange-400: #fca121; +$orange-400: #fca429; $orange-500: #fc9403; $orange-600: #de7e00; $orange-700: #c26700; @@ -70,7 +78,7 @@ $orange-950: #592800; $red-50: #fef6f5; $red-100: #fbe5e1; $red-200: #f2b4a9; -$red-300: #e67664; +$red-300: #ea8271; $red-400: #e05842; $red-500: #db3b21; $red-600: #c0341d; @@ -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%); @@ -332,7 +314,8 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%); $monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, - 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; + 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', + 'Noto Color Emoji'; /* * Dropdowns @@ -341,12 +324,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 +400,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 +411,6 @@ $identicon-indigo: #e8eaf6; $identicon-blue: #e3f2fd; $identicon-teal: #e0f2f1; $identicon-orange: #fbe9e7; -$identicon-fg-color: #555555; /* * Calendar @@ -505,16 +479,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 +499,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 +509,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 +532,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 +555,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 +566,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 +599,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); @@ -703,5 +635,4 @@ Modals */ $modal-body-height: 134px; - $priority-label-empty-state-width: 114px; diff --git a/app/assets/stylesheets/framework/variables_overrides.scss b/app/assets/stylesheets/framework/variables_overrides.scss index 7d90452e1f4..759b4f333ca 100644 --- a/app/assets/stylesheets/framework/variables_overrides.scss +++ b/app/assets/stylesheets/framework/variables_overrides.scss @@ -18,3 +18,4 @@ $success: $green-500; $info: $blue-500; $warning: $orange-500; $danger: $red-500; +$zindex-modal-backdrop: 1040; 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..07d82e984ba 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 { @@ -604,21 +517,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding; } } -.ide-file-addition, -.ide-file-addition-solid { - color: $green-500; -} - -.ide-file-modified, -.ide-file-modified-solid { - color: $orange-500; -} - -.ide-file-deletion, -.ide-file-deletion-solid { - color: $red-500; -} - .multi-file-commit-list-collapsed { display: flex; flex-direction: column; @@ -638,8 +536,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 +544,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 +578,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 +586,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 +709,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 +1302,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 +1351,46 @@ $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; +} + +.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/boards.scss b/app/assets/stylesheets/pages/boards.scss index 69d7de886b4..b3c5c693824 100644 --- a/app/assets/stylesheets/pages/boards.scss +++ b/app/assets/stylesheets/pages/boards.scss @@ -136,15 +136,23 @@ right: 0; bottom: 0; left: 0; + + button { + display: none; + } } .board-title { padding: 0; border-bottom: 0; + justify-content: center; > span { + width: 100%; + margin-top: -12px; display: block; - transform: rotate(90deg) translate(35px, 10px); + transform: rotate(90deg) translate(35px, 0); + overflow: initial; } } @@ -265,7 +273,7 @@ margin-bottom: 0; padding: 5px; list-style: none; - overflow-y: scroll; + overflow-y: auto; overflow-x: hidden; } 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/builds.scss b/app/assets/stylesheets/pages/builds.scss index 14ba8b1df83..ed877f625b5 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -328,23 +328,6 @@ } } - .build-dropdown { - margin: $gl-padding 0; - padding: 0; - - .dropdown-menu-toggle { - margin-top: #{$gl-padding / 2}; - } - - svg { - position: relative; - top: 3px; - margin-right: 3px; - width: 14px; - height: 14px; - } - } - .builds-container { background-color: $white-light; border-top: 1px solid $border-color; @@ -381,15 +364,11 @@ position: absolute; left: 15px; top: 20px; - display: none; + display: block; } &.active { font-weight: $gl-font-weight-bold; - - .icon-arrow-right { - display: block; - } } &.retried { 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/commits.scss b/app/assets/stylesheets/pages/commits.scss index 10764e0f3df..628a4ca38da 100644 --- a/app/assets/stylesheets/pages/commits.scss +++ b/app/assets/stylesheets/pages/commits.scss @@ -223,6 +223,7 @@ } } +.clipboard-group, .commit-sha-group { display: inline-flex; diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss index 7d7143631f2..17b02c6e31e 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; @@ -570,8 +571,6 @@ } .files { - margin-top: 1px; - .diff-file:last-child { margin-bottom: 0; } @@ -749,6 +748,10 @@ left: $gl-padding; } + .dropdown-input .dropdown-input-search { + pointer-events: all; + } + .diff-changed-file { display: flex; padding-top: 8px; @@ -982,3 +985,64 @@ .discussion-body .image .frame { position: relative; } + +.diff-tree-list { + width: 320px; +} + +.diff-files-holder { + flex: 1; + min-width: 0; +} + +.compare-versions-container { + min-width: 0; +} + +.tree-list-holder { + position: -webkit-sticky; + position: sticky; + top: 100px; + max-height: calc(100vh - 100px); + padding-right: $gl-padding; + + .file-row { + margin-left: 0; + margin-right: 0; + } + + .with-performance-bar & { + top: 135px; + } +} + +.tree-list-scroll { + max-height: 100%; + padding-top: $grid-size; + padding-bottom: $grid-size; + border-top: 1px solid $border-color; + border-bottom: 1px solid $border-color; + overflow-y: scroll; + overflow-x: auto; +} + +.tree-list-search .form-control { + padding-left: 30px; +} + +.tree-list-icon { + top: 50%; + left: 10px; + transform: translateY(-50%); + + &, + svg { + fill: $gl-text-color-tertiary; + } +} + +.tree-list-clear-icon { + right: 10px; + left: auto; + line-height: 0; +} 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..45382d4ea43 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; } @@ -723,6 +723,17 @@ align-items: center; padding: 16px; z-index: 199; + white-space: nowrap; + + .dropdown-menu-toggle { + width: auto; + max-width: 170px; + + svg { + top: 10px; + right: 8px; + } + } } .content-block { @@ -910,7 +921,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..bfba1bf1b2b 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; @@ -357,6 +343,10 @@ ul.notes { &.parallel { border-width: 1px; + + &.new { + border-right-width: 0; + } } .discussion-notes { @@ -533,59 +523,7 @@ ul.notes { } .note-action-button { - line-height: 1; - padding: 0; - min-width: 16px; - color: $gray-darkest; - fill: $gray-darkest; - - .fa { - position: relative; - font-size: 16px; - } - - svg { - @include btn-svg; - margin: 0; - } - - .award-control-icon-positive, - .award-control-icon-super-positive { - position: absolute; - top: 0; - left: 0; - opacity: 0; - } - - &:hover, - &.is-active { - .danger-highlight { - color: $red-500; - } - - .link-highlight { - color: $blue-600; - fill: $blue-600; - } - - .award-control-icon-neutral { - opacity: 0; - } - - .award-control-icon-positive { - opacity: 1; - } - } - - &.is-active { - .award-control-icon-positive { - opacity: 0; - } - - .award-control-icon-super-positive { - opacity: 1; - } - } + @include emoji-menu-toggle-button; } .discussion-toggle-button { @@ -804,7 +742,7 @@ ul.notes { padding-top: 0; .discussion-wrapper { - border-color: transparent; + border: 0; } } } diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss index 8bb8b83dc5e..14395cc59b0 100644 --- a/app/assets/stylesheets/pages/pipelines.scss +++ b/app/assets/stylesheets/pages/pipelines.scss @@ -760,6 +760,7 @@ } &.ci-status-icon-canceled, + &.ci-status-icon-scheduled, &.ci-status-icon-disabled, &.ci-status-icon-not-found, &.ci-status-icon-manual { diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index 17f34319050..f084adaf5d3 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -81,14 +81,14 @@ // Middle dot divider between each element in a list of items. .middle-dot-divider { &::after { - content: "\00B7"; // Middle Dot + content: '\00B7'; // Middle Dot padding: 0 6px; font-weight: $gl-font-weight-bold; } &:last-child { &::after { - content: ""; + content: ''; padding: 0; } } @@ -191,7 +191,6 @@ @include media-breakpoint-down(xs) { width: auto; } - } .profile-crop-image-container { @@ -215,7 +214,6 @@ } } - .user-profile { .cover-controls a { margin-left: 5px; @@ -279,6 +277,10 @@ table.u2f-registrations { } } +.codes { + padding-top: 14px; +} + .oauth-application-show { .scope-name { font-weight: $gl-font-weight-bold; @@ -414,7 +416,7 @@ table.u2f-registrations { } &.unverified { - @include status-color($gray-dark, color("gray"), $common-gray-dark); + @include status-color($gray-dark, color('gray'), $common-gray-dark); } } } @@ -427,7 +429,7 @@ table.u2f-registrations { } .emoji-menu-toggle-button { - @extend .note-action-button; + @include emoji-menu-toggle-button; .no-emoji-placeholder { position: relative; diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index a95e78931b1..da3d8aa53ad 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; } } @@ -769,6 +830,14 @@ } } +.repository-language-bar-tooltip-language { + font-weight: $gl-font-weight-bold; +} + +.repository-language-bar-tooltip-share { + color: $theme-gray-400; +} + pre.light-well { border-color: $well-light-border; } @@ -868,7 +937,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/status.scss b/app/assets/stylesheets/pages/status.scss index 620297e589d..7d59dd3b5d1 100644 --- a/app/assets/stylesheets/pages/status.scss +++ b/app/assets/stylesheets/pages/status.scss @@ -27,6 +27,7 @@ &.ci-canceled, &.ci-disabled, + &.ci-scheduled, &.ci-manual { color: $gl-text-color; border-color: $gl-text-color; 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..59fdbf31fe9 100644 --- a/app/assets/stylesheets/performance_bar.scss +++ b/app/assets/stylesheets/performance_bar.scss @@ -1,4 +1,5 @@ @import 'framework/variables'; +@import 'framework/variables_overrides'; @import 'peek/views/rblineprof'; #js-peek { @@ -6,15 +7,15 @@ left: 0; top: 0; width: 100%; - z-index: 1039; + z-index: #{$zindex-modal-backdrop + 1}; 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 +54,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..8040a14ef56 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 @@ -28,8 +67,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController end end - def reset_runners_token + def reset_registration_token @application_setting.reset_runners_registration_token! + flash[:notice] = 'New runners registration token has been generated!' redirect_to admin_runners_path end @@ -76,14 +116,20 @@ 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, disabled_oauth_sign_in_sources: [], import_sources: [], repository_storages: [], - restricted_visibility_levels: [], - sidekiq_throttling_queues: [] + restricted_visibility_levels: [] ] end end 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..d7dbc712743 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' @@ -10,6 +12,7 @@ class ApplicationController < ActionController::Base include WorkhorseHelper include EnforcesTwoFactorAuthentication include WithPerformanceBar + include InvalidUTF8ErrorHandler before_action :authenticate_sessionless_user! before_action :authenticate_user! @@ -22,6 +25,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 @@ -105,11 +109,21 @@ class ApplicationController < ActionController::Base request.env['rack.session.options'][:expire_after] = Settings.gitlab['unauthenticated_session_expire_delay'] end + def render(*args) + super.tap do + # Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse + if response.content_type == 'text/html' && (400..599).cover?(response.status) + response.headers['X-GitLab-Custom-Error'] = '1' + end + end + end + protected def append_info_to_payload(payload) super + payload[:ua] = request.env["HTTP_USER_AGENT"] payload[:remote_ip] = request.remote_ip logged_user = auth_user @@ -268,9 +282,10 @@ class ApplicationController < ActionController::Base end def event_filter - # Split using comma to maintain backward compatibility Ex/ "filter1,filter2" - filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present? - @event_filter ||= EventFilter.new(filters) + @event_filter ||= + EventFilter.new(params[:event_filter].presence || cookies[:event_filter]).tap do |new_event_filter| + cookies[:event_filter] = new_event_filter.filter + end end # JSON for infinite scroll via Pager object @@ -433,4 +448,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/invalid_utf8_error_handler.rb b/app/controllers/concerns/invalid_utf8_error_handler.rb new file mode 100644 index 00000000000..44c6d6b0da0 --- /dev/null +++ b/app/controllers/concerns/invalid_utf8_error_handler.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module InvalidUTF8ErrorHandler + extend ActiveSupport::Concern + + included do + rescue_from ArgumentError, with: :handle_invalid_utf8 + end + + private + + def handle_invalid_utf8(error) + if error.message == "invalid byte sequence in UTF-8" + render_412 + else + raise(error) + end + end + + def render_412 + respond_to do |format| + format.html { render "errors/precondition_failed", layout: "errors", status: 412 } + format.js { render json: { error: 'Invalid UTF-8' }, status: :precondition_failed, content_type: 'application/json' } + format.any { head :precondition_failed } + end + 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 22b39f47bf0..5217b4be928 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -1,5 +1,8 @@ +# frozen_string_literal: true + module IssuableCollections extend ActiveSupport::Concern + include CookiesHelper include SortingHelper include Gitlab::IssuableMetadata include Gitlab::Utils::StrongMemoize @@ -47,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? @@ -80,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 @@ -100,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 @@ -107,11 +114,14 @@ module IssuableCollections end def set_sort_order_from_cookie - cookies[remember_sorting_key] = params[:sort] if params[:sort].present? + sort_param = params[:sort] if params[:sort].present? # fallback to legacy cookie value for backward compatibility - cookies[remember_sorting_key] ||= cookies['issuable_sort'] - cookies[remember_sorting_key] = update_cookie_value(cookies[remember_sorting_key]) - params[:sort] = cookies[remember_sorting_key] + sort_param ||= cookies['issuable_sort'] + sort_param ||= cookies[remember_sorting_key] + + sort_value = update_cookie_value(sort_param) + set_secure_cookie(remember_sorting_key, sort_value) + params[:sort] = sort_value end def remember_sorting_key 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/labels_as_hash.rb b/app/controllers/concerns/labels_as_hash.rb new file mode 100644 index 00000000000..1171aa9cf44 --- /dev/null +++ b/app/controllers/concerns/labels_as_hash.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module LabelsAsHash + extend ActiveSupport::Concern + + def labels_as_hash(target = nil, params = {}) + available_labels = LabelsFinder.new( + current_user, + params + ).execute + + label_hashes = available_labels.as_json(only: [:title, :color]) + + if target&.respond_to?(:labels) + already_set_labels = available_labels & target.labels + if already_set_labels.present? + titles = already_set_labels.map(&:title) + label_hashes.each do |hash| + if titles.include?(hash['title']) + hash[:set] = true + end + end + end + end + + label_hashes + end +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..77c3d476ac6 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 @@ -8,11 +10,7 @@ module WithPerformanceBar def peek_enabled? return false unless Gitlab::PerformanceBar.enabled?(current_user) - if RequestStore.active? - RequestStore.fetch(:peek_enabled) { cookie_or_default_value } - else - cookie_or_default_value - end + Gitlab::SafeRequestStore.fetch(:peek_enabled) { cookie_or_default_value } end private 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/application_controller.rb b/app/controllers/dashboard/application_controller.rb index 9fb5c525425..cee0753a021 100644 --- a/app/controllers/dashboard/application_controller.rb +++ b/app/controllers/dashboard/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::ApplicationController < ApplicationController include ControllerWithCrossProjectAccessCheck diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb index 79f563bef86..f82cde8e10a 100644 --- a/app/controllers/dashboard/groups_controller.rb +++ b/app/controllers/dashboard/groups_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::GroupsController < Dashboard::ApplicationController include GroupTree diff --git a/app/controllers/dashboard/labels_controller.rb b/app/controllers/dashboard/labels_controller.rb index 9dcb3a0eb6d..89d87c2d5c8 100644 --- a/app/controllers/dashboard/labels_controller.rb +++ b/app/controllers/dashboard/labels_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::LabelsController < Dashboard::ApplicationController def index respond_to do |format| diff --git a/app/controllers/dashboard/milestones_controller.rb b/app/controllers/dashboard/milestones_controller.rb index 0469e7e1e1f..6e17bc212e4 100644 --- a/app/controllers/dashboard/milestones_controller.rb +++ b/app/controllers/dashboard/milestones_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::MilestonesController < Dashboard::ApplicationController include MilestoneActions @@ -22,7 +24,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..e9686ed8d06 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::ProjectsController < Dashboard::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess @@ -23,6 +25,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 +41,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord private @@ -46,6 +50,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 +60,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/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb index 0ba97e4fd59..161c22046f9 100644 --- a/app/controllers/dashboard/snippets_controller.rb +++ b/app/controllers/dashboard/snippets_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::SnippetsController < Dashboard::ApplicationController skip_cross_project_access_check :index diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index bd7111e28bc..b82caf30a91 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Dashboard::TodosController < Dashboard::ApplicationController include ActionView::Helpers::NumberHelper @@ -73,6 +75,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 +94,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..c032fb2efb5 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 @@ -38,7 +40,7 @@ class DashboardController < Dashboard::ApplicationController end @events = EventCollection - .new(projects, offset: params[:offset].to_i, filter: @event_filter) + .new(projects, offset: params[:offset].to_i, filter: event_filter) .to_a Events::RenderService.new(current_user).execute(@events) diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb index baf54520b9c..8eee3742d89 100644 --- a/app/controllers/explore/application_controller.rb +++ b/app/controllers/explore/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Explore::ApplicationController < ApplicationController skip_before_action :authenticate_user! diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb index fa0a0f68fbc..67db797b80a 100644 --- a/app/controllers/explore/groups_controller.rb +++ b/app/controllers/explore/groups_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Explore::GroupsController < Explore::ApplicationController include GroupTree diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb index c7273606a85..7ecbc32cf4e 100644 --- a/app/controllers/explore/projects_controller.rb +++ b/app/controllers/explore/projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Explore::ProjectsController < Explore::ApplicationController include ParamsBackwardCompatibility include RendersMemberAccess @@ -34,6 +36,7 @@ class Explore::ProjectsController < Explore::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def starred @projects = load_projects.reorder('star_count DESC') @@ -46,9 +49,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 +63,5 @@ class Explore::ProjectsController < Explore::ApplicationController prepare_projects_for_rendering(projects) end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb index d3f0e033068..76ed142c939 100644 --- a/app/controllers/explore/snippets_controller.rb +++ b/app/controllers/explore/snippets_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Explore::SnippetsController < Explore::ApplicationController def index @snippets = SnippetsFinder.new(current_user).execute diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb index 5551057ff55..dd9f5af61b3 100644 --- a/app/controllers/google_api/authorizations_controller.rb +++ b/app/controllers/google_api/authorizations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module GoogleApi class AuthorizationsController < ApplicationController def callback 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/application_controller.rb b/app/controllers/groups/application_controller.rb index 62213561898..5f92333c2c3 100644 --- a/app/controllers/groups/application_controller.rb +++ b/app/controllers/groups/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::ApplicationController < ApplicationController include RoutableActions include ControllerWithCrossProjectAccessCheck diff --git a/app/controllers/groups/avatars_controller.rb b/app/controllers/groups/avatars_controller.rb index 35a61b359c8..8e4dc2bb6e9 100644 --- a/app/controllers/groups/avatars_controller.rb +++ b/app/controllers/groups/avatars_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::AvatarsController < Groups::ApplicationController before_action :authorize_admin_group! diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb index e892d1f8dbf..8d259b4052e 100644 --- a/app/controllers/groups/boards_controller.rb +++ b/app/controllers/groups/boards_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::BoardsController < Groups::ApplicationController include BoardsResponses diff --git a/app/controllers/groups/children_controller.rb b/app/controllers/groups/children_controller.rb index 0e8125d6113..d549f793ad7 100644 --- a/app/controllers/groups/children_controller.rb +++ b/app/controllers/groups/children_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Groups class ChildrenController < Groups::ApplicationController before_action :group diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb index 7dc51f4c357..0bc082246a1 100644 --- a/app/controllers/groups/group_members_controller.rb +++ b/app/controllers/groups/group_members_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::GroupMembersController < Groups::ApplicationController include MembershipActions include MembersPresentation diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb index 863f50e8e66..26768c628ca 100644 --- a/app/controllers/groups/labels_controller.rb +++ b/app/controllers/groups/labels_controller.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + 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 +12,11 @@ class Groups::LabelsController < Groups::ApplicationController def index respond_to do |format| format.html do - @labels = @group.labels.page(params[:page]) + @labels = GroupLabelsFinder + .new(current_user, @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 @@ -113,7 +115,11 @@ class Groups::LabelsController < Groups::ApplicationController group_id: @group.id, only_group_labels: params[:only_group_labels], include_ancestor_groups: params[:include_ancestor_groups], - include_descendant_groups: params[:include_descendant_groups] - ).execute + 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/milestones_controller.rb b/app/controllers/groups/milestones_controller.rb index 6bdc0f79ef2..a7cee426cf1 100644 --- a/app/controllers/groups/milestones_controller.rb +++ b/app/controllers/groups/milestones_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::MilestonesController < Groups::ApplicationController include MilestoneActions diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index 1036b4e6ed3..dd8fbf7a029 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::RunnersController < Groups::ApplicationController # Proper policies should be implemented per # https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 diff --git a/app/controllers/groups/settings/badges_controller.rb b/app/controllers/groups/settings/badges_controller.rb deleted file mode 100644 index ccbd0a3bc02..00000000000 --- a/app/controllers/groups/settings/badges_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Groups - module Settings - class BadgesController < Groups::ApplicationController - include API::Helpers::RelatedResourcesHelpers - - before_action :authorize_admin_group! - - def index - @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) - end - end - end -end diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 4bf6a2a3ad1..93f3eb2be6d 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Groups module Settings class CiCdController < Groups::ApplicationController @@ -8,6 +10,13 @@ module Groups define_secret_variables end + def reset_registration_token + @group.reset_runners_token! + + flash[:notice] = 'New runners registration token has been generated!' + redirect_to group_settings_ci_cd_path + end + private def define_secret_variables diff --git a/app/controllers/groups/shared_projects_controller.rb b/app/controllers/groups/shared_projects_controller.rb index 7dec1f5f402..30b7bfc70ae 100644 --- a/app/controllers/groups/shared_projects_controller.rb +++ b/app/controllers/groups/shared_projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Groups class SharedProjectsController < Groups::ApplicationController respond_to :json diff --git a/app/controllers/groups/uploads_controller.rb b/app/controllers/groups/uploads_controller.rb index 74760194a1f..7e5cdae0ce3 100644 --- a/app/controllers/groups/uploads_controller.rb +++ b/app/controllers/groups/uploads_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Groups::UploadsController < Groups::ApplicationController include UploadsActions include WorkhorseRequest diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb index 4d8a20de017..4f641de0357 100644 --- a/app/controllers/groups/variables_controller.rb +++ b/app/controllers/groups/variables_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Groups class VariablesController < Groups::ApplicationController before_action :authorize_admin_build! diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 83169636ccf..062c8c4e9e1 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + class GroupsController < Groups::ApplicationController + include API::Helpers::RelatedResourcesHelpers include IssuesAction include MergeRequestsAction include ParamsBackwardCompatibility @@ -16,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 @@ -52,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 @@ -77,6 +76,7 @@ class GroupsController < Groups::ApplicationController end def edit + @badge_api_endpoint = expose_url(api_v4_groups_badges_path(id: @group.id)) end def projects @@ -99,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) @@ -111,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]) @@ -124,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) @@ -158,6 +162,7 @@ class GroupsController < Groups::ApplicationController ] end + # rubocop: disable CodeReuse/ActiveRecord def load_events params[:sort] ||= 'latest_activity_desc' @@ -177,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..042b6b1264f 100644 --- a/app/controllers/import/base_controller.rb +++ b/app/controllers/import/base_controller.rb @@ -1,16 +1,22 @@ +# frozen_string_literal: true + 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..1b30b4dda36 100644 --- a/app/controllers/import/bitbucket_controller.rb +++ b/app/controllers/import/bitbucket_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::BitbucketController < Import::BaseController before_action :verify_bitbucket_import_enabled before_action :bitbucket_auth, except: :callback @@ -16,6 +18,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 +30,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..5a439e6de78 100644 --- a/app/controllers/import/fogbugz_controller.rb +++ b/app/controllers/import/fogbugz_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::FogbugzController < Import::BaseController before_action :verify_fogbugz_import_enabled before_action :user_map, only: [:new_user_map, :create_user_map] @@ -39,6 +41,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 +54,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/gitea_controller.rb b/app/controllers/import/gitea_controller.rb index fbd851c64a7..382c684a408 100644 --- a/app/controllers/import/gitea_controller.rb +++ b/app/controllers/import/gitea_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::GiteaController < Import::GithubController def new if session[access_token_key].present? && session[host_key].present? diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb index c9870332c0f..1dfa814cdd5 100644 --- a/app/controllers/import/github_controller.rb +++ b/app/controllers/import/github_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::GithubController < Import::BaseController before_action :verify_import_enabled before_action :provider_auth, only: [:status, :jobs, :create] @@ -22,6 +24,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 +32,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 +108,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..498de0b07b8 100644 --- a/app/controllers/import/gitlab_controller.rb +++ b/app/controllers/import/gitlab_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::GitlabController < Import::BaseController MAX_PROJECT_PAGES = 15 PER_PAGE_PROJECTS = 100 @@ -12,6 +14,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 +23,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..354fba5d204 100644 --- a/app/controllers/import/gitlab_projects_controller.rb +++ b/app/controllers/import/gitlab_projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::GitlabProjectsController < Import::BaseController before_action :whitelist_query_limiting, only: [:create] before_action :verify_gitlab_project_import_enabled @@ -11,7 +13,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 +31,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..331f06c3dd6 100644 --- a/app/controllers/import/google_code_controller.rb +++ b/app/controllers/import/google_code_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::GoogleCodeController < Import::BaseController before_action :verify_google_code_import_enabled before_action :user_map, only: [:new_user_map, :create_user_map] @@ -65,6 +67,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 +81,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..320cd45b925 100644 --- a/app/controllers/import/manifest_controller.rb +++ b/app/controllers/import/manifest_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Import::ManifestController < Import::BaseController before_action :whitelist_query_limiting, only: [:create] before_action :verify_import_enabled @@ -6,6 +8,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 +17,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 +68,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 +82,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/ldap/omniauth_callbacks_controller.rb b/app/controllers/ldap/omniauth_callbacks_controller.rb index fb24edb8602..5e872804448 100644 --- a/app/controllers/ldap/omniauth_callbacks_controller.rb +++ b/app/controllers/ldap/omniauth_callbacks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Ldap::OmniauthCallbacksController < OmniauthCallbacksController extend ::Gitlab::Utils::Override 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..b50f140dc80 100644 --- a/app/controllers/oauth/applications_controller.rb +++ b/app/controllers/oauth/applications_controller.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + class Oauth::ApplicationsController < Doorkeeper::ApplicationsController include Gitlab::GonHelper include Gitlab::Allowable 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/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb index 05190103767..894a6a431e3 100644 --- a/app/controllers/oauth/authorizations_controller.rb +++ b/app/controllers/oauth/authorizations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController layout 'profile' diff --git a/app/controllers/oauth/authorized_applications_controller.rb b/app/controllers/oauth/authorized_applications_controller.rb index 656107d2b26..a59ade559b3 100644 --- a/app/controllers/oauth/authorized_applications_controller.rb +++ b/app/controllers/oauth/authorized_applications_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Oauth::AuthorizedApplicationsController < Doorkeeper::AuthorizedApplicationsController include PageLayoutHelper 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..cb3180f4196 100644 --- a/app/controllers/profiles/accounts_controller.rb +++ b/app/controllers/profiles/accounts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::AccountsController < Profiles::ApplicationController include AuthHelper @@ -5,6 +7,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 +22,5 @@ class Profiles::AccountsController < Profiles::ApplicationController redirect_to profile_account_path end + # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/profiles/active_sessions_controller.rb index f1e77d68acd..efe7ede5efa 100644 --- a/app/controllers/profiles/active_sessions_controller.rb +++ b/app/controllers/profiles/active_sessions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::ActiveSessionsController < Profiles::ApplicationController def index @sessions = ActiveSession.list(current_user) diff --git a/app/controllers/profiles/application_controller.rb b/app/controllers/profiles/application_controller.rb index c8be288b9a0..52b046ef64f 100644 --- a/app/controllers/profiles/application_controller.rb +++ b/app/controllers/profiles/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::ApplicationController < ApplicationController layout 'profile' end diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb index 4f030ded80f..3378a09628c 100644 --- a/app/controllers/profiles/avatars_controller.rb +++ b/app/controllers/profiles/avatars_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::AvatarsController < Profiles::ApplicationController def destroy @user = current_user diff --git a/app/controllers/profiles/chat_names_controller.rb b/app/controllers/profiles/chat_names_controller.rb index a186c5f36a8..2e78b9e6dc7 100644 --- a/app/controllers/profiles/chat_names_controller.rb +++ b/app/controllers/profiles/chat_names_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::ChatNamesController < Profiles::ApplicationController before_action :chat_name_token, only: [:new] before_action :chat_name_params, only: [:new, :create, :deny] diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb index a39824ec9c8..503eda250b4 100644 --- a/app/controllers/profiles/emails_controller.rb +++ b/app/controllers/profiles/emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::EmailsController < Profiles::ApplicationController before_action :find_email, only: [:destroy, :resend_confirmation_instructions] diff --git a/app/controllers/profiles/gpg_keys_controller.rb b/app/controllers/profiles/gpg_keys_controller.rb index c32507756e8..8c34a66c374 100644 --- a/app/controllers/profiles/gpg_keys_controller.rb +++ b/app/controllers/profiles/gpg_keys_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::GpgKeysController < Profiles::ApplicationController before_action :set_gpg_key, only: [:destroy, :revoke] diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb index 6035258667e..01801c31327 100644 --- a/app/controllers/profiles/keys_controller.rb +++ b/app/controllers/profiles/keys_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::KeysController < Profiles::ApplicationController skip_before_action :authenticate_user!, only: [:get_keys] diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb index 8a38ba65d4c..b719b70c56e 100644 --- a/app/controllers/profiles/notifications_controller.rb +++ b/app/controllers/profiles/notifications_controller.rb @@ -1,10 +1,14 @@ +# frozen_string_literal: true + 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/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb index b8ccc6e3c99..a0391d677c4 100644 --- a/app/controllers/profiles/passwords_controller.rb +++ b/app/controllers/profiles/passwords_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::PasswordsController < Profiles::ApplicationController skip_before_action :check_password_expiration, only: [:new, :create] skip_before_action :check_two_factor_requirement, only: [:new, :create] diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb index 346eab4ba19..4b6ec2697b7 100644 --- a/app/controllers/profiles/personal_access_tokens_controller.rb +++ b/app/controllers/profiles/personal_access_tokens_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::PersonalAccessTokensController < Profiles::ApplicationController def index set_index_vars @@ -38,6 +40,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 +49,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/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb index ed0f98179eb..37ac11dc6a1 100644 --- a/app/controllers/profiles/preferences_controller.rb +++ b/app/controllers/profiles/preferences_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::PreferencesController < Profiles::ApplicationController before_action :user diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 29ff18a1219..ba94196b2f9 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::TwoFactorAuthsController < Profiles::ApplicationController skip_before_action :check_two_factor_requirement @@ -30,7 +32,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController unless two_factor_grace_period_expired? grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours - flash.now[:alert] << " You need to do this before #{l(grace_period_deadline)}." + flash.now[:alert] = flash.now[:alert] + " You need to do this before #{l(grace_period_deadline)}." end end diff --git a/app/controllers/profiles/u2f_registrations_controller.rb b/app/controllers/profiles/u2f_registrations_controller.rb index e3d7737f44a..e6a154fb6aa 100644 --- a/app/controllers/profiles/u2f_registrations_controller.rb +++ b/app/controllers/profiles/u2f_registrations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Profiles::U2fRegistrationsController < Profiles::ApplicationController def destroy u2f_registration = current_user.u2f_registrations.find(params[:id]) 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/application_controller.rb b/app/controllers/projects/application_controller.rb index b4f814fd3a4..a2bdcaefa9b 100644 --- a/app/controllers/projects/application_controller.rb +++ b/app/controllers/projects/application_controller.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + class Projects::ApplicationController < ApplicationController + include CookiesHelper include RoutableActions include ChecksCollaboration @@ -74,7 +77,7 @@ class Projects::ApplicationController < ApplicationController end def apply_diff_view_cookie! - cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? + set_secure_cookie(:diff_view, params.delete(:view), permanent: true) if params[:view].present? end def require_pages_enabled! diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 6484a713f8e..d0f59aa8162 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ArtifactsController < Projects::ApplicationController include ExtractsPath include RendersBlob @@ -12,6 +14,8 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :entry, only: [:file] def download + return render_404 unless artifacts_file + send_upload(artifacts_file, attachment: artifacts_file.filename) end @@ -82,19 +86,23 @@ 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 + @artifacts_file ||= build.artifacts_file_for_type(params[:file_type] || :archive) end def entry diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb index a8f73ed5cb0..d386fb63d9f 100644 --- a/app/controllers/projects/autocomplete_sources_controller.rb +++ b/app/controllers/projects/autocomplete_sources_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::AutocompleteSourcesController < Projects::ApplicationController before_action :load_autocomplete_service, except: [:members] @@ -25,6 +27,10 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController render json: @autocomplete_service.commands(target, params[:type]) end + def snippets + render json: @autocomplete_service.snippets + end + private def load_autocomplete_service diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb index 878c82cd183..1c385c0e15a 100644 --- a/app/controllers/projects/avatars_controller.rb +++ b/app/controllers/projects/avatars_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::AvatarsController < Projects::ApplicationController include SendsBlob diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb index 06ba73d8e8d..c24bf211760 100644 --- a/app/controllers/projects/badges_controller.rb +++ b/app/controllers/projects/badges_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::BadgesController < Projects::ApplicationController layout 'project_settings' before_action :authorize_admin_project!, only: [:index] diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb index 6461eeac11c..9076bdb9f04 100644 --- a/app/controllers/projects/blame_controller.rb +++ b/app/controllers/projects/blame_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for viewing a file's blame class Projects::BlameController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index ebc61264b39..92d26a13da9 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for viewing a file's blame class Projects::BlobController < Projects::ApplicationController include ExtractsPath @@ -127,7 +129,7 @@ class Projects::BlobController < Projects::ApplicationController add_match_line - render json: @lines + render json: DiffLineSerializer.new.represent(@lines) end def add_match_line @@ -177,6 +179,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 +189,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/boards_controller.rb b/app/controllers/projects/boards_controller.rb index e7354a9e1f7..77b818347c7 100644 --- a/app/controllers/projects/boards_controller.rb +++ b/app/controllers/projects/boards_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::BoardsController < Projects::ApplicationController include BoardsResponses include IssuableCollections diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb index d1dc9fe9600..b7750f4517b 100644 --- a/app/controllers/projects/branches_controller.rb +++ b/app/controllers/projects/branches_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::BranchesController < Projects::ApplicationController include ActionView::Helpers::SanitizeHelper include SortingHelper @@ -48,6 +50,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 +91,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..46449a4aae9 100644 --- a/app/controllers/projects/build_artifacts_controller.rb +++ b/app/controllers/projects/build_artifacts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::BuildArtifactsController < Projects::ApplicationController include ExtractsPath include RendersBlob @@ -42,14 +44,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/builds_controller.rb b/app/controllers/projects/builds_controller.rb index 230b072dcea..6b3d70cb720 100644 --- a/app/controllers/projects/builds_controller.rb +++ b/app/controllers/projects/builds_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::BuildsController < Projects::ApplicationController before_action :authorize_read_build! diff --git a/app/controllers/projects/ci/lints_controller.rb b/app/controllers/projects/ci/lints_controller.rb index a2185572a20..2090af0a111 100644 --- a/app/controllers/projects/ci/lints_controller.rb +++ b/app/controllers/projects/ci/lints_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::Ci::LintsController < Projects::ApplicationController before_action :authorize_create_pipeline! diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb index a5c82caa897..c356f8d2987 100644 --- a/app/controllers/projects/clusters/applications_controller.rb +++ b/app/controllers/projects/clusters/applications_controller.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + class Projects::Clusters::ApplicationsController < Projects::ApplicationController before_action :cluster before_action :application_class, only: [:create] 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 +26,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..bcdbf48bb35 100644 --- a/app/controllers/projects/clusters_controller.rb +++ b/app/controllers/projects/clusters_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ClustersController < Projects::ApplicationController before_action :cluster, except: [:index, :new, :create_gcp, :create_user] before_action :authorize_read_cluster! @@ -141,7 +143,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 +160,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..00b63f55710 100644 --- a/app/controllers/projects/commit_controller.rb +++ b/app/controllers/projects/commit_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for a specific Commit # # Not to be confused with CommitsController, plural. @@ -38,6 +40,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 +61,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 +148,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 +181,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..84a2a461da7 100644 --- a/app/controllers/projects/commits_controller.rb +++ b/app/controllers/projects/commits_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "base64" class Projects::CommitsController < Projects::ApplicationController @@ -15,6 +17,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 +35,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..c2df7b34f90 100644 --- a/app/controllers/projects/compare_controller.rb +++ b/app/controllers/projects/compare_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'addressable/uri' class Projects::CompareController < Projects::ApplicationController @@ -96,8 +98,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/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb index 26f3c114108..fb43356ff10 100644 --- a/app/controllers/projects/cycle_analytics/events_controller.rb +++ b/app/controllers/projects/cycle_analytics/events_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module CycleAnalytics class EventsController < Projects::ApplicationController diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index d1b8fd80c4e..8c071496ba9 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::CycleAnalyticsController < Projects::ApplicationController include ActionView::Helpers::DateHelper include ActionView::Helpers::TextHelper diff --git a/app/controllers/projects/deploy_keys_controller.rb b/app/controllers/projects/deploy_keys_controller.rb index 28fea322334..92ef10a9ef5 100644 --- a/app/controllers/projects/deploy_keys_controller.rb +++ b/app/controllers/projects/deploy_keys_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::DeployKeysController < Projects::ApplicationController include RepositorySettingsRedirect respond_to :html @@ -52,6 +54,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 +66,7 @@ class Projects::DeployKeysController < Projects::ApplicationController format.json { head :ok } end end + # rubocop: enable CodeReuse/ActiveRecord protected diff --git a/app/controllers/projects/deploy_tokens_controller.rb b/app/controllers/projects/deploy_tokens_controller.rb index 83abda64fe0..830b1f4fe4a 100644 --- a/app/controllers/projects/deploy_tokens_controller.rb +++ b/app/controllers/projects/deploy_tokens_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::DeployTokensController < Projects::ApplicationController before_action :authorize_admin_project! diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb index b68cdc39cb8..0a009477d61 100644 --- a/app/controllers/projects/deployments_controller.rb +++ b/app/controllers/projects/deployments_controller.rb @@ -1,7 +1,10 @@ +# frozen_string_literal: true + 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 +12,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 +45,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..b62606067c0 100644 --- a/app/controllers/projects/discussions_controller.rb +++ b/app/controllers/projects/discussions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::DiscussionsController < Projects::ApplicationController include NotesHelper include RendersNotes @@ -50,9 +52,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..de10783df1a 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! @@ -31,6 +33,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 +54,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/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb index cf53ad0a670..c026e9ff332 100644 --- a/app/controllers/projects/find_file_controller.rb +++ b/app/controllers/projects/find_file_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for viewing a repository's file structure class Projects::FindFileController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb index f43bba18d81..7a1700a206a 100644 --- a/app/controllers/projects/forks_controller.rb +++ b/app/controllers/projects/forks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ForksController < Projects::ApplicationController include ContinueParams @@ -7,6 +9,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 +30,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 +60,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/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index a52814e6e52..d439db97252 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This file should be identical in GitLab Community Edition and Enterprise Edition class Projects::GitHttpClientController < Projects::ApplicationController diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb index 1dcf837f78e..be708835e30 100644 --- a/app/controllers/projects/git_http_controller.rb +++ b/app/controllers/projects/git_http_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::GitHttpController < Projects::GitHttpClientController include WorkhorseRequest diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb index 475d4c86294..925b6ed9bfd 100644 --- a/app/controllers/projects/graphs_controller.rb +++ b/app/controllers/projects/graphs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::GraphsController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb index bc5f38f3c2b..7c713c19762 100644 --- a/app/controllers/projects/group_links_controller.rb +++ b/app/controllers/projects/group_links_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::GroupLinksController < Projects::ApplicationController layout 'project_settings' before_action :authorize_admin_project! diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb index 745e89fc843..a7afc3d77a5 100644 --- a/app/controllers/projects/hook_logs_controller.rb +++ b/app/controllers/projects/hook_logs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::HookLogsController < Projects::ApplicationController include HooksExecution diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb index 2da2aad9b33..bc84418b79f 100644 --- a/app/controllers/projects/hooks_controller.rb +++ b/app/controllers/projects/hooks_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::HooksController < Projects::ApplicationController include HooksExecution @@ -66,6 +68,7 @@ class Projects::HooksController < Projects::ApplicationController :enable_ssl_verification, :token, :url, + :push_events_branch_filter, *ProjectHook.triggers.values ) end diff --git a/app/controllers/projects/imports_controller.rb b/app/controllers/projects/imports_controller.rb index 49aa32119ef..e55065c5817 100644 --- a/app/controllers/projects/imports_controller.rb +++ b/app/controllers/projects/imports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ImportsController < Projects::ApplicationController include ContinueParams diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index c3ac8e107fb..b06a6f3bb0d 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::IssuesController < Projects::ApplicationController include RendersNotes include ToggleSubscriptionAction @@ -125,7 +127,7 @@ class Projects::IssuesController < Projects::ApplicationController end def related_branches - @related_branches = @issue.related_branches(current_user) + @related_branches = Issues::RelatedBranchesService.new(project, current_user).execute(issue) respond_to do |format| format.json do @@ -161,6 +163,7 @@ class Projects::IssuesController < Projects::ApplicationController protected + # rubocop: disable CodeReuse/ActiveRecord def issue return @issue if defined?(@issue) @@ -172,6 +175,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..9c9bbe04947 100644 --- a/app/controllers/projects/jobs_controller.rb +++ b/app/controllers/projects/jobs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::JobsController < Projects::ApplicationController include SendFileUpload @@ -11,6 +13,7 @@ class Projects::JobsController < Projects::ApplicationController layout 'project' + # rubocop: disable CodeReuse/ActiveRecord def index @scope = params[:scope] @all_builds = project.builds.relevant @@ -33,6 +36,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 +48,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 +66,7 @@ class Projects::JobsController < Projects::ApplicationController end end end + # rubocop: enable CodeReuse/ActiveRecord def trace build.trace.read do |stream| @@ -104,6 +110,13 @@ class Projects::JobsController < Projects::ApplicationController redirect_to build_path(@build) end + def unschedule + return respond_422 unless @build.scheduled? + + @build.unschedule! + redirect_to build_path(@build) + end + def status render json: BuildSerializer .new(project: @project, current_user: @current_user) diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb index 8a2bce6e7b5..640038818f2 100644 --- a/app/controllers/projects/labels_controller.rb +++ b/app/controllers/projects/labels_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::LabelsController < Projects::ApplicationController include ToggleSubscriptionAction @@ -90,6 +92,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 +108,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) @@ -136,12 +140,7 @@ class Projects::LabelsController < Projects::ApplicationController end def flash_notice_for(label, group) - notice = ''.html_safe - notice << label.title - notice << ' promoted to ' - notice << view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group)) - notice << '.' - notice + ''.html_safe + "#{label.title} promoted to " + view_context.link_to('<u>group label</u>'.html_safe, group_labels_path(group)) + '.' end protected @@ -163,7 +162,13 @@ 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], + subscribed: params[:subscribed], + 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..be40077d389 100644 --- a/app/controllers/projects/lfs_api_controller.rb +++ b/app/controllers/projects/lfs_api_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::LfsApiController < Projects::GitHttpClientController include LfsRequest @@ -41,11 +43,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_locks_api_controller.rb b/app/controllers/projects/lfs_locks_api_controller.rb index 3fff0fd69ae..fc67cd72faa 100644 --- a/app/controllers/projects/lfs_locks_api_controller.rb +++ b/app/controllers/projects/lfs_locks_api_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::LfsLocksApiController < Projects::GitHttpClientController include LfsRequest diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb index dd7e673ec75..babeee48ef3 100644 --- a/app/controllers/projects/lfs_storage_controller.rb +++ b/app/controllers/projects/lfs_storage_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::LfsStorageController < Projects::GitHttpClientController include LfsRequest include WorkhorseRequest @@ -56,6 +58,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 +69,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 +79,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/mattermosts_controller.rb b/app/controllers/projects/mattermosts_controller.rb index 0f6add3e287..085b1bc1498 100644 --- a/app/controllers/projects/mattermosts_controller.rb +++ b/app/controllers/projects/mattermosts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MattermostsController < Projects::ApplicationController include TriggersHelper include ActionView::Helpers::AssetUrlHelper diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index fead81dd472..368ee89ff5c 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MergeRequests::ApplicationController < Projects::ApplicationController before_action :check_merge_requests_available! before_action :merge_request @@ -5,9 +7,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/conflicts_controller.rb b/app/controllers/projects/merge_requests/conflicts_controller.rb index 366524b0783..ac1969adc6e 100644 --- a/app/controllers/projects/merge_requests/conflicts_controller.rb +++ b/app/controllers/projects/merge_requests/conflicts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MergeRequests::ConflictsController < Projects::MergeRequests::ApplicationController include IssuableActions diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 03d0290ac1d..5639402a1e9 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MergeRequests::CreationsController < Projects::MergeRequests::ApplicationController include DiffForPath include DiffHelper @@ -104,11 +106,16 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap @commits = set_commits_for_rendering(@merge_request.commits) @commit = @merge_request.diff_head_commit + # FIXME: We have to assign a presenter to another instance variable + # due to class_name checks being made with issuable classes + @mr_presenter = @merge_request.present(current_user: current_user) + @labels = LabelsFinder.new(current_user, project_id: @project.id).execute 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 +126,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..5307cd0720a 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController include DiffForPath include DiffHelper @@ -21,7 +23,15 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic def render_diffs @environment = @merge_request.environments_for(current_user).last - render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes) + @diffs.write_cache + + request = { + current_user: current_user, + project: @merge_request.project, + render: ->(partial, locals) { view_to_html_string(partial, locals) } + } + + render json: DiffsSerializer.new(request).represent(@diffs, additional_attributes) end def define_diff_vars @@ -32,13 +42,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 +79,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..6a5da9b8292 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationController include ToggleSubscriptionAction include IssuableActions @@ -205,7 +207,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo environments = begin @merge_request.environments_for(current_user).map do |environment| - project = environment.project + project = environment.project deployment = environment.first_deployment_for(@merge_request.diff_head_sha) stop_url = @@ -215,7 +217,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo metrics_url = if can?(current_user, :read_environment, environment) && environment.has_metrics? - metrics_project_environment_deployment_path(environment.project, environment, deployment) + metrics_project_environment_deployment_path(project, environment, deployment) end metrics_monitoring_url = @@ -330,6 +332,11 @@ 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 + + # FIXME: We have to assign a presenter to another instance variable + # due to class_name checks being made with issuable classes + @mr_presenter = @merge_request.present(current_user: current_user) end def finder_type diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb index b9b3dcd5a85..20998c97730 100644 --- a/app/controllers/projects/milestones_controller.rb +++ b/app/controllers/projects/milestones_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MilestonesController < Projects::ApplicationController include Gitlab::Utils::StrongMemoize include MilestoneActions @@ -91,12 +93,7 @@ class Projects::MilestonesController < Projects::ApplicationController end def flash_notice_for(milestone, group) - notice = ''.html_safe - notice << milestone.title - notice << ' promoted to ' - notice << view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid)) - notice << '.' - notice + ''.html_safe + "#{milestone.title} promoted to " + view_context.link_to('<u>group milestone</u>'.html_safe, group_milestone_path(group, milestone.iid)) + '.' end def destroy @@ -118,9 +115,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/mirrors_controller.rb b/app/controllers/projects/mirrors_controller.rb index 3739608e4c0..78d5faf2326 100644 --- a/app/controllers/projects/mirrors_controller.rb +++ b/app/controllers/projects/mirrors_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::MirrorsController < Projects::ApplicationController include RepositorySettingsRedirect diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb index 35fec229db7..ad2466a8588 100644 --- a/app/controllers/projects/network_controller.rb +++ b/app/controllers/projects/network_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::NetworkController < Projects::ApplicationController include ExtractsPath include ApplicationHelper diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 21e2145b73b..4bac763d000 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::NotesController < Projects::ApplicationController include RendersNotes include NotesActions diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb index ff49911d892..c1ad6707c97 100644 --- a/app/controllers/projects/pages_controller.rb +++ b/app/controllers/projects/pages_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::PagesController < Projects::ApplicationController layout 'project_settings' @@ -5,9 +7,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..439ec9b1731 100644 --- a/app/controllers/projects/pages_domains_controller.rb +++ b/app/controllers/projects/pages_domains_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::PagesDomainsController < Projects::ApplicationController layout 'project_settings' @@ -70,7 +72,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..acf56f0eb6a 100644 --- a/app/controllers/projects/pipeline_schedules_controller.rb +++ b/app/controllers/projects/pipeline_schedules_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::PipelineSchedulesController < Projects::ApplicationController before_action :schedule, except: [:index, :new, :create] @@ -8,12 +10,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..53b29d4146e 100644 --- a/app/controllers/projects/pipelines_controller.rb +++ b/app/controllers/projects/pipelines_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::PipelinesController < Projects::ApplicationController before_action :whitelist_query_limiting, only: [:create, :retry] before_action :pipeline, except: [:index, :new, :create, :charts] @@ -96,7 +98,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 +161,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 +169,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/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb index 73c613b26f3..192e6d38f36 100644 --- a/app/controllers/projects/pipelines_settings_controller.rb +++ b/app/controllers/projects/pipelines_settings_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::PipelinesSettingsController < Projects::ApplicationController before_action :authorize_admin_pipeline! diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb index cfa5e72af64..8938cfbad54 100644 --- a/app/controllers/projects/project_members_controller.rb +++ b/app/controllers/projects/project_members_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ProjectMembersController < Projects::ApplicationController include MembershipActions include MembersPresentation @@ -6,6 +8,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 +28,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/prometheus/metrics_controller.rb b/app/controllers/projects/prometheus/metrics_controller.rb index c6b6243b553..3a9f9aab4a5 100644 --- a/app/controllers/projects/prometheus/metrics_controller.rb +++ b/app/controllers/projects/prometheus/metrics_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Prometheus class MetricsController < Projects::ApplicationController diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb index 64954ac9a42..a860be83e95 100644 --- a/app/controllers/projects/protected_branches_controller.rb +++ b/app/controllers/projects/protected_branches_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ProtectedBranchesController < Projects::ProtectedRefsController protected diff --git a/app/controllers/projects/protected_refs_controller.rb b/app/controllers/projects/protected_refs_controller.rb index cc62ce2f11b..3a3a29ddd0d 100644 --- a/app/controllers/projects/protected_refs_controller.rb +++ b/app/controllers/projects/protected_refs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ProtectedRefsController < Projects::ApplicationController include RepositorySettingsRedirect diff --git a/app/controllers/projects/protected_tags_controller.rb b/app/controllers/projects/protected_tags_controller.rb index 198c938ff35..01cedba95ac 100644 --- a/app/controllers/projects/protected_tags_controller.rb +++ b/app/controllers/projects/protected_tags_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ProtectedTagsController < Projects::ProtectedRefsController protected diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 91cf35bc70b..1dd5d1ff2e8 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for viewing a file's raw class Projects::RawController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb index 48a09e1ddb8..b97fbe19bbf 100644 --- a/app/controllers/projects/refs_controller.rb +++ b/app/controllers/projects/refs_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::RefsController < Projects::ApplicationController include ExtractsPath include TreeHelper @@ -36,54 +38,47 @@ class Projects::RefsController < Projects::ApplicationController end def logs_tree - @offset = if params[:offset].present? - params[:offset].to_i - else - 0 - end - - @limit = 25 - - @path = params[:path] - - contents = [] - contents.push(*tree.trees) - contents.push(*tree.blobs) - contents.push(*tree.submodules) + summary = ::Gitlab::TreeSummary.new( + @commit, + @project, + path: @path, + offset: params[:offset], + limit: 25 + ) - # 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/application_controller.rb b/app/controllers/projects/registry/application_controller.rb index a56f9c58726..2f891d78c91 100644 --- a/app/controllers/projects/registry/application_controller.rb +++ b/app/controllers/projects/registry/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Registry class ApplicationController < Projects::ApplicationController diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb index 32c0fc6d14a..6d60117c37d 100644 --- a/app/controllers/projects/registry/repositories_controller.rb +++ b/app/controllers/projects/registry/repositories_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Registry class RepositoriesController < ::Projects::Registry::ApplicationController @@ -18,14 +20,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 +39,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/registry/tags_controller.rb b/app/controllers/projects/registry/tags_controller.rb index e602aa3f393..567d750caae 100644 --- a/app/controllers/projects/registry/tags_controller.rb +++ b/app/controllers/projects/registry/tags_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Registry class TagsController < ::Projects::Registry::ApplicationController diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 19e09b3af6f..55827075896 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ReleasesController < Projects::ApplicationController # Authorize before_action :require_non_empty_project @@ -28,9 +30,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/repositories_controller.rb b/app/controllers/projects/repositories_controller.rb index ecb2ece7532..4eeaeb860ee 100644 --- a/app/controllers/projects/repositories_controller.rb +++ b/app/controllers/projects/repositories_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::RepositoriesController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb index c098c82081e..cbeb32fd610 100644 --- a/app/controllers/projects/runner_projects_controller.rb +++ b/app/controllers/projects/runner_projects_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::RunnerProjectsController < Projects::ApplicationController before_action :authorize_admin_build! diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb index d118cec977c..91f40b90aa8 100644 --- a/app/controllers/projects/runners_controller.rb +++ b/app/controllers/projects/runners_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::RunnersController < Projects::ApplicationController before_action :authorize_admin_build! before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index d55046047ae..f1c9d0d0f77 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::ServicesController < Projects::ApplicationController include ServiceParams diff --git a/app/controllers/projects/settings/badges_controller.rb b/app/controllers/projects/settings/badges_controller.rb deleted file mode 100644 index 7887bee49c5..00000000000 --- a/app/controllers/projects/settings/badges_controller.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Projects - module Settings - class BadgesController < Projects::ApplicationController - include API::Helpers::RelatedResourcesHelpers - - before_action :authorize_admin_project! - - def index - @badge_api_endpoint = expose_url(api_v4_projects_badges_path(id: @project.id)) - end - end - end -end diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 322ec096ffb..3a1344651df 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Settings class CiCdController < Projects::ApplicationController @@ -34,6 +36,13 @@ module Projects end end + def reset_registration_token + @project.reset_runners_token! + + flash[:notice] = 'New runners registration token has been generated!' + redirect_to namespace_project_settings_ci_cd_path + end + private def update_params diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb index d9fecfecc40..388fcb32c35 100644 --- a/app/controllers/projects/settings/integrations_controller.rb +++ b/app/controllers/projects/settings/integrations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Settings class IntegrationsController < Projects::ApplicationController diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb index 4697af4f26a..1d76c90d4eb 100644 --- a/app/controllers/projects/settings/repository_controller.rb +++ b/app/controllers/projects/settings/repository_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Projects module Settings class RepositoryController < Projects::ApplicationController @@ -31,6 +33,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 +45,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/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 7c03d8ce827..a44acb12bdf 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::SnippetsController < Projects::ApplicationController include RendersNotes include ToggleAwardEmoji diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb index b17753222a0..c8442ff3592 100644 --- a/app/controllers/projects/tags_controller.rb +++ b/app/controllers/projects/tags_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::TagsController < Projects::ApplicationController include SortingHelper @@ -7,6 +9,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 @@ -17,8 +20,15 @@ class Projects::TagsController < Projects::ApplicationController tag_names = @tags.map(&:name) @tags_pipelines = @project.pipelines.latest_successful_for_refs(tag_names) @releases = project.releases.where(tag: tag_names) + + respond_to do |format| + format.html + format.atom { render layout: 'xml.atom' } + end end + # rubocop: enable CodeReuse/ActiveRecord + # rubocop: disable CodeReuse/ActiveRecord def show @tag = @repository.find_tag(params[:id]) @@ -27,6 +37,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/templates_controller.rb b/app/controllers/projects/templates_controller.rb index 52d6fb82093..7ceea4e5b96 100644 --- a/app/controllers/projects/templates_controller.rb +++ b/app/controllers/projects/templates_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::TemplatesController < Projects::ApplicationController before_action :authenticate_user!, :get_template_class diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 93fb9da6510..0b11ee9edc0 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::TodosController < Projects::ApplicationController include Gitlab::Utils::StrongMemoize include TodosActions diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb index ee9b5458282..3fe300dcfc0 100644 --- a/app/controllers/projects/tree_controller.rb +++ b/app/controllers/projects/tree_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Controller for viewing a repository's file structure class Projects::TreeController < Projects::ApplicationController include ExtractsPath diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb index cb12b707087..f5fdfb8accc 100644 --- a/app/controllers/projects/triggers_controller.rb +++ b/app/controllers/projects/triggers_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::TriggersController < Projects::ApplicationController before_action :authorize_admin_build! before_action :authorize_manage_trigger!, except: [:index, :create] diff --git a/app/controllers/projects/uploads_controller.rb b/app/controllers/projects/uploads_controller.rb index 7a85046164c..4ffcc2ac805 100644 --- a/app/controllers/projects/uploads_controller.rb +++ b/app/controllers/projects/uploads_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::UploadsController < Projects::ApplicationController include UploadsActions include WorkhorseRequest diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb index bf09ea7e4d8..bb658bfcc19 100644 --- a/app/controllers/projects/variables_controller.rb +++ b/app/controllers/projects/variables_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::VariablesController < Projects::ApplicationController before_action :authorize_admin_build! diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb index da7aeb26a75..8c6d87a421f 100644 --- a/app/controllers/projects/wikis_controller.rb +++ b/app/controllers/projects/wikis_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Projects::WikisController < Projects::ApplicationController include PreviewMarkdown include Gitlab::Utils::StrongMemoize diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index e9ae8c13142..ee438e160f2 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -1,4 +1,7 @@ +# frozen_string_literal: true + class ProjectsController < Projects::ApplicationController + include API::Helpers::RelatedResourcesHelpers include IssuableCollections include ExtractsPath include PreviewMarkdown @@ -13,6 +16,7 @@ class ProjectsController < Projects::ApplicationController before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?] before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export] + before_action :present_project, only: [:edit] # Authorize before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export] @@ -24,14 +28,17 @@ 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)) render 'edit' end @@ -73,6 +80,7 @@ class ProjectsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def transfer return access_denied! unless can?(current_user, :change_namespace, @project) @@ -83,6 +91,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) @@ -189,10 +198,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'), @@ -231,6 +238,7 @@ class ProjectsController < Projects::ApplicationController } end + # rubocop: disable CodeReuse/ActiveRecord def refs find_refs = params['find'] @@ -265,6 +273,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 @@ -303,6 +312,7 @@ class ProjectsController < Projects::ApplicationController end end + # rubocop: disable CodeReuse/ActiveRecord def load_events projects = Project.where(id: @project.id) @@ -312,6 +322,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) @@ -355,6 +366,7 @@ class ProjectsController < Projects::ApplicationController repository_access_level snippets_access_level wiki_access_level + pages_access_level ] ] end @@ -424,11 +436,7 @@ class ProjectsController < Projects::ApplicationController 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? + def present_project + @project = @project.present(current_user: current_user) 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/sherlock/application_controller.rb b/app/controllers/sherlock/application_controller.rb index 6bdd3568a78..c048254d348 100644 --- a/app/controllers/sherlock/application_controller.rb +++ b/app/controllers/sherlock/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sherlock class ApplicationController < ::ApplicationController before_action :find_transaction diff --git a/app/controllers/sherlock/file_samples_controller.rb b/app/controllers/sherlock/file_samples_controller.rb index 0c3bc100106..900446bb75a 100644 --- a/app/controllers/sherlock/file_samples_controller.rb +++ b/app/controllers/sherlock/file_samples_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sherlock class FileSamplesController < Sherlock::ApplicationController def show diff --git a/app/controllers/sherlock/queries_controller.rb b/app/controllers/sherlock/queries_controller.rb index 63b26aab1a4..49a25c682b5 100644 --- a/app/controllers/sherlock/queries_controller.rb +++ b/app/controllers/sherlock/queries_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sherlock class QueriesController < Sherlock::ApplicationController def show diff --git a/app/controllers/sherlock/transactions_controller.rb b/app/controllers/sherlock/transactions_controller.rb index ae4953c3259..46e382e594e 100644 --- a/app/controllers/sherlock/transactions_controller.rb +++ b/app/controllers/sherlock/transactions_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Sherlock class TransactionsController < Sherlock::ApplicationController def index diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb index 217da89a1fd..091bcb1253d 100644 --- a/app/controllers/snippets/notes_controller.rb +++ b/app/controllers/snippets/notes_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Snippets::NotesController < ApplicationController include NotesActions include ToggleAwardEmoji @@ -17,9 +19,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/terms_controller.rb b/app/controllers/users/terms_controller.rb index 1b1560a2a00..3c16d934b4d 100644 --- a/app/controllers/users/terms_controller.rb +++ b/app/controllers/users/terms_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module Users class TermsController < ApplicationController include InternalRedirect diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 2f65f4a7403..d16240af404 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 @@ -27,11 +29,17 @@ class UsersController < ApplicationController format.json do load_events - pager_json("events/_events", @events.count) + pager_json("events/_events", @events.count, events: @events) end end end + def activity + respond_to do |format| + format.html { render 'show' } + end + end + def groups load_groups @@ -51,9 +59,7 @@ class UsersController < ApplicationController respond_to do |format| format.html { render 'show' } format.json do - render json: { - html: view_to_html_string("shared/projects/_list", projects: @projects) - } + pager_json("shared/projects/_list", @projects.count, projects: @projects) end end end @@ -123,6 +129,7 @@ class UsersController < ApplicationController @projects = PersonalProjectsFinder.new(user).execute(current_user) .page(params[:page]) + .per(params[:limit]) prepare_projects_for_rendering(@projects) end 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..fbb1cfc5c66 --- /dev/null +++ b/app/finders/admin/runners_finder.rb @@ -0,0 +1,62 @@ +# 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! + filter_by_runner_type! + 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! + filter_by!(:status_status, Ci::Runner::AVAILABLE_STATUSES) + end + + def filter_by_runner_type! + filter_by!(:type_type, Ci::Runner::AVAILABLE_TYPES) + end + + def sort! + @runners = @runners.order_by(sort_key) + end + + def paginate! + @runners = @runners.page(@params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE) + end + + def filter_by!(scope_name, available_scopes) + scope = @params[scope_name] + + if scope.present? && available_scopes.include?(scope) + @runners = @runners.public_send(scope) # rubocop:disable GitlabSecurity/PublicSend + end + 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..2e82bda8730 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 @@ -10,6 +12,7 @@ class EventsFinder # Arguments: # source - which user or project to looks for events on # current_user - only return events for projects visible to this user + # WARNING: does not consider project feature visibility! # params: # action: string # target_type: string @@ -36,32 +39,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..a668a0f0fae --- /dev/null +++ b/app/finders/group_labels_finder.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +class GroupLabelsFinder + attr_reader :current_user, :group, :params + + def initialize(current_user, group, params = {}) + @current_user = current_user + @group = group + @params = params + end + + def execute + group.labels + .optionally_subscribed_by(subscriber_id) + .optionally_search(params[:search]) + .order_by(params[:sort]) + .page(params[:page]) + end + + private + + def subscriber_id + current_user&.id if subscribed? + end + + def subscribed? + params[:subscribed] == 'true' + 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..1f98ecf95ca 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) @@ -125,13 +128,14 @@ class IssuableFinder labels_count = 1 if use_cte_for_search? finder.execute.reorder(nil).group(:state).count.each do |key, value| - counts[Array(key).last.to_sym] += value / labels_count + counts[count_key(key)] += value / labels_count end counts[:all] = counts.values.sum 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,30 +222,33 @@ 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 + params[:assignee_id].present? && params[:assignee_id].to_s != NONE end def assignee_username? - params[:assignee_username].present? && params[:assignee_username] != NONE + params[:assignee_username].present? && params[:assignee_username].to_s != NONE end def no_assignee? # Assignee_id takes precedence over assignee_username - params[:assignee_id] == NONE || params[:assignee_username] == NONE + params[:assignee_id].to_s == NONE || params[:assignee_username].to_s == 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,11 @@ class IssuableFinder klass.all end + def count_key(value) + Array(value).last.to_sym + end + + # rubocop: disable CodeReuse/ActiveRecord def by_scope(items) return items.none if current_user_related? && !current_user @@ -295,6 +314,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 +323,7 @@ class IssuableFinder items end + # rubocop: disable CodeReuse/ActiveRecord def by_state(items) case params[:state].to_s when 'closed' @@ -317,12 +338,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,14 +358,17 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord def use_cte_for_search? return false unless search return false unless Gitlab::Database.postgresql? + return false unless Feature.enabled?(:use_cte_for_group_issues_search, default_enabled: true) params[:use_cte_for_search] end + # rubocop: disable CodeReuse/ActiveRecord def by_search(items) return items unless search @@ -355,17 +381,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 +409,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,19 +423,27 @@ class IssuableFinder items end + # rubocop: enable CodeReuse/ActiveRecord def filter_by_upcoming_milestone? params[:milestone_title] == Milestone::Upcoming.name end + def filter_by_any_milestone? + params[:milestone_title] == Milestone::Any.title + end + def filter_by_started_milestone? params[:milestone_title] == Milestone::Started.name end + # rubocop: disable CodeReuse/ActiveRecord def by_milestone(items) if milestones? if filter_by_no_milestone? items = items.left_joins_milestones.where(milestone_id: [-1, nil]) + elsif filter_by_any_milestone? + items = items.any_milestone elsif filter_by_upcoming_milestone? upcoming_ids = Milestone.upcoming_ids_by_projects(projects(items)) items = items.left_joins_milestones.where(milestone_id: upcoming_ids) @@ -414,6 +456,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..abdc47b9866 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 @@ -114,9 +120,13 @@ class IssuesFinder < IssuableFinder return @user_can_see_all_confidential_issues = true if current_user.full_private_access? @user_can_see_all_confidential_issues = - project? && - project && - project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + if project? && project + project.team.max_member_access(current_user.id) >= CONFIDENTIAL_ACCESS_LEVEL + elsif group + group.max_member_access_for_user(current_user) >= CONFIDENTIAL_ACCESS_LEVEL + else + false + end end def user_cannot_see_confidential_issues? @@ -125,6 +135,7 @@ class IssuesFinder < IssuableFinder current_user.blank? end + # rubocop: disable CodeReuse/ActiveRecord def by_assignee(items) if assignee items.assigned_to(assignee) @@ -136,4 +147,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..4d8128dd824 100644 --- a/app/finders/joined_groups_finder.rb +++ b/app/finders/joined_groups_finder.rb @@ -1,4 +1,6 @@ -class JoinedGroupsFinder < UnionFinder +# frozen_string_literal: true + +class JoinedGroupsFinder def initialize(user) @user = user end @@ -6,19 +8,8 @@ class JoinedGroupsFinder < UnionFinder # Finds the groups of the source user, optionally limited to those visible to # the current user. def execute(current_user = nil) - segments = all_groups(current_user) - - find_union(segments, Group).order_id_desc - end - - private - - def all_groups(current_user) - groups = [] - - groups << @user.authorized_groups.visible_to_user(current_user) if current_user - groups << @user.authorized_groups.public_to_user(current_user) - - groups + @user.authorized_groups + .public_or_visible_to_user(current_user) + .order_id_desc end end diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 1d05bf28438..d000af21be3 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,18 +12,22 @@ 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 items = with_title(items) + items = by_subscription(items) 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 +58,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? @@ -70,6 +85,18 @@ class LabelsFinder < UnionFinder labels.search(params[:search]) end + def by_subscription(labels) + labels.optionally_subscribed_by(subscriber_id) + end + + def subscriber_id + current_user&.id if subscribed? + end + + def subscribed? + params[:subscribed] == 'true' + end + # Gets redacted array of group ids # which can include the ancestors and descendants of the requested group. def group_ids_for(group) @@ -102,7 +129,7 @@ class LabelsFinder < UnionFinder end def project? - params[:project_id].present? + params[:project].present? || params[:project_id].present? end def projects? @@ -125,7 +152,7 @@ class LabelsFinder < UnionFinder return @project if defined?(@project) if project? - @project = Project.find(params[:project_id]) + @project = params[:project] || Project.find(params[:project_id]) @project = nil unless authorized_to_read_labels?(@project) else @project = nil @@ -134,13 +161,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 +177,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..d735a4c1d69 100644 --- a/app/finders/license_template_finder.rb +++ b/app/finders/license_template_finder.rb @@ -1,35 +1,51 @@ +# frozen_string_literal: true + # LicenseTemplateFinder # # Used to find license templates, which may come from a variety of external # sources # -# Arguments: +# Params can be any of the following: # popular: boolean. When set to true, only "popular" licenses are shown. When # false, all licenses except popular ones are shown. When nil (the # default), *all* licenses will be shown. +# name: string. If set, return a single license matching that name (or nil) class LicenseTemplateFinder - attr_reader :params + include Gitlab::Utils::StrongMemoize + + attr_reader :project, :params - def initialize(params = {}) + def initialize(project, params = {}) + @project = project @params = params end def execute - Licensee::License.all(featured: popular_only?).map do |license| - LicenseTemplate.new( - id: license.key, - name: license.name, - nickname: license.nickname, - category: (license.featured? ? :Popular : :Other), - content: license.content, - url: license.url, - meta: license.meta - ) + if params[:name] + vendored_licenses.find { |template| template.key == params[:name] } + else + vendored_licenses end end private + def vendored_licenses + strong_memoize(:vendored_licenses) do + Licensee::License.all(featured: popular_only?).map do |license| + LicenseTemplate.new( + key: license.key, + name: license.name, + nickname: license.nickname, + category: (license.featured? ? :Popular : :Other), + content: license.content, + url: license.url, + meta: license.meta + ) + end + end + end + def popular_only? params.fetch(:popular, nil) end 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..e190d5d90c9 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 @@ -25,13 +27,17 @@ # updated_before: datetime # class MergeRequestsFinder < IssuableFinder + def self.scalar_params + @scalar_params ||= super + [:wip] + end + def klass MergeRequest end def filter_items(_items) items = by_source_branch(super) - + items = by_wip(items) by_target_branch(items) end @@ -41,19 +47,38 @@ 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 + + def by_wip(items) + if params[:wip] == 'yes' + items.where(wip_match(items.arel_table)) + elsif params[:wip] == 'no' + items.where.not(wip_match(items.arel_table)) + else + items + end + end + + def wip_match(table) + table[:title].matches('WIP:%') + .or(table[:title].matches('WIP %')) + .or(table[:title].matches('[WIP]%')) + end 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 new file mode 100644 index 00000000000..3e483716064 --- /dev/null +++ b/app/finders/template_finder.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class TemplateFinder + include Gitlab::Utils::StrongMemoize + + VENDORED_TEMPLATES = HashWithIndifferentAccess.new( + dockerfiles: ::Gitlab::Template::DockerfileTemplate, + gitignores: ::Gitlab::Template::GitignoreTemplate, + gitlab_ci_ymls: ::Gitlab::Template::GitlabCiYmlTemplate + ).freeze + + class << self + def build(type, project, params = {}) + if type.to_s == 'licenses' + LicenseTemplateFinder.new(project, params) # rubocop: disable CodeReuse/Finder + else + new(type, project, params) + end + end + end + + attr_reader :type, :project, :params + + attr_reader :vendored_templates + private :vendored_templates + + def initialize(type, project, params = {}) + @type = type + @project = project + @params = params + + @vendored_templates = VENDORED_TEMPLATES.fetch(type) + end + + def execute + if params[:name] + vendored_templates.find(params[:name]) + else + vendored_templates.all + end + end +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..3f2e813d381 100644 --- a/app/finders/user_recent_events_finder.rb +++ b/app/finders/user_recent_events_finder.rb @@ -1,6 +1,9 @@ +# 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 +# WARNING: does not consider project feature visibility! # - user: The user for which to load the events # - params: # - offset: The page of events to return @@ -21,17 +24,20 @@ class UserRecentEventsFinder @params = params end + # rubocop: disable CodeReuse/ActiveRecord def execute return Event.none unless can?(current_user, :read_user_profile, target_user) recent_events(params[:offset] || 0) .joins(:project) .with_associations - .limit_recent(LIMIT, params[:offset]) + .limit_recent(params[:limit].presence || 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 +48,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..ab37c282fe5 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 @@ -14,7 +16,7 @@ module Types :create_deployment, :push_to_delete_protected_branch, :admin_wiki, :admin_project, :update_pages, :admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki, - :create_pages, :destroy_pages + :create_pages, :destroy_pages, :read_pages_content end end end 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..4f91e3e4117 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: { @@ -273,7 +292,8 @@ module ApplicationHelper mergeRequests: merge_requests_project_autocomplete_sources_path(object), labels: labels_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]), milestones: milestones_project_autocomplete_sources_path(object), - commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]) + commands: commands_project_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]), + snippets: snippets_project_autocomplete_sources_path(object) } end end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index 684c84c3006..15cbfeea609 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 @@ -106,10 +108,6 @@ module ApplicationSettingsHelper options_for_select(options, selected) end - def sidekiq_queue_options_for_select - options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues) - end - def circuitbreaker_failure_count_help_text health_link = link_to(s_('AdminHealthPageLink|health page'), admin_health_check_path) api_link = link_to(s_('CircuitBreakerApiLink|circuitbreaker api'), help_page_path("api/repository_storage_health")) @@ -220,6 +218,7 @@ module ApplicationSettingsHelper :recaptcha_enabled, :recaptcha_private_key, :recaptcha_site_key, + :receive_max_input_size, :repository_checks_enabled, :repository_storages, :require_two_factor_authentication, @@ -231,9 +230,6 @@ module ApplicationSettingsHelper :session_expire_delay, :shared_runners_enabled, :shared_runners_text, - :sidekiq_throttling_enabled, - :sidekiq_throttling_factor, - :sidekiq_throttling_queues, :sign_in_text, :signup_enabled, :terminal_max_session_time, @@ -258,7 +254,12 @@ module ApplicationSettingsHelper :user_default_internal_regex, :user_oauth_applications, :version_check_enabled, - :web_ide_clientside_preview_enabled + :web_ide_clientside_preview_enabled, + :diff_max_patch_bytes ] 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..7fc4c1a023f 100644 --- a/app/helpers/avatars_helper.rb +++ b/app/helpers/avatars_helper.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + module AvatarsHelper - def project_icon(project_id, options = {}) - source_icon(Project, project_id, options) + def project_icon(project, options = {}) + source_icon(project, options) end - def group_icon(group_id, options = {}) - source_icon(Group, group_id, options) + def group_icon(group, options = {}) + source_icon(group, options) end # Takes both user and email and returns the avatar_icon by @@ -108,16 +110,11 @@ module AvatarsHelper private - def source_icon(klass, source_id, options = {}) - source = - if source_id.respond_to?(:avatar_url) - source_id - else - klass.find_by_full_path(source_id) - end + def source_icon(source, options = {}) + avatar_url = source.try(:avatar_url) - if source.avatar_url - image_tag source.avatar_url, options + if avatar_url + image_tag avatar_url, options else source_identicon(source, options) end @@ -125,9 +122,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 00ebafd177b..883e5ddff57 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 @@ -157,40 +159,44 @@ module BlobHelper end end - def licenses_for_select - return @licenses_for_select if defined?(@licenses_for_select) + def ref_project + @ref_project ||= @target_project || @project + end - grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category) - categories = grouped_licenses.keys + def template_dropdown_names(items) + grouped = items.group_by(&:category) + categories = grouped.keys - @licenses_for_select = categories.each_with_object({}) do |category, hash| - hash[category] = grouped_licenses[category].map do |license| - { name: license.name, id: license.id } + categories.each_with_object({}) do |category, hash| + hash[category] = grouped[category].map do |item| + { name: item.name, id: item.key } end end end + private :template_dropdown_names - def ref_project - @ref_project ||= @target_project || @project + def licenses_for_select(project = @project) + @licenses_for_select ||= template_dropdown_names(TemplateFinder.build(:licenses, project).execute) end - def gitignore_names - @gitignore_names ||= Gitlab::Template::GitignoreTemplate.dropdown_names + def gitignore_names(project = @project) + @gitignore_names ||= template_dropdown_names(TemplateFinder.build(:gitignores, project).execute) end - def gitlab_ci_ymls - @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names(params[:context]) + def gitlab_ci_ymls(project = @project) + @gitlab_ci_ymls ||= template_dropdown_names(TemplateFinder.build(:gitlab_ci_ymls, project).execute) end - def dockerfile_names - @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names + def dockerfile_names(project = @project) + @dockerfile_names ||= template_dropdown_names(TemplateFinder.build(:dockerfiles, project).execute) end - def blob_editor_paths + def blob_editor_paths(project = @project) { 'relative-url-root' => Rails.application.config.relative_url_root, 'assets-prefix' => Gitlab::Application.config.assets.prefix, - 'blob-language' => @blob && @blob.language.try(:ace_mode) + 'blob-language' => @blob && @blob.language.try(:ace_mode), + 'project-id' => project.id } end diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb index af878bcf9a0..be1e7016a1e 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 @@ -57,8 +59,8 @@ module BoardsHelper { toggle: "dropdown", - list_labels_path: labels_filter_path(true, include_ancestor_groups: true), - labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups), + list_labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_ancestor_groups: true), + labels: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: include_descendant_groups), labels_endpoint: @labels_endpoint, namespace_path: @namespace_path, project_path: @project&.path, 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 7adc882bc47..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,13 +63,13 @@ 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) if current_user.try(:require_password_creation_for_git?) _("Set a password on your account to pull or push via %{protocol}.") % { protocol: protocol } - else + elsif current_user.try(:require_personal_access_token_creation_for_git_auth?) _("Create a personal access token on your account to pull or push via %{protocol}.") % { protocol: protocol } end end @@ -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..6f9e2ef78cd 100644 --- a/app/helpers/ci_status_helper.rb +++ b/app/helpers/ci_status_helper.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + ## # DEPRECATED # @@ -18,6 +20,8 @@ module CiStatusHelper 'passed with warnings' when 'manual' 'waiting for manual action' + when 'scheduled' + 'waiting for delayed job' else status end @@ -37,6 +41,8 @@ module CiStatusHelper s_('CiStatusText|passed') when 'manual' s_('CiStatusText|blocked') + when 'scheduled' + s_('CiStatusText|scheduled') else # All states are already being translated inside the detailed statuses: # :running => Gitlab::Ci::Status::Running @@ -81,6 +87,8 @@ module CiStatusHelper 'status_skipped' when 'manual' 'status_manual' + when 'scheduled' + 'status_scheduled' else 'status_canceled' end @@ -121,11 +129,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..19eb763e1de 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 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/cookies_helper.rb b/app/helpers/cookies_helper.rb new file mode 100644 index 00000000000..3a7e9987190 --- /dev/null +++ b/app/helpers/cookies_helper.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module CookiesHelper + def set_secure_cookie(key, value, httponly: false, permanent: false) + cookie_jar = permanent ? cookies.permanent : cookies + + cookie_jar[key] = { value: value, secure: Gitlab.config.gitlab.https, httponly: httponly } + end +end 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 19aa55a8d49..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) @@ -19,6 +21,29 @@ module DashboardHelper links.any? { |link| dashboard_nav_link?(link) } end + def controller_action_to_child_dashboards(controller = controller_name, action = action_name) + case "#{controller}##{action}" + when 'projects#index', 'root#index', 'projects#starred', 'projects#trending' + %w(projects stars) + when 'dashboard#activity' + %w(starred_project_activity project_activity) + when 'groups#index' + %w(groups) + when 'todos#index' + %w(todos) + when 'dashboard#issues' + %w(issues) + when 'dashboard#merge_requests' + %w(merge_requests) + else + [] + end + end + + def user_default_dashboard?(user = current_user) + controller_action_to_child_dashboards.any? {|dashboard| dashboard == user.dashboard } + end + private def get_dashboard_nav_links 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 cb6f709c604..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 @@ -110,10 +112,12 @@ module EventsHelper event.note_target) elsif event.note? if event.note_target - event_note_target_path(event) + event_note_target_url(event) end elsif event.push? push_event_feed_url(event) + elsif event.created_project? + project_url(event.project) end end @@ -145,14 +149,14 @@ module EventsHelper end end - def event_note_target_path(event) + def event_note_target_url(event) if event.commit_note? - project_commit_path(event.project, event.note_target, anchor: dom_id(event.target)) + project_commit_url(event.project, event.note_target, anchor: dom_id(event.target)) elsif event.project_snippet_note? - project_snippet_path(event.project, event.note_target, anchor: dom_id(event.target)) + project_snippet_url(event.project, event.note_target, anchor: dom_id(event.target)) else - polymorphic_path([event.project.namespace.becomes(Namespace), - event.project, event.note_target], + polymorphic_url([event.project.namespace.becomes(Namespace), + event.project, event.note_target], anchor: dom_id(event.target)) end end @@ -166,7 +170,7 @@ module EventsHelper event.note_target_reference end - link_to(text, event_note_target_path(event), title: event.target_title, class: 'has-tooltip') + link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip') else content_tag(:strong, '(deleted)') 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..97406fefd43 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 } @@ -317,11 +327,15 @@ module IssuablesHelper end def issuable_button_visibility(issuable, closed) + return 'hidden' if issuable_button_hidden?(issuable, closed) + end + + def issuable_button_hidden?(issuable, closed) case issuable when Issue - issue_button_visibility(issuable, closed) + issue_button_hidden?(issuable, closed) when MergeRequest - merge_request_button_visibility(issuable, closed) + merge_request_button_hidden?(issuable, closed) end end diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb index 5b27d1d9404..957ab06b0ca 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> @@ -62,7 +64,11 @@ module IssuesHelper end def issue_button_visibility(issue, closed) - return 'hidden' if issue.closed? == closed + return 'hidden' if issue_button_hidden?(issue, closed) + end + + def issue_button_hidden?(issue, closed) + issue.closed? == closed || (!closed && issue.discussion_locked) end def confidential_icon(issue) @@ -105,8 +111,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 +121,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..76ed8efe2c6 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 @@ -129,20 +131,26 @@ module LabelsHelper end end - def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false) - project = @target_project || @project - + def labels_filter_path_with_defaults(only_group_labels: false, include_ancestor_groups: true, include_descendant_groups: false) options = {} options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups + options[:only_group_labels] = only_group_labels if only_group_labels && @group + options[:format] = :json + + labels_filter_path(options) + end + + def labels_filter_path(options = {}) + project = @target_project || @project + format = options.delete(:format) || :html if project - project_labels_path(project, :json, options) + project_labels_path(project, format, options) elsif @group - options[:only_group_labels] = only_group_labels if only_group_labels - group_labels_path(@group, :json, options) + group_labels_path(@group, format, options) else - dashboard_labels_path(:json) + dashboard_labels_path(format, options) end end 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..23d7aa427bb 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) @@ -78,7 +80,11 @@ module MergeRequestsHelper end def merge_request_button_visibility(merge_request, closed) - return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? + return 'hidden' if merge_request_button_hidden?(merge_request, closed) + end + + def merge_request_button_hidden?(merge_request, closed) + merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? end def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil) 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 30585cb403d..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,14 +57,16 @@ 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(name: extra_group.name) + existing_group = Group.find_by(path: extra_group.path) extra_group = existing_group if existing_group&.persisted? end 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..e0905584803 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 @@ -176,7 +178,7 @@ module NotesHelper notesPath: notes_url, totalNotes: issuable.discussions.length, lastFetchedAt: Time.now.to_i - }.to_json + } end def discussion_resolved_intro(discussion) 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..0016f89db5c 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 @@ -441,6 +454,7 @@ module ProjectsHelper buildsAccessLevel: feature.builds_access_level, wikiAccessLevel: feature.wiki_access_level, snippetsAccessLevel: feature.snippets_access_level, + pagesAccessLevel: feature.pages_access_level, containerRegistryEnabled: !!project.container_registry_enabled, lfsEnabled: !!project.lfs_enabled } @@ -455,7 +469,10 @@ module ProjectsHelper registryAvailable: Gitlab.config.registry.enabled, registryHelpPath: help_page_path('user/project/container_registry'), lfsAvailable: Gitlab.config.lfs.enabled, - lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs') + lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs'), + pagesAvailable: Gitlab.config.pages.enabled, + pagesAccessControlEnabled: Gitlab.config.pages.access_control, + pagesHelpPath: help_page_path('user/project/pages/index.md') } end diff --git a/app/helpers/repository_languages_helper.rb b/app/helpers/repository_languages_helper.rb index 9a842cf5ce0..cf7eee7fff3 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? @@ -11,6 +13,7 @@ module RepositoryLanguagesHelper content_tag :div, nil, class: "progress-bar has-tooltip", style: "width: #{lang.share}%; background-color:#{lang.color}", - title: lang.name + data: { html: true }, + title: "<span class=\"repository-language-bar-tooltip-language\">#{escape_javascript(lang.name)}</span> <span class=\"repository-language-bar-tooltip-share\">#{lang.share.round(1)}%</span>" end end 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..3e6a301b77d 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 @@ -19,9 +21,17 @@ module TimeHelper "#{from.to_s(:short)} - #{to.to_s(:short)}" end - def duration_in_numbers(duration) - time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S" + def duration_in_numbers(duration_in_seconds, allow_overflow = false) + if allow_overflow + seconds = duration_in_seconds % 1.minute + minutes = (duration_in_seconds / 1.minute) % (1.hour / 1.minute) + hours = duration_in_seconds / 1.hour + + "%02d:%02d:%02d" % [hours, minutes, seconds] + else + time_format = duration_in_seconds < 1.hour ? "%M:%S" : "%H:%M:%S" - Time.at(duration).utc.strftime(time_format) + Time.at(duration_in_seconds).utc.strftime(time_format) + end end end 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..42b533ad772 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), @@ -74,7 +76,7 @@ module UsersHelper tabs = [] if can?(current_user, :read_user_profile, @user) - tabs += [:activity, :groups, :contributed, :projects, :snippets] + tabs += [:overview, :activity, :groups, :contributed, :projects, :snippets] end tabs diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb index c20753ece72..ab77b149072 100644 --- a/app/helpers/version_check_helper.rb +++ b/app/helpers/version_check_helper.rb @@ -1,8 +1,25 @@ +# 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' + 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 + + def link_to_version + if Gitlab.pre_release? + commit_link = link_to(Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', source_code_project, Gitlab.revision)) + [Gitlab::VERSION, content_tag(:small, commit_link)].join(' ').html_safe + else + link_to Gitlab::VERSION, Gitlab::COM_URL + namespace_project_tag_path('gitlab-org', source_code_project, "v#{Gitlab::VERSION}") end end + + def source_code_project + 'gitlab-ce' + 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 41f9eedd4bd..647f34e57ed 100644 --- a/app/helpers/wiki_helper.rb +++ b/app/helpers/wiki_helper.rb @@ -1,4 +1,8 @@ +# frozen_string_literal: true + module WikiHelper + include API::Helpers::RelatedResourcesHelpers + # Produces a pure text breadcrumb for a given page. # # page_slug - The slug of a WikiPage object. @@ -39,4 +43,8 @@ module WikiHelper end end end + + def wiki_attachment_upload_url + expose_url(api_v4_projects_wikis_attachments_path(id: @project.id)) + end end 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..be085496731 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -3,13 +3,14 @@ module Emails module MergeRequests def new_merge_request_email(recipient_id, merge_request_id, reason = nil) - setup_merge_request_mail(merge_request_id, recipient_id) + setup_merge_request_mail(merge_request_id, recipient_id, present: true) mail_new_thread(@merge_request, merge_request_thread_options(@merge_request.author_id, recipient_id, reason)) end def new_mention_in_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil) - setup_merge_request_mail(merge_request_id, recipient_id) + setup_merge_request_mail(merge_request_id, recipient_id, present: true) + mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason)) end @@ -22,12 +23,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) @@ -73,11 +76,16 @@ module Emails private - def setup_merge_request_mail(merge_request_id, recipient_id) + def setup_merge_request_mail(merge_request_id, recipient_id, present: false) @merge_request = MergeRequest.find(merge_request_id) @project = @merge_request.project @target_url = project_merge_request_url(@project, @merge_request) + if present + recipient = User.find(recipient_id) + @mr_presenter = @merge_request.present(current_user: recipient) + end + @sent_notification = SentNotification.record(@merge_request, recipient_id, reply_key) end 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/ability.rb b/app/models/ability.rb index a853106e5bd..1466407d0d1 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -74,7 +74,7 @@ class Ability end def policy_for(user, subject = :global) - cache = RequestStore.active? ? RequestStore : {} + cache = Gitlab::SafeRequestStore.active? ? Gitlab::SafeRequestStore : {} DeclarativePolicy.policy_for(user, subject, cache: cache) end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index 03bd7fa016e..65a2f760f93 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -26,7 +26,6 @@ class ApplicationSetting < ActiveRecord::Base serialize :domain_whitelist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :domain_blacklist, Array # rubocop:disable Cop/ActiveRecordSerialize serialize :repository_storages # rubocop:disable Cop/ActiveRecordSerialize - serialize :sidekiq_throttling_queues, Array # rubocop:disable Cop/ActiveRecordSerialize cache_markdown_field :sign_in_text cache_markdown_field :help_page_text @@ -131,15 +130,6 @@ class ApplicationSetting < ActiveRecord::Base presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' }, if: :domain_blacklist_enabled? - validates :sidekiq_throttling_factor, - numericality: { greater_than: 0, less_than: 1 }, - presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' }, - if: :sidekiq_throttling_enabled? - - validates :sidekiq_throttling_queues, - presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' }, - if: :sidekiq_throttling_enabled? - validates :housekeeping_incremental_repack_period, presence: true, numericality: { only_integer: true, greater_than: 0 } @@ -192,6 +182,12 @@ class ApplicationSetting < ActiveRecord::Base numericality: { less_than_or_equal_to: :gitaly_timeout_default }, if: :gitaly_timeout_default + validates :diff_max_patch_bytes, + presence: true, + numericality: { only_integer: true, + greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, + less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND } + validates :user_default_internal_regex, js_regex: true, allow_nil: true SUPPORTED_KEY_TYPES.each do |type| @@ -219,6 +215,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 @@ -281,7 +278,6 @@ class ApplicationSetting < ActiveRecord::Base send_user_confirmation_email: false, shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'], shared_runners_text: nil, - sidekiq_throttling_enabled: false, sign_in_text: nil, signup_enabled: Settings.gitlab['signup_enabled'], terminal_max_session_time: 0, @@ -302,7 +298,9 @@ 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, + diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES } end @@ -326,10 +324,6 @@ class ApplicationSetting < ActiveRecord::Base ::Gitlab::Database.cached_column_exists?(:application_settings, :help_page_support_url) end - def sidekiq_throttling_column_exists? - ::Gitlab::Database.cached_column_exists?(:application_settings, :sidekiq_throttling_enabled) - end - def disabled_oauth_sign_in_sources=(sources) sources = (sources || []).map(&:to_s) & Devise.omniauth_providers.map(&:to_s) super(sources) @@ -381,6 +375,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 @@ -404,12 +403,6 @@ class ApplicationSetting < ActiveRecord::Base ensure_health_check_access_token! end - def sidekiq_throttling_enabled? - return false unless sidekiq_throttling_column_exists? - - sidekiq_throttling_enabled - end - def usage_ping_can_be_configured? Settings.gitlab.usage_ping_enabled 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/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb index d12dd93ce2e..7cae60a74d6 100644 --- a/app/models/blob_viewer/package_json.rb +++ b/app/models/blob_viewer/package_json.rb @@ -33,7 +33,8 @@ module BlobViewer end def homepage - json_data['homepage'] + url = json_data['homepage'] + url if Gitlab::UrlSanitizer.valid?(url) end def npm_url diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb index cd0b31482d2..d87d6a5cb2f 100644 --- a/app/models/ci/artifact_blob.rb +++ b/app/models/ci/artifact_blob.rb @@ -4,7 +4,7 @@ module Ci class ArtifactBlob include BlobLike - EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json].freeze + EXTENSIONS_SERVED_BY_PAGES = %w[.html .htm .txt .json .log].freeze attr_reader :entry diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index faa160ad6ba..cdfe8175a42 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! @@ -67,8 +68,12 @@ module Ci '', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive) end + scope :with_existing_job_artifacts, ->(query) do + where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query)) + end + scope :with_archived_trace, ->() do - where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace) + with_existing_job_artifacts(Ci::JobArtifact.trace) end scope :without_archived_trace, ->() do @@ -76,16 +81,19 @@ module Ci end scope :with_test_reports, ->() do - includes(:job_artifacts_junit) # Prevent N+1 problem when iterating each ci_job_artifact row - .where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').test_reports) + with_existing_job_artifacts(Ci::JobArtifact.test_reports) + .eager_load_job_artifacts end + scope :eager_load_job_artifacts, -> { includes(:job_artifacts) } + scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) } scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) } scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } - scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } + scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) } + scope :scheduled_actions, ->() { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) } scope :ref_protected, -> { where(protected: true) } scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) } @@ -139,9 +147,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 @@ -150,6 +160,34 @@ module Ci transition created: :manual end + event :schedule do + transition created: :scheduled + end + + event :unschedule do + transition scheduled: :manual + end + + event :enqueue_scheduled do + transition scheduled: :pending, if: ->(build) do + build.scheduled_at && build.scheduled_at < Time.now + end + end + + before_transition scheduled: any do |build| + build.scheduled_at = nil + end + + before_transition created: :scheduled do |build| + build.scheduled_at = build.options_scheduled_at + end + + after_transition created: :scheduled do |build| + build.run_after_commit do + Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id) + end + end + after_transition any => [:pending] do |build| build.run_after_commit do BuildQueueWorker.perform_async(id) @@ -217,18 +255,29 @@ module Ci end def playable? - action? && (manual? || retryable?) + action? && (manual? || scheduled? || retryable?) + end + + def schedulable? + Feature.enabled?('ci_enable_scheduled_build', default_enabled: true) && + self.when == 'delayed' && options[:start_in].present? + end + + def options_scheduled_at + ChronicDuration.parse(options[:start_in])&.seconds&.from_now end def action? - self.when == 'manual' + %w[manual delayed].include?(self.when) 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 +434,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) @@ -397,8 +448,8 @@ module Ci trace.exist? end - def has_test_reports? - job_artifacts.test_reports.any? + def has_job_artifacts? + job_artifacts.any? end def has_old_trace? @@ -462,28 +513,23 @@ module Ci end end - def erase_artifacts! - remove_artifacts_file! - remove_artifacts_metadata! - save - end - - def erase_test_reports! - # TODO: Use fast_destroy_all in the context of https://gitlab.com/gitlab-org/gitlab-ce/issues/35240 - job_artifacts_junit&.destroy + # and use that for `ExpireBuildInstanceArtifactsWorker`? + def erase_erasable_artifacts! + job_artifacts.erasable.destroy_all # rubocop: disable DestroyAll + erase_old_artifacts! end def erase(opts = {}) return false unless erasable? - erase_artifacts! - erase_test_reports! + job_artifacts.destroy_all # rubocop: disable DestroyAll + erase_old_artifacts! erase_trace! update_erased!(opts[:erased_by]) end def erasable? - complete? && (artifacts? || has_test_reports? || has_trace?) + complete? && (artifacts? || has_job_artifacts? || has_trace?) end def erased? @@ -514,6 +560,13 @@ module Ci self.job_artifacts.update_all(expire_at: nil) end + def artifacts_file_for_type(type) + file = job_artifacts.find_by(file_type: Ci::JobArtifact.file_types[type])&.file + # TODO: to be removed once legacy artifacts is removed + file ||= legacy_artifacts_file if type == :archive + file + end + def coverage_regex super || project.try(:build_coverage_regex) end @@ -641,22 +694,57 @@ module Ci def collect_test_reports!(test_reports) test_reports.get_suite(group_name).tap do |test_suite| - each_test_report do |file_type, blob| - Gitlab::Ci::Parsers.fabricate!(file_type).parse!(blob, test_suite) + each_report(Ci::JobArtifact::TEST_REPORT_FILE_TYPES) do |file_type, blob| + Gitlab::Ci::Parsers::Test.fabricate!(file_type).parse!(blob, test_suite) end 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 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 - yield file_type, blob + def erase_old_artifacts! + # TODO: To be removed once we get rid of + remove_artifacts_file! + remove_artifacts_metadata! + save + end + + 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_report(report_types) + job_artifacts_for_types(report_types).each do |report_artifact| + report_artifact.each_blob do |blob| + yield report_artifact.file_type, blob end end end + def job_artifacts_for_types(report_types) + # Use select to leverage cached associations and avoid N+1 queries + job_artifacts.select { |artifact| artifact.file_type.in?(report_types) } + end + def update_artifacts_size self.artifacts_size = legacy_artifacts_file&.size end @@ -700,6 +788,9 @@ module Ci variables.append(key: 'GITLAB_FEATURES', value: project.licensed_features.join(',')) variables.append(key: 'CI_SERVER_NAME', value: 'GitLab') variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION) + variables.append(key: 'CI_SERVER_VERSION_MAJOR', value: gitlab_version_info.major.to_s) + variables.append(key: 'CI_SERVER_VERSION_MINOR', value: gitlab_version_info.minor.to_s) + variables.append(key: 'CI_SERVER_VERSION_PATCH', value: gitlab_version_info.patch.to_s) variables.append(key: 'CI_SERVER_REVISION', value: Gitlab.revision) variables.append(key: 'CI_JOB_NAME', value: name) variables.append(key: 'CI_JOB_STAGE', value: stage) @@ -714,6 +805,10 @@ module Ci end end + def gitlab_version_info + @gitlab_version_info ||= Gitlab::VersionInfo.parse(Gitlab::VERSION) + end + def legacy_variables Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_BUILD_REF', value: sha) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 17b7ee4f07e..cb73fc74bb6 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -9,8 +9,30 @@ module Ci NotSupportedAdapterError = Class.new(StandardError) TEST_REPORT_FILE_TYPES = %w[junit].freeze - DEFAULT_FILE_NAMES = { junit: 'junit.xml' }.freeze - TYPE_AND_FORMAT_PAIRS = { archive: :zip, metadata: :gzip, trace: :raw, junit: :gzip }.freeze + NON_ERASABLE_FILE_TYPES = %w[trace].freeze + DEFAULT_FILE_NAMES = { + archive: nil, + metadata: nil, + trace: nil, + junit: 'junit.xml', + codequality: 'codequality.json', + sast: 'gl-sast-report.json', + dependency_scanning: 'gl-dependency-scanning-report.json', + container_scanning: 'gl-container-scanning-report.json', + dast: 'gl-dast-report.json' + }.freeze + + TYPE_AND_FORMAT_PAIRS = { + archive: :zip, + metadata: :gzip, + trace: :raw, + junit: :gzip, + codequality: :gzip, + sast: :gzip, + dependency_scanning: :gzip, + container_scanning: :gzip, + dast: :gzip + }.freeze belongs_to :project belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id @@ -27,8 +49,18 @@ module Ci scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) } + scope :with_file_types, -> (file_types) do + types = self.file_types.select { |file_type| file_types.include?(file_type) }.values + + where(file_type: types) + end + scope :test_reports, -> do - types = self.file_types.select { |file_type| TEST_REPORT_FILE_TYPES.include?(file_type) }.values + with_file_types(TEST_REPORT_FILE_TYPES) + end + + scope :erasable, -> do + types = self.file_types.reject { |file_type| NON_ERASABLE_FILE_TYPES.include?(file_type) }.values where(file_type: types) end @@ -39,7 +71,12 @@ module Ci archive: 1, metadata: 2, trace: 3, - junit: 4 + junit: 4, + sast: 5, ## EE-specific + dependency_scanning: 6, ## EE-specific + container_scanning: 7, ## EE-specific + dast: 8, ## EE-specific + codequality: 9 ## EE-specific } enum file_format: { @@ -48,6 +85,20 @@ module Ci gzip: 3 } + # `file_location` indicates where actual files are stored. + # Ideally, actual files should be stored in the same directory, and use the same + # convention to generate its path. However, sometimes we can't do so due to backward-compatibility. + # + # legacy_path ... The actual file is stored at a path consists of a timestamp + # and raw project/model IDs. Those rows were migrated from + # `ci_builds.artifacts_file` and `ci_builds.artifacts_metadata` + # hashed_path ... The actual file is stored at a path consists of a SHA2 based on the project ID. + # This is the default value. + enum file_location: { + legacy_path: 1, + hashed_path: 2 + } + FILE_FORMAT_ADAPTERS = { gzip: Gitlab::Ci::Build::Artifacts::GzipFileAdapter }.freeze @@ -72,6 +123,12 @@ module Ci [nil, ::JobArtifactUploader::Store::LOCAL].include?(self.file_store) end + def hashed_path? + return true if trace? # ArchiveLegacyTraces background migration might not have `file_location` column + + super || self.file_location.nil? + end + def expire_in expire_at - Time.now if expire_at end @@ -108,7 +165,7 @@ module Ci end def update_project_statistics_after_destroy - update_project_statistics(-self.size) + update_project_statistics(-self.size.to_i) end def update_project_statistics(difference) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 526bf7af99b..17024e8a0af 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -35,6 +35,7 @@ module Ci has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus' has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' + has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build' has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id' @@ -80,7 +81,7 @@ module Ci state_machine :status, initial: :created do event :enqueue do - transition [:created, :skipped] => :pending + transition [:created, :skipped, :scheduled] => :pending transition [:success, :failed, :canceled] => :running end @@ -108,6 +109,10 @@ module Ci transition any - [:manual] => :manual end + event :delay do + transition any - [:scheduled] => :scheduled + end + # IMPORTANT # Do not add any operations to this state_machine # Create a separate worker for each new operation @@ -161,6 +166,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 +392,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 +471,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 +532,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 @@ -534,6 +549,7 @@ module Ci when 'canceled' then cancel when 'skipped' then skip when 'manual' then block + when 'scheduled' then delay else raise HasStatus::UnknownStatusError, "Unknown status `#{latest_builds_status}`" @@ -617,6 +633,22 @@ module Ci end end + def branch_updated? + strong_memoize(:branch_updated) do + push_details.branch_updated? + end + end + + def modified_paths + strong_memoize(:modified_paths) do + push_details.modified_paths + end + end + + def default_branch? + ref == project.default_branch + end + private def ci_yaml_from_repo @@ -640,6 +672,22 @@ module Ci Gitlab::DataBuilder::Pipeline.build(self) end + def push_details + strong_memoize(:push_details) do + Gitlab::Git::Push.new(project, before_sha, sha, push_ref) + end + end + + def push_ref + if branch? + Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s + elsif tag? + Gitlab::Git::TAG_REF_PREFIX + ref.to_s + else + raise ArgumentError, 'Invalid pipeline type!' + end + end + def latest_builds_status return 'failed' unless yaml_errors.blank? diff --git a/app/models/ci/pipeline_variable.rb b/app/models/ci/pipeline_variable.rb index 017ec0b145a..08514d6af4e 100644 --- a/app/models/ci/pipeline_variable.rb +++ b/app/models/ci/pipeline_variable.rb @@ -10,5 +10,9 @@ module Ci alias_attribute :secret_value, :value validates :key, uniqueness: { scope: :pipeline_id } + + def hook_attrs + { key: key, value: value } + end end end diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb index f41955f43e7..31330d0682e 100644 --- a/app/models/ci/runner.rb +++ b/app/models/ci/runner.rb @@ -7,11 +7,26 @@ module Ci include IgnorableColumn include RedisCacheable include ChronicDurationAttribute + include FromUnion + + enum access_level: { + not_protected: 0, + ref_protected: 1 + } + + enum runner_type: { + instance_type: 1, + group_type: 2, + project_type: 3 + } 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_LEGACY = %w[specific shared].freeze + AVAILABLE_TYPES = runner_types.keys.freeze + AVAILABLE_STATUSES = %w[active paused online offline].freeze + AVAILABLE_SCOPES = (AVAILABLE_TYPES_LEGACY + 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 +44,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 +70,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 @@ -76,17 +109,6 @@ module Ci after_destroy :cleanup_runner_queue - enum access_level: { - not_protected: 0, - ref_protected: 1 - } - - enum runner_type: { - instance_type: 1, - group_type: 2, - project_type: 3 - } - cached_attr_reader :version, :revision, :platform, :architecture, :ip_address, :contacted_at chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout @@ -115,6 +137,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/stage.rb b/app/models/ci/stage.rb index 511ded55dc3..58f3fe2460a 100644 --- a/app/models/ci/stage.rb +++ b/app/models/ci/stage.rb @@ -65,6 +65,10 @@ module Ci event :block do transition any - [:manual] => :manual end + + event :delay do + transition any - [:scheduled] => :scheduled + end end def update_status @@ -77,6 +81,7 @@ module Ci when 'failed' then drop when 'canceled' then cancel when 'manual' then block + when 'scheduled' then delay when 'skipped', nil then skip else raise HasStatus::UnknownStatusError, 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..7be6a14f585 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 @@ -72,6 +73,11 @@ module Clusters "clientSecret" => oauth_application.secret, "callbackUrl" => callback_url } + }, + "singleuser" => { + "extraEnv" => { + "GITLAB_CLUSTER_ID" => cluster.id + } } } end 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/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb index d4d3859dfd5..a9df59fc059 100644 --- a/app/models/clusters/concerns/application_status.rb +++ b/app/models/clusters/concerns/application_status.rb @@ -15,6 +15,9 @@ module Clusters state :scheduled, value: 1 state :installing, value: 2 state :installed, value: 3 + state :updating, value: 4 + state :updated, value: 5 + state :update_errored, value: 6 event :make_scheduled do transition [:installable, :errored] => :scheduled @@ -32,6 +35,18 @@ module Clusters transition any => :errored end + event :make_updating do + transition [:installed, :updated, :update_errored] => :updating + end + + event :make_updated do + transition [:updating] => :updated + end + + event :make_update_errored do + transition any => :update_errored + end + before_transition any => [:scheduled] do |app_status, _| app_status.status_reason = nil end @@ -40,6 +55,15 @@ module Clusters status_reason = transition.args.first app_status.status_reason = status_reason if status_reason end + + before_transition any => [:updating] do |app_status, _| + app_status.status_reason = nil + end + + before_transition any => [:update_errored] do |app_status, transition| + status_reason = transition.args.first + app_status.status_reason = status_reason if status_reason + end end end end 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..a61ed03cf35 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] @@ -318,7 +319,11 @@ class Commit def status(ref = nil) return @statuses[ref] if @statuses.key?(ref) - @statuses[ref] = project.pipelines.latest_status_per_commit(id, ref)[id] + @statuses[ref] = status_for_project(ref, project) + end + + def status_for_project(ref, pipeline_project) + pipeline_project.pipelines.latest_status_per_commit(id, ref)[id] end def set_status_for_ref(ref, status) diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index b65d7672973..06507345fe8 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -49,7 +49,8 @@ class CommitStatus < ActiveRecord::Base stuck_or_timeout_failure: 3, runner_system_failure: 4, missing_dependency_failure: 5, - runner_unsupported: 6 + runner_unsupported: 6, + stale_schedule: 7 } ## @@ -58,9 +59,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 @@ -69,7 +72,7 @@ class CommitStatus < ActiveRecord::Base end event :enqueue do - transition [:created, :skipped, :manual] => :pending + transition [:created, :skipped, :manual, :scheduled] => :pending end event :run do @@ -81,7 +84,7 @@ class CommitStatus < ActiveRecord::Base end event :drop do - transition [:created, :pending, :running] => :failed + transition [:created, :pending, :running, :scheduled] => :failed end event :success do @@ -89,10 +92,10 @@ class CommitStatus < ActiveRecord::Base end event :cancel do - transition [:created, :pending, :running, :manual] => :canceled + transition [:created, :pending, :running, :manual, :scheduled] => :canceled end - before_transition [:created, :skipped, :manual] => :pending do |commit_status| + before_transition [:created, :skipped, :manual, :scheduled] => :pending do |commit_status| commit_status.queued_at = Time.now end @@ -130,10 +133,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/avatarable.rb b/app/models/concerns/avatarable.rb index c0233661a9b..0d5311a9985 100644 --- a/app/models/concerns/avatarable.rb +++ b/app/models/concerns/avatarable.rb @@ -9,7 +9,7 @@ module Avatarable include Gitlab::Utils::StrongMemoize validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? } - validates :avatar, file_size: { maximum: 200.kilobytes.to_i } + validates :avatar, file_size: { maximum: 200.kilobytes.to_i }, if: :avatar_changed? mount_uploader :avatar, AvatarUploader diff --git a/app/models/concerns/bulk_member_access_load.rb b/app/models/concerns/bulk_member_access_load.rb index c4346d5dd17..041ed3755e0 100644 --- a/app/models/concerns/bulk_member_access_load.rb +++ b/app/models/concerns/bulk_member_access_load.rb @@ -16,9 +16,9 @@ module BulkMemberAccessLoad key = max_member_access_for_resource_key(resource_klass, memoization_index) access = {} - if RequestStore.active? - RequestStore.store[key] ||= {} - access = RequestStore.store[key] + if Gitlab::SafeRequestStore.active? + Gitlab::SafeRequestStore[key] ||= {} + access = Gitlab::SafeRequestStore[key] end # Look up only the IDs we need diff --git a/app/models/concerns/cacheable_attributes.rb b/app/models/concerns/cacheable_attributes.rb index 62b78c3611c..f8034be8376 100644 --- a/app/models/concerns/cacheable_attributes.rb +++ b/app/models/concerns/cacheable_attributes.rb @@ -27,11 +27,7 @@ module CacheableAttributes end def cached - if RequestStore.active? - RequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache - else - retrieve_from_cache - end + Gitlab::SafeRequestStore[:"#{name}_cached_attributes"] ||= retrieve_from_cache end def retrieve_from_cache 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/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb new file mode 100644 index 00000000000..b61bf29e6ad --- /dev/null +++ b/app/models/concerns/diff_positionable_note.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true +module DiffPositionableNote + extend ActiveSupport::Concern + + included do + before_validation :set_original_position, on: :create + before_validation :update_position, on: :create, if: :on_text? + + serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize + end + + %i(original_position position change_position).each do |meth| + define_method "#{meth}=" do |new_position| + if new_position.is_a?(String) + new_position = JSON.parse(new_position) rescue nil + end + + if new_position.is_a?(Hash) + new_position = new_position.with_indifferent_access + new_position = Gitlab::Diff::Position.new(new_position) + end + + return if new_position == read_attribute(meth) + + super(new_position) + end + end + + def on_text? + position&.position_type == "text" + end + + def on_image? + position&.position_type == "image" + end + + def supported? + for_commit? || self.noteable.has_complete_diff_refs? + end + + def active?(diff_refs = nil) + return false unless supported? + return true if for_commit? + + diff_refs ||= noteable.diff_refs + + self.position.diff_refs == diff_refs + end + + def set_original_position + return unless position + + self.original_position = self.position.dup unless self.original_position&.complete? + end + + def update_position + return unless supported? + return if for_commit? + + return if active? + return unless position + + tracer = Gitlab::Diff::PositionTracer.new( + project: self.project, + old_diff_refs: self.position.diff_refs, + new_diff_refs: self.noteable.diff_refs, + paths: self.position.paths + ) + + result = tracer.trace(self.position) + return unless result + + if result[:outdated] + self.change_position = result[:position] + else + self.position = result[:position] + 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/has_status.rb b/app/models/concerns/has_status.rb index b3960cbad1a..b92643f87f8 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -4,14 +4,15 @@ module HasStatus extend ActiveSupport::Concern DEFAULT_STATUS = 'created'.freeze - BLOCKED_STATUS = 'manual'.freeze - AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual].freeze - STARTED_STATUSES = %w[running success failed skipped manual].freeze + BLOCKED_STATUS = %w[manual scheduled].freeze + AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped manual scheduled].freeze + STARTED_STATUSES = %w[running success failed skipped manual scheduled].freeze ACTIVE_STATUSES = %w[pending running].freeze COMPLETED_STATUSES = %w[success failed canceled skipped].freeze - ORDERED_STATUSES = %w[failed pending running manual canceled success skipped created].freeze + ORDERED_STATUSES = %w[failed pending running manual scheduled canceled success skipped created].freeze STATUSES_ENUM = { created: 0, pending: 1, running: 2, success: 3, - failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze + failed: 4, canceled: 5, skipped: 6, manual: 7, + scheduled: 8 }.freeze UnknownStatusError = Class.new(StandardError) @@ -24,6 +25,7 @@ module HasStatus created = scope_relevant.created.select('count(*)').to_sql success = scope_relevant.success.select('count(*)').to_sql manual = scope_relevant.manual.select('count(*)').to_sql + scheduled = scope_relevant.scheduled.select('count(*)').to_sql pending = scope_relevant.pending.select('count(*)').to_sql running = scope_relevant.running.select('count(*)').to_sql skipped = scope_relevant.skipped.select('count(*)').to_sql @@ -40,6 +42,7 @@ module HasStatus WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending' WHEN (#{running})+(#{pending})>0 THEN 'running' WHEN (#{manual})>0 THEN 'manual' + WHEN (#{scheduled})>0 THEN 'scheduled' WHEN (#{created})>0 THEN 'running' ELSE 'failed' END)" @@ -74,6 +77,7 @@ module HasStatus state :canceled, value: 'canceled' state :skipped, value: 'skipped' state :manual, value: 'manual' + state :scheduled, value: 'scheduled' end scope :created, -> { where(status: 'created') } @@ -85,6 +89,7 @@ module HasStatus scope :canceled, -> { where(status: 'canceled') } scope :skipped, -> { where(status: 'skipped') } scope :manual, -> { where(status: 'manual') } + scope :scheduled, -> { where(status: 'scheduled') } scope :alive, -> { where(status: [:created, :pending, :running]) } scope :created_or_pending, -> { where(status: [:created, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) } @@ -92,7 +97,7 @@ module HasStatus scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :cancelable, -> do - where(status: [:running, :pending, :created]) + where(status: [:running, :pending, :created, :scheduled]) end end @@ -109,7 +114,7 @@ module HasStatus end def blocked? - BLOCKED_STATUS == status + BLOCKED_STATUS.include?(status) end private diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index f881ce2321c..2aa52bbaeea 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 @@ -76,6 +76,7 @@ module Issuable scope :recent, -> { reorder(id: :desc) } scope :of_projects, ->(ids) { where(project_id: ids) } scope :of_milestones, ->(ids) { where(milestone_id: ids) } + scope :any_milestone, -> { where('milestone_id IS NOT NULL') } scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) } scope :opened, -> { with_state(:opened) } scope :only_opened, -> { with_state(:opened) } @@ -109,10 +110,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.rb b/app/models/concerns/protected_ref.rb index e62e680af6e..af387c99f3d 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -50,14 +50,20 @@ module ProtectedRef .map(&:"#{action}_access_levels").flatten end + # Returns all protected refs that match the given ref name. + # This checks all records from the scope built up so far, and does + # _not_ return a relation. + # + # This method optionally takes in a list of `protected_refs` to search + # through, to avoid calling out to the database. def matching(ref_name, protected_refs: nil) - ProtectedRefMatcher.matching(self, ref_name, protected_refs: protected_refs) + (protected_refs || self.all).select { |protected_ref| protected_ref.matches?(ref_name) } end end private def ref_matcher - @ref_matcher ||= ProtectedRefMatcher.new(self) + @ref_matcher ||= RefMatcher.new(self.name) 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..7723c07279d 100644 --- a/app/models/concerns/storage/legacy_namespace.rb +++ b/app/models/concerns/storage/legacy_namespace.rb @@ -5,8 +5,10 @@ module Storage extend ActiveSupport::Concern def move_dir - if any_project_has_container_registry_tags? - raise Gitlab::UpdatePathError.new('Namespace cannot be moved, because at least one project has tags in container registry') + proj_with_tags = first_project_with_container_registry_tags + + if proj_with_tags + raise Gitlab::UpdatePathError.new("Namespace #{name} (#{id}) cannot be moved because at least one project (e.g. #{proj_with_tags.name} (#{proj_with_tags.id})) has tags in container registry") end parent_was = if parent_changed? && parent_id_was.present? @@ -25,8 +27,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 +101,6 @@ module Storage end end end - - remove_exports! end def remove_legacy_exports! diff --git a/app/models/concerns/subscribable.rb b/app/models/concerns/subscribable.rb index 1d0a61364b0..92a5c1112af 100644 --- a/app/models/concerns/subscribable.rb +++ b/app/models/concerns/subscribable.rb @@ -31,9 +31,11 @@ module Subscribable end def subscribers(project) - subscriptions_available(project) - .where(subscribed: true) - .map(&:user) + relation = subscriptions_available(project) + .where(subscribed: true) + .select(:user_id) + + User.where(id: relation) end def toggle_subscription(user, project = nil) diff --git a/app/models/concerns/triggerable_hooks.rb b/app/models/concerns/triggerable_hooks.rb index f55ab2fcaf3..c52baa0524c 100644 --- a/app/models/concerns/triggerable_hooks.rb +++ b/app/models/concerns/triggerable_hooks.rb @@ -6,6 +6,7 @@ module TriggerableHooks push_hooks: :push_events, tag_push_hooks: :tag_push_events, issue_hooks: :issues_events, + confidential_note_hooks: :confidential_note_events, confidential_issue_hooks: :confidential_issues_events, note_hooks: :note_events, merge_request_hooks: :merge_requests_events, @@ -28,6 +29,12 @@ module TriggerableHooks public_send(trigger) # rubocop:disable GitlabSecurity/PublicSend end + def select_active(hooks_scope, data) + select do |hook| + ActiveHookFilter.new(hook).matches?(hooks_scope, data) + end + end + private def triggerable_hooks(hooks) 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..95694377fe3 100644 --- a/app/models/diff_note.rb +++ b/app/models/diff_note.rb @@ -5,14 +5,11 @@ # A note of this type can be resolvable. class DiffNote < Note include NoteOnDiff + include DiffPositionableNote include Gitlab::Utils::StrongMemoize NOTEABLE_TYPES = %w(MergeRequest Commit).freeze - serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize - validates :original_position, presence: true validates :position, presence: true validates :line_code, presence: true, line_code: true, if: :on_text? @@ -21,8 +18,6 @@ class DiffNote < Note validate :verify_supported validate :diff_refs_match_commit, if: :for_commit? - before_validation :set_original_position, on: :create - before_validation :update_position, on: :create, if: :on_text? before_validation :set_line_code, if: :on_text? after_save :keep_around_commits after_commit :create_diff_file, on: :create @@ -31,31 +26,6 @@ class DiffNote < Note DiffDiscussion end - %i(original_position position change_position).each do |meth| - define_method "#{meth}=" do |new_position| - if new_position.is_a?(String) - new_position = JSON.parse(new_position) rescue nil - end - - if new_position.is_a?(Hash) - new_position = new_position.with_indifferent_access - new_position = Gitlab::Diff::Position.new(new_position) - end - - return if new_position == read_attribute(meth) - - super(new_position) - end - end - - def on_text? - position.position_type == "text" - end - - def on_image? - position.position_type == "image" - end - def create_diff_file return unless should_create_diff_file? @@ -87,15 +57,6 @@ class DiffNote < Note self.diff_file.line_code(self.diff_line) end - def active?(diff_refs = nil) - return false unless supported? - return true if for_commit? - - diff_refs ||= noteable.diff_refs - - self.position.diff_refs == diff_refs - end - def created_at_diff?(diff_refs) return false unless supported? return true if for_commit? @@ -131,7 +92,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 @@ -141,37 +102,10 @@ class DiffNote < Note for_commit? || self.noteable.has_complete_diff_refs? end - def set_original_position - self.original_position = self.position.dup unless self.original_position&.complete? - end - def set_line_code self.line_code = self.position.line_code(self.project.repository) end - def update_position - return unless supported? - return if for_commit? - - return if active? - - tracer = Gitlab::Diff::PositionTracer.new( - project: self.project, - old_diff_refs: self.position.diff_refs, - new_diff_refs: self.noteable.diff_refs, - paths: self.position.paths - ) - - result = tracer.trace(self.position) - return unless result - - if result[:outdated] - self.change_position = result[:position] - else - self.position = result[:position] - end - end - def verify_supported return if supported? 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..2e690f8c013 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 @@ -147,21 +148,31 @@ class Event < ActiveRecord::Base end end + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity def visible_to_user?(user = nil) 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 personal_snippet_note? + Ability.allowed?(user, :read_personal_snippet, note_target) + elsif project_snippet_note? + Ability.allowed?(user, :read_project_snippet, note_target) + elsif milestone? + Ability.allowed?(user, :read_milestone, project) else - milestone? + false # No other event types are visible end end + # rubocop:enable Metrics/PerceivedComplexity + # rubocop:enable Metrics/CyclomaticComplexity def project_name if project @@ -303,6 +314,10 @@ class Event < ActiveRecord::Base note? && target && target.for_snippet? end + def personal_snippet_note? + note? && target && target.for_personal_snippet? + end + def note_target target.noteable 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..612c546ca57 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -82,8 +82,17 @@ class Group < Namespace User.reference_pattern end - def visible_to_user(user) - where(id: user.authorized_groups.select(:id).reorder(nil)) + # WARNING: This method should never be used on its own + # please do make sure the number of rows you are filtering is small + # enough for this query + def public_or_visible_to_user(user) + return public_to_user unless user + + public_for_user = public_to_user_arel(user) + visible_for_user = visible_to_user_arel(user) + public_or_visible = public_for_user.or(visible_for_user) + + where(public_or_visible) end def select_for_project_authorization @@ -95,6 +104,23 @@ class Group < Namespace super end end + + private + + def public_to_user_arel(user) + self.arel_table[:visibility_level] + .in(Gitlab::VisibilityLevel.levels_for_user(user)) + end + + def visible_to_user_arel(user) + groups_table = self.arel_table + authorized_groups = user.authorized_groups.as('authorized') + + groups_table.project(1) + .from(authorized_groups) + .where(authorized_groups[:id].eq(groups_table[:id])) + .exists + end end # Overrides notification_settings has_many association @@ -236,14 +262,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 +330,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 new file mode 100644 index 00000000000..283e2d680f4 --- /dev/null +++ b/app/models/hooks/active_hook_filter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class ActiveHookFilter + def initialize(hook) + @hook = hook + @push_events_filter_matcher = RefMatcher.new(@hook.push_events_branch_filter) + end + + def matches?(hooks_scope, data) + return true if hooks_scope != :push_hooks + return true if @hook.push_events_branch_filter.blank? + + branch_name = Gitlab::Git.branch_name(data[:ref]) + @push_events_filter_matcher.matches?(branch_name) + end +end 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 f18aadefa5c..68ba4b213b2 100644 --- a/app/models/hooks/web_hook.rb +++ b/app/models/hooks/web_hook.rb @@ -3,23 +3,72 @@ class WebHook < ActiveRecord::Base include Sortable + attr_encrypted :token, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + + attr_encrypted :url, + mode: :per_attribute_iv, + algorithm: 'aes-256-gcm', + key: Settings.attr_encrypted_db_key_base_truncated + has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?), allow_local_network: lambda(&:allow_local_requests?) } 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? false end + + # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. + # Ensure that the encrypted version always takes precedence if present. + alias_method :attr_encrypted_token, :token + def token + attr_encrypted_token.presence || read_attribute(:token) + end + + # In 11.4, the web_hooks table has both `token` and `encrypted_token` fields. + # Pending a background migration to encrypt all fields, we should just clear + # the unencrypted value whenever the new value is set. + alias_method :'attr_encrypted_token=', :'token=' + def token=(value) + self.attr_encrypted_token = value + + write_attribute(:token, nil) + end + + # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. + # Ensure that the encrypted version always takes precedence if present. + alias_method :attr_encrypted_url, :url + def url + attr_encrypted_url.presence || read_attribute(:url) + end + + # In 11.4, the web_hooks table has both `url` and `encrypted_url` fields. + # Pending a background migration to encrypt all fields, we should just clear + # the unencrypted value whenever the new value is set. + alias_method :'attr_encrypted_url=', :'url=' + def url=(value) + self.attr_encrypted_url = value + + write_attribute(:url, nil) + end end diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb index 7d8ce0bbd05..11289887e00 100644 --- a/app/models/instance_configuration.rb +++ b/app/models/instance_configuration.rb @@ -64,10 +64,10 @@ class InstanceConfiguration end def ssh_algorithm_md5(ssh_file_content) - OpenSSL::Digest::MD5.hexdigest(ssh_file_content).scan(/../).join(':') + Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint end def ssh_algorithm_sha256(ssh_file_content) - OpenSSL::Digest::SHA256.hexdigest(ssh_file_content) + Gitlab::SSHPublicKey.new(ssh_file_content).fingerprint('SHA256') end end diff --git a/app/models/issue.rb b/app/models/issue.rb index d0cd7461daa..4ace5d3ab97 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -170,22 +170,6 @@ class Issue < ActiveRecord::Base "#{project.to_reference(from, full: full)}#{reference}" end - # All branches containing the current issue's ID, except for - # those with a merge request open referencing the current issue. - def related_branches(current_user) - branches_with_iid = project.repository.branch_names.select do |branch| - branch =~ /\A#{iid}-(?!\d+-stable)/i - end - - branches_with_merge_request = - Issues::ReferencedMergeRequestsService - .new(project, current_user) - .referenced_merge_requests(self) - .map(&:source_branch) - - branches_with_iid - branches_with_merge_request - end - def suggested_branch_name return to_branch_name unless project.repository.branch_exists?(to_branch_name) @@ -278,9 +262,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..43b49445765 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,9 @@ 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) } + scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) } def self.prioritized(project) joins(:priorities) @@ -69,6 +75,14 @@ class Label < ActiveRecord::Base joins(label_priorities) end + def self.optionally_subscribed_by(user_id) + if user_id + subscribed_by(user_id) + else + all + end + end + alias_attribute :name, :title def self.reference_prefix 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/legacy_diff_note.rb b/app/models/legacy_diff_note.rb index 20f9b18e4ca..00dec6bb92b 100644 --- a/app/models/legacy_diff_note.rb +++ b/app/models/legacy_diff_note.rb @@ -20,11 +20,7 @@ class LegacyDiffNote < Note end def project_repository - if RequestStore.active? - RequestStore.fetch("project:#{project_id}:repository") { self.project.repository } - else - self.project.repository - end + Gitlab::SafeRequestStore.fetch("project:#{project_id}:repository") { self.project.repository } end def diff_file_hash diff --git a/app/models/license_template.rb b/app/models/license_template.rb index 0ad75b27827..73e403f98b4 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{[\<\{\[] @@ -10,12 +12,10 @@ class LicenseTemplate (fullname|name\sof\s(author|copyright\sowner)) [\>\}\]]}xi.freeze - attr_reader :id, :name, :category, :nickname, :url, :meta - - alias_method :key, :id + attr_reader :key, :name, :category, :nickname, :url, :meta - def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {}) - @id = id + def initialize(key:, name:, category:, content:, nickname: nil, url: nil, meta: {}) + @key = key @name = name @category = category @content = content 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..6559f94a696 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -6,6 +6,7 @@ class MergeRequest < ActiveRecord::Base include Issuable include Noteable include Referable + include Presentable include IgnorableColumn include TimeTrackable include ManualInverseAssociation @@ -14,6 +15,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 +139,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 +239,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 @@ -258,7 +261,7 @@ class MergeRequest < ActiveRecord::Base end end - WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze + WIP_REGEX = /\A*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze def self.work_in_progress?(title) !!(title =~ WIP_REGEX) @@ -623,11 +626,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 +741,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 +1038,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 +1053,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 +1065,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 +1131,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 +1161,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 +1197,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..599bedde27d 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 @@ -134,6 +135,10 @@ class Namespace < ActiveRecord::Base all_projects.any?(&:has_container_registry_tags?) end + def first_project_with_container_registry_tags + all_projects.find(&:has_container_registry_tags?) + end + def send_update_instructions projects.each do |project| project.send_move_instructions("#{full_path_was}/#{project.path}") @@ -147,8 +152,8 @@ class Namespace < ActiveRecord::Base def find_fork_of(project) return nil unless project.fork_network - if RequestStore.active? - forks_in_namespace = RequestStore.fetch("namespaces:#{id}:forked_projects") do + if Gitlab::SafeRequestStore.active? + forks_in_namespace = Gitlab::SafeRequestStore.fetch("namespaces:#{id}:forked_projects") do Hash.new do |found_forks, project| found_forks[project] = project.fork_network.find_forks_in(projects).first end @@ -253,18 +258,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..1b595ef60b4 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 @@ -37,10 +38,12 @@ class Note < ActiveRecord::Base alias_attribute :last_edited_at, :updated_at alias_attribute :last_edited_by, :updated_by - # Attribute containing rendered and redacted Markdown as generated by - # Banzai::ObjectRenderer. + # Number of user visible references as generated by Banzai::ObjectRenderer attr_accessor :redacted_note_html + # Total of all references as generated by Banzai::ObjectRenderer + attr_accessor :total_reference_count + # An Array containing the number of visible references as generated by # Banzai::ObjectRenderer attr_accessor :user_visible_reference_count @@ -181,6 +184,7 @@ class Note < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def cross_reference? return unless system? @@ -190,6 +194,7 @@ class Note < ActiveRecord::Base SystemNoteService.cross_reference?(note) end end + # rubocop: enable CodeReuse/ServiceClass def diff_note? false @@ -285,15 +290,7 @@ class Note < ActiveRecord::Base end def cross_reference_not_visible_for?(user) - cross_reference? && !has_referenced_mentionables?(user) - end - - def has_referenced_mentionables?(user) - if user_visible_reference_count.present? - user_visible_reference_count > 0 - else - referenced_mentionables(user).any? - end + cross_reference? && !all_referenced_mentionables_allowed?(user) end def award_emoji? @@ -389,18 +386,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) @@ -474,9 +460,18 @@ class Note < ActiveRecord::Base self.discussion_id ||= discussion_class.discussion_id(self) end + def all_referenced_mentionables_allowed?(user) + if user_visible_reference_count.present? && total_reference_count.present? + # if they are not equal, then there are private/confidential references as well + user_visible_reference_count > 0 && user_visible_reference_count == total_reference_count + else + referenced_mentionables(user).any? + end + end + def force_cross_reference_regex_check? return unless system? - SystemNoteMetadata::TYPES_WITH_CROSS_REFERENCES.include?(system_note_metadata&.action) + system_note_metadata&.cross_reference_types&.include?(system_note_metadata&.action) end end 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 67593c9b2fe..0cdd876dc20 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 @@ -54,8 +55,8 @@ class Project < ActiveRecord::Base cache_markdown_field :description, pipeline: :description delegate :feature_available?, :builds_enabled?, :wiki_enabled?, - :merge_requests_enabled?, :issues_enabled?, to: :project_feature, - allow_nil: true + :merge_requests_enabled?, :issues_enabled?, :pages_enabled?, :public_pages?, + to: :project_feature, allow_nil: true delegate :base_dir, :disk_path, :ensure_storage_path_exists, to: :storage @@ -85,7 +86,7 @@ class Project < ActiveRecord::Base after_create :create_project_feature, unless: :project_feature after_create -> { SiteStatistic.track(STATISTICS_ATTRIBUTE) } - before_destroy :untrack_site_statistics + before_destroy -> { SiteStatistic.untrack(STATISTICS_ATTRIBUTE) } after_create :create_ci_cd_settings, unless: :ci_cd_settings, @@ -110,7 +111,7 @@ class Project < ActiveRecord::Base after_create :ensure_storage_path_exists after_save :ensure_storage_path_exists, if: :namespace_id_changed? - acts_as_taggable + acts_as_ordered_taggable attr_accessor :old_path_with_namespace attr_accessor :template_name @@ -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) } @@ -353,7 +356,7 @@ class Project < ActiveRecord::Base # "enabled" here means "not disabled". It includes private features! scope :with_feature_enabled, ->(feature) { access_level_attribute = ProjectFeature.access_level_attribute(feature) - with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED] }) + with_project_feature.where(project_features: { access_level_attribute => [nil, ProjectFeature::PRIVATE, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] }) } # Picks a feature where the level is exactly that given. @@ -415,15 +418,15 @@ class Project < ActiveRecord::Base end end - # project features may be "disabled", "internal" or "enabled". If "internal", + # project features may be "disabled", "internal", "enabled" or "public". If "internal", # they are only available to team members. This scope returns projects where - # the feature is either enabled, or internal with permission for the user. + # the feature is either public, enabled, or internal with permission for the user. # # This method uses an optimised version of `with_feature_access_level` for # logged in users to more efficiently get private projects with the given # feature. def self.with_feature_available_for_user(feature, user) - visible = [nil, ProjectFeature::ENABLED] + visible = [nil, ProjectFeature::ENABLED, ProjectFeature::PUBLIC] if user&.admin? with_feature_enabled(feature) @@ -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 @@ -1078,31 +1082,13 @@ class Project < ActiveRecord::Base end def find_or_initialize_services(exceptions: []) - services_templates = Service.where(template: true) - available_services_names = Service.available_services_names - exceptions available_services = available_services_names.map do |service_name| - service = find_service(services, service_name) - - if service - service - else - # We should check if template for the service exists - template = find_service(services_templates, service_name) - - if template.nil? - # If no template, we should create an instance. Ex `build_gitlab_ci_service` - public_send("build_#{service_name}_service") # rubocop:disable GitlabSecurity/PublicSend - else - Service.build_from_template(id, template) - end - end + find_or_initialize_service(service_name) end - available_services.reject do |service| - disabled_services.include?(service.to_param) - end + available_services.compact end def disabled_services @@ -1110,15 +1096,30 @@ class Project < ActiveRecord::Base end def find_or_initialize_service(name) - find_or_initialize_services.find { |service| service.to_param == name } + return if disabled_services.include?(name) + + service = find_service(services, name) + return service if service + + # We should check if template for the service exists + template = find_service(services_templates, name) + + if template + Service.build_from_template(id, template) + else + # If no template, we should create an instance. Ex `build_gitlab_ci_service` + public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend + end 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 +1167,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 +1175,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,15 +1185,16 @@ 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).each do |hook| + hooks.hooks_for(hooks_scope).select_active(hooks_scope, data).each do |hook| hook.async_execute(data, hooks_scope.to_s) end - 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 @@ -1356,6 +1360,18 @@ class Project < ActiveRecord::Base end end + # Filters `users` to return only authorized users of the project + def members_among(users) + if users.is_a?(ActiveRecord::Relation) && !users.loaded? + authorized_users.merge(users) + else + return [] if users.empty? + + user_ids = authorized_users.where(users: { id: users.map(&:id) }).pluck(:id) + users.select { |user| user_ids.include?(user.id) } + end + end + def default_branch @default_branch ||= repository.root_ref if repository.exists? end @@ -1487,8 +1503,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 @@ -1505,13 +1520,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? @@ -1592,6 +1611,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 @@ -1607,6 +1627,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 @@ -1667,6 +1688,7 @@ class Project < ActiveRecord::Base end end + # rubocop: disable CodeReuse/ServiceClass def after_create_default_branch return unless default_branch @@ -1687,6 +1709,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 @@ -1734,16 +1757,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 @@ -1758,21 +1777,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 @@ -1923,9 +1940,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) @@ -2012,12 +2031,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) @@ -2070,6 +2087,7 @@ class Project < ActiveRecord::Base private + # rubocop: disable CodeReuse/ServiceClass def rename_or_migrate_repository! if Gitlab::CurrentSettings.hashed_storage_enabled? && storage_upgradable? && @@ -2079,6 +2097,7 @@ class Project < ActiveRecord::Base storage.rename_repo end end + # rubocop: enable CodeReuse/ServiceClass def storage_upgradable? storage_version != LATEST_STORAGE_VERSION @@ -2098,11 +2117,7 @@ class Project < ActiveRecord::Base Gitlab::PagesTransfer.new.rename_project(path_before, self.path, namespace.full_path) end - def untrack_site_statistics - SiteStatistic.untrack(STATISTICS_ATTRIBUTE) - 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. @@ -2113,6 +2128,7 @@ class Project < ActiveRecord::Base reload_repository! end + # rubocop: enable CodeReuse/ServiceClass def storage @storage ||= @@ -2252,12 +2268,12 @@ class Project < ActiveRecord::Base end end - if RequestStore.active? - RequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do - check_access.call - end - else + Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do check_access.call end end + + def services_templates + @services_templates ||= Service.where(template: true) + end end 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_auto_devops.rb b/app/models/project_auto_devops.rb index dc6736dd9cd..2253ad7b543 100644 --- a/app/models/project_auto_devops.rb +++ b/app/models/project_auto_devops.rb @@ -5,7 +5,8 @@ class ProjectAutoDevops < ActiveRecord::Base enum deploy_strategy: { continuous: 0, - manual: 1 + manual: 1, + timed_incremental: 2 } scope :enabled, -> { where(enabled: true) } @@ -30,10 +31,7 @@ class ProjectAutoDevops < ActiveRecord::Base value: domain.presence || instance_domain) end - if manual? - variables.append(key: 'STAGING_ENABLED', value: '1') - variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1') - end + variables.concat(deployment_strategy_default_variables) end end @@ -51,4 +49,16 @@ class ProjectAutoDevops < ActiveRecord::Base !project.public? && !project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present? end + + def deployment_strategy_default_variables + Gitlab::Ci::Variables::Collection.new.tap do |variables| + if manual? + variables.append(key: 'STAGING_ENABLED', value: '1') + variables.append(key: 'INCREMENTAL_ROLLOUT_ENABLED', value: '1') # deprecated + variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'manual') + elsif timed_incremental? + variables.append(key: 'INCREMENTAL_ROLLOUT_MODE', value: 'timed') + end + end + end end diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb index d74cb2506ba..39f2b8fe0de 100644 --- a/app/models/project_feature.rb +++ b/app/models/project_feature.rb @@ -13,15 +13,16 @@ class ProjectFeature < ActiveRecord::Base # Disabled: not enabled for anyone # Private: enabled only for team members # Enabled: enabled for everyone able to access the project + # Public: enabled for everyone (only allowed for pages) # # Permission levels DISABLED = 0 PRIVATE = 10 ENABLED = 20 + PUBLIC = 30 - FEATURES = %i(issues merge_requests wiki snippets builds repository).freeze - STATISTICS_ATTRIBUTE = 'wikis_count'.freeze + FEATURES = %i(issues merge_requests wiki snippets builds repository pages).freeze class << self def access_level_attribute(feature) @@ -47,6 +48,7 @@ class ProjectFeature < ActiveRecord::Base validates :project, presence: true validate :repository_children_level + validate :allowed_access_levels default_value_for :builds_access_level, value: ENABLED, allows_nil: false default_value_for :issues_access_level, value: ENABLED, allows_nil: false @@ -55,10 +57,10 @@ class ProjectFeature < ActiveRecord::Base default_value_for :wiki_access_level, value: ENABLED, allows_nil: false default_value_for :repository_access_level, value: ENABLED, allows_nil: false - after_create ->(model) { SiteStatistic.track(STATISTICS_ATTRIBUTE) if model.wiki_enabled? } - after_update :update_site_statistics - def feature_available?(feature, user) + # This feature might not be behind a feature flag at all, so default to true + return false unless ::Feature.enabled?(feature, user, default_enabled: true) + get_permission(user, access_level(feature)) end @@ -82,30 +84,18 @@ class ProjectFeature < ActiveRecord::Base issues_access_level > DISABLED end - # This is a workaround for the removal hooks not been triggered when removing a Project. - # - # ProjectFeature is removed using database cascade index rule. - # This method is called by Project model when deletion starts. - def untrack_statistics_for_deletion! - return unless wiki_enabled? - - SiteStatistic.untrack(STATISTICS_ATTRIBUTE) + def pages_enabled? + pages_access_level > DISABLED end - private - - def update_site_statistics - return unless wiki_access_level_changed? + def public_pages? + return true unless Gitlab.config.pages.access_control - if self.wiki_access_level_was == DISABLED - # possible new states are PRIVATE / ENABLED, both should be tracked - SiteStatistic.track(STATISTICS_ATTRIBUTE) - elsif self.wiki_access_level == DISABLED - # old state was either PRIVATE / ENABLED, only untrack if new state is DISABLED - SiteStatistic.untrack(STATISTICS_ATTRIBUTE) - end + pages_access_level == PUBLIC || pages_access_level == ENABLED && project.public? end + private + # Validates builds and merge requests access level # which cannot be higher than repository access level def repository_children_level @@ -118,6 +108,17 @@ class ProjectFeature < ActiveRecord::Base %i(merge_requests_access_level builds_access_level).each(&validator) end + # Validates access level for other than pages cannot be PUBLIC + def allowed_access_levels + validator = lambda do |field| + level = public_send(field) || ProjectFeature::ENABLED # rubocop:disable GitlabSecurity/PublicSend + not_allowed = level > ProjectFeature::ENABLED + self.errors.add(field, "cannot have public visibility level") if not_allowed + end + + (FEATURES - %i(pages)).each {|f| validator.call("#{f}_access_level")} + end + def get_permission(user, level) case level when DISABLED @@ -126,6 +127,8 @@ class ProjectFeature < ActiveRecord::Base user && (project.team.member?(user) || user.full_private_access?) when ENABLED true + when PUBLIC + true else true 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/hipchat_service.rb b/app/models/project_services/hipchat_service.rb index 66012f0da99..a69b7b4c4b6 100644 --- a/app/models/project_services/hipchat_service.rb +++ b/app/models/project_services/hipchat_service.rb @@ -149,7 +149,7 @@ class HipchatService < Service context.merge!(options) - html = Banzai.post_process(Banzai.render(text, context), context) + html = Banzai.render_and_post_process(text, context) sanitized_html = sanitize(html, tags: HIPCHAT_ALLOWED_TAGS, attributes: %w[href title alt]) sanitized_html.truncate(200, separator: ' ', omission: '...') 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/protected_ref_matcher.rb b/app/models/protected_ref_matcher.rb deleted file mode 100644 index bfa9180ac93..00000000000 --- a/app/models/protected_ref_matcher.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -class ProtectedRefMatcher - def initialize(protected_ref) - @protected_ref = protected_ref - end - - # Returns all protected refs that match the given ref name. - # This checks all records from the scope built up so far, and does - # _not_ return a relation. - # - # This method optionally takes in a list of `protected_refs` to search - # through, to avoid calling out to the database. - def self.matching(type, ref_name, protected_refs: nil) - (protected_refs || type.all).select { |protected_ref| protected_ref.matches?(ref_name) } - end - - # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) - # that match the current protected ref. - def matching(refs) - refs.select { |ref| @protected_ref.matches?(ref.name) } - end - - # Checks if the protected ref matches the given ref name. - def matches?(ref_name) - return false if @protected_ref.name.blank? - - exact_match?(ref_name) || wildcard_match?(ref_name) - end - - # Checks if this protected ref contains a wildcard - def wildcard? - @protected_ref.name && @protected_ref.name.include?('*') - end - - protected - - def exact_match?(ref_name) - @protected_ref.name == ref_name - end - - def wildcard_match?(ref_name) - return false unless wildcard? - - wildcard_regex === ref_name - end - - def wildcard_regex - @wildcard_regex ||= begin - name = @protected_ref.name.gsub('*', 'STAR_DONT_ESCAPE') - quoted_name = Regexp.quote(name) - regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') - /\A#{regex_string}\z/ - end - end -end diff --git a/app/models/ref_matcher.rb b/app/models/ref_matcher.rb new file mode 100644 index 00000000000..fa7d2c0f06c --- /dev/null +++ b/app/models/ref_matcher.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class RefMatcher + def initialize(ref_name_or_pattern) + @ref_name_or_pattern = ref_name_or_pattern + end + + # Returns all branches/tags (among the given list of refs [`Gitlab::Git::Branch`]) + # that match the current protected ref. + def matching(refs) + refs.select { |ref| matches?(ref.name) } + end + + # Checks if the protected ref matches the given ref name. + def matches?(ref_name) + return false if @ref_name_or_pattern.blank? + + exact_match?(ref_name) || wildcard_match?(ref_name) + end + + # Checks if this protected ref contains a wildcard + def wildcard? + @ref_name_or_pattern && @ref_name_or_pattern.include?('*') + end + + protected + + def exact_match?(ref_name) + @ref_name_or_pattern == ref_name + end + + def wildcard_match?(ref_name) + return false unless wildcard? + + wildcard_regex === ref_name + end + + def wildcard_regex + @wildcard_regex ||= begin + name = @ref_name_or_pattern.gsub('*', 'STAR_DONT_ESCAPE') + quoted_name = Regexp.quote(name) + regex_string = quoted_name.gsub('STAR_DONT_ESCAPE', '.*?') + /\A#{regex_string}\z/ + end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index cf255c8951f..a3a3ce179fc 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 ||= @@ -514,7 +510,7 @@ class Repository raw_repository.exists? end - cache_method :exists? + cache_method_asymmetrically :exists? # We don't need to cache the output of this method because both exists? and # has_visible_content? are already memoized and cached. There's no guarantee @@ -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 @@ -611,7 +612,7 @@ class Repository Licensee::License.new(license_key) end - cache_method :license, memoize_only: true + memoize_method :license def gitignore file_on_head(:gitignore) @@ -667,6 +668,14 @@ class Repository end end + def list_last_commits_for_tree(sha, path, offset: 0, limit: 25) + commits = raw_repository.list_last_commits_for_tree(sha, path, offset: offset, limit: limit) + + commits.each do |path, commit| + commits[path] = ::Commit.new(commit, @project) + end + end + def last_commit_for_path(sha, path) commit = raw_repository.last_commit_for_path(sha, path) ::Commit.new(commit, @project) if commit @@ -994,14 +1003,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 +1011,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 @@ -1028,6 +1037,10 @@ class Repository @cache ||= Gitlab::RepositoryCache.new(self) end + def request_store_cache + @request_store_cache ||= Gitlab::RepositoryCache.new(self, backend: Gitlab::SafeRequestStore) + end + def tags_sorted_by_committed_date tags.sort_by do |tag| # Annotated tags can point to any object (e.g. a blob), but generally 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/site_statistic.rb b/app/models/site_statistic.rb index 48324570f0b..3a7912ed53a 100644 --- a/app/models/site_statistic.rb +++ b/app/models/site_statistic.rb @@ -4,7 +4,7 @@ class SiteStatistic < ActiveRecord::Base # prevents the creation of multiple rows default_value_for :id, 1 - COUNTER_ATTRIBUTES = %w(repositories_count wikis_count).freeze + COUNTER_ATTRIBUTES = %w(repositories_count).freeze REQUIRED_SCHEMA_VERSION = 20180629153018 # Tracks specific attribute 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..d555ebe5322 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -9,13 +9,14 @@ class SystemNoteMetadata < ActiveRecord::Base TYPES_WITH_CROSS_REFERENCES = %w[ commit cross_reference close duplicate + moved ].freeze ICON_TYPES = %w[ 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 @@ -26,4 +27,8 @@ class SystemNoteMetadata < ActiveRecord::Base def icon_types ICON_TYPES end + + def cross_reference_types + TYPES_WITH_CROSS_REFERENCES + end end 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..cd3b1c95b7e 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,12 @@ 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.unscoped do + Group.from_union([ + groups, + authorized_projects.joins(:namespace).select('namespaces.*') + ]) + end end # Returns the groups a user is a member of, either directly or through a parent group @@ -652,9 +697,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 +744,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 +762,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 +832,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 +842,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 +911,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 +987,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 +1057,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 +1146,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 +1184,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 +1251,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 +1264,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 +1362,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 +1380,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 +1496,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/user_callout.rb b/app/models/user_callout.rb index 97e955ace36..2c0e8659fc1 100644 --- a/app/models/user_callout.rb +++ b/app/models/user_callout.rb @@ -5,7 +5,9 @@ class UserCallout < ActiveRecord::Base enum feature_name: { gke_cluster_integration: 1, - gcp_signup_offer: 2 + gcp_signup_offer: 2, + cluster_security_warning: 3, + gold_trial: 4 } validates :user, presence: true diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb index 33790afc35e..42fd213d03b 100644 --- a/app/models/wiki_page.rb +++ b/app/models/wiki_page.rb @@ -51,14 +51,14 @@ 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. attr_reader :page # The attributes Hash used for storing and validating - # new Page values before writing to the Gollum repository. + # new Page values before writing to the raw repository. attr_accessor :attributes def hook_attrs @@ -111,10 +111,7 @@ class WikiPage # The processed/formatted content of this page. def formatted_content - # Assuming @page exists, nil formatted_data means we didn't load it - # before hand (i.e. page was fetched by Gitaly), so we fetch it separately. - # If the page was fetched by Gollum, formatted_data would've been a String. - @attributes[:formatted_content] ||= @page&.formatted_data || @wiki.page_formatted_data(@page) + @attributes[:formatted_content] ||= @wiki.page_formatted_data(@page) end # The markup format for the page. @@ -127,7 +124,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/issuable_policy.rb b/app/policies/issuable_policy.rb index 198bb168d85..6d8b575102e 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -14,6 +14,7 @@ class IssuablePolicy < BasePolicy rule { assignee_or_author }.policy do enable :read_issue enable :update_issue + enable :reopen_issue enable :read_merge_request enable :update_merge_request end diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb index 94b5f37c682..a0706eaa46c 100644 --- a/app/policies/issue_policy.rb +++ b/app/policies/issue_policy.rb @@ -19,4 +19,8 @@ class IssuePolicy < IssuablePolicy prevent :update_issue prevent :admin_issue end + + rule { locked }.policy do + prevent :reopen_issue + end end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index fd6cc504a3b..a76a083bceb 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -110,6 +110,7 @@ class ProjectPolicy < BasePolicy snippets wiki builds + pages ] features.each do |f| @@ -167,6 +168,7 @@ class ProjectPolicy < BasePolicy enable :upload_file enable :read_cycle_analytics enable :award_emoji + enable :read_pages_content end # These abilities are not allowed to admins that are not members of the project, @@ -180,6 +182,7 @@ class ProjectPolicy < BasePolicy enable :fork_project enable :create_project_snippet enable :update_issue + enable :reopen_issue enable :admin_issue enable :admin_label enable :admin_list @@ -285,6 +288,8 @@ class ProjectPolicy < BasePolicy prevent(*create_read_update_admin_destroy(:merge_request)) end + rule { pages_disabled }.prevent :read_pages_content + rule { issues_disabled & merge_requests_disabled }.policy do prevent(*create_read_update_admin_destroy(:label)) prevent(*create_read_update_admin_destroy(:milestone)) @@ -344,6 +349,7 @@ class ProjectPolicy < BasePolicy enable :download_code enable :download_wiki_code enable :read_cycle_analytics + enable :read_pages_content # NOTE: may be overridden by IssuePolicy enable :read_issue @@ -389,7 +395,11 @@ class ProjectPolicy < BasePolicy greedy_load_subject ||= !@user.persisted? if greedy_load_subject - project.team.members.include?(user) + # We want to load all the members with one query. Calling #include? on + # project.team.members will perform a separate query for each user, unless + # project.team.members was loaded before somewhere else. Calling #to_a + # ensures it's always loaded before checking for membership. + project.team.members.to_a.include?(user) else # otherwise we just make a specific query for # this particular user. @@ -397,6 +407,7 @@ class ProjectPolicy < BasePolicy end end + # rubocop: disable CodeReuse/ActiveRecord def project_group_member? return false if @user.nil? @@ -406,6 +417,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/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb index 5331cdf632b..33056a809b7 100644 --- a/app/presenters/ci/build_presenter.rb +++ b/app/presenters/ci/build_presenter.rb @@ -35,6 +35,10 @@ module Ci "#{subject.name} - #{detailed_status.status_tooltip}" end + def execute_in + scheduled? && scheduled_at && [0, scheduled_at - Time.now].max + end + private def tooltip_for_badge diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb index a08f34e2335..29eaad759bb 100644 --- a/app/presenters/commit_status_presenter.rb +++ b/app/presenters/commit_status_presenter.rb @@ -8,13 +8,20 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated stuck_or_timeout_failure: 'There has been a timeout failure or the job got stuck. Check your timeout limits or try again', runner_system_failure: 'There has been a runner system failure, please try again', missing_dependency_failure: 'There has been a missing dependency failure', - runner_unsupported: 'Your runner is outdated, please upgrade your runner' + runner_unsupported: 'Your runner is outdated, please upgrade your runner', + stale_schedule: 'Delayed job could not be executed by some reason, please try again' }.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_action_entity.rb b/app/serializers/build_action_entity.rb index f9da3f63911..0db7875aa87 100644 --- a/app/serializers/build_action_entity.rb +++ b/app/serializers/build_action_entity.rb @@ -12,6 +12,11 @@ class BuildActionEntity < Grape::Entity end expose :playable?, as: :playable + expose :scheduled_at, if: -> (build) { build.scheduled? } + + expose :unschedule_path, if: -> (build) { build.scheduled? } do |build| + unschedule_project_job_path(build.project, build) + end private diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb index b887b99d31c..3d508a9a407 100644 --- a/app/serializers/build_details_entity.rb +++ b/app/serializers/build_details_entity.rb @@ -3,17 +3,50 @@ class BuildDetailsEntity < JobEntity expose :coverage, :erased_at, :duration expose :tag_list, as: :tags + expose :has_trace?, as: :has_trace 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 :persisted_environment, as: :environment, with: EnvironmentEntity + end + expose :metadata, using: BuildMetadataEntity + expose :artifact, if: -> (*) { can?(current_user, :read_build, build) } do + expose :download_path, if: -> (*) { build.artifacts? } do |build| + download_project_job_artifacts_path(project, build) + end + + expose :browse_path, if: -> (*) { build.browsable_artifacts? } do |build| + browse_project_job_artifacts_path(project, build) + end + + expose :keep_path, if: -> (*) { build.has_expiring_artifacts? && can?(current_user, :update_build, build) } do |build| + keep_project_job_artifacts_path(project, build) + end + + expose :expire_at, if: -> (*) { build.artifacts_expire_at.present? } do |build| + build.artifacts_expire_at + end + + expose :expired, if: -> (*) { build.artifacts_expire_at.present? } do |build| + build.artifacts_expired? + end + end + expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build| 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 @@ -33,6 +66,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 @@ -47,4 +100,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..a94e32478ce 100644 --- a/app/serializers/commit_entity.rb +++ b/app/serializers/commit_entity.rb @@ -1,19 +1,49 @@ # frozen_string_literal: true class CommitEntity < API::Entities::Commit + include MarkupHelper include RequestAwareEntity 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| - project_commit_url(request.project, commit) + expose :commit_url do |commit, options| + project_commit_url(request.project, commit, params: options.fetch(:commit_url_params, {})) end - expose :commit_path do |commit| - project_commit_path(request.project, commit) + expose :commit_path do |commit, options| + project_commit_path(request.project, commit, params: options.fetch(:commit_url_params, {})) + end + + expose :description_html, if: { type: :full } do |commit| + markdown_field(commit, :description) + end + + expose :title_html, if: { type: :full } do |commit| + markdown_field(commit, :title) + end + + expose :signature_html, if: { type: :full } do |commit| + render('projects/commit/_signature', signature: commit.signature) if commit.has_signature? + end + + expose :pipeline_status_path, if: { type: :full } do |commit, options| + pipeline_ref = options[:pipeline_ref] + pipeline_project = options[:pipeline_project] || commit.project + next unless pipeline_ref && pipeline_project + + status = commit.status_for_project(pipeline_ref, pipeline_project) + next unless status + + pipelines_project_commit_path(pipeline_project, commit.id, ref: pipeline_ref) + end + + def render(*args) + return unless request.respond_to?(:render) && request.render.respond_to?(:call) + + request.render.call(*args) end end diff --git a/app/serializers/status_entity.rb b/app/serializers/detailed_status_entity.rb index 306c30f0323..da994d78286 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,19 @@ class StatusEntity < Grape::Entity expose :has_details?, as: :has_details expose :details_path + expose :illustration do |status| + begin + illustration = { + image: ActionController::Base.helpers.image_path(status.illustration[:image]) + } + illustration = status.illustration.merge(illustration) + + illustration + rescue NotImplementedError + # ignored + end + end + expose :favicon do |status| Gitlab::Favicon.status_overlay(status.favicon) end @@ -17,5 +30,6 @@ class StatusEntity < Grape::Entity expose :action_title, as: :title expose :action_path, as: :path expose :action_method, as: :method + expose :action_button_title, as: :button_title end end diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index d49d4895d89..63ea8e8f95f 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -84,7 +84,7 @@ class DiffFileEntity < Grape::Entity end expose :old_path_html do |diff_file| - old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path) + old_path, _ = mark_inline_diffs(diff_file.old_path, diff_file.new_path) old_path end @@ -116,6 +116,10 @@ class DiffFileEntity < Grape::Entity project_blob_path(project, tree_join(diff_file.content_sha, diff_file.new_path)) end + expose :viewer, using: DiffViewerEntity do |diff_file| + diff_file.rich_viewer || diff_file.simple_viewer + end + expose :replaced_view_path, if: -> (_, options) { options[:merge_request] } do |diff_file| image_diff = diff_file.rich_viewer && diff_file.rich_viewer.partial_name == 'image' image_replaced = diff_file.old_content_sha && diff_file.old_content_sha != diff_file.content_sha @@ -135,12 +139,12 @@ class DiffFileEntity < Grape::Entity end # Used for inline diffs - expose :highlighted_diff_lines, if: -> (diff_file, _) { diff_file.text? } do |diff_file| + expose :highlighted_diff_lines, using: DiffLineEntity, if: -> (diff_file, _) { diff_file.text? } do |diff_file| diff_file.diff_lines_for_serializer end # Used for parallel diffs - expose :parallel_diff_lines, if: -> (diff_file, _) { diff_file.text? } + expose :parallel_diff_lines, using: DiffLineParallelEntity, if: -> (diff_file, _) { diff_file.text? } def current_user request.current_user diff --git a/app/serializers/diff_line_entity.rb b/app/serializers/diff_line_entity.rb new file mode 100644 index 00000000000..942714b7787 --- /dev/null +++ b/app/serializers/diff_line_entity.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class DiffLineEntity < Grape::Entity + expose :line_code + expose :type + expose :old_line + expose :new_line + expose :text + expose :meta_positions, as: :meta_data + + expose :rich_text do |line| + ERB::Util.html_escape(line.rich_text || line.text) + end +end diff --git a/app/serializers/diff_line_parallel_entity.rb b/app/serializers/diff_line_parallel_entity.rb new file mode 100644 index 00000000000..0438a67d51b --- /dev/null +++ b/app/serializers/diff_line_parallel_entity.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class DiffLineParallelEntity < Grape::Entity + expose :left, using: DiffLineEntity + expose :right, using: DiffLineEntity +end diff --git a/app/serializers/diff_line_serializer.rb b/app/serializers/diff_line_serializer.rb new file mode 100644 index 00000000000..7f1f2d9aa7c --- /dev/null +++ b/app/serializers/diff_line_serializer.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class DiffLineSerializer < BaseSerializer + entity DiffLineEntity +end diff --git a/app/serializers/diff_viewer_entity.rb b/app/serializers/diff_viewer_entity.rb new file mode 100644 index 00000000000..27fba03cb3f --- /dev/null +++ b/app/serializers/diff_viewer_entity.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class DiffViewerEntity < Grape::Entity + # Partial name refers directly to a Rails feature, let's avoid + # using this on the frontend. + expose :partial_name, as: :name +end diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb index f75ace14d9c..b51e4a7e6d0 100644 --- a/app/serializers/diffs_entity.rb +++ b/app/serializers/diffs_entity.rb @@ -15,8 +15,13 @@ class DiffsEntity < Grape::Entity merge_request&.target_branch end - expose :commit do |diffs| - options[:commit] + expose :commit do |diffs, options| + CommitEntity.represent options[:commit], options.merge( + type: :full, + commit_url_params: { merge_request_iid: merge_request&.iid }, + pipeline_ref: merge_request&.source_branch, + pipeline_project: merge_request&.source_project + ) end expose :merge_request_diff, using: MergeRequestDiffEntity do |diffs| @@ -35,13 +40,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 b8321037fa5..b6786a0d597 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? } @@ -26,7 +27,7 @@ class DiscussionEntity < Grape::Entity expose :resolved?, as: :resolved expose :resolved_by_push?, as: :resolved_by_push - expose :resolved_by + expose :resolved_by, using: NoteUserEntity expose :resolved_at expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion| resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id) @@ -43,7 +44,7 @@ class DiscussionEntity < Grape::Entity project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion) end - expose :truncated_diff_lines, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } + expose :truncated_diff_lines, using: DiffLineEntity, if: -> (d, _) { d.diff_discussion? && d.on_text? && (d.expanded? || render_truncated_diff_lines?) } expose :image_diff_html, if: -> (d, _) { d.diff_discussion? && d.on_image? } do |discussion| diff_file = discussion.diff_file 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..0b19cb16955 100644 --- a/app/serializers/job_entity.rb +++ b/app/serializers/job_entity.rb @@ -24,10 +24,15 @@ class JobEntity < Grape::Entity path_to(:play_namespace_project_job, build) end + expose :unschedule_path, if: -> (*) { scheduled? } do |build| + path_to(:unschedule_namespace_project_job, build) + end + expose :playable?, as: :playable + expose :scheduled_at, if: -> (*) { scheduled? } 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? } @@ -47,6 +52,10 @@ class JobEntity < Grape::Entity build.playable? && can?(request.current_user, :update_build, build) end + def scheduled? + build.scheduled? + end + def detailed_status build.detailed_status(request.current_user) end 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_details_entity.rb b/app/serializers/pipeline_details_entity.rb index 3b56767f774..d78ad4af4dc 100644 --- a/app/serializers/pipeline_details_entity.rb +++ b/app/serializers/pipeline_details_entity.rb @@ -5,5 +5,6 @@ class PipelineDetailsEntity < PipelineEntity expose :ordered_stages, as: :stages, using: StageEntity expose :artifacts, using: BuildArtifactEntity expose :manual_actions, using: BuildActionEntity + expose :scheduled_actions, using: BuildActionEntity end end 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..7451433a841 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([ @@ -12,6 +13,7 @@ class PipelineSerializer < BaseSerializer :cancelable_statuses, :trigger_requests, :manual_actions, + :scheduled_actions, :artifacts, { pending_builds: :project, @@ -33,6 +35,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/enqueue_build_service.rb b/app/services/ci/enqueue_build_service.rb deleted file mode 100644 index 8140651d980..00000000000 --- a/app/services/ci/enqueue_build_service.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true -module Ci - class EnqueueBuildService < BaseService - def execute(build) - build.enqueue - end - 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_build_service.rb b/app/services/ci/process_build_service.rb new file mode 100644 index 00000000000..d9f8e7cb452 --- /dev/null +++ b/app/services/ci/process_build_service.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Ci + class ProcessBuildService < BaseService + def execute(build, current_status) + if valid_statuses_for_when(build.when).include?(current_status) + if build.schedulable? + build.schedule + elsif build.action? + build.actionize + else + enqueue(build) + end + + true + else + build.skip + false + end + end + + private + + def enqueue(build) + build.enqueue + end + + def valid_statuses_for_when(value) + case value + when 'on_success' + %w[success skipped] + when 'on_failure' + %w[failed] + when 'always' + %w[success failed skipped] + when 'manual' + %w[success skipped] + when 'delayed' + %w[success skipped] + else + [] + end + end + end +end diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index cafee76a33c..446188347df 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -24,53 +24,35 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) - return if HasStatus::BLOCKED_STATUS == current_status + return if HasStatus::BLOCKED_STATUS.include?(current_status) if HasStatus::COMPLETED_STATUSES.include?(current_status) created_builds_in_stage(index).select do |build| Gitlab::OptimisticLocking.retry_lock(build) do |subject| - process_build(subject, current_status) + Ci::ProcessBuildService.new(project, @user) + .execute(build, current_status) end end end end - def process_build(build, current_status) - if valid_statuses_for_when(build.when).include?(current_status) - build.action? ? build.actionize : enqueue_build(build) - true - else - build.skip - false - end - end - - def valid_statuses_for_when(value) - case value - when 'on_success' - %w[success skipped] - when 'on_failure' - %w[failed] - when 'always' - %w[success failed skipped] - when 'manual' - %w[success skipped] - else - [] - 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 +62,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,9 +76,6 @@ module Ci .where.not(id: latest_statuses.map(&:first)) .update_all(retried: true) if latest_statuses.any? end - - def enqueue_build(build) - Ci::EnqueueBuildService.new(project, @user).execute(build) - end + # rubocop: enable CodeReuse/ActiveRecord end end 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/ci/run_scheduled_build_service.rb b/app/services/ci/run_scheduled_build_service.rb new file mode 100644 index 00000000000..8e4a628296f --- /dev/null +++ b/app/services/ci/run_scheduled_build_service.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Ci + class RunScheduledBuildService < ::BaseService + def execute(build) + unless can?(current_user, :update_build, build) + raise Gitlab::Access::AccessDeniedError + end + + build.enqueue_scheduled! + end + end +end diff --git a/app/services/clusters/applications/check_installation_progress_service.rb b/app/services/clusters/applications/check_installation_progress_service.rb index 35f5cff0e0c..5017fa093f3 100644 --- a/app/services/clusters/applications/check_installation_progress_service.rb +++ b/app/services/clusters/applications/check_installation_progress_service.rb @@ -14,8 +14,8 @@ module Clusters else check_timeout end - rescue Kubeclient::HttpError => ke - app.make_errored!("Kubernetes error: #{ke.message}") unless app.errored? + rescue Kubeclient::HttpError + app.make_errored!("Kubernetes error") unless app.errored? end private @@ -27,7 +27,7 @@ module Clusters end def on_failed - app.make_errored!(installation_errors || 'Installation silently failed') + app.make_errored!('Installation failed') ensure remove_installation_pod end diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb index 7e3c0e77a83..dd8d2ed5eb6 100644 --- a/app/services/clusters/applications/install_service.rb +++ b/app/services/clusters/applications/install_service.rb @@ -12,10 +12,10 @@ module Clusters ClusterWaitForAppInstallationWorker.perform_in( ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id) - rescue Kubeclient::HttpError => ke - app.make_errored!("Kubernetes error: #{ke.message}") - rescue StandardError => e - app.make_errored!("Can't start installation process. #{e.message}") + rescue Kubeclient::HttpError + app.make_errored!("Kubernetes error.") + rescue StandardError + app.make_errored!("Can't start installation process.") end 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 025f093a428..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] - @author_name = params[:author_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/files/multi_service.rb b/app/services/files/multi_service.rb index 08088f8c592..c9d3ee31d82 100644 --- a/app/services/files/multi_service.rb +++ b/app/services/files/multi_service.rb @@ -2,7 +2,7 @@ module Files class MultiService < Files::BaseService - UPDATE_FILE_ACTIONS = %w(update move delete).freeze + UPDATE_FILE_ACTIONS = %w(update move delete chmod).freeze def create_commit! transformer = Lfs::FileTransformer.new(project, @branch_name) 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/related_branches_service.rb b/app/services/issues/related_branches_service.rb new file mode 100644 index 00000000000..76af482b7ac --- /dev/null +++ b/app/services/issues/related_branches_service.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# This service fetches all branches containing the current issue's ID, except for +# those with a merge request open referencing the current issue. +module Issues + class RelatedBranchesService < Issues::BaseService + def execute(issue) + branches_with_iid_of(issue) - branches_with_merge_request_for(issue) + end + + private + + def branches_with_merge_request_for(issue) + Issues::ReferencedMergeRequestsService + .new(project, current_user) + .referenced_merge_requests(issue) + .map(&:source_branch) + end + + def branches_with_iid_of(issue) + project.repository.branch_names.select do |branch| + branch =~ /\A#{issue.iid}-(?!\d+-stable)/i + end + end + end +end diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb index 3bd53f9ccdc..56d59b235a7 100644 --- a/app/services/issues/reopen_service.rb +++ b/app/services/issues/reopen_service.rb @@ -3,7 +3,7 @@ module Issues class ReopenService < Issues::BaseService def execute(issue) - return issue unless can?(current_user, :update_issue, issue) + return issue unless can?(current_user, :reopen_issue, issue) if issue.reopen event_service.reopen_issue(issue, current_user) 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..28c3219b37b 100644 --- a/app/services/merge_requests/base_service.rb +++ b/app/services/merge_requests/base_service.rb @@ -55,13 +55,15 @@ 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 + @project.source_of_merge_requests .with_state(mr_states) - .where(source_branch: source_branch, source_project_id: @project.id) - .preload(:source_project) # we don't need a #includes since we're just preloading for the #select + .where(source_branch: source_branch) + .preload(:source_project) # we don't need #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..b03d14fa3cc 100644 --- a/app/services/merge_requests/refresh_service.rb +++ b/app/services/merge_requests/refresh_service.rb @@ -3,16 +3,16 @@ module MergeRequests class RefreshService < MergeRequests::BaseService def execute(oldrev, newrev, ref) - return true unless Gitlab::Git.branch_ref?(ref) + push = Gitlab::Git::Push.new(@project, oldrev, newrev, ref) + return true unless push.branch_push? - do_execute(oldrev, newrev, ref) + refresh_merge_requests!(push) end private - def do_execute(oldrev, newrev, ref) - @oldrev, @newrev = oldrev, newrev - @branch_name = Gitlab::Git.ref_name(ref) + def refresh_merge_requests!(push) + @push = push Gitlab::GitalyClient.allow_n_plus_1_calls(&method(:find_new_commits)) # Be sure to close outstanding MRs before reloading them to avoid generating an @@ -25,7 +25,7 @@ module MergeRequests cache_merge_requests_closing_issues # Leave a system note if a branch was deleted/added - if branch_added? || branch_removed? + if @push.branch_added? || @push.branch_removed? comment_mr_branch_presence_changed end @@ -51,10 +51,13 @@ 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 - merge_requests = merge_requests.select(&:diff_head_commit) + merge_requests = @project.merge_requests.opened + .preload(:latest_merge_request_diff) + .where(target_branch: @push.branch_name).to_a + .select(&:diff_head_commit) merge_requests = merge_requests.select do |merge_request| commit_ids.include?(merge_request.diff_head_sha) && @@ -67,24 +70,22 @@ module MergeRequests .execute(merge_request) end end - - def force_push? - Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev) - end + # rubocop: enable CodeReuse/ActiveRecord # 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 + .by_source_or_target_branch(@push.branch_name).to_a # Fork merge requests merge_requests += MergeRequest.opened - .where(source_branch: @branch_name, source_project: @project) + .where(source_branch: @push.branch_name, source_project: @project) .where.not(target_project: @project).to_a filter_merge_requests(merge_requests).each do |merge_request| - if merge_request.source_branch == @branch_name || force_push? + if merge_request.source_branch == @push.branch_name || @push.force_push? merge_request.reload_diff(current_user) else mr_commit_ids = merge_request.commit_shas @@ -101,6 +102,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) @@ -113,7 +115,7 @@ module MergeRequests end def find_new_commits - if branch_added? + if @push.branch_added? @commits = [] merge_request = merge_requests_for_source_branch.first @@ -122,28 +124,28 @@ module MergeRequests begin # Since any number of commits could have been made to the restored branch, # find the common root to see what has been added. - common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @newrev) + common_ref = @project.repository.merge_base(merge_request.diff_head_sha, @push.newrev) # If the a commit no longer exists in this repo, gitlab_git throws # a Rugged::OdbError. This is fixed in https://gitlab.com/gitlab-org/gitlab_git/merge_requests/52 - @commits = @project.repository.commits_between(common_ref, @newrev) if common_ref + @commits = @project.repository.commits_between(common_ref, @push.newrev) if common_ref rescue end - elsif branch_removed? + elsif @push.branch_removed? # No commits for a deleted branch. @commits = [] else - @commits = @project.repository.commits_between(@oldrev, @newrev) + @commits = @project.repository.commits_between(@push.oldrev, @push.newrev) end end # Add comment about branches being deleted or added to merge requests def comment_mr_branch_presence_changed - presence = branch_added? ? :add : :delete + presence = @push.branch_added? ? :add : :delete merge_requests_for_source_branch.each do |merge_request| SystemNoteService.change_branch_presence( merge_request, merge_request.project, @current_user, - :source, @branch_name, presence) + :source, @push.branch_name, presence) end end @@ -160,7 +162,7 @@ module MergeRequests SystemNoteService.add_commits(merge_request, merge_request.project, @current_user, new_commits, - existing_commits, @oldrev) + existing_commits, @push.oldrev) notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits) end @@ -191,17 +193,19 @@ module MergeRequests # Call merge request webhook with update branches def execute_mr_web_hooks merge_requests_for_source_branch.each do |merge_request| - execute_hooks(merge_request, 'update', old_rev: @oldrev) + execute_hooks(merge_request, 'update', old_rev: @push.oldrev) end end # 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| + @project.merge_requests.where(source_branch: @push.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) @@ -209,15 +213,7 @@ module MergeRequests def merge_requests_for_source_branch(reload: false) @source_merge_requests = nil if reload - @source_merge_requests ||= merge_requests_for(@branch_name) - end - - def branch_added? - Gitlab::Git.blank_ref?(@oldrev) - end - - def branch_removed? - Gitlab::Git.blank_ref?(@newrev) + @source_merge_requests ||= merge_requests_for(@push.branch_name) end end end 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/notes/build_service.rb b/app/services/notes/build_service.rb index df5fe65de3c..7b92fe6fe14 100644 --- a/app/services/notes/build_service.rb +++ b/app/services/notes/build_service.rb @@ -3,6 +3,7 @@ module Notes class BuildService < ::BaseService def execute + should_resolve = false in_reply_to_discussion_id = params.delete(:in_reply_to_discussion_id) if in_reply_to_discussion_id.present? @@ -15,12 +16,17 @@ module Notes end params.merge!(discussion.reply_attributes) + should_resolve = discussion.resolved? end note = Note.new(params) note.project = project note.author = current_user + if should_resolve + note.resolve_without_save(current_user) + end + note 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/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb index 5286b92ab6b..61f6402a810 100644 --- a/app/services/projects/autocomplete_service.rb +++ b/app/services/projects/autocomplete_service.rb @@ -2,6 +2,7 @@ module Projects class AutocompleteService < BaseService + include LabelsAsHash def issues IssuesFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) end @@ -22,34 +23,18 @@ module Projects MergeRequestsFinder.new(current_user, project_id: project.id, state: 'opened').execute.select([:iid, :title]) end - def labels_as_hash(target = nil) - available_labels = LabelsFinder.new( - current_user, - project_id: project.id, - include_ancestor_groups: true - ).execute - - label_hashes = available_labels.as_json(only: [:title, :color]) - - if target&.respond_to?(:labels) - already_set_labels = available_labels & target.labels - if already_set_labels.present? - titles = already_set_labels.map(&:title) - label_hashes.each do |hash| - if titles.include?(hash['title']) - hash[:set] = true - end - end - end - end - - label_hashes - end - def commands(noteable, type) return [] unless noteable QuickActions::InterpretService.new(project, current_user).available_commands(noteable) end + + def snippets + SnippetsFinder.new(current_user, project: project).execute.select([:id, :title]) + end + + def labels_as_hash(target) + super(target, project_id: project.id, include_ancestor_groups: true) + 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_pages_configuration_service.rb b/app/services/projects/update_pages_configuration_service.rb index efbd4c7b323..abf40b3ad7a 100644 --- a/app/services/projects/update_pages_configuration_service.rb +++ b/app/services/projects/update_pages_configuration_service.rb @@ -21,7 +21,9 @@ module Projects def pages_config { domains: pages_domains_config, - https_only: project.pages_https_only? + https_only: project.pages_https_only?, + id: project.project_id, + access_control: !project.public_pages? } end @@ -31,7 +33,9 @@ module Projects domain: domain.domain, certificate: domain.certificate, key: domain.key, - https_only: project.pages_https_only? && domain.https? + https_only: project.pages_https_only? && domain.https?, + id: project.project_id, + access_control: !project.public_pages? } end end 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..f25a4e30938 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') @@ -70,7 +72,11 @@ module Projects system_hook_service.execute_hooks_for(project, :update) end - update_pages_config if changing_pages_https_only? + update_pages_config if changing_pages_related_config? + end + + def changing_pages_related_config? + changing_pages_https_only? || changing_pages_access_level? end def update_failed! @@ -100,6 +106,10 @@ module Projects params.dig(:project_feature_attributes, :wiki_access_level).to_i > ProjectFeature::DISABLED end + def changing_pages_access_level? + params.dig(:project_feature_attributes, :pages_access_level) + end + def ensure_wiki_exists ProjectWiki.new(project, project.owner).wiki rescue ProjectWiki::CouldNotCreateWikiError diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index a4c4c9e4812..defa579f9a8 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 @@ -127,12 +129,12 @@ module QuickActions command :assign do |users| next if users.empty? - @updates[:assignee_ids] = - if issuable.allows_multiple_assignees? - issuable.assignees.pluck(:id) + users.map(&:id) - else - [users.first.id] - end + if issuable.allows_multiple_assignees? + @updates[:assignee_ids] ||= issuable.assignees.map(&:id) + @updates[:assignee_ids] += users.map(&:id) + else + @updates[:assignee_ids] = [users.first.id] + end end desc do @@ -161,12 +163,12 @@ module QuickActions extract_users(unassign_param) if issuable.allows_multiple_assignees? end command :unassign do |users = nil| - @updates[:assignee_ids] = - if users&.any? - issuable.assignees.pluck(:id) - users.map(&:id) - else - [] - end + if issuable.allows_multiple_assignees? && users&.any? + @updates[:assignee_ids] ||= issuable.assignees.map(&:id) + @updates[:assignee_ids] -= users.map(&:id) + else + @updates[:assignee_ids] = [] + end end desc 'Set milestone' @@ -208,9 +210,14 @@ module QuickActions end params '~label1 ~"label 2"' condition do - available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute + if project + available_labels = LabelsFinder + .new(current_user, project_id: project.id, include_ancestor_groups: true) + .execute + end - current_user.can?(:"admin_#{issuable.to_ability_name}", project) && + project && + current_user.can?(:"admin_#{issuable.to_ability_name}", project) && available_labels.any? end command :label do |labels_param| @@ -284,7 +291,7 @@ module QuickActions end params '#issue | !merge_request' condition do - issuable.persisted? && + [MergeRequest, Issue].include?(issuable.class) && current_user.can?(:"update_#{issuable.to_ability_name}", issuable) end parse_params do |issuable_param| @@ -442,7 +449,8 @@ module QuickActions end params '<time(1h30m | -1h30m)> <date(YYYY-MM-DD)>' condition do - current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) + issuable.is_a?(TimeTrackable) && + current_user.can?(:"admin_#{issuable.to_ability_name}", issuable) end parse_params do |raw_time_date| Gitlab::QuickActions::SpendTimeAndDateSeparator.new(raw_time_date).execute @@ -489,6 +497,30 @@ module QuickActions "#{comment} #{TABLEFLIP}" end + desc "Lock the discussion" + explanation "Locks the discussion" + condition do + [MergeRequest, Issue].include?(issuable.class) && + 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 + [MergeRequest, Issue].include?(issuable.class) && + 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 +554,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 +569,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 +635,7 @@ module QuickActions @updates[:tag_message] = message end + # rubocop: disable CodeReuse/ActiveRecord def extract_users(params) return [] if params.nil? @@ -617,6 +652,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 +689,7 @@ module QuickActions end end + # rubocop: disable CodeReuse/ActiveRecord def extract_references(arg, type) ext = Gitlab::ReferenceExtractor.new(project, current_user) @@ -660,5 +697,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/build_service.rb b/app/services/users/build_service.rb index 9417c63c43a..de6ff92d1da 100644 --- a/app/services/users/build_service.rb +++ b/app/services/users/build_service.rb @@ -55,7 +55,6 @@ module Users :force_random_password, :hide_no_password, :hide_no_ssh_key, - :key_id, :linkedin, :name, :password, @@ -69,7 +68,10 @@ module Users :twitter, :username, :website_url, - :private_profile + :private_profile, + :organization, + :location, + :public_email ] end 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 new file mode 100644 index 00000000000..df31ad7c8ea --- /dev/null +++ b/app/services/wikis/create_attachment_service.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module Wikis + class CreateAttachmentService < Files::CreateService + ATTACHMENT_PATH = 'uploads'.freeze + MAX_FILENAME_LENGTH = 255 + + delegate :wiki, to: :project + delegate :repository, to: :wiki + + def initialize(*args) + super + + @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 + end + + def create_commit! + commit_result(create_transformed_commit(@file_content)) + end + + private + + 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) + truncate_at = MAX_FILENAME_LENGTH - extension.length - 1 + base_name = File.basename(file_name, extension)[0..truncate_at] + base_name + extension + end + + def validate! + validate_file_name! + validate_permissions! + end + + def validate_file_name! + raise_error('The file name cannot be empty') unless @file_name + end + + def validate_permissions! + unless can?(current_user, :create_wiki, project) + raise_error('You are not allowed to push to the wiki') + end + end + + def create_transformed_commit(content) + repository.create_file( + current_user, + @file_path, + content, + message: @commit_message, + branch_name: @branch_name, + author_email: @author_email, + author_name: @author_name) + end + + def commit_result(commit_id) + { + file_name: @file_name, + file_path: @file_path, + branch: @branch_name, + commit: commit_id + } + end + end +end 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/file_uploader.rb b/app/uploaders/file_uploader.rb index b1365659834..ffc1e5f75ca 100644 --- a/app/uploaders/file_uploader.rb +++ b/app/uploaders/file_uploader.rb @@ -122,12 +122,6 @@ class FileUploader < GitlabUploader } end - def markdown_link - markdown = +"[#{markdown_name}](#{secure_url})" - markdown.prepend("!") if image_or_video? || dangerous? - markdown - end - def to_h { alt: markdown_name, @@ -192,10 +186,6 @@ class FileUploader < GitlabUploader storage.delete_dir!(store_dir) # only remove when empty end - def markdown_name - (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") - end - def identifier @identifier ||= filename end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 719bd6ef418..cefcd3d3f5a 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -63,6 +63,12 @@ class GitlabUploader < CarrierWave::Uploader::Base super || file&.filename end + def relative_path + return path if pathname.relative? + + pathname.relative_path_from(Pathname.new(root)) + end + def model_valid? !!model end @@ -115,4 +121,8 @@ class GitlabUploader < CarrierWave::Uploader::Base # the cache directory. File.join(work_dir, cache_id, version_name.to_s, for_file) end + + def pathname + @pathname ||= Pathname.new(path) + end end diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index f6af023e0f9..400f0b3dcc6 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -5,9 +5,12 @@ class JobArtifactUploader < GitlabUploader include ObjectStorage::Concern ObjectNotReadyError = Class.new(StandardError) + UnknownFileLocationError = Class.new(StandardError) storage_options Gitlab.config.artifacts + alias_method :upload, :model + def cached_size return model.size if model.size.present? && !model.file_changed? @@ -23,10 +26,22 @@ class JobArtifactUploader < GitlabUploader def dynamic_segment raise ObjectNotReadyError, 'JobArtifact is not ready' unless model.id - creation_date = model.created_at.utc.strftime('%Y_%m_%d') + if model.hashed_path? + hashed_path + elsif model.legacy_path? + legacy_path + else + raise UnknownFileLocationError + end + end + def hashed_path File.join(disk_hash[0..1], disk_hash[2..3], disk_hash, - creation_date, model.job_id.to_s, model.id.to_s) + model.created_at.utc.strftime('%Y_%m_%d'), model.job_id.to_s, model.id.to_s) + end + + def legacy_path + File.join(model.created_at.utc.strftime('%Y_%m'), model.project_id.to_s, model.job_id.to_s) end def disk_hash diff --git a/app/uploaders/legacy_artifact_uploader.rb b/app/uploaders/legacy_artifact_uploader.rb index b4d0d752016..a9afc104ed1 100644 --- a/app/uploaders/legacy_artifact_uploader.rb +++ b/app/uploaders/legacy_artifact_uploader.rb @@ -8,6 +8,8 @@ class LegacyArtifactUploader < GitlabUploader storage_options Gitlab.config.artifacts + alias_method :upload, :model + def store_dir dynamic_segment end diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb index f3d32e6b39d..0a966f3d44f 100644 --- a/app/uploaders/lfs_object_uploader.rb +++ b/app/uploaders/lfs_object_uploader.rb @@ -6,6 +6,8 @@ class LfsObjectUploader < GitlabUploader storage_options Gitlab.config.lfs + alias_method :upload, :model + def filename model.oid[4..-1] end 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/uploaders/uploader_helper.rb b/app/uploaders/uploader_helper.rb index 2a2b54a9270..e8a2dce7755 100644 --- a/app/uploaders/uploader_helper.rb +++ b/app/uploaders/uploader_helper.rb @@ -2,32 +2,7 @@ # Extra methods for uploader module UploaderHelper - IMAGE_EXT = %w[png jpg jpeg gif bmp tiff ico].freeze - # We recommend using the .mp4 format over .mov. Videos in .mov format can - # still be used but you really need to make sure they are served with the - # proper MIME type video/mp4 and not video/quicktime or your videos won't play - # on IE >= 9. - # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html - VIDEO_EXT = %w[mp4 m4v mov webm ogv].freeze - # These extension types can contain dangerous code and should only be embedded inline with - # proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline". - DANGEROUS_EXT = %w[svg].freeze - - def image? - extension_match?(IMAGE_EXT) - end - - def video? - extension_match?(VIDEO_EXT) - end - - def image_or_video? - image? || video? - end - - def dangerous? - extension_match?(DANGEROUS_EXT) - end + include Gitlab::FileMarkdownLinkBuilder private diff --git a/app/validators/branch_filter_validator.rb b/app/validators/branch_filter_validator.rb new file mode 100644 index 00000000000..6a0899be850 --- /dev/null +++ b/app/validators/branch_filter_validator.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +# BranchFilterValidator +# +# Custom validator for branch names. Squishes whitespace and ignores empty +# string. This only checks that a string is a valid git branch name. It does +# not check whether a branch already exists. +# +# Example: +# +# class Webhook < ActiveRecord::Base +# validates :push_events_branch_filter, branch_name: true +# end +# +class BranchFilterValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + value.squish! unless value.nil? + + if value.present? + value_without_wildcards = value.tr('*', 'x') + + unless Gitlab::GitRefValidator.validate(value_without_wildcards) + record.errors[attribute] << "is not a valid branch name" + end + + unless value.length <= 4000 + record.errors[attribute] << "is longer than the allowed length of 4000 characters." + end + end + end + + private + + def contains_wildcard?(value) + value.include?('*') + end +end 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/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml deleted file mode 100644 index 7d1a64b645a..00000000000 --- a/app/views/admin/application_settings/_background_jobs.html.haml +++ /dev/null @@ -1,27 +0,0 @@ -= form_for @application_setting, url: admin_application_settings_path(anchor: 'js-background-settings'), html: { class: 'fieldset-form' } do |f| - = form_errors(@application_setting) - - %fieldset - %p - These settings require a - = link_to 'restart', help_page_path('administration/restart_gitlab') - to take effect. - .form-group - .form-check - = f.check_box :sidekiq_throttling_enabled, class: 'form-check-input' - = f.label :sidekiq_throttling_enabled, class: 'form-check-label' do - Enable Sidekiq Job Throttling - .form-text.text-muted - Limit the amount of resources slow running jobs are assigned. - .form-group - = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'label-bold' - = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' } - .form-text.text-muted - Choose which queues you wish to throttle. - .form-group - = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'label-bold' - = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01' - .form-text.text-muted - The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive. - - = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/_diff_limits.html.haml b/app/views/admin/application_settings/_diff_limits.html.haml new file mode 100644 index 00000000000..408e569fe07 --- /dev/null +++ b/app/views/admin/application_settings/_diff_limits.html.haml @@ -0,0 +1,16 @@ += form_for @application_setting, url: admin_application_settings_path(anchor: 'js-merge-request-settings'), html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + = f.label :diff_max_patch_bytes, 'Maximum diff patch size (Bytes)', class: 'label-light' + = f.number_field :diff_max_patch_bytes, class: 'form-control' + %span.form-text.text-muted + Diff files surpassing this limit will be presented as 'too large' + and won't be expandable. + + = link_to icon('question-circle'), + help_page_path('user/admin_area/diff_limits', + anchor: 'maximum-diff-patch-size') + + = f.submit _('Save changes'), class: 'btn btn-success' 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/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml index 4523332493b..908b30cc3ce 100644 --- a/app/views/admin/application_settings/_repository_storage.html.haml +++ b/app/views/admin/application_settings/_repository_storage.html.haml @@ -5,7 +5,7 @@ .sub-section .form-group .form-check - = f.check_box :hashed_storage_enabled, class: 'form-check-input' + = f.check_box :hashed_storage_enabled, class: 'form-check-input qa-hashed-storage-checkbox' = f.label :hashed_storage_enabled, class: 'form-check-label' do Use hashed storage paths for newly created and renamed projects .form-text.text-muted @@ -48,4 +48,4 @@ .form-text.text-muted = circuitbreaker_failure_reset_time_help_text - = f.submit 'Save changes', class: "btn btn-success" + = f.submit 'Save changes', class: "btn btn-success qa-save-changes-button" 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..00000b86ab7 --- /dev/null +++ b/app/views/admin/application_settings/preferences.html.haml @@ -0,0 +1,58 @@ +- 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-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..be13138a764 --- /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.qa-repository-storage-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 6133a7646f4..279db189a24 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -1,351 +1,104 @@ -- 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-diff-limits.no-animate#js-merge-request-settings{ class: ('expanded' if expanded_by_default?) } + .settings-header + %h4 + = _('Diff limits') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded_by_default? ? _('Collapse') : _('Expand') + %p + = _('Diff content limits') + .settings-content + = render 'diff_limits' + +%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.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/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 @@ -362,5 +115,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/applications/show.html.haml b/app/views/admin/applications/show.html.haml index 593a6d816e3..e69143abe45 100644 --- a/app/views/admin/applications/show.html.haml +++ b/app/views/admin/applications/show.html.haml @@ -1,4 +1,5 @@ - page_title @application.name, "Applications" + %h3.page-title Application: #{@application.name} @@ -6,23 +7,29 @@ %table.table %tr %td - Application Id + = _('Application ID') %td - %code#application_id= @application.uid + .clipboard-group + .input-group + %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") %tr %td - Secret: + = _('Secret') %td - %code#secret= @application.secret - + .clipboard-group + .input-group + %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td - Callback url + = _('Callback URL') %td - @application.redirect_uri.split.each do |uri| %div %span.monospace= uri - %tr %td Trusted 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/projects/show.html.haml b/app/views/admin/projects/show.html.haml index ccba1c461fc..fefb4c7455d 100644 --- a/app/views/admin/projects/show.html.haml +++ b/app/views/admin/projects/show.html.haml @@ -1,6 +1,8 @@ - add_to_breadcrumbs "Projects", admin_projects_path - breadcrumb_title @project.full_name - page_title @project.full_name, "Projects" +- @content_class = "admin-projects" + %h3.page-title Project: #{@project.full_name} = link_to edit_project_path(@project), class: "btn btn-nr float-right" do @@ -110,6 +112,8 @@ = visibility_level_icon(@project.visibility_level) = visibility_level_label(@project.visibility_level) + = render_if_exists 'admin/projects/geo_status_widget', locals: { project: @project } + .card .card-header Transfer project 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..e9e4e0847d3 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -1,77 +1,122 @@ -- 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. - %br - Runners can be placed on separate users, servers, even on your local machine. - %br + .row + .col-sm-6 + .bs-callout + %p + = (_"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.') + %br - %div - %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 - %li - %span.badge.badge-success group - \- Runner runs jobs from all unassigned projects in its group - %li - %span.badge.badge-info specific - \- Runner runs jobs from assigned projects - %li - %span.badge.badge-warning locked - \- Runner cannot be assigned to other projects - %li - %span.badge.badge-danger paused - \- Runner will not receive any new jobs + %div + %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') + %li + %span.badge.badge-success group + \- + = _('Runner runs jobs from all unassigned projects in its group') + %li + %span.badge.badge-info specific + \- + = _('Runner runs jobs from assigned projects') + %li + %span.badge.badge-warning locked + \- + = _('Runner cannot be assigned to other projects') + %li + %span.badge.badge-danger paused + \- + = _('Runner will not receive any new jobs') - .bs-callout.clearfix - .float-left - %p - 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, - method: :put, class: 'btn btn-default', - data: { confirm: _("Are you sure you want to reset registration token?") } + .col-sm-6 + .bs-callout + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token, + type: 'shared', + reset_token_url: reset_registration_token_admin_application_settings_path } - = render partial: 'ci/runner/how_to_setup_shared_runner', - locals: { registration_token: Gitlab::CurrentSettings.runners_registration_token } + .row + .col-sm-9 + = 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}} - .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' + #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 - .float-right.light - Runners currently online: #{@active_runners_cnt} + #js-dropdown-admin-runner-type.filtered-search-input-dropdown-menu.dropdown-menu + %ul{ data: { dropdown: true } } + - Ci::Runner::AVAILABLE_TYPES.each do |runner_type| + %li.filter-dropdown-item{ data: { value: runner_type } } + = button_tag class: %w[btn btn-link] do + = runner_type.titleize - %br + = button_tag class: %w[clear-search hidden] do + = icon('times') + .filter-dropdown-container + = render 'sort_dropdown' + + .col-sm-3.text-right-lg + = _('Runners currently online: %{active_runners_count}') % { active_runners_count: @active_runners_count } - 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/award_emoji/_awards_block.html.haml b/app/views/award_emoji/_awards_block.html.haml index 30d7b21b1b8..a758a63dfb3 100644 --- a/app/views/award_emoji/_awards_block.html.haml +++ b/app/views/award_emoji/_awards_block.html.haml @@ -1,5 +1,4 @@ - grouped_emojis = awardable.grouped_awards(with_thumbs: inline) -- user_authored = awardable.user_authored?(current_user) .awards.js-awards-block{ class: ("hidden" if !inline && grouped_emojis.empty?), data: { award_url: toggle_award_url(awardable) } } - awards_sort(grouped_emojis).each do |emoji, awards| %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", 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..4307060d748 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 @@ -13,5 +13,9 @@ = _("Use the following registration token during setup:") %code#registration_token= registration_token = clipboard_button(target: '#registration_token', title: _("Copy token to clipboard"), class: "btn-transparent btn-clipboard") + .prepend-top-10.append-bottom-10 + = button_to _("Reset runners registration token"), reset_token_url, + method: :put, class: 'btn btn-default', + data: { confirm: _("Are you sure you want to reset registration token?") } %li = _("Start the Runner!") diff --git a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml b/app/views/ci/runner/_how_to_setup_shared_runner.html.haml deleted file mode 100644 index 2a190cb9250..00000000000 --- a/app/views/ci/runner/_how_to_setup_shared_runner.html.haml +++ /dev/null @@ -1,3 +0,0 @@ -.bs-callout.help-callout - = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: registration_token, type: 'shared' } 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 deleted file mode 100644 index e765a353fe4..00000000000 --- a/app/views/ci/runner/_how_to_setup_specific_runner.html.haml +++ /dev/null @@ -1,26 +0,0 @@ -.bs-callout.help-callout - .append-bottom-10 - %h4= _('Setup a specific Runner automatically') - - %p - - link_to_help_page = link_to(_('Learn more about Kubernetes'), - help_page_path('user/project/clusters/index'), - target: '_blank', - rel: 'noopener noreferrer') - - = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } - - %ol - %li - = _('Click the button below to begin the install process by navigating to the Kubernetes page') - %li - = _('Select an existing Kubernetes cluster or create a new one') - %li - = _('From the Kubernetes cluster details view, install Runner from the applications list') - - = link_to _('Install Runner on Kubernetes'), - project_clusters_path(@project), - class: 'btn btn-info' - %hr - = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: registration_token, type: 'specific' } 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/activity.html.haml b/app/views/dashboard/activity.html.haml index 31d4b3da4f1..3cee5841bbc 100644 --- a/app/views/dashboard/activity.html.haml +++ b/app/views/dashboard/activity.html.haml @@ -4,6 +4,9 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - page_title "Activity" - header_title "Activity", activity_dashboard_path diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml index 50f39f93283..985928305a2 100644 --- a/app/views/dashboard/groups/index.html.haml +++ b/app/views/dashboard/groups/index.html.haml @@ -3,6 +3,9 @@ - header_title "Groups", dashboard_groups_path = render 'dashboard/groups_head' += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if params[:filter].blank? && @groups.empty? = render 'shared/groups/empty_state' - else 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/issues.html.haml b/app/views/dashboard/issues.html.haml index 86a21e24ac9..91f58ddcfcc 100644 --- a/app/views/dashboard/issues.html.haml +++ b/app/views/dashboard/issues.html.haml @@ -4,6 +4,9 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues") += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + .top-area = render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set .nav-controls diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml index 61aae31be60..27f53a8d1c6 100644 --- a/app/views/dashboard/merge_requests.html.haml +++ b/app/views/dashboard/merge_requests.html.haml @@ -2,6 +2,9 @@ - page_title _("Merge Requests") - @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id) += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + .top-area = render 'shared/issuable/nav', type: :merge_requests, display_count: !@no_filters_set .nav-controls diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml index deed774a4a5..f0d16936a51 100644 --- a/app/views/dashboard/projects/index.html.haml +++ b/app/views/dashboard/projects/index.html.haml @@ -4,6 +4,9 @@ = content_for :meta_tags do = auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity") += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - page_title "Projects" - header_title "Projects", dashboard_projects_path diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml index 8933d9e31ff..42638b8528d 100644 --- a/app/views/dashboard/projects/starred.html.haml +++ b/app/views/dashboard/projects/starred.html.haml @@ -4,6 +4,9 @@ - page_title "Starred Projects" - header_title "Projects", dashboard_projects_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + %div{ class: container_class } = render "projects/last_push" = render 'dashboard/projects_head' 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/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 8b3974d97f8..bbfa4cc7413 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -2,6 +2,9 @@ - page_title "Todos" - header_title "Todos", dashboard_todos_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if current_user.todos.any? .top-area %ul.nav-links.mobile-separator.nav.nav-tabs 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..7dced0942f5 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' + = link_to 'Register', '#register-pane', class: 'nav-link qa-register-tab', '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/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml index 646e89e9bd1..44c898e0fac 100644 --- a/app/views/discussions/_diff_with_notes.html.haml +++ b/app/views/discussions/_diff_with_notes.html.haml @@ -1,6 +1,4 @@ - diff_file = discussion.diff_file -- blob = discussion.blob -- discussions = { discussion.original_line_code => [discussion] } - diff_file_class = diff_file.text? ? 'text-file' : 'js-image-file' - diff_data = {} - expanded = discussion.expanded? || local_assigns.fetch(:expanded, nil) 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/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml index bb76ac6d5f6..776bbc36ec2 100644 --- a/app/views/doorkeeper/applications/show.html.haml +++ b/app/views/doorkeeper/applications/show.html.haml @@ -10,18 +10,25 @@ %table.table %tr %td - = _('Application Id') + = _('Application ID') %td - %code#application_id= @application.uid + .clipboard-group + .input-group + %input.label.label-monospace{ id: "application_id", type: "text", autocomplete: 'off', value: @application.uid, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy ID to clipboard"), class: "btn btn btn-default") %tr %td - = _('Secret:') + = _('Secret') %td - %code#secret= @application.secret - + .clipboard-group + .input-group + %input.label.label-monospace{ id: "secret", type: "text", autocomplete: 'off', value: @application.secret, readonly: true } + .input-group-append + = clipboard_button(target: '#application_id', title: _("Copy secret to clipboard"), class: "btn btn btn-default") %tr %td - = _('Callback url') + = _('Callback URL') %td - @application.redirect_uri.split.each do |uri| %div diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml index 08f2442f025..69cc510e9c1 100644 --- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml +++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml @@ -1,4 +1,3 @@ -- submit_btn_css ||= 'btn btn-link btn-remove' - if defined?(token) - path = oauth_authorized_application_path(0, token_id: token) - else diff --git a/app/views/errors/precondition_failed.html.haml b/app/views/errors/precondition_failed.html.haml new file mode 100644 index 00000000000..aa3869f33a9 --- /dev/null +++ b/app/views/errors/precondition_failed.html.haml @@ -0,0 +1,8 @@ +- content_for(:title, 'Encoding Error') +%img{ :alt => "GitLab Logo", :src => image_path('logo.svg') } + %h1 + 412 +.container + %h3 Precondition failed + %hr + %p Page can't be loaded because of invalid parameters. 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_push.atom.haml b/app/views/events/_event_push.atom.haml index bc1d32607e4..c5b033b1185 100644 --- a/app/views/events/_event_push.atom.haml +++ b/app/views/events/_event_push.atom.haml @@ -1,7 +1,7 @@ %div{ xmlns: "http://www.w3.org/1999/xhtml" } %p %strong= event.author_name - = link_to "(#{truncate_sha(event.commit_id)})", project_commit_path(event.project, event.commit_id) + = link_to "(#{truncate_sha(event.commit_id)})", event_feed_url(event) %i at = event.created_at.to_s(:short) 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/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml index 387c37b7a91..1d8b9c5bc8f 100644 --- a/app/views/explore/groups/index.html.haml +++ b/app/views/explore/groups/index.html.haml @@ -2,6 +2,9 @@ - page_title _("Groups") - header_title _("Groups"), dashboard_groups_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if current_user = render 'dashboard/groups_head' - else diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml index 452f390695c..16be5791f83 100644 --- a/app/views/explore/projects/index.html.haml +++ b/app/views/explore/projects/index.html.haml @@ -2,6 +2,9 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if current_user = render 'dashboard/projects_head' - else diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml index 452f390695c..16be5791f83 100644 --- a/app/views/explore/projects/starred.html.haml +++ b/app/views/explore/projects/starred.html.haml @@ -2,6 +2,9 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if current_user = render 'dashboard/projects_head' - else diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml index 452f390695c..16be5791f83 100644 --- a/app/views/explore/projects/trending.html.haml +++ b/app/views/explore/projects/trending.html.haml @@ -2,6 +2,9 @@ - page_title _("Projects") - header_title _("Projects"), dashboard_projects_path += content_for :above_breadcrumbs_content do + = render_if_exists "shared/gold_trial_callout" + - if current_user = render 'dashboard/projects_head' - else 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/edit.html.haml b/app/views/groups/edit.html.haml index cae2df4699e..fc17dd2d310 100644 --- a/app/views/groups/edit.html.haml +++ b/app/views/groups/edit.html.haml @@ -25,6 +25,18 @@ .settings-content = render 'groups/settings/permissions' +%section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('GroupSettings|Badges') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = s_('GroupSettings|Customize your group badges.') + = link_to s_('GroupSettings|Learn more about badges.'), help_page_path('user/project/badges') + .settings-content + = render 'shared/badges/badge_settings' + %section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) } .settings-header %h4 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 db7eaff6658..5b78ce910b8 100644 --- a/app/views/groups/labels/index.html.haml +++ b/app/views/groups/labels/index.html.haml @@ -1,29 +1,36 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @group) -- hide_class = '' -- hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') - issuables = ['issues', 'merge requests'] +- search = params[:search] +- subscribed = params[:subscribed] +- labels_or_filters = @labels.exists? || search.present? || subscribed.present? - 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? +- if labels_or_filters #promote-label-modal %div{ class: container_class } - .top-area.adjust - .nav-text - = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } + = render 'shared/labels/nav' .labels-container.prepend-top-5 - .other-labels - - if can_admin_label - %h5{ class: ('hide' if hide) } Labels - %ul.content-list.manage-labels-list.js-other-labels - = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } - = paginate @labels, theme: 'gitlab' + - if @labels.any? + .text-muted + = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuables.to_sentence } + .other-labels + %h5= _('Labels') + %ul.content-list.manage-labels-list.js-other-labels + = render partial: 'shared/label', subject: @group, collection: @labels, as: :label, locals: { use_label_priority: false } + = paginate @labels, theme: 'gitlab' + - elsif search.present? + .nothing-here-block + = _('No labels with such name or description') + - elsif subscribed.present? + .nothing-here-block + = _('You do not have any subscriptions yet') - else = render 'shared/empty_states/labels' 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/runners/_group_runners.html.haml b/app/views/groups/runners/_group_runners.html.haml index e6c089c3494..bcfb6d99716 100644 --- a/app/views/groups/runners/_group_runners.html.haml +++ b/app/views/groups/runners/_group_runners.html.haml @@ -11,7 +11,9 @@ -# https://gitlab.com/gitlab-org/gitlab-ce/issues/45894 - if can?(current_user, :admin_pipeline, @group) = render partial: 'ci/runner/how_to_setup_runner', - locals: { registration_token: @group.runners_token, type: 'group' } + locals: { registration_token: @group.runners_token, + type: 'group', + reset_token_url: reset_registration_token_group_settings_ci_cd_path } - if @group.runners.empty? %h4.underlined-title diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml index b7c673db705..3814d45929d 100644 --- a/app/views/groups/settings/_advanced.html.haml +++ b/app/views/groups/settings/_advanced.html.haml @@ -12,8 +12,8 @@ .group-root-path.input-group-prepend.has-tooltip{ title: group_path(@group), :'data-placement' => 'bottom' } .input-group-text %span>= root_url - - if parent - %strong= parent.full_path + '/' + - if @group.parent + %strong= @group.parent.full_path + '/' = f.hidden_field :parent_id = f.text_field :path, placeholder: 'open-source', class: 'form-control', autofocus: local_assigns[:autofocus] || false, required: true, 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..6a293daaf95 100644 --- a/app/views/groups/show.html.haml +++ b/app/views/groups/show.html.haml @@ -7,21 +7,20 @@ = 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") - if can_create_subgroups - .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } - %input.btn.btn-success.dropdown-primary.js-new-group-child{ type: "button", value: new_project_label, data: { action: "new-project" } } - %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } + .btn-group.new-project-subgroup.droplab-dropdown.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } + %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } } + %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } = icon("caret-down", class: "dropdown-btn-icon") %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } - %li.droplab-item-selected{ role: "button", data: { value: "new-project", text: new_project_label } } + %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } } .menu-item .icon-container = icon("check", class: "list-item-checkmark") @@ -29,7 +28,7 @@ %strong= new_project_label %span= s_("GroupsTree|Create a project in this group.") %li.divider.droplap-item-ignore - %li{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } + %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } .menu-item .icon-container = icon("check", class: "list-item-checkmark") @@ -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/index.html.haml b/app/views/help/index.html.haml index 7a66bac09cb..dfa5d820ce9 100644 --- a/app/views/help/index.html.haml +++ b/app/views/help/index.html.haml @@ -7,8 +7,7 @@ GitLab Community Edition - if user_signed_in? - %span= Gitlab::VERSION - %small= link_to Gitlab.revision, Gitlab::COM_URL + namespace_project_commits_path('gitlab-org', 'gitlab-ce', Gitlab.revision) + %span= link_to_version = version_status_badge %hr 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..5e4595d930b 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 .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/_search.html.haml b/app/views/layouts/_search.html.haml index 9a7a67cfa83..a86972d8cf3 100644 --- a/app/views/layouts/_search.html.haml +++ b/app/views/layouts/_search.html.haml @@ -1,7 +1,3 @@ -- if controller.controller_path =~ /^groups/ && @group.persisted? - - label = _('This group') -- if controller.controller_path =~ /^projects/ && @project.persisted? - - label = _('This project') - if @group && @group.persisted? && @group.path - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) } - if @project && @project.persisted? diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0ca34b276a7..1f4d24d996c 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -4,7 +4,7 @@ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } } = render "layouts/init_auto_complete" if @gfm_form = render 'peek/bar' - = render "layouts/header/default" + = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'layouts/page', sidebar: sidebar, nav: nav = yield :scripts_body diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml index 2ab9e55441b..80bda34a3f5 100644 --- a/app/views/layouts/explore.html.haml +++ b/app/views/layouts/explore.html.haml @@ -1,5 +1,5 @@ -- page_title = _("Explore") +- page_title _("Explore") - unless current_user - - header_title = _("Explore GitLab"), explore_root_path + - header_title _("Explore GitLab"), explore_root_path = render template: "layouts/application" diff --git a/app/views/layouts/fullscreen.html.haml b/app/views/layouts/fullscreen.html.haml index 95db8313821..e29f646ed4f 100644 --- a/app/views/layouts/fullscreen.html.haml +++ b/app/views/layouts/fullscreen.html.haml @@ -3,7 +3,7 @@ = render "layouts/head" %body{ class: "#{user_application_theme} #{@body_class} fullscreen-layout", data: { page: body_data_page } } = render 'peek/bar' - = render "layouts/header/default" + = render partial: "layouts/header/default", locals: { project: @project, group: @group } = render 'shared/outdated_browser' .mobile-overlay .alert-wrapper diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml index 14c5f0ce04c..9db78ec58e4 100644 --- a/app/views/layouts/group_settings.html.haml +++ b/app/views/layouts/group_settings.html.haml @@ -1,4 +1,4 @@ -- page_title = _("Settings") -- nav "group" +- page_title _("Settings") +- nav "group" = render template: "layouts/group" diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index 9ed05d6e3d0..261d758622b 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -5,7 +5,14 @@ .user-name.bold = current_user.name = current_user.to_reference + - if current_user.status + .user-status-emoji.str-truncated.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } } + = emoji_icon current_user.status.emoji + = current_user.status.message_html.html_safe %li.divider + - if can?(current_user, :update_user_status, current_user) + %li + .js-set-status-modal-trigger{ data: { has_status: current_user.status.present? ? 'true' : 'false' } } - if current_user_menu?(:profile) %li = link_to s_("CurrentUser|Profile"), current_user, class: 'profile-link', data: { user: current_user.username } diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index e8d31992149..39604611440 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -1,3 +1,10 @@ +- if project + - search_path_url = search_path(project_id: project.id) +- elsif group + - search_path_url = search_path(group_id: group.id) +- else + - search_path_url = search_path + %header.navbar.navbar-gitlab.qa-navbar.navbar-expand-sm %a.sr-only.gl-accessibility{ href: "#content-body", tabindex: "1" } Skip to content .container-fluid @@ -24,26 +31,25 @@ %li.nav-item.d-none.d-sm-none.d-md-block.m-auto = render 'layouts/search' unless current_controller?(:search) %li.nav-item.d-inline-block.d-sm-none.d-md-none - = link_to search_path, title: _('Search'), aria: { label: _("Search") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to search_path_url, title: _('Search'), aria: { label: _('Search') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('search', size: 16) - - if header_link?(:issues) = nav_link(path: 'dashboard#issues', html_options: { class: "user-counter" }) do - = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _("Issues") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to assigned_issues_dashboard_path, title: _('Issues'), class: 'dashboard-shortcuts-issues', aria: { label: _('Issues') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('issues', size: 16) - issues_count = assigned_issuables_count(:issues) %span.badge.badge-pill.issues-count{ class: ('hidden' if issues_count.zero?) } = number_with_delimiter(issues_count) - if header_link?(:merge_requests) = nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter" }) do - = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _("Merge requests") }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to assigned_mrs_dashboard_path, title: _('Merge requests'), class: 'dashboard-shortcuts-merge_requests', aria: { label: _('Merge requests') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('git-merge', size: 16) - merge_requests_count = assigned_issuables_count(:merge_requests) %span.badge.badge-pill.merge-requests-count{ class: ('hidden' if merge_requests_count.zero?) } = number_with_delimiter(merge_requests_count) - if header_link?(:todos) = nav_link(controller: 'dashboard/todos', html_options: { class: "user-counter" }) do - = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _("Todos") }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do + = link_to dashboard_todos_path, title: _('Todos'), aria: { label: _('Todos') }, class: 'shortcuts-todos', data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = sprite_icon('todo-done', size: 16) %span.badge.badge-pill.todos-count{ class: ('hidden' if todos_pending_count.zero?) } = todos_count_format(todos_pending_count) @@ -56,7 +62,7 @@ = render 'layouts/header/current_user_dropdown' - if header_link?(:admin_impersonation) %li.nav-item.impersonation - = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _("Stop impersonation"), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('user-secret') - if header_link?(:sign_in) %li.nav-item @@ -64,8 +70,10 @@ - sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in') = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' - %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } - %span.sr-only= _("Toggle navigation") + %span.sr-only= _('Toggle navigation') = sprite_icon('ellipsis_h', size: 12, css_class: 'more-icon js-navbar-toggle-right') = sprite_icon('close', size: 12, css_class: 'close-icon js-navbar-toggle-left') + +- if can?(current_user, :update_user_status, current_user) + .js-set-status-modal-wrapper{ data: { current_emoji: current_user.status.present? ? current_user.status.emoji : '', current_message: current_user.status.present? ? current_user.status.message : '' } } diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml index f53bd2b5e4d..c35451827c8 100644 --- a/app/views/layouts/nav/_breadcrumbs.html.haml +++ b/app/views/layouts/nav/_breadcrumbs.html.haml @@ -1,6 +1,7 @@ - container = @no_breadcrumb_container ? 'container-fluid' : container_class - hide_top_links = @hide_top_links || false += yield :above_breadcrumbs_content %nav.breadcrumbs{ role: "navigation", class: [container, @content_class] } .breadcrumbs-container - if defined?(@left_sidebar) diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index ff25b040913..5f15ba87729 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -1,4 +1,4 @@ -.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } +.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) } .nav-sidebar-inner-scroll .context-header = link_to admin_root_path, title: _('Admin Overview') do @@ -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') @@ -197,12 +197,56 @@ = link_to admin_application_settings_path do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name + %span.nav-item-name.qa-admin-settings-item = _('Settings') - %ul.sidebar-sub-level-items.is-fly-out-only + + %ul.sidebar-sub-level-items.qa-admin-sidebar-submenu = 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'), class: 'qa-admin-settings-repository-item' 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/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml index d471dd84550..4aa22138498 100644 --- a/app/views/layouts/nav/sidebar/_group.html.haml +++ b/app/views/layouts/nav/sidebar/_group.html.haml @@ -10,7 +10,7 @@ = group_icon(@group, class: "avatar s40 avatar-tile") .sidebar-context-title = @group.name - %ul.sidebar-top-level-items + %ul.sidebar-top-level-items.qa-group-sidebar - if group_sidebar_link?(:overview) = nav_link(path: ['groups#show', 'groups#activity', 'groups#subgroups', 'analytics#show'], html_options: { class: 'home' }) do = link_to group_path(@group) do @@ -109,9 +109,9 @@ = link_to edit_group_path(@group) do .nav-icon-container = sprite_icon('settings') - %span.nav-item-name + %span.nav-item-name.qa-group-settings-item = _('Settings') - %ul.sidebar-sub-level-items + %ul.sidebar-sub-level-items.qa-group-sidebar-submenu = nav_link(path: %w[groups#projects groups#edit badges#index ci_cd#show], html_options: { class: "fly-out-top-item" } ) do = link_to edit_group_path(@group) do %strong.fly-out-top-item-name @@ -122,12 +122,6 @@ %span = _('General') - = nav_link(controller: :badges) do - = link_to group_settings_badges_path(@group), title: _('Project Badges') do - %span - = _('Project Badges') - - = nav_link(path: 'groups#projects') do = link_to projects_group_path(@group), title: _('Projects') do %span 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 34f47806205..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') @@ -313,11 +313,6 @@ %span = _('Members') - if can_edit - = nav_link(controller: :badges) do - = link_to project_settings_badges_path(@project), title: _('Badges') do - %span - = _('Badges') - - if can_edit = nav_link(controller: [:integrations, :services, :hooks, :hook_logs]) do = link_to project_settings_integrations_path(@project), title: _('Integrations') do %span 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/new_merge_request_email.html.haml b/app/views/notify/new_merge_request_email.html.haml index dd6a84e503d..5acd45b74a7 100644 --- a/app/views/notify/new_merge_request_email.html.haml +++ b/app/views/notify/new_merge_request_email.html.haml @@ -9,7 +9,7 @@ %p Assignee: #{@merge_request.assignee_name} -= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request += render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter - if @merge_request.description %div diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb index d5b8f8d764f..754f4bca1cd 100644 --- a/app/views/notify/new_merge_request_email.text.erb +++ b/app/views/notify/new_merge_request_email.text.erb @@ -5,6 +5,6 @@ New Merge Request <%= @merge_request.to_reference %> <%= merge_path_description(@merge_request, 'to') %> Author: <%= @merge_request.author_name %> Assignee: <%= @merge_request.assignee_name %> -<%= render_if_exists 'notify/merge_request_approvers', merge_request: @merge_request %> +<%= render_if_exists 'notify/merge_request_approvers', presenter: @mr_presenter %> <%= @merge_request.description %> 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..ea215e3e718 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,70 @@ .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 - - private_profile_label = capture do - Don't display activity-related personal information on your profile + %h5= ("Private profile") + .checkbox-icon-inline-wrapper + - private_profile_label = capture do + = s_("Profiles|Don't display activity-related personal information on your profiles") + = f.check_box :private_profile, label: private_profile_label = 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 +141,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..759d39cf5f5 100644 --- a/app/views/profiles/two_factor_auths/_codes.html.haml +++ b/app/views/profiles/two_factor_auths/_codes.html.haml @@ -1,5 +1,5 @@ %p.slead - Should you ever lose your phone, each of these recovery codes can be used one + Should you ever lose your phone or access to your one time password secret, each of these recovery codes can be used one time each to regain access to your account. Please save them in a safe place, or you %b will lose access to your account. @@ -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/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml index cd10b8758f6..94ec0cc5db8 100644 --- a/app/views/profiles/two_factor_auths/show.html.haml +++ b/app/views/profiles/two_factor_auths/show.html.haml @@ -6,13 +6,13 @@ .row.prepend-top-default .col-lg-4 %h4.prepend-top-0 - Register Two-Factor Authentication App + Register Two-Factor Authenticator %p - Use an app on your mobile device to enable two-factor authentication (2FA). + Use an one time password authenticator on your mobile device or computer to enable two-factor authentication (2FA). .col-lg-8 - if current_user.two_factor_otp_enabled? %p - You've already enabled two-factor authentication using mobile authenticator applications. In order to register a different device, you must first disable two-factor authentication. + You've already enabled two-factor authentication using one time password authenticators. In order to register a different device, you must first disable two-factor authentication. %p If you lose your recovery codes you can generate new ones, invalidating all previous codes. %div 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 fbe88ec9618..ced6a2a0399 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,17 +1,35 @@ - empty_repo = @project.empty_repo? -- fork_network = @project.fork_network -.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 } @@ -26,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..2b425f18389 100644 --- a/app/views/projects/_import_project_pane.html.haml +++ b/app/views/projects/_import_project_pane.html.haml @@ -1,4 +1,5 @@ - active_tab = local_assigns.fetch(:active_tab, 'blank') +- track_label = local_assigns.fetch(:track_label, 'import_project') .project-import .form-group.import-btn-container.clearfix @@ -7,60 +8,63 @@ .import-buttons - if gitlab_project_import_enabled? .import_gitlab_project.has-tooltip{ data: { container: 'body' } } - = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do + = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_export" } do = icon('gitlab', text: 'GitLab export') - if github_import_enabled? %div - = link_to new_import_github_path, class: 'btn js-import-github' do + = link_to new_import_github_path, class: 'btn js-import-github', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "github" } do = icon('github', text: 'GitHub') - if bitbucket_import_enabled? %div - = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}" do + = link_to status_import_bitbucket_path, class: "btn import_bitbucket #{'how_to_import_link' unless bitbucket_import_configured?}", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_cloud" } do = icon('bitbucket', text: 'Bitbucket Cloud') - unless bitbucket_import_configured? = render 'bitbucket_import_modal' - if bitbucket_server_import_enabled? %div - = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket" do + = link_to status_import_bitbucket_server_path, class: "btn import_bitbucket", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "bitbucket_server" } do = icon('bitbucket-square', text: 'Bitbucket Server') %div - if gitlab_import_enabled? %div - = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do + = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}", + data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitlab_com" } do = icon('gitlab', text: 'GitLab.com') - unless gitlab_import_configured? = render 'gitlab_import_modal' - if google_code_import_enabled? %div - = link_to new_import_google_code_path, class: 'btn import_google_code' do + = link_to new_import_google_code_path, class: 'btn import_google_code', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "google_code" } do = icon('google', text: 'Google Code') - if fogbugz_import_enabled? %div - = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do + = link_to new_import_fogbugz_path, class: 'btn import_fogbugz', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "fogbugz" } do = icon('bug', text: 'Fogbugz') - if gitea_import_enabled? %div - = link_to new_import_gitea_path, class: 'btn import_gitea' do + = link_to new_import_gitea_path, class: 'btn import_gitea', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "gitea" } do = custom_icon('go_logo') Gitea - if git_import_enabled? %div - %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } } + %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active', data: { toggle_open_class: 'active', track_label: "#{track_label}" , track_event: "click_button", track_property: "repo_url" } } } = icon('git', text: 'Repo by URL') - if manifest_import_enabled? %div - = link_to new_import_manifest_path, class: 'btn import_manifest' do + = link_to new_import_manifest_path, class: 'btn import_manifest', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "manifest_file" } do = icon('file-text-o', text: 'Manifest file') .js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') } = 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, track_label: track_label diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml index 8fb6aa55436..c62328e3fb9 100644 --- a/app/views/projects/_md_preview.html.haml +++ b/app/views/projects/_md_preview.html.haml @@ -18,14 +18,17 @@ Preview %li.md-header-toolbar.active - = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" }) - = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" }) - = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" }) - = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: "Insert code" }) - = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" }) - = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" }) - = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" }) - %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: "Go full screen", data: { container: "body" } } + = markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: s_("MarkdownToolbar|Add bold text") }) + = markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: s_("MarkdownToolbar|Add italic text") }) + = markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: s_("MarkdownToolbar|Insert a quote") }) + = markdown_toolbar_button({ icon: "code", data: { "md-tag" => "`", "md-block" => "```" }, title: s_("MarkdownToolbar|Insert code") }) + = markdown_toolbar_button({ icon: "link", data: { "md-tag" => "[{text}](url)", "md-select" => "url" }, title: s_("MarkdownToolbar|Add a link") }) + = markdown_toolbar_button({ icon: "list-bulleted", data: { "md-tag" => "* ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a bullet list") }) + = markdown_toolbar_button({ icon: "list-numbered", data: { "md-tag" => "1. ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a numbered list") }) + = markdown_toolbar_button({ icon: "task-done", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a task list") }) + = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header 1 | header 2 |\n| -------- | -------- |\n| cell 1 | cell 2 |\n| cell 3 | cell 4 |", "md-prepend" => true }, title: s_("MarkdownToolbar|Add a table") }) + = markdown_toolbar_button({ icon: "table", data: { "md-tag" => "| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |", "md-prepend" => true }, title: _('Add a table') }) + %button.toolbar-btn.toolbar-fullscreen-btn.js-zen-enter.has-tooltip{ type: "button", tabindex: -1, "aria-label": "Go full screen", title: s_("MarkdownToolbar|Go full screen"), data: { container: "body" } } = sprite_icon("screen-full") .md-write-holder diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index 540e996e4d8..935581643cd 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -1,5 +1,4 @@ - form = local_assigns.fetch(:form) -- project = local_assigns.fetch(:project) .form-group = label_tag :merge_method_merge, class: 'label-bold' do diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index ad8c7911fad..e1f28364e19 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -1,12 +1,17 @@ - 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) +- track_label = local_assigns.fetch(:track_label, 'blank_project') .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, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_name", track_value: "" } .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 } @@ -18,7 +23,7 @@ display_path: true, extra_group: namespace_id), {}, - { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1}) + { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) - else .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } @@ -27,34 +32,34 @@ = 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 Project description %span (optional) - = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250 + = f.text_area :description, placeholder: 'Description format', class: "form-control", rows: 3, maxlength: 250, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_description", track_value: "" } = f.label :visibility_level, class: 'label-bold' do Visibility Level = 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', data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "init_with_readme" } + = 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 -= link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel' += f.submit 'Create project', class: "btn btn-success project-submit", tabindex: 4, data: { track_label: "#{track_label}", track_event: "click_button", track_property: "create_project", track_value: "" } += link_to 'Cancel', dashboard_projects_path, class: 'btn btn-cancel', data: { track_label: "#{track_label}", track_event: "click_button", track_property: "cancel" } diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index e90a6355214..98fdb1d7a0b 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, track_label: "create_from_template" 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/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml index 44c1453e239..59c297c46a5 100644 --- a/app/views/projects/ci/builds/_build.html.haml +++ b/app/views/projects/ci/builds/_build.html.haml @@ -47,7 +47,9 @@ %span.badge.badge-info triggered - if job.try(:allow_failure) %span.badge.badge-danger allowed to fail - - if job.action? + - if job.schedulable? + %span.badge.badge-info= s_('DelayedJobs|scheduled') + - elsif job.action? %span.badge.badge-info manual - if pipeline_link @@ -101,6 +103,24 @@ - if job.active? = link_to cancel_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Cancel', class: 'btn btn-build' do = icon('remove', class: 'cred') + - elsif job.scheduled? + .btn-group + .btn.btn-default.has-tooltip{ disabled: true, + title: job.scheduled_at } + = sprite_icon('planning') + = duration_in_numbers(job.execute_in, true) + - confirmation_message = s_("DelayedJobs|Are you sure you want to run %{job_name} immediately? This job will run automatically after it's timer finishes.") % { job_name: job.name } + = link_to play_project_job_path(job.project, job, return_to: request.original_url), + method: :post, + title: s_('DelayedJobs|Start now'), + class: 'btn btn-default btn-build has-tooltip', + data: { confirm: confirmation_message } do + = sprite_icon('play') + = link_to unschedule_project_job_path(job.project, job, return_to: request.original_url), + method: :post, + title: s_('DelayedJobs|Unschedule'), + class: 'btn btn-default btn-build has-tooltip' do + = sprite_icon('time-out') - elsif allow_retry - if job.playable? && !admin && can?(current_user, :update_build, job) = link_to play_project_job_path(job.project, job, return_to: request.original_url), method: :post, title: 'Play', class: 'btn btn-build' do diff --git a/app/views/projects/clusters/_banner.html.haml b/app/views/projects/clusters/_banner.html.haml index f18caa3f4ac..141314b4e4e 100644 --- a/app/views/projects/clusters/_banner.html.haml +++ b/app/views/projects/clusters/_banner.html.haml @@ -1,14 +1,15 @@ -%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") - .hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' } - = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - - %p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab') +- if show_cluster_security_warning? + .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning{ data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } + %button.close.js-close{ type: "button" } × + = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") + = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml index 73b11d509d3..85d1002243b 100644 --- a/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml +++ b/app/views/projects/clusters/_gcp_signup_offer_banner.html.haml @@ -1,6 +1,6 @@ - link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer') -.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' } - %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } × +.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } + %button.close.js-close{ type: "button" } × .gcp-signup-offer--content .gcp-signup-offer--icon.append-right-8 = sprite_icon("information", size: 16) 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..eaf3a93bd15 100644 --- a/app/views/projects/clusters/gcp/_form.html.haml +++ b/app/views/projects/clusters/gcp/_form.html.haml @@ -62,4 +62,13 @@ = s_('ClusterIntegration|Learn more about %{help_link_start_machine_type}machine types%{help_link_end} and %{help_link_start_pricing}pricing%{help_link_end}.').html_safe % { help_link_start_machine_type: help_link_start % { url: machine_type_link_url }, help_link_start_pricing: help_link_start % { url: pricing_link_url }, help_link_end: help_link_end } .form-group + .form-check + = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (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..779c9c245c1 100644 --- a/app/views/projects/clusters/gcp/_show.html.haml +++ b/app/views/projects/clusters/gcp/_show.html.haml @@ -38,4 +38,12 @@ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .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..56551ed4d65 100644 --- a/app/views/projects/clusters/user/_form.html.haml +++ b/app/views/projects/clusters/user/_form.html.haml @@ -26,4 +26,13 @@ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .form-group + .form-check + = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (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..5b57f7ceb7d 100644 --- a/app/views/projects/clusters/user/_show.html.haml +++ b/app/views/projects/clusters/user/_show.html.haml @@ -27,4 +27,12 @@ = platform_kubernetes_field.text_field :namespace, class: 'form-control', placeholder: s_('ClusterIntegration|Project namespace') .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/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml index 7951a5ddc9e..c6789e32dbe 100644 --- a/app/views/projects/commits/_commit.html.haml +++ b/app/views/projects/commits/_commit.html.haml @@ -1,3 +1,7 @@ +-#----------------------------------------------------------------- + WARNING: Please keep changes up-to-date with the following files: + - `assets/javascripts/diffs/components/commit_item.vue` +-#----------------------------------------------------------------- - view_details = local_assigns.fetch(:view_details, false) - merge_request = local_assigns.fetch(:merge_request, nil) - project = local_assigns.fetch(:project) { merge_request&.project } @@ -37,7 +41,7 @@ %button.text-expander.js-toggle-button = sprite_icon('ellipsis_h', size: 12) - .commiter + .committer - commit_author_link = commit_author_link(commit, avatar: false, size: 24) - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom') - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } 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/default_branch/_show.html.haml b/app/views/projects/default_branch/_show.html.haml new file mode 100644 index 00000000000..ff6a9d49a61 --- /dev/null +++ b/app/views/projects/default_branch/_show.html.haml @@ -0,0 +1,21 @@ +- expanded = Rails.env.test? + +%section.settings.no-animate#default-branch-settings{ class: ('expanded' if expanded) } + .settings-header + %h4= _('Default Branch') + %button.btn.js-settings-toggle + = expanded ? _('Collapse') : _('Expand') + %p + = _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.') + + .settings-content + - if @project.empty_repo? + .text-secondary + = _('A default branch cannot be chosen for an empty project.') + - else + = form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f| + %fieldset + .form-group + = f.label :default_branch, "Default Branch", class: 'label-bold' + = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) + = f.submit 'Save changes', class: "btn btn-success" 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/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml index e47361354f3..4b1d4b3ea17 100644 --- a/app/views/projects/diffs/_parallel_view.html.haml +++ b/app/views/projects/diffs/_parallel_view.html.haml @@ -5,7 +5,6 @@ - diff_file.parallel_diff_lines.each do |line| - left = line[:left] - right = line[:right] - - last_line = right.new_pos if right - discussions_left, discussions_right = parallel_diff_discussions(left, right, diff_file) %tr.line_holder.parallel - if left diff --git a/app/views/projects/diffs/_single_image_diff.html.haml b/app/views/projects/diffs/_single_image_diff.html.haml index 12be8beab39..454f814795a 100644 --- a/app/views/projects/diffs/_single_image_diff.html.haml +++ b/app/views/projects/diffs/_single_image_diff.html.haml @@ -1,7 +1,5 @@ - blob = diff_file.blob -- old_blob = diff_file.old_blob - blob_raw_url = diff_file_blob_raw_url(diff_file) -- old_blob_raw_url = diff_file_old_blob_raw_url(diff_file) - click_to_comment = local_assigns.fetch(:click_to_comment, true) - diff_view_data = local_assigns.fetch(:diff_view_data, '') - class_name = '' 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 e37a444c1c9..07fc9e1c682 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -36,23 +36,18 @@ = render_if_exists 'projects/classification_policy_settings', f: f - - unless @project.empty_repo? - .form-group - = f.label :default_branch, "Default Branch", class: 'label-bold' - = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) - = render_if_exists 'shared/repository_size_limit_setting', form: f, type: :project .form-group = f.label :tag_list, "Tags", class: 'label-bold' - = f.text_field :tag_list, value: @project.tag_list.sort.join(', '), maxlength: 2000, class: "form-control" + = f.text_field :tag_list, value: @project.tag_list.join(', '), maxlength: 2000, class: "form-control" %p.form-text.text-muted Separate tags with commas. %fieldset.features %h5.prepend-top-0= _("Project avatar") .form-group - if @project.avatar? .avatar-container.s160.append-bottom-15 - = project_icon(@project.full_path, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) + = project_icon(@project, alt: '', class: 'avatar project-avatar s160', width: 160, height: 160) - if @project.avatar_in_git %p.light = _("Project avatar in repository: %{link}").html_safe % { link: @project.avatar_in_git } @@ -64,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 @@ -80,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' @@ -98,10 +93,22 @@ = 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' + %section.settings.no-animate{ class: ('expanded' if expanded) } + .settings-header + %h4 + = s_('ProjectSettings|Badges') + %button.btn.js-settings-toggle{ type: 'button' } + = expanded ? 'Collapse' : 'Expand' + %p + = s_('ProjectSettings|Customize your project badges.') + = link_to s_('ProjectSettings|Learn more about badges.'), help_page_path('user/project/badges') + .settings-content + = render 'shared/badges/badge_settings' + = render 'export', project: @project %section.qa-advanced-settings.settings.advanced-settings.no-animate#js-project-advanced-settings{ class: ('expanded' if expanded) } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index d47dc3d8143..75f35360e5e 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -9,7 +9,7 @@ .project-empty-note-panel %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] } .prepend-top-20 - %h4 + %h4.append-bottom-20 = _('The repository for this project is empty') - if @project.can_current_user_push_code? @@ -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/empty.html.haml b/app/views/projects/environments/empty.html.haml index 1413930ebdb..129dbbf4e56 100644 --- a/app/views/projects/environments/empty.html.haml +++ b/app/views/projects/environments/empty.html.haml @@ -1,14 +1,14 @@ - page_title _("Metrics") -.row +.row.empty-state .col-sm-12 .svg-content = image_tag 'illustrations/operations_metrics_empty.svg' -.row.empty-environments - .col-sm-12.text-center - %h4 - = s_('Metrics|No deployed environments') - .state-description - = s_('Metrics|Check out the CI/CD documentation on deploying to an environment') - .prepend-top-10 - = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success' + .col-12 + .text-content + %h4.text-center + = s_('Metrics|No deployed environments') + %p.state-description + = s_('Metrics|Check out the CI/CD documentation on deploying to an environment') + .text-center + = link_to s_("Metrics|Learn about environments"), help_page_path('ci/environments'), class: 'btn btn-success' diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index c7890b37381..8c5b6e089ea 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -49,15 +49,16 @@ .environments-container - if @deployments.blank? - .blank-state-row - .blank-state-center - %h2.blank-state-title + .empty-state + .text-content + %h4.state-title You don't have any deployments right now. %p.blank-state-text Define environments in the deploy stage(s) in %code .gitlab-ci.yml to track deployments here. - = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success" + .text-center + = link_to _("Read more"), help_page_path("ci/environments"), class: "btn btn-success" - else .table-holder .ci-table.environments{ role: 'grid' } 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/_fork_button.html.haml b/app/views/projects/forks/_fork_button.html.haml index 12cf40bb65f..a69146513d8 100644 --- a/app/views/projects/forks/_fork_button.html.haml +++ b/app/views/projects/forks/_fork_button.html.haml @@ -5,7 +5,7 @@ .bordered-box.fork-thumbnail.text-center.prepend-left-default.append-right-default.prepend-top-default.append-bottom-default.forked = link_to project_path(forked_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_icon(namespace, class: "avatar s100 identicon") + = group_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") @@ -18,7 +18,7 @@ class: ("disabled has-tooltip" unless can_create_project), title: (_('You have reached your project limit') unless can_create_project) do - if /no_((\w*)_)*avatar/.match(avatar) - = project_icon(namespace, class: "avatar s100 identicon") + = group_icon(namespace, class: "avatar s100 identicon") - else .avatar-container.s100 = image_tag(avatar, class: "avatar s100") 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/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml index 665968a64e1..28998acdc13 100644 --- a/app/views/projects/issues/_discussion.html.haml +++ b/app/views/projects/issues/_discussion.html.haml @@ -6,7 +6,7 @@ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %section.js-vue-notes-event - #js-vue-notes{ data: { notes_data: notes_data(@issue), + #js-vue-notes{ data: { notes_data: notes_data(@issue).to_json, noteable_data: serialize_issuable(@issue), noteable_type: 'Issue', target_type: 'issue', 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/new.html.haml b/app/views/projects/issues/new.html.haml index 60fe442014f..9a081a42b6f 100644 --- a/app/views/projects/issues/new.html.haml +++ b/app/views/projects/issues/new.html.haml @@ -1,4 +1,5 @@ -- breadcrumb_title "Issues" +- add_to_breadcrumbs "Issues", project_issues_path(@project) +- breadcrumb_title "New" - page_title "New Issue" %h3.page-title diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml index 2d036bd4e3e..c39fd0063be 100644 --- a/app/views/projects/issues/show.html.haml +++ b/app/views/projects/issues/show.html.haml @@ -6,6 +6,7 @@ - page_card_attributes @issue.card_attributes - can_update_issue = can?(current_user, :update_issue, @issue) +- can_reopen_issue = can?(current_user, :reopen_issue, @issue) - can_report_spam = @issue.submittable_as_spam_by?(current_user) - can_create_issue = show_new_issue_link?(@project) @@ -40,6 +41,7 @@ %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) - if can_update_issue %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close js-btn-issue-action #{issue_button_visibility(@issue, true)}", title: 'Close issue' + - if can_reopen_issue %li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen js-btn-issue-action #{issue_button_visibility(@issue, false)}", title: 'Reopen issue' - if can_report_spam %li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam' @@ -48,12 +50,12 @@ %li.divider %li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link' - = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue + = render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue, can_reopen: can_reopen_issue - 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/_empty_state.html.haml b/app/views/projects/jobs/_empty_state.html.haml deleted file mode 100644 index ea552c73c92..00000000000 --- a/app/views/projects/jobs/_empty_state.html.haml +++ /dev/null @@ -1,18 +0,0 @@ -- illustration = local_assigns.fetch(:illustration) -- illustration_size = local_assigns.fetch(:illustration_size) -- title = local_assigns.fetch(:title) -- content = local_assigns.fetch(:content, nil) -- action = local_assigns.fetch(:action, nil) - -.row.empty-state - .col-12 - .svg-content{ class: illustration_size } - = image_tag illustration - .col-12 - .text-content - %h4.text-center= title - - if content - %p= content - - if action - .text-center - = action diff --git a/app/views/projects/jobs/_empty_states.html.haml b/app/views/projects/jobs/_empty_states.html.haml deleted file mode 100644 index e5198d047df..00000000000 --- a/app/views/projects/jobs/_empty_states.html.haml +++ /dev/null @@ -1,9 +0,0 @@ -- detailed_status = @build.detailed_status(current_user) -- illustration = detailed_status.illustration - -= render 'empty_state', - illustration: illustration[:image], - illustration_size: illustration[:size], - title: illustration[:title], - content: illustration[:content], - action: detailed_status.has_action? ? link_to(detailed_status.action_button_title, detailed_status.action_path, method: detailed_status.action_method, class: 'btn btn-primary', title: detailed_status.action_button_title) : nil 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/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml deleted file mode 100644 index acc1e17b811..00000000000 --- a/app/views/projects/jobs/_sidebar.html.haml +++ /dev/null @@ -1,95 +0,0 @@ -%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar.js-right-sidebar{ data: { "offset-top" => "101", "spy" => "affix" } } - .sidebar-container - .blocks-container - #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } - - - if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?) - .block - .title - Job artifacts - - if @build.artifacts_expired? - %p.build-detail-row - The artifacts were removed - #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - elsif @build.has_expiring_artifacts? - %p.build-detail-row - The artifacts will be removed - #{time_ago_with_tooltip(@build.artifacts_expire_at)} - - - if @build.artifacts? - .btn-group.d-flex{ role: :group } - - if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build) - = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do - Keep - - = link_to download_project_job_artifacts_path(@project, @build), rel: 'nofollow', download: '', class: 'btn btn-sm btn-default' do - Download - - - if @build.browsable_artifacts? - = link_to browse_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default' do - Browse - - - if @build.trigger_request - .build-widget.block - %h4.title - Trigger - - - if @build.trigger_request&.trigger&.short_token - %p - %span.build-light-text Token: - #{@build.trigger_request.trigger.short_token} - - - if @build.trigger_variables.any? - %p - %button.btn.group.js-reveal-variables Reveal Variables - - %dl.js-build-variables.trigger-build-variables.hide - - @build.trigger_variables.each do |trigger_variable| - %dt.js-build-variable.trigger-build-variable= trigger_variable[:key] - %dd.js-build-value.trigger-build-value= trigger_variable[:value] - - %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } - %p - Commit - = link_to @build.pipeline.short_sha, project_commit_path(@project, @build.pipeline.sha), class: 'commit-sha link-commit' - = clipboard_button(text: @build.pipeline.short_sha, title: "Copy commit SHA to clipboard") - - if @build.merge_request - in - = link_to "#{@build.merge_request.to_reference}", merge_request_path(@build.merge_request), class: 'link-commit' - - %p.build-light-text.append-bottom-0 - #{@build.pipeline.git_commit_title} - - - if @build.pipeline.stages_count > 1 - .block-last.dropdown.build-dropdown - %div - %span{ class: "ci-status-icon-#{@build.pipeline.status}" } - = ci_icon_for_status(@build.pipeline.status) - Pipeline - = link_to "##{@build.pipeline.id}", project_pipeline_path(@project, @build.pipeline), class: 'link-commit' - from - = link_to "#{@build.pipeline.ref}", project_ref_path(@project, @build.pipeline.ref), class: 'link-commit ref-name' - %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' } - %span.stage-selection More - = icon('chevron-down') - %ul.dropdown-menu - - @build.pipeline.legacy_stages.each do |stage| - %li - %a.stage-item= stage.name - - .builds-container - - HasStatus::ORDERED_STATUSES.each do |build_status| - - builds.select{|build| build.status == build_status}.each do |build| - .build-job{ class: sidebar_build_class(build, @build), data: { stage: build.stage } } - - tooltip = sanitize(build.tooltip_message.dup) - = link_to(project_job_path(@project, build), data: { toggle: 'tooltip', title: tooltip, container: 'body' }) do - = sprite_icon('arrow-right', size:16, css_class: 'icon-arrow-right') - %span{ class: "ci-status-icon-#{build.status}" } - = ci_icon_for_status(build.status) - %span - - if build.name - = build.name - - else - = build.id - - if build.retried? - = sprite_icon('retry', size:16, css_class: 'icon-retry') 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..a5f814b722d 100644 --- a/app/views/projects/jobs/show.html.haml +++ b/app/views/projects/jobs/show.html.haml @@ -3,57 +3,12 @@ - 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 - - if @build.stuck? - - unless @build.any_runners_online? - .bs-callout.bs-callout-warning.js-build-stuck - %p - - if no_runners_for_project?(@build.project) - 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: - - @build.tags.each do |tag| - %span.badge.badge-primary - = tag - - else - This job is stuck, because you don't have any active runners that can run this job. - - %br - Go to - = link_to project_runners_path(@build.project, anchor: 'js-runners-settings') do - Runners page - - - if @build.starts_environment? - .prepend-top-default.js-environment-container - .environment-information - - if @build.outdated_deployment? - = ci_icon_for_status('success_with_warnings') - - else - = ci_icon_for_status(@build.status) - - - environment = environment_for_build(@build.project, @build) - - if @build.success? && @build.last_deployment.present? - - if @build.last_deployment.last? - This job is the most recent deployment to #{environment_link_for_build(@build.project, @build)}. - - else - This job is an out-of-date deployment to #{environment_link_for_build(@build.project, @build)}. - View the most recent deployment #{deployment_link(environment.last_deployment)}. - - elsif @build.complete? && !@build.success? - The deployment of this job to #{environment_link_for_build(@build.project, @build)} did not succeed. - - else - This job is creating a deployment to #{environment_link_for_build(@build.project, @build)} - - if environment.try(:last_deployment) - and will overwrite the #{deployment_link(environment.last_deployment, text: 'latest deployment')} - - - if @build.erased? - .prepend-top-default.js-build-erased - .erased.alert.alert-warning - - if @build.erased_by_user? - Job has been erased by #{link_to(@build.erased_by_name, user_path(@build.erased_by))} #{time_ago_with_tooltip(@build.erased_at)} - - else - Job has been erased #{time_ago_with_tooltip(@build.erased_at)} - if @build.running? || @build.has_trace? .build-trace-container.prepend-top-default @@ -87,10 +42,8 @@ = custom_icon('scroll_down') = render 'shared/builds/build_output' - - else - = render "empty_states" - = render "sidebar", builds: @builds + #js-details-block-vue{ data: { terminal_path: can?(current_user, :create_build_terminal, @build) && @build.has_terminal? ? terminal_project_job_path(@project, @build) : nil } } .js-build-options{ data: javascript_build_options } diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml index 768ce9bd103..11a05eada30 100644 --- a/app/views/projects/labels/index.html.haml +++ b/app/views/projects/labels/index.html.haml @@ -1,33 +1,26 @@ - @no_container = true - page_title "Labels" - can_admin_label = can?(current_user, :admin_label, @project) -- hide_class = '' - search = params[:search] +- subscribed = params[:subscribed] +- labels_or_filters = @labels.exists? || @prioritized_labels.exists? || search.present? || subscribed.present? - 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? +- if labels_or_filters #promote-label-modal %div{ class: container_class } - .top-area.adjust - .nav-text - = _('Labels can be applied to issues and merge requests.') - - .nav-controls - = form_tag project_labels_path(@project), method: :get do - .input-group - = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false } - %span.input-group-append - %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } - = icon("search") + = render 'shared/labels/nav' .labels-container.prepend-top-10 - if can_admin_label - if search.blank? %p.text-muted + = _('Labels can be applied to issues and merge requests.') + %br = _('Star a label to make it a priority label. Order the prioritized labels to change their relative priority, by dragging.') -# Only show it in the first page - hide = @available_labels.empty? || (params[:page].present? && params[:page] != '1') @@ -59,7 +52,9 @@ - else .nothing-here-block = _('No labels with such name or description') - + - elsif subscribed.present? + .nothing-here-block + = _('You do not have any subscriptions yet') - else = render 'shared/empty_states/labels' 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/_form.html.haml b/app/views/projects/merge_requests/_form.html.haml index 5a59f956cb5..13b967beba1 100644 --- a/app/views/projects/merge_requests/_form.html.haml +++ b/app/views/projects/merge_requests/_form.html.haml @@ -1,4 +1,4 @@ = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' }, data: { markdown_version: @merge_request.cached_markdown_version } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request + = render 'shared/issuable/form', f: f, issuable: @merge_request, presenter: @mr_presenter 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/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml index a58179091ae..1bf42ded97a 100644 --- a/app/views/projects/merge_requests/_mr_title.html.haml +++ b/app/views/projects/merge_requests/_mr_title.html.haml @@ -39,4 +39,4 @@ - if can_update_merge_request = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-sm-none d-md-block btn btn-grouped js-issuable-edit" - = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request + = render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request, can_reopen: can_update_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/creations/_new_submit.html.haml b/app/views/projects/merge_requests/creations/_new_submit.html.haml index d5c4134dee2..464f8fa65e9 100644 --- a/app/views/projects/merge_requests/creations/_new_submit.html.haml +++ b/app/views/projects/merge_requests/creations/_new_submit.html.haml @@ -11,7 +11,7 @@ = link_to 'Change branches', mr_change_branches_path(@merge_request) %hr = form_for [@project.namespace.becomes(Namespace), @project, @merge_request], html: { class: 'merge-request-form common-note-form js-requires-input js-quick-submit' } do |f| - = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits + = render 'shared/issuable/form', f: f, issuable: @merge_request, commits: @commits, presenter: @mr_presenter = f.hidden_field :source_project_id = f.hidden_field :source_branch = f.hidden_field :target_project_id diff --git a/app/views/projects/merge_requests/creations/new.html.haml b/app/views/projects/merge_requests/creations/new.html.haml index 3220512d60d..0f618826305 100644 --- a/app/views/projects/merge_requests/creations/new.html.haml +++ b/app/views/projects/merge_requests/creations/new.html.haml @@ -1,4 +1,5 @@ -- breadcrumb_title "Merge Requests" +- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project) +- breadcrumb_title "New" - page_title "New Merge Request" - if @merge_request.can_be_created && !params[:change_branches] diff --git a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml index dab95b97346..066c8d5dba6 100644 --- a/app/views/projects/merge_requests/diffs/_commit_widget.html.haml +++ b/app/views/projects/merge_requests/diffs/_commit_widget.html.haml @@ -1,3 +1,7 @@ +-#----------------------------------------------------------------- + WARNING: Please keep changes up-to-date with the following files: + - `assets/javascripts/diffs/components/commit_widget.vue` +-#----------------------------------------------------------------- - if @commit .info-well.d-none.d-sm-block.prepend-top-default .well-segment 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/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml index b23baa22d8b..ef2fa8668c0 100644 --- a/app/views/projects/merge_requests/show.html.haml +++ b/app/views/projects/merge_requests/show.html.haml @@ -60,7 +60,7 @@ %section.col-md-12 %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe .issuable-discussion.js-vue-notes-event - #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request), + #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json, noteable_data: serialize_issuable(@merge_request), noteable_type: 'MergeRequest', target_type: 'merge_request', 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/new.html.haml b/app/views/projects/new.html.haml index 6c363345e38..d99b809c387 100644 --- a/app/views/projects/new.html.haml +++ b/app/views/projects/new.html.haml @@ -3,7 +3,6 @@ - @hide_top_links = true - page_title 'New Project' - header_title "Projects", dashboard_projects_path -- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility - active_tab = local_assigns.fetch(:active_tab, 'blank') .project-edit-container @@ -30,15 +29,15 @@ .col-lg-9.js-toggle-container %ul.nav.nav-tabs.nav-links.gitlab-tabs{ role: 'tablist' } %li.nav-item{ role: 'presentation' } - %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link.active{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab', track_label: 'blank_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Blank project %span.d-block.d-sm-none Blank %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab', track_label: 'create_from_template', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Create from template %span.d-block.d-sm-none Template %li.nav-item{ role: 'presentation' } - %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' } + %a.nav-link{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab', track_label: 'import_project', track_event: "click_tab" }, role: 'tab' } %span.d-none.d-sm-block Import project %span.d-block.d-sm-none Import diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml index e9008d60098..eb6838cec8d 100644 --- a/app/views/projects/notes/_actions.html.haml +++ b/app/views/projects/notes/_actions.html.haml @@ -38,7 +38,6 @@ - if can?(current_user, :award_emoji, note) - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) .note-actions-item = button_tag title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji} has-tooltip btn btn-transparent", data: { position: 'right', container: 'body' } do = icon('spinner spin') 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/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml index c13e3194340..5b6823da1f6 100644 --- a/app/views/projects/pipelines/new.html.haml +++ b/app/views/projects/pipelines/new.html.haml @@ -1,5 +1,5 @@ - breadcrumb_title "Pipelines" -- page_title = s_("Pipeline|Run Pipeline") +- page_title s_("Pipeline|Run Pipeline") - settings_link = link_to _('CI/CD settings'), project_settings_ci_cd_path(@project) %h3.page-title 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/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml index e7636099be6..233c3adba0e 100644 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ b/app/views/projects/project_templates/_built_in_templates.html.haml @@ -10,8 +10,8 @@ = template.description .controls.d-flex.align-items-center %label.btn.btn-success.template-button.choose-template.append-right-10.append-bottom-0{ for: template.name } - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "create_from_template", track_property: "template_use", track_event: "click_button" } } %span = _("Use template") - %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank' } + %a.btn.btn-default{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "create_from_template", track_property: "template_preview", track_event: "click_button", track_value: template.name } } = _("Preview") 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/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml index 314af44490e..ec503cd8bef 100644 --- a/app/views/projects/runners/_specific_runners.html.haml +++ b/app/views/projects/runners/_specific_runners.html.haml @@ -1,8 +1,34 @@ %h3 = _('Specific Runners') -= render partial: 'ci/runner/how_to_setup_specific_runner', - locals: { registration_token: @project.runners_token } +.bs-callout.help-callout + .append-bottom-10 + %h4= _('Set up a specific Runner automatically') + + %p + - link_to_help_page = link_to(_('Learn more about Kubernetes'), + help_page_path('user/project/clusters/index'), + target: '_blank', + rel: 'noopener noreferrer') + + = _('You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page } + + %ol + %li + = _('Click the button below to begin the install process by navigating to the Kubernetes page') + %li + = _('Select an existing Kubernetes cluster or create a new one') + %li + = _('From the Kubernetes cluster details view, install Runner from the applications list') + + = link_to _('Install Runner on Kubernetes'), + project_clusters_path(@project), + class: 'btn btn-info' + %hr + = render partial: 'ci/runner/how_to_setup_runner', + locals: { registration_token: @project.runners_token, + type: 'specific', + reset_token_url: reset_registration_token_namespace_project_settings_ci_cd_path } - if @project_runners.any? %h4.underlined-title Runners activated for this project 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/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml index ab9ba5c7569..5ec5a06396e 100644 --- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml +++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml @@ -5,7 +5,6 @@ %fieldset.builds-feature.js-auto-devops-settings .form-group - message = auto_devops_warning_message(@project) - - ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe - if message %p.auto-devops-warning-message.settings-message.text-center = message.html_safe @@ -40,10 +39,17 @@ = form.label :deploy_strategy_continuous, class: 'form-check-label' do = s_('CICD|Continuous deployment to production') = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'auto-deploy'), target: '_blank' + + .form-check + = form.radio_button :deploy_strategy, 'timed_incremental', class: 'form-check-input' + = form.label :deploy_strategy_timed_incremental, class: 'form-check-label' do + = s_('CICD|Continuous deployment to production using timed incremental rollout') + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank' + .form-check = form.radio_button :deploy_strategy, 'manual', class: 'form-check-input' = form.label :deploy_strategy_manual, class: 'form-check-label' do = s_('CICD|Automatic deployment to staging, manual deployment to production') - = link_to icon('question-circle'), help_page_path('ci/environments.md', anchor: 'manually-deploying-to-environments'), target: '_blank' + = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md', anchor: 'incremental-rollout-to-production'), target: '_blank' = f.submit _('Save changes'), class: "btn btn-success prepend-top-15" diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml index 9134257b631..41afaa9ffc0 100644 --- a/app/views/projects/settings/ci_cd/_form.html.haml +++ b/app/views/projects/settings/ci_cd/_form.html.haml @@ -3,16 +3,6 @@ = form_for @project, url: project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings') do |f| = form_errors(@project) %fieldset.builds-feature - .form-group.append-bottom-default.js-secret-runner-token - = f.label :runners_token, _("Runner token"), class: 'label-bold' - .form-control.js-secret-value-placeholder - = '*' * 20 - = f.text_field :runners_token, class: "form-control hide js-secret-value", placeholder: 'xEeFCaDAB89' - %p.form-text.text-muted= _("The secure token used by the Runner to checkout the project") - %button.btn.btn-info.prepend-top-10.js-secret-value-reveal-button{ type: 'button', data: { secret_reveal_status: 'false' } } - = _('Reveal value') - - %hr .form-group %h5.prepend-top-0 = _("Git strategy for pipelines") @@ -121,7 +111,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/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index 16961784e00..98e2829ba43 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -12,7 +12,7 @@ %button.btn.js-settings-toggle{ type: 'button' } = expanded ? _('Collapse') : _('Expand') %p - = _("Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.") + = _("Customize your pipeline configuration, view your pipeline status and coverage report.") .settings-content = render 'form' diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml index 98c609d7bd4..a0bcaaf3c54 100644 --- a/app/views/projects/settings/repository/show.html.haml +++ b/app/views/projects/settings/repository/show.html.haml @@ -2,6 +2,7 @@ - page_title _("Repository") - @content_class = "limit-container-width" unless fluid_layout += render "projects/default_branch/show" = render "projects/mirrors/show" -# Protected branches & tags use a lot of nested partials. 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/_tag.atom.builder b/app/views/projects/tags/_tag.atom.builder new file mode 100644 index 00000000000..60d4b21b9d1 --- /dev/null +++ b/app/views/projects/tags/_tag.atom.builder @@ -0,0 +1,19 @@ +commit = @repository.commit(tag.dereferenced_target) +release = @releases.find { |r| r.tag == tag.name } +tag_url = project_tag_url(@project, tag.name) + +if commit + xml.entry do + xml.id tag_url + xml.link href: tag_url + xml.title truncate(tag.name, length: 80) + xml.summary strip_gpg_signature(tag.message) + xml.content markdown_field(release, :description), type: 'html' + xml.updated release.updated_at.xmlschema if release + xml.media :thumbnail, width: '40', height: '40', url: image_url(avatar_icon_for_email(commit.author_email)) + xml.author do |author| + xml.name commit.author_name + xml.email commit.author_email + end + end +end diff --git a/app/views/projects/tags/index.atom.builder b/app/views/projects/tags/index.atom.builder new file mode 100644 index 00000000000..b9b58b7beaa --- /dev/null +++ b/app/views/projects/tags/index.atom.builder @@ -0,0 +1,7 @@ +xml.title "#{@project.name} tags" +xml.link href: project_tags_url(@project, @ref, rss_url_options), rel: 'self', type: 'application/atom+xml' +xml.link href: project_tags_url(@project, @ref), rel: 'alternate', type: 'text/html' +xml.id project_tags_url(@project, @ref) +xml.updated @releases.first.updated_at.xmlschema if @releases.any? + +xml << render(partial: 'tag', collection: @tags) if @tags.any? diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml index dab95ba09f2..37535370940 100644 --- a/app/views/projects/tags/index.html.haml +++ b/app/views/projects/tags/index.html.haml @@ -1,6 +1,8 @@ - @no_container = true - @sort ||= sort_value_recently_updated - page_title s_('TagsPage|Tags') += content_for :meta_tags do + = auto_discovery_link_tag(:atom, project_tags_url(@project, rss_url_options), title: "#{@project.name} tags") .flex-list{ class: container_class } .top-area.adjust @@ -23,8 +25,10 @@ %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") = render_if_exists 'projects/commits/mirror_status' 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 d80d2957466..80aa1500d53 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -22,22 +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 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/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml index 7afb7b3a93b..6612497e7e2 100644 --- a/app/views/shared/_event_filter.html.haml +++ b/app/views/shared/_event_filter.html.haml @@ -2,13 +2,13 @@ .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs - = event_filter_link EventFilter.all, _('All'), s_('EventFilterBy|Filter by all') + = event_filter_link EventFilter::ALL, _('All'), s_('EventFilterBy|Filter by all') - if event_filter_visible(:repository) - = event_filter_link EventFilter.push, _('Push events'), s_('EventFilterBy|Filter by push events') + = event_filter_link EventFilter::PUSH, _('Push events'), s_('EventFilterBy|Filter by push events') - if event_filter_visible(:merge_requests) - = event_filter_link EventFilter.merged, _('Merge events'), s_('EventFilterBy|Filter by merge events') + = event_filter_link EventFilter::MERGED, _('Merge events'), s_('EventFilterBy|Filter by merge events') - if event_filter_visible(:issues) - = event_filter_link EventFilter.issue, _('Issue events'), s_('EventFilterBy|Filter by issue events') + = event_filter_link EventFilter::ISSUE, _('Issue events'), s_('EventFilterBy|Filter by issue events') - if comments_visible? - = event_filter_link EventFilter.comments, _('Comments'), s_('EventFilterBy|Filter by comments') - = event_filter_link EventFilter.team, _('Team'), s_('EventFilterBy|Filter by team') + = event_filter_link EventFilter::COMMENTS, _('Comments'), s_('EventFilterBy|Filter by comments') + = event_filter_link EventFilter::TEAM, _('Team'), s_('EventFilterBy|Filter by team') diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml index 2c3cbd0b986..71f34c0d85b 100644 --- a/app/views/shared/_label.html.haml +++ b/app/views/shared/_label.html.haml @@ -4,8 +4,6 @@ - use_label_priority = local_assigns.fetch(:use_label_priority, false) - force_priority = local_assigns.fetch(:force_priority, use_label_priority ? label.priority.present? : false) - toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user -- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project) -- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project) - tooltip_title = label_status_tooltip(label, status) if status %li.label-list-item{ id: label_css_id, data: { id: label.id } } diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml index ac2164a4a71..28b34e38b15 100644 --- a/app/views/shared/_mini_pipeline_graph.html.haml +++ b/app/views/shared/_mini_pipeline_graph.html.haml @@ -3,7 +3,6 @@ - if stage.status - detailed_status = stage.detailed_status(current_user) - icon_status = "#{detailed_status.icon}_borderless" - - status_klass = "ci-status-icon ci-status-icon-#{detailed_status.group}" .stage-container.dropdown{ class: klass } %button.mini-pipeline-graph-dropdown-toggle.has-tooltip.js-builds-dropdown-button{ class: "ci-status-icon-#{detailed_status.group}", type: 'button', data: { toggle: 'dropdown', title: "#{stage.name}: #{detailed_status.label}", placement: 'top', "stage-endpoint" => stage_ajax_project_pipeline_path(pipeline.project, pipeline, stage: stage.name) } } 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/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 4e7061eef1c..7cbc5810c10 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -1,5 +1,3 @@ -- show_create = local_assigns.fetch(:show_create, false) - - dropdown_toggle_text = @ref || @project.default_branch = form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do = hidden_field_tag :destination, destination 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/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml index dd6b9cce58e..9fc46afe177 100644 --- a/app/views/shared/_visibility_radios.html.haml +++ b/app/views/shared/_visibility_radios.html.haml @@ -3,7 +3,7 @@ - restricted = restricted_visibility_levels.include?(level) - disabled = disallowed || restricted .form-check{ class: [('disabled' if disabled), ('restricted' if restricted)] } - = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input' + = form.radio_button model_method, level, checked: (selected_level == level), disabled: disabled, class: 'form-check-input', data: { track_label: "blank_project", track_event: "activate_form_input", track_property: "#{model_method}", track_value: "#{level}" } = form.label "#{model_method}_#{level}", class: 'form-check-label' do = visibility_level_icon(level) .option-title diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml index 28e6fe1b16d..0d2f6bb77d6 100644 --- a/app/views/shared/boards/_show.html.haml +++ b/app/views/shared/boards/_show.html.haml @@ -33,7 +33,7 @@ - if @project %board-add-issues-modal{ "new-issue-path" => new_project_issue_path(@project), "milestone-path" => milestones_filter_dropdown_path, - "label-path" => labels_filter_path, + "label-path" => labels_filter_path_with_defaults, "empty-state-svg" => image_path('illustrations/issues.svg'), ":issue-link-base" => "issueLinkBase", ":root-path" => "rootPath", diff --git a/app/views/shared/boards/components/sidebar/_labels.html.haml b/app/views/shared/boards/components/sidebar/_labels.html.haml index 532045f3697..6138914206b 100644 --- a/app/views/shared/boards/components/sidebar/_labels.html.haml +++ b/app/views/shared/boards/components/sidebar/_labels.html.haml @@ -25,7 +25,7 @@ show_no: "true", show_any: "true", project_id: @project&.try(:id), - labels: labels_filter_path(false), + labels: labels_filter_path_with_defaults, namespace_path: @namespace_path, project_path: @project.try(:path) } } %span.dropdown-toggle-text 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 13bb4baee3f..f6b3a49eacb 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 - = custom_icon("icon_empty_groups") +.group-empty-state.row.align-items-center.justify-content-center + .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..49b812baefc 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 qa-groups-filter', spellcheck: false, id: 'group-filter-form-field', tabindex: "2" diff --git a/app/views/shared/icons/_icon_status_scheduled.svg b/app/views/shared/icons/_icon_status_scheduled.svg new file mode 100644 index 00000000000..ca6e4efce50 --- /dev/null +++ b/app/views/shared/icons/_icon_status_scheduled.svg @@ -0,0 +1 @@ +<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><circle cx="7" cy="7" r="7"/><circle fill="#FFF" cx="7" cy="7" r="6"/><g transform="translate(2.75 2.75)" fill-rule="nonzero"><path d="M4.165 7.81a3.644 3.644 0 1 1 0-7.29 3.644 3.644 0 0 1 0 7.29zm0-1.042a2.603 2.603 0 1 0 0-5.206 2.603 2.603 0 0 0 0 5.206z"/><rect x="3.644" y="2.083" width="1.041" height="2.603" rx=".488"/><rect x="3.644" y="3.644" width="2.083" height="1.041" rx=".488"/></g></svg>
\ No newline at end of file diff --git a/app/views/shared/icons/_icon_status_scheduled_borderless.svg b/app/views/shared/icons/_icon_status_scheduled_borderless.svg new file mode 100644 index 00000000000..dc38c01d898 --- /dev/null +++ b/app/views/shared/icons/_icon_status_scheduled_borderless.svg @@ -0,0 +1 @@ +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M6.16 11.55a5.39 5.39 0 1 1 0-10.78 5.39 5.39 0 0 1 0 10.78zm0-1.54a3.85 3.85 0 1 0 0-7.7 3.85 3.85 0 0 0 0 7.7z"/><rect x="5.39" y="3.08" width="1.54" height="3.85" rx=".767"/><rect x="5.39" y="5.39" width="3.08" height="1.54" rx=".767"/></svg>
\ No newline at end of file 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/_close_reopen_button.html.haml b/app/views/shared/issuable/_close_reopen_button.html.haml index 933d4b2ea65..4f6a71b6071 100644 --- a/app/views/shared/issuable/_close_reopen_button.html.haml +++ b/app/views/shared/issuable/_close_reopen_button.html.haml @@ -1,14 +1,18 @@ - is_current_user = issuable_author_is_current_user(issuable) - display_issuable_type = issuable_display_type(issuable) - button_method = issuable_close_reopen_button_method(issuable) +- are_close_and_open_buttons_hidden = issuable_button_hidden?(issuable, true) && issuable_button_hidden?(issuable, false) -- if can_update && is_current_user - = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" - = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, - class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" -- elsif can_update && !is_current_user - = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable +- if is_current_user + - if can_update + = link_to "Close #{display_issuable_type}", close_issuable_path(issuable), method: button_method, + class: "d-none d-sm-none d-md-block btn btn-grouped btn-close js-btn-issue-action #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}" + - if can_reopen + = link_to "Reopen #{display_issuable_type}", reopen_issuable_path(issuable), method: button_method, + class: "d-none d-sm-none d-md-block btn btn-grouped btn-reopen js-btn-issue-action #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" - else - = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), - class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' + - if can_update && !are_close_and_open_buttons_hidden + = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable + - else + = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), + class: 'd-none d-sm-none d-md-block btn btn-grouped btn-close-color', title: 'Report abuse' diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml index 1cd8ce0826c..c7037335866 100644 --- a/app/views/shared/issuable/_filter.html.haml +++ b/app/views/shared/issuable/_filter.html.haml @@ -1,5 +1,3 @@ -- boards_page = controller.controller_name == 'boards' - .issues-filters .issues-details-filters.row-content-block.second-block = form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search]), method: :get, class: 'filter-form js-filter-form' do diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml index b49e47a7266..b33c758b464 100644 --- a/app/views/shared/issuable/_form.html.haml +++ b/app/views/shared/issuable/_form.html.haml @@ -1,6 +1,7 @@ - form = local_assigns.fetch(:f) - commits = local_assigns[:commits] - project = @target_project || @project +- presenter = local_assigns.fetch(:presenter, nil) = form_errors(issuable) @@ -29,7 +30,7 @@ = render 'shared/issuable/form/metadata', issuable: issuable, form: form -= render_if_exists 'shared/issuable/approvals', issuable: issuable, form: form += render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form = render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form @@ -69,9 +70,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/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml index 34911fd2712..6eb1f8f0853 100644 --- a/app/views/shared/issuable/_label_dropdown.html.haml +++ b/app/views/shared/issuable/_label_dropdown.html.haml @@ -7,9 +7,8 @@ - data_options = local_assigns.fetch(:data_options, {}) - classes = local_assigns.fetch(:classes, []) - selected = local_assigns.fetch(:selected, nil) -- selected_toggle = local_assigns.fetch(:selected_toggle, nil) - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by label") -- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"} +- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), labels: labels_filter_path_with_defaults, default_label: "Labels"} - dropdown_data.merge!(data_options) - label_name = local_assigns.fetch(:label_name, "Labels") - no_default_styles = local_assigns.fetch(:no_default_styles, false) diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml index 9ce7f6fe269..c4d177361e7 100644 --- a/app/views/shared/issuable/_search_bar.html.haml +++ b/app/views/shared/issuable/_search_bar.html.haml @@ -33,16 +33,17 @@ #js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { action: 'submit' } } - %button.btn.btn-link - = icon('search') + %button.btn.btn-link{ type: 'button' } + = sprite_icon('search') %span Press Enter or click to search %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } -# 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 @@ -59,7 +60,7 @@ #js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Assignee %li.divider.droplab-item-ignore - if current_user @@ -72,38 +73,46 @@ #js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Milestone %li.filter-dropdown-item{ data: { value: 'upcoming' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } Upcoming %li.filter-dropdown-item{ 'data-value' => 'started' } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } Started %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link.js-data-value + %button.btn.btn-link.js-data-value{ type: 'button' } {{title}} #js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu %ul{ data: { dropdown: true } } %li.filter-dropdown-item{ data: { value: 'none' } } - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } No Label %li.divider.droplab-item-ignore %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } %span.dropdown-label-box{ style: 'background: {{color}}' } %span.label-title.js-data-value {{title}} #js-dropdown-my-reaction.filtered-search-input-dropdown-menu.dropdown-menu %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } } %li.filter-dropdown-item - %button.btn.btn-link + %button.btn.btn-link{ type: 'button' } %gl-emoji %span.js-data-value.prepend-left-10 {{name}} + #js-dropdown-wip.filtered-search-input-dropdown-menu.dropdown-menu + %ul.filter-dropdown{ data: { dropdown: true } } + %li.filter-dropdown-item{ data: { value: 'yes', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('Yes') + %li.filter-dropdown-item{ data: { value: 'no', capitalize: true } } + %button.btn.btn-link{ type: 'button' } + = _('No') = render_if_exists 'shared/issuable/filter_weight', type: type diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 0ca35ea1298..aa136af1955 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -109,7 +109,7 @@ - selected_labels.each do |label| = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil .dropdown - %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project), display: 'static' } } + %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path_with_defaults if @project), display: 'static' } } %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) } = multi_label_name(selected_labels, "Labels") = icon('chevron-down', 'aria-hidden': 'true') @@ -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 3b017c62a80..ac8d58c0bfe 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -3,7 +3,6 @@ - return unless can?(current_user, :"admin_#{issuable.to_ability_name}", issuable.project) - has_due_date = issuable.has_attribute?(:due_date) -- has_labels = @labels && @labels.any? - form = local_assigns.fetch(:form) %hr @@ -35,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/_nav.html.haml b/app/views/shared/labels/_nav.html.haml new file mode 100644 index 00000000000..98572db738b --- /dev/null +++ b/app/views/shared/labels/_nav.html.haml @@ -0,0 +1,20 @@ +- subscribed = params[:subscribed] + +.top-area.adjust + %ul.nav-links.nav.nav-tabs + %li{ class: active_when(subscribed != 'true') }> + = link_to labels_filter_path do + = _('All') + - if current_user + %li{ class: active_when(subscribed == 'true') }> + = link_to labels_filter_path(subscribed: 'true') do + = _('Subscribed') + .nav-controls + = form_tag labels_filter_path, method: :get do + = hidden_field_tag :subscribed, params[:subscribed] + .input-group + = search_field_tag :search, params[:search], { placeholder: _('Filter'), id: 'label-search', class: 'form-control search-text-input input-short', spellcheck: false } + %span.input-group-append + %button.btn.btn-default{ type: "submit", "aria-label" => _('Submit search') } + = icon("search") + = render 'shared/labels/sort_dropdown' 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..8a7d037e15b --- /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, subscribed: params[:subscribed]), 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/members/_member.html.haml b/app/views/shared/members/_member.html.haml index af29c0fe59e..2682d92fc56 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -22,7 +22,7 @@ %strong Blocked - if user.two_factor_enabled? - %label.label.label-info + %label.badge.badge-info 2FA - if source.instance_of?(Group) && source != @group 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/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml index be053d481e4..aba790e1217 100644 --- a/app/views/shared/projects/_project.html.haml +++ b/app/views/shared/projects/_project.html.haml @@ -1,9 +1,7 @@ - avatar = true unless local_assigns[:avatar] == false - stars = true unless local_assigns[:stars] == false - forks = false unless local_assigns[:forks] == true -- ci = false unless local_assigns[:ci] == true - skip_namespace = false unless local_assigns[:skip_namespace] == true -- user = local_assigns[:user] - access = max_project_member_access(project) - css_class = '' unless local_assigns[:css_class] - show_last_commit_as_description = false unless local_assigns[:show_last_commit_as_description] == true && can_show_last_commit_in_list?(project) 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/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml index 07ebb8680d2..9c5b9593bba 100644 --- a/app/views/shared/web_hooks/_form.html.haml +++ b/app/views/shared/web_hooks/_form.html.haml @@ -17,6 +17,7 @@ %strong Push events %p.light.ml-1 This URL will be triggered by a push to the repository + = form.text_field :push_events_branch_filter, class: 'form-control', placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)' %li = form.check_box :tag_push_events, class: 'form-check-input' = form.label :tag_push_events, class: 'list-label form-check-label ml-1' do 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/snippets/new.html.haml b/app/views/snippets/new.html.haml index f01915107e3..c8a5e199674 100644 --- a/app/views/snippets/new.html.haml +++ b/app/views/snippets/new.html.haml @@ -1,5 +1,6 @@ - @hide_top_links = true -- breadcrumb_title "Snippets" +- add_to_breadcrumbs "Snippets", dashboard_snippets_path +- breadcrumb_title "New" - page_title "New Snippet" %h3.page-title New Snippet diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml index e1f7ee80ebb..220ba2b49e6 100644 --- a/app/views/snippets/notes/_actions.html.haml +++ b/app/views/snippets/notes/_actions.html.haml @@ -1,6 +1,5 @@ - if current_user - if note.emoji_awardable? - - user_authored = note.user_authored?(current_user) .note-actions-item = link_to '#', title: 'Add reaction', class: "note-action-button note-emoji-button js-add-award js-note-emoji has-tooltip", data: { position: 'right' } do = icon('spinner spin') 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/_overview.html.haml b/app/views/users/_overview.html.haml new file mode 100644 index 00000000000..f8b3754840d --- /dev/null +++ b/app/views/users/_overview.html.haml @@ -0,0 +1,32 @@ +.row + .col-md-12.col-lg-6 + .calendar-block + .content-block.hide-bottom-border + %h4 + = s_('UserProfile|Activity') + .user-calendar.d-none.d-sm-block.text-left{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } + %h4.center.light + %i.fa.fa-spinner.fa-spin + .user-calendar-activities.d-none.d-sm-block + + - if can?(current_user, :read_cross_project) + .activities-block + .content-block + %h5.prepend-top-10 + = s_('UserProfile|Recent contributions') + .overview-content-list{ data: { href: user_path } } + .center.light.loading + %i.fa.fa-spinner.fa-spin + .prepend-top-10 + = link_to s_('UserProfile|View all'), user_activity_path, class: "hide js-view-all" + + .col-md-12.col-lg-6 + .projects-block + .content-block + %h4 + = s_('UserProfile|Personal projects') + .overview-content-list{ data: { href: user_projects_path } } + .center.light.loading + %i.fa.fa-spinner.fa-spin + .prepend-top-10 + = link_to s_('UserProfile|View all'), user_projects_path, class: "hide js-view-all" diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml index 6b1d75c6e72..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_path(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/views/users/show.html.haml b/app/views/users/show.html.haml index 7a38d290915..d6c8420b744 100644 --- a/app/views/users/show.html.haml +++ b/app/views/users/show.html.haml @@ -12,22 +12,22 @@ .cover-block.user-cover-block.top-area .cover-controls - if @user == current_user - = link_to profile_path, class: 'btn btn-default has-tooltip', title: 'Edit profile', 'aria-label': 'Edit profile' do + = link_to profile_path, class: 'btn btn-default has-tooltip', title: s_('UserProfile|Edit profile'), 'aria-label': 'Edit profile' do = icon('pencil') - elsif current_user - if @user.abuse_report - %button.btn.btn-danger{ title: 'Already reported for abuse', + %button.btn.btn-danger{ title: s_('UserProfile|Already reported for abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } } = icon('exclamation-circle') - else = link_to new_abuse_report_path(user_id: @user.id, ref_url: request.referrer), class: 'btn', - title: 'Report abuse', data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do + title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do = icon('exclamation-circle') - if can?(current_user, :read_user_profile, @user) - = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: 'Subscribe', 'aria-label': 'Subscribe' do + = link_to user_path(@user, rss_url_options), class: 'btn btn-default has-tooltip', title: s_('UserProfile|Subscribe'), 'aria-label': 'Subscribe' do = icon('rss') - if current_user && current_user.admin? - = link_to [:admin, @user], class: 'btn btn-default', title: 'View user in admin area', + = link_to [:admin, @user], class: 'btn btn-default', title: s_('UserProfile|View user in admin area'), data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do = icon('users') @@ -51,7 +51,7 @@ @#{@user.username} - if can?(current_user, :read_user_profile, @user) %span.middle-dot-divider - Member since #{@user.created_at.to_date.to_s(:long)} + = s_('Member since %{date}') % { date: @user.created_at.to_date.to_s(:long) } .cover-desc - unless @user.public_email.blank? @@ -91,32 +91,40 @@ .fade-left= icon('angle-left') .fade-right= icon('angle-right') %ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs + - if profile_tab?(:overview) + %li.js-overview-tab + = link_to user_path, data: { target: 'div#js-overview', action: 'overview', toggle: 'tab' } do + = s_('UserProfile|Overview') - if profile_tab?(:activity) %li.js-activity-tab - = link_to user_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do - Activity + = link_to user_activity_path, data: { target: 'div#activity', action: 'activity', toggle: 'tab' } do + = s_('UserProfile|Activity') - if profile_tab?(:groups) %li.js-groups-tab = link_to user_groups_path, data: { target: 'div#groups', action: 'groups', toggle: 'tab', endpoint: user_groups_path(format: :json) } do - Groups + = s_('UserProfile|Groups') - if profile_tab?(:contributed) %li.js-contributed-tab = link_to user_contributed_projects_path, data: { target: 'div#contributed', action: 'contributed', toggle: 'tab', endpoint: user_contributed_projects_path(format: :json) } do - Contributed projects + = s_('UserProfile|Contributed projects') - if profile_tab?(:projects) %li.js-projects-tab = link_to user_projects_path, data: { target: 'div#projects', action: 'projects', toggle: 'tab', endpoint: user_projects_path(format: :json) } do - Personal projects + = s_('UserProfile|Personal projects') - if profile_tab?(:snippets) %li.js-snippets-tab = link_to user_snippets_path, data: { target: 'div#snippets', action: 'snippets', toggle: 'tab', endpoint: user_snippets_path(format: :json) } do - Snippets + = s_('UserProfile|Snippets') %div{ class: container_class } .tab-content + - if profile_tab?(:overview) + #js-overview.tab-pane + = render "users/overview" + - if profile_tab?(:activity) #activity.tab-pane - .row-content-block.calender-block.white.second-block.d-none.d-sm-block + .row-content-block.calendar-block.white.second-block.d-none.d-sm-block .user-calendar{ data: { calendar_path: user_calendar_path(@user, :json), calendar_activities_path: user_calendar_activities_path, utc_offset: Time.zone.utc_offset } } %h4.center.light %i.fa.fa-spinner.fa-spin @@ -124,7 +132,7 @@ - if can?(current_user, :read_cross_project) %h4.prepend-top-20 - Most Recent Activity + = s_('UserProfile|Most Recent Activity') .content_list{ data: { href: user_path } } = spinner @@ -155,4 +163,4 @@ .col-12.text-center .text-content %h4 - This user has a private profile + = s_('UserProfile|This user has a private profile') 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..f21789de37d 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 @@ -68,6 +70,7 @@ - pipeline_processing:pipeline_update - pipeline_processing:stage_update - pipeline_processing:update_head_pipeline_for_merge_request +- pipeline_processing:ci_build_schedule - repository_check:repository_check_clear - repository_check:repository_check_batch @@ -85,6 +88,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_schedule_worker.rb b/app/workers/ci/build_schedule_worker.rb new file mode 100644 index 00000000000..da219adffc6 --- /dev/null +++ b/app/workers/ci/build_schedule_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Ci + class BuildScheduleWorker + include ApplicationWorker + include PipelineQueue + + queue_namespace :pipeline_processing + + def perform(build_id) + ::Ci::Build.find_by_id(build_id).try do |build| + break unless build.scheduled? + + Ci::RunScheduledBuildService + .new(build.project, build.user).execute(build) + end + end + end +end 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..94426dcf921 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 @@ -12,6 +13,7 @@ class ExpireBuildInstanceArtifactsWorker return unless build&.project && !build.project.pending_delete Rails.logger.info "Removing artifacts for build #{build.id}..." - build.erase_artifacts! + build.erase_erasable_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..25809f68080 100644 --- a/app/workers/stuck_ci_jobs_worker.rb +++ b/app/workers/stuck_ci_jobs_worker.rb @@ -8,6 +8,7 @@ class StuckCiJobsWorker BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour BUILD_PENDING_OUTDATED_TIMEOUT = 1.day + BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour BUILD_PENDING_STUCK_TIMEOUT = 1.hour def perform @@ -15,9 +16,10 @@ class StuckCiJobsWorker Rails.logger.info "#{self.class}: Cleaning stuck builds" - drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT - drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT - drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT + drop :running, BUILD_RUNNING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure + drop :pending, BUILD_PENDING_OUTDATED_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure + drop :scheduled, BUILD_SCHEDULED_OUTDATED_TIMEOUT, 'scheduled_at IS NOT NULL AND scheduled_at < ?', :stale_schedule + drop_stuck :pending, BUILD_PENDING_STUCK_TIMEOUT, 'ci_builds.updated_at < ?', :stuck_or_timeout_failure remove_lease end @@ -32,24 +34,25 @@ class StuckCiJobsWorker Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid) end - def drop(status, timeout) - search(status, timeout) do |build| - drop_build :outdated, build, status, timeout + def drop(status, timeout, condition, reason) + search(status, timeout, condition) do |build| + drop_build :outdated, build, status, timeout, reason end end - def drop_stuck(status, timeout) - search(status, timeout) do |build| + def drop_stuck(status, timeout, condition, reason) + search(status, timeout, condition) do |build| break unless build.stuck? - drop_build :stuck, build, status, timeout + drop_build :stuck, build, status, timeout, reason end end - def search(status, timeout) + # rubocop: disable CodeReuse/ActiveRecord + def search(status, timeout, condition) loop do jobs = Ci::Build.where(status: status) - .where('ci_builds.updated_at < ?', timeout.ago) + .where(condition, timeout.ago) .includes(:tags, :runner, project: :namespace) .limit(100) .to_a @@ -60,11 +63,12 @@ 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})" + def drop_build(type, build, status, timeout, reason) + Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout}, reason: #{reason})" Gitlab::OptimisticLocking.retry_lock(build, 3) do |b| - b.drop(:stuck_or_timeout_failure) + b.drop(reason) end end end 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 |