diff options
Diffstat (limited to 'app')
92 files changed, 1196 insertions, 1083 deletions
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js index 1638e09132b..b0c85c2572e 100644 --- a/app/assets/javascripts/confirm_danger_modal.js +++ b/app/assets/javascripts/confirm_danger_modal.js @@ -2,13 +2,16 @@ import $ from 'jquery'; import { rstrip } from './lib/utils/common_utils'; function openConfirmDangerModal($form, text) { + const $input = $('.js-confirm-danger-input'); + $input.val(''); + $('.js-confirm-text').text(text || ''); - $('.js-confirm-danger-input').val(''); $('#modal-confirm-danger').modal('show'); const confirmTextMatch = $('.js-confirm-danger-match').text(); const $submit = $('.js-confirm-danger-submit'); $submit.disable(); + $input.focus(); $('.js-confirm-danger-input').off('input').on('input', function handleInput() { const confirmText = rstrip($(this).val()); diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue index 060386c3ecb..a61e368249a 100644 --- a/app/assets/javascripts/diffs/components/diff_file.vue +++ b/app/assets/javascripts/diffs/components/diff_file.vue @@ -31,9 +31,6 @@ export default { }; }, computed: { - isDiscussionsExpanded() { - return true; // TODO: @fatihacet - Fix this. - }, isCollapsed() { return this.file.collapsed || false; }, @@ -131,7 +128,6 @@ export default { :diff-file="file" :collapsible="true" :expanded="!isCollapsed" - :discussions-expanded="isDiscussionsExpanded" :add-merge-request-buttons="true" class="js-file-title file-title" @toggleFile="handleToggle" diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 1957698c6c1..c5abd0a9568 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -1,5 +1,6 @@ <script> import _ from 'underscore'; +import { mapActions, mapGetters } from 'vuex'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import Icon from '~/vue_shared/components/icon.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; @@ -38,11 +39,6 @@ export default { required: false, default: true, }, - discussionsExpanded: { - type: Boolean, - required: false, - default: true, - }, currentUser: { type: Object, required: true, @@ -54,6 +50,10 @@ export default { }; }, computed: { + ...mapGetters('diffs', ['diffHasExpandedDiscussions']), + hasExpandedDiscussions() { + return this.diffHasExpandedDiscussions(this.diffFile); + }, icon() { if (this.diffFile.submodule) { return 'archive'; @@ -88,9 +88,6 @@ export default { collapseIcon() { return this.expanded ? 'chevron-down' : 'chevron-right'; }, - isDiscussionsExpanded() { - return this.discussionsExpanded && this.expanded; - }, viewFileButtonText() { const truncatedContentSha = _.escape(truncateSha(this.diffFile.contentSha)); return sprintf( @@ -113,7 +110,8 @@ export default { }, }, methods: { - handleToggle(e, checkTarget) { + ...mapActions('diffs', ['toggleFileDiscussions']), + handleToggleFile(e, checkTarget) { if ( !checkTarget || e.target === this.$refs.header || @@ -125,6 +123,9 @@ export default { showForkMessage() { this.$emit('showForkMessage'); }, + handleToggleDiscussions() { + this.toggleFileDiscussions(this.diffFile); + }, }, }; </script> @@ -133,7 +134,7 @@ export default { <div ref="header" class="js-file-title file-title file-title-flex-parent" - @click="handleToggle($event, true)" + @click="handleToggleFile($event, true)" > <div class="file-header-content"> <icon @@ -216,10 +217,11 @@ export default { v-if="diffFile.blob && diffFile.blob.readableText" > <button - :class="{ active: isDiscussionsExpanded }" + :class="{ active: hasExpandedDiscussions }" :title="s__('MergeRequests|Toggle comments for this file')" - class="btn js-toggle-diff-comments" + class="js-btn-vue-toggle-comments btn" type="button" + @click="handleToggleDiscussions" > <icon name="comment" /> </button> diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js index 5e0fd5109bb..27001142257 100644 --- a/app/assets/javascripts/diffs/store/actions.js +++ b/app/assets/javascripts/diffs/store/actions.js @@ -82,14 +82,32 @@ export const expandAllFiles = ({ commit }) => { commit(types.EXPAND_ALL_FILES); }; -export default { - setBaseConfig, - fetchDiffFiles, - setInlineDiffViewType, - setParallelDiffViewType, - showCommentForm, - cancelCommentForm, - loadMoreLines, - loadCollapsedDiff, - expandAllFiles, +/** + * Toggles the file discussions after user clicked on the toggle discussions button. + * + * Gets the discussions for the provided diff. + * + * If all discussions are expanded, it will collapse all of them + * If all discussions are collapsed, it will expand all of them + * If some discussions are open and others closed, it will expand the closed ones. + * + * @param {Object} diff + */ +export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { + const discussions = getters.getDiffFileDiscussions(diff); + const shouldCloseAll = getters.diffHasAllExpandedDiscussions(diff); + const shouldExpandAll = getters.diffHasAllCollpasedDiscussions(diff); + + discussions.forEach(discussion => { + const data = { discussionId: discussion.id }; + + if (shouldCloseAll) { + dispatch('collapseDiscussion', data, { root: true }); + } else if (shouldExpandAll || (!shouldCloseAll && !shouldExpandAll && !discussion.expanded)) { + dispatch('expandDiscussion', data, { root: true }); + } + }); }; + +// 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 f3c2d7427e7..f89acb73ed8 100644 --- a/app/assets/javascripts/diffs/store/getters.js +++ b/app/assets/javascripts/diffs/store/getters.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import { PARALLEL_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE } from '../constants'; export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW_TYPE; @@ -8,5 +9,52 @@ export const areAllFilesCollapsed = state => state.diffFiles.every(file => file. export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null); -// prevent babel-plugin-rewire from generating an invalid default during karma tests +/** + * Checks if the diff has all discussions expanded + * @param {Object} diff + * @returns {Boolean} + */ +export const diffHasAllExpandedDiscussions = (state, getters) => diff => { + const discussions = getters.getDiffFileDiscussions(diff); + + return (discussions.length && discussions.every(discussion => discussion.expanded)) || false; +}; + +/** + * Checks if the diff has all discussions collpased + * @param {Object} diff + * @returns {Boolean} + */ +export const diffHasAllCollpasedDiscussions = (state, getters) => diff => { + const discussions = getters.getDiffFileDiscussions(diff); + + return (discussions.length && discussions.every(discussion => !discussion.expanded)) || false; +}; + +/** + * Checks if the diff has any open discussions + * @param {Object} diff + * @returns {Boolean} + */ +export const diffHasExpandedDiscussions = (state, getters) => diff => { + const discussions = getters.getDiffFileDiscussions(diff); + + return ( + (discussions.length && discussions.find(discussion => discussion.expanded) !== undefined) || + false + ); +}; + +/** + * Returns an array with the discussions of the given diff + * @param {Object} diff + * @returns {Array} + */ +export const getDiffFileDiscussions = (state, getters, rootState, rootGetters) => diff => + rootGetters.discussions.filter( + discussion => + discussion.diff_discussion && _.isEqual(discussion.diff_file.file_hash, diff.fileHash), + ) || []; + +// prevent babel-plugin-rewire from generating an invalid default during karma∂ tests export default () => {}; diff --git a/app/assets/javascripts/diffs/store/index.js b/app/assets/javascripts/diffs/store/index.js deleted file mode 100644 index e6aa8f5b12a..00000000000 --- a/app/assets/javascripts/diffs/store/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; -import diffsModule from './modules'; - -Vue.use(Vuex); - -export default new Vuex.Store({ - modules: { - diffs: diffsModule, - }, -}); diff --git a/app/assets/javascripts/diffs/store/modules/index.js b/app/assets/javascripts/diffs/store/modules/index.js index 90505f83b60..20d1ebbe049 100644 --- a/app/assets/javascripts/diffs/store/modules/index.js +++ b/app/assets/javascripts/diffs/store/modules/index.js @@ -1,4 +1,4 @@ -import actions from '../actions'; +import * as actions from '../actions'; import * as getters from '../getters'; import mutations from '../mutations'; import createState from './diff_state'; diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue index e3652fe739e..63d83e307ee 100644 --- a/app/assets/javascripts/environments/components/environment_actions.vue +++ b/app/assets/javascripts/environments/components/environment_actions.vue @@ -1,50 +1,50 @@ <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'; +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 { - directives: { - tooltip, +export default { + directives: { + tooltip, + }, + components: { + loadingIcon, + Icon, + }, + props: { + actions: { + type: Array, + required: false, + default: () => [], }, - components: { - loadingIcon, - Icon, + }, + data() { + return { + isLoading: false, + }; + }, + computed: { + title() { + return 'Deploy to...'; }, - props: { - actions: { - type: Array, - required: false, - default: () => [], - }, - }, - data() { - return { - isLoading: false, - }; - }, - computed: { - title() { - return 'Deploy to...'; - }, - }, - methods: { - onClickAction(endpoint) { - this.isLoading = true; + }, + methods: { + onClickAction(endpoint) { + this.isLoading = true; - eventHub.$emit('postAction', endpoint); - }, + eventHub.$emit('postAction', { endpoint }); + }, - isActionDisabled(action) { - if (action.playable === undefined) { - return false; - } + isActionDisabled(action) { + if (action.playable === undefined) { + return false; + } - return !action.playable; - }, + return !action.playable; }, - }; + }, +}; </script> <template> <div @@ -61,10 +61,7 @@ data-toggle="dropdown" > <span> - <icon - :size="12" - name="play" - /> + <icon name="play" /> <i class="fa fa-caret-down" aria-hidden="true" @@ -85,10 +82,6 @@ class="js-manual-action-link no-btn btn" @click="onClickAction(action.play_path)" > - <icon - :size="12" - name="play" - /> <span> {{ action.name }} </span> diff --git a/app/assets/javascripts/environments/components/environment_external_url.vue b/app/assets/javascripts/environments/components/environment_external_url.vue index 68195225d50..7446196de13 100644 --- a/app/assets/javascripts/environments/components/environment_external_url.vue +++ b/app/assets/javascripts/environments/components/environment_external_url.vue @@ -1,30 +1,30 @@ <script> - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; - import { s__ } from '../../locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; +import { s__ } from '../../locale'; - /** - * Renders the external url link in environments table. - */ - export default { - components: { - Icon, +/** + * Renders the external url link in environments table. + */ +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + externalUrl: { + type: String, + required: true, }, - directives: { - tooltip, + }, + computed: { + title() { + return s__('Environments|Open live environment'); }, - props: { - externalUrl: { - type: String, - required: true, - }, - }, - computed: { - title() { - return s__('Environments|Open'); - }, - }, - }; + }, +}; </script> <template> <a @@ -37,9 +37,6 @@ target="_blank" rel="noopener noreferrer nofollow" > - <icon - :size="12" - name="external-link" - /> + <icon name="external-link" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 5ecdccf63ad..39f3790a286 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,429 +1,450 @@ <script> - import Timeago from 'timeago.js'; - import _ from 'underscore'; - import tooltip from '~/vue_shared/directives/tooltip'; - import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; - import { humanize } from '~/lib/utils/text_utility'; - import ActionsComponent from './environment_actions.vue'; - import ExternalUrlComponent from './environment_external_url.vue'; - import StopComponent from './environment_stop.vue'; - import RollbackComponent from './environment_rollback.vue'; - import TerminalButtonComponent from './environment_terminal_button.vue'; - import MonitoringButtonComponent from './environment_monitoring.vue'; - import CommitComponent from '../../vue_shared/components/commit.vue'; - import eventHub from '../event_hub'; - - /** - * Envrionment Item Component - * - * Renders a table row for each environment. - */ - const timeagoInstance = new Timeago(); - - export default { - components: { - UserAvatarLink, - CommitComponent, - ActionsComponent, - ExternalUrlComponent, - StopComponent, - RollbackComponent, - TerminalButtonComponent, - MonitoringButtonComponent, +import Timeago from 'timeago.js'; +import _ from 'underscore'; +import tooltip from '~/vue_shared/directives/tooltip'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; +import { humanize } from '~/lib/utils/text_utility'; +import ActionsComponent from './environment_actions.vue'; +import ExternalUrlComponent from './environment_external_url.vue'; +import StopComponent from './environment_stop.vue'; +import RollbackComponent from './environment_rollback.vue'; +import TerminalButtonComponent from './environment_terminal_button.vue'; +import MonitoringButtonComponent from './environment_monitoring.vue'; +import CommitComponent from '../../vue_shared/components/commit.vue'; +import eventHub from '../event_hub'; + +/** + * Envrionment Item Component + * + * Renders a table row for each environment. + */ +const timeagoInstance = new Timeago(); + +export default { + components: { + UserAvatarLink, + CommitComponent, + ActionsComponent, + ExternalUrlComponent, + StopComponent, + RollbackComponent, + TerminalButtonComponent, + MonitoringButtonComponent, + }, + + directives: { + tooltip, + }, + + props: { + model: { + type: Object, + required: true, + default: () => ({}), }, - directives: { - tooltip, + canCreateDeployment: { + type: Boolean, + required: false, + default: false, }, - props: { - model: { - type: Object, - required: true, - default: () => ({}), - }, - - canCreateDeployment: { - type: Boolean, - required: false, - default: false, - }, - - canReadEnvironment: { - type: Boolean, - required: false, - default: false, - }, + canReadEnvironment: { + type: Boolean, + required: false, + default: false, + }, + }, + + computed: { + /** + * Verifies if `last_deployment` key exists in the current Envrionment. + * This key is required to render most of the html - this method works has + * an helper. + * + * @returns {Boolean} + */ + hasLastDeploymentKey() { + if (this.model && this.model.last_deployment && !_.isEmpty(this.model.last_deployment)) { + return true; + } + return false; + }, + + /** + * Verifies is the given environment has manual actions. + * Used to verify if we should render them or nor. + * + * @returns {Boolean|Undefined} + */ + hasManualActions() { + return ( + this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0 + ); + }, + + /** + * Returns whether the environment can be stopped. + * + * @returns {Boolean} + */ + canStopEnvironment() { + return this.model && this.model.can_stop; + }, + + /** + * Verifies if the `deployable` key is present in `last_deployment` key. + * Used to verify whether we should or not render the rollback partial. + * + * @returns {Boolean|Undefined} + */ + canRetry() { + return ( + this.model && + this.hasLastDeploymentKey && + this.model.last_deployment && + this.model.last_deployment.deployable + ); + }, + + /** + * Verifies if the date to be shown is present. + * + * @returns {Boolean|Undefined} + */ + canShowDate() { + return ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined + ); + }, + + /** + * Human readable date. + * + * @returns {String} + */ + createdDate() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at + ) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); + } + return ''; + }, + + /** + * Returns the manual actions with the name parsed. + * + * @returns {Array.<Object>|Undefined} + */ + manualActions() { + if (this.hasManualActions) { + return this.model.last_deployment.manual_actions.map(action => { + const parsedAction = { + name: humanize(action.name), + play_path: action.play_path, + playable: action.playable, + }; + return parsedAction; + }); + } + return []; + }, + + /** + * Builds the string used in the user image alt attribute. + * + * @returns {String} + */ + userImageAltDescription() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username + ) { + return `${this.model.last_deployment.user.username}'s avatar'`; + } + return ''; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {String|Undefined} + */ + commitTag() { + if (this.model && this.model.last_deployment && this.model.last_deployment.tag) { + return this.model.last_deployment.tag; + } + return undefined; + }, + + /** + * If provided, returns the commit ref. + * + * @returns {Object|Undefined} + */ + commitRef() { + if (this.model && this.model.last_deployment && this.model.last_deployment.ref) { + return this.model.last_deployment.ref; + } + return undefined; + }, + + /** + * If provided, returns the commit url. + * + * @returns {String|Undefined} + */ + commitUrl() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path + ) { + return this.model.last_deployment.commit.commit_path; + } + return undefined; + }, + + /** + * If provided, returns the commit short sha. + * + * @returns {String|Undefined} + */ + commitShortSha() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id + ) { + return this.model.last_deployment.commit.short_id; + } + return undefined; + }, + + /** + * If provided, returns the commit title. + * + * @returns {String|Undefined} + */ + commitTitle() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title + ) { + return this.model.last_deployment.commit.title; + } + return undefined; + }, + + /** + * If provided, returns the commit tag. + * + * @returns {Object|Undefined} + */ + commitAuthor() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author + ) { + return this.model.last_deployment.commit.author; + } + + return undefined; + }, + + /** + * Verifies if the `retry_path` key is present and returns its value. + * + * @returns {String|Undefined} + */ + retryUrl() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path + ) { + return this.model.last_deployment.deployable.retry_path; + } + return undefined; + }, + + /** + * Verifies if the `last?` key is present and returns its value. + * + * @returns {Boolean|Undefined} + */ + isLastDeployment() { + return this.model && this.model.last_deployment && this.model.last_deployment['last?']; + }, + + /** + * Builds the name of the builds needed to display both the name and the id. + * + * @returns {String} + */ + buildName() { + if (this.model && this.model.last_deployment && this.model.last_deployment.deployable) { + const { deployable } = this.model.last_deployment; + return `${deployable.name} #${deployable.id}`; + } + return ''; + }, + + /** + * Builds the needed string to show the internal id. + * + * @returns {String} + */ + deploymentInternalId() { + if (this.model && this.model.last_deployment && this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; + } + return ''; }, - computed: { - /** - * Verifies if `last_deployment` key exists in the current Envrionment. - * This key is required to render most of the html - this method works has - * an helper. - * - * @returns {Boolean} - */ - hasLastDeploymentKey() { - if (this.model && - this.model.last_deployment && - !_.isEmpty(this.model.last_deployment)) { - return true; - } - return false; - }, - - /** - * Verifies is the given environment has manual actions. - * Used to verify if we should render them or nor. - * - * @returns {Boolean|Undefined} - */ - hasManualActions() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.manual_actions && - this.model.last_deployment.manual_actions.length > 0; - }, - - /** - * Returns the value of the `stop_action?` key provided in the response. - * - * @returns {Boolean} - */ - hasStopAction() { - return this.model && this.model['stop_action?']; - }, - - /** - * Verifies if the `deployable` key is present in `last_deployment` key. - * Used to verify whether we should or not render the rollback partial. - * - * @returns {Boolean|Undefined} - */ - canRetry() { - return this.model && - this.hasLastDeploymentKey && - this.model.last_deployment && - this.model.last_deployment.deployable; - }, - - /** - * Verifies if the date to be shown is present. - * - * @returns {Boolean|Undefined} - */ - canShowDate() { - return this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable !== undefined; - }, - - /** - * Human readable date. - * - * @returns {String} - */ - createdDate() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.last_deployment.deployable.created_at); - } - return ''; - }, - - /** - * Returns the manual actions with the name parsed. - * - * @returns {Array.<Object>|Undefined} - */ - manualActions() { - if (this.hasManualActions) { - return this.model.last_deployment.manual_actions.map((action) => { - const parsedAction = { - name: humanize(action.name), - play_path: action.play_path, - playable: action.playable, - }; - return parsedAction; - }); - } - return []; - }, - - /** - * Builds the string used in the user image alt attribute. - * - * @returns {String} - */ - userImageAltDescription() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.user && - this.model.last_deployment.user.username) { - return `${this.model.last_deployment.user.username}'s avatar'`; - } - return ''; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {String|Undefined} - */ - commitTag() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.tag) { - return this.model.last_deployment.tag; - } - return undefined; - }, - - /** - * If provided, returns the commit ref. - * - * @returns {Object|Undefined} - */ - commitRef() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.ref) { - return this.model.last_deployment.ref; - } - return undefined; - }, - - /** - * If provided, returns the commit url. - * - * @returns {String|Undefined} - */ - commitUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.commit_path) { - return this.model.last_deployment.commit.commit_path; - } - return undefined; - }, - - /** - * If provided, returns the commit short sha. - * - * @returns {String|Undefined} - */ - commitShortSha() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.short_id) { - return this.model.last_deployment.commit.short_id; - } - return undefined; - }, - - /** - * If provided, returns the commit title. - * - * @returns {String|Undefined} - */ - commitTitle() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.title) { - return this.model.last_deployment.commit.title; - } - return undefined; - }, - - /** - * If provided, returns the commit tag. - * - * @returns {Object|Undefined} - */ - commitAuthor() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.commit && - this.model.last_deployment.commit.author) { - return this.model.last_deployment.commit.author; - } - - return undefined; - }, - - /** - * Verifies if the `retry_path` key is present and returns its value. - * - * @returns {String|Undefined} - */ - retryUrl() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.retry_path) { - return this.model.last_deployment.deployable.retry_path; - } - return undefined; - }, - - /** - * Verifies if the `last?` key is present and returns its value. - * - * @returns {Boolean|Undefined} - */ - isLastDeployment() { - return this.model && this.model.last_deployment && - this.model.last_deployment['last?']; - }, - - /** - * Builds the name of the builds needed to display both the name and the id. - * - * @returns {String} - */ - buildName() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable) { - const { deployable } = this.model.last_deployment; - return `${deployable.name} #${deployable.id}`; - } - return ''; - }, - - /** - * Builds the needed string to show the internal id. - * - * @returns {String} - */ - deploymentInternalId() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.iid) { - return `#${this.model.last_deployment.iid}`; - } - return ''; - }, - - /** - * Verifies if the user object is present under last_deployment object. - * - * @returns {Boolean} - */ - deploymentHasUser() { - return this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user); - }, - - /** - * Returns the user object nested with the last_deployment object. - * Used to render the template. - * - * @returns {Object} - */ - deploymentUser() { - if (this.model && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.user)) { - return this.model.last_deployment.user; - } - return {}; - }, - - /** - * Verifies if the build name column should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderBuildName() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - !_.isEmpty(this.model.last_deployment.deployable); - }, - - /** - * Verifies the presence of all the keys needed to render the buil_path. - * - * @return {String} - */ - buildPath() { - if (this.model && - this.model.last_deployment && - this.model.last_deployment.deployable && - this.model.last_deployment.deployable.build_path) { - return this.model.last_deployment.deployable.build_path; - } - - return ''; - }, - - /** - * Verifies the presence of all the keys needed to render the external_url. - * - * @return {String} - */ - externalURL() { - if (this.model && this.model.external_url) { - return this.model.external_url; - } - - return ''; - }, - - /** - * Verifies if deplyment internal ID should be rendered by verifing - * if all the information needed is present - * and if the environment is not a folder. - * - * @returns {Boolean} - */ - shouldRenderDeploymentID() { - return !this.model.isFolder && - !_.isEmpty(this.model.last_deployment) && - this.model.last_deployment.iid !== undefined; - }, - - environmentPath() { - if (this.model && this.model.environment_path) { - return this.model.environment_path; - } - - return ''; - }, - - monitoringUrl() { - if (this.model && this.model.metrics_path) { - return this.model.metrics_path; - } - - return ''; - }, - - displayEnvironmentActions() { - return this.hasManualActions || - this.externalURL || - this.monitoringUrl || - this.hasStopAction || - this.canRetry; - }, + /** + * Verifies if the user object is present under last_deployment object. + * + * @returns {Boolean} + */ + deploymentHasUser() { + return ( + this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user) + ); }, - methods: { - onClickFolder() { - eventHub.$emit('toggleFolder', this.model); - }, + /** + * Returns the user object nested with the last_deployment object. + * Used to render the template. + * + * @returns {Object} + */ + deploymentUser() { + if ( + this.model && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.user) + ) { + return this.model.last_deployment.user; + } + return {}; }, - }; + + /** + * Verifies if the build name column should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderBuildName() { + return ( + !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + !_.isEmpty(this.model.last_deployment.deployable) + ); + }, + + /** + * Verifies the presence of all the keys needed to render the buil_path. + * + * @return {String} + */ + buildPath() { + if ( + this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path + ) { + return this.model.last_deployment.deployable.build_path; + } + + return ''; + }, + + /** + * Verifies the presence of all the keys needed to render the external_url. + * + * @return {String} + */ + externalURL() { + if (this.model && this.model.external_url) { + return this.model.external_url; + } + + return ''; + }, + + /** + * Verifies if deplyment internal ID should be rendered by verifing + * if all the information needed is present + * and if the environment is not a folder. + * + * @returns {Boolean} + */ + shouldRenderDeploymentID() { + return ( + !this.model.isFolder && + !_.isEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined + ); + }, + + environmentPath() { + if (this.model && this.model.environment_path) { + return this.model.environment_path; + } + + return ''; + }, + + monitoringUrl() { + if (this.model && this.model.metrics_path) { + return this.model.metrics_path; + } + + return ''; + }, + + displayEnvironmentActions() { + return ( + this.hasManualActions || + this.externalURL || + this.monitoringUrl || + this.canStopEnvironment || + this.canRetry + ); + }, + }, + + methods: { + onClickFolder() { + eventHub.$emit('toggleFolder', this.model); + }, + }, +}; </script> <template> <div @@ -580,11 +601,6 @@ class="btn-group table-action-buttons" role="group"> - <actions-component - v-if="hasManualActions && canCreateDeployment" - :actions="manualActions" - /> - <external-url-component v-if="externalURL && canReadEnvironment" :external-url="externalURL" @@ -595,21 +611,26 @@ :monitoring-url="monitoringUrl" /> + <actions-component + v-if="hasManualActions && canCreateDeployment" + :actions="manualActions" + /> + <terminal-button-component v-if="model && model.terminal_path" :terminal-path="model.terminal_path" /> - <stop-component - v-if="hasStopAction && canCreateDeployment" - :stop-url="model.stop_path" - /> - <rollback-component v-if="canRetry && canCreateDeployment" :is-last-deployment="isLastDeployment" :retry-url="retryUrl" /> + + <stop-component + v-if="canStopEnvironment" + :environment="model" + /> </div> </div> </div> diff --git a/app/assets/javascripts/environments/components/environment_monitoring.vue b/app/assets/javascripts/environments/components/environment_monitoring.vue index 947e8c901e9..ccc8419ca6d 100644 --- a/app/assets/javascripts/environments/components/environment_monitoring.vue +++ b/app/assets/javascripts/environments/components/environment_monitoring.vue @@ -1,29 +1,29 @@ <script> - /** - * Renders the Monitoring (Metrics) link in environments table. - */ - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +/** + * Renders the Monitoring (Metrics) link in environments table. + */ +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - Icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + monitoringUrl: { + type: String, + required: true, }, - directives: { - tooltip, + }, + computed: { + title() { + return 'Monitoring'; }, - props: { - monitoringUrl: { - type: String, - required: true, - }, - }, - computed: { - title() { - return 'Monitoring'; - }, - }, - }; + }, +}; </script> <template> <a @@ -35,9 +35,6 @@ data-container="body" rel="noopener noreferrer nofollow" > - <icon - :size="12" - name="chart" - /> + <icon name="chart" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environment_rollback.vue b/app/assets/javascripts/environments/components/environment_rollback.vue index 310835c5ea9..4deeef4beb9 100644 --- a/app/assets/javascripts/environments/components/environment_rollback.vue +++ b/app/assets/javascripts/environments/components/environment_rollback.vue @@ -1,56 +1,74 @@ <script> - /** - * Renders Rollback or Re deploy button in environments table depending - * of the provided property `isLastDeployment`. - * - * Makes a post request when the button is clicked. - */ - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - - export default { - components: { - loadingIcon, +/** + * Renders Rollback or Re deploy button in environments table depending + * of the provided property `isLastDeployment`. + * + * Makes a post request when the button is clicked. + */ +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: { + tooltip, + }, + + props: { + retryUrl: { + type: String, + default: '', }, - props: { - retryUrl: { - type: String, - default: '', - }, - - isLastDeployment: { - type: Boolean, - default: true, - }, + + isLastDeployment: { + type: Boolean, + default: true, }, - data() { - return { - isLoading: false, - }; + }, + data() { + return { + isLoading: false, + }; + }, + + computed: { + title() { + return this.isLastDeployment ? s__('Environments|Re-deploy to environment') : s__('Environments|Rollback environment'); }, - methods: { - onClick() { - this.isLoading = true; + }, + + methods: { + onClick() { + this.isLoading = true; - eventHub.$emit('postAction', this.retryUrl); - }, + eventHub.$emit('postAction', { endpoint: this.retryUrl }); }, - }; + }, +}; </script> <template> <button + v-tooltip :disabled="isLoading" + :title="title" type="button" class="btn d-none d-sm-none d-md-block" @click="onClick" > - <span v-if="isLastDeployment"> - {{ s__("Environments|Re-deploy") }} - </span> - <span v-else> - {{ s__("Environments|Rollback") }} - </span> + <icon + v-if="isLastDeployment" + name="repeat" /> + <icon + v-else + name="redo"/> <loading-icon v-if="isLoading" /> </button> diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue index eba58bedd6d..a814b3405f5 100644 --- a/app/assets/javascripts/environments/components/environment_stop.vue +++ b/app/assets/javascripts/environments/components/environment_stop.vue @@ -1,72 +1,78 @@ <script> - /** - * Renders the stop "button" that allows stop an environment. - * Used in environments table. - */ +/** + * Renders the stop "button" that allows stop an environment. + * Used in environments table. + */ - import $ from 'jquery'; - import eventHub from '../event_hub'; - import loadingIcon from '../../vue_shared/components/loading_icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +import $ from 'jquery'; +import Icon from '~/vue_shared/components/icon.vue'; +import { s__ } from '~/locale'; +import eventHub from '../event_hub'; +import LoadingButton from '../../vue_shared/components/loading_button.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - loadingIcon, - }, +export default { + components: { + Icon, + LoadingButton, + }, - directives: { - tooltip, - }, + directives: { + tooltip, + }, - props: { - stopUrl: { - type: String, - default: '', - }, + props: { + environment: { + type: Object, + required: true, }, + }, - data() { - return { - isLoading: false, - }; - }, + data() { + return { + isLoading: false, + }; + }, - computed: { - title() { - return 'Stop'; - }, + computed: { + title() { + return s__('Environments|Stop environment'); }, + }, - methods: { - onClick() { - // eslint-disable-next-line no-alert - if (window.confirm('Are you sure you want to stop this environment?')) { - this.isLoading = true; + mounted() { + eventHub.$on('stopEnvironment', this.onStopEnvironment); + }, - $(this.$el).tooltip('dispose'); + beforeDestroy() { + eventHub.$off('stopEnvironment', this.onStopEnvironment); + }, - eventHub.$emit('postAction', this.stopUrl); - } - }, + methods: { + onClick() { + $(this.$el).tooltip('dispose'); + eventHub.$emit('requestStopEnvironment', this.environment); + }, + onStopEnvironment(environment) { + if (this.environment.id === environment.id) { + this.isLoading = true; + } }, - }; + }, +}; </script> <template> - <button + <loading-button v-tooltip - :disabled="isLoading" + :loading="isLoading" :title="title" :aria-label="title" - type="button" - class="btn stop-env-link d-none d-sm-none d-md-block" + container-class="btn btn-danger d-none d-sm-none d-md-block" data-container="body" + data-toggle="modal" + data-target="#stop-environment-modal" @click="onClick" > - <i - class="fa fa-stop stop-env-icon" - aria-hidden="true" - > - </i> - <loading-icon v-if="isLoading" /> - </button> + <icon name="stop"/> + </loading-button> </template> diff --git a/app/assets/javascripts/environments/components/environment_terminal_button.vue b/app/assets/javascripts/environments/components/environment_terminal_button.vue index f8e3165f8cd..350417e5ad0 100644 --- a/app/assets/javascripts/environments/components/environment_terminal_button.vue +++ b/app/assets/javascripts/environments/components/environment_terminal_button.vue @@ -1,31 +1,31 @@ <script> - /** - * Renders a terminal button to open a web terminal. - * Used in environments table. - */ - import Icon from '~/vue_shared/components/icon.vue'; - import tooltip from '../../vue_shared/directives/tooltip'; +/** + * Renders a terminal button to open a web terminal. + * Used in environments table. + */ +import Icon from '~/vue_shared/components/icon.vue'; +import tooltip from '../../vue_shared/directives/tooltip'; - export default { - components: { - Icon, +export default { + components: { + Icon, + }, + directives: { + tooltip, + }, + props: { + terminalPath: { + type: String, + required: false, + default: '', }, - directives: { - tooltip, + }, + computed: { + title() { + return 'Terminal'; }, - props: { - terminalPath: { - type: String, - required: false, - default: '', - }, - }, - computed: { - title() { - return 'Terminal'; - }, - }, - }; + }, +}; </script> <template> <a @@ -36,9 +36,6 @@ class="btn terminal-button d-none d-sm-none d-md-block" data-container="body" > - <icon - :size="12" - name="terminal" - /> + <icon name="terminal" /> </a> </template> diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue index b18f02343d6..8efdfb8abe0 100644 --- a/app/assets/javascripts/environments/components/environments_app.vue +++ b/app/assets/javascripts/environments/components/environments_app.vue @@ -5,10 +5,12 @@ import eventHub from '../event_hub'; import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; + import StopEnvironmentModal from './stop_environment_modal.vue'; export default { components: { emptyState, + StopEnvironmentModal, }, mixins: [ @@ -90,6 +92,8 @@ </script> <template> <div :class="cssContainerClass"> + <stop-environment-modal :environment="environmentInStopModal" /> + <div class="top-area"> <tabs :tabs="tabs" diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue new file mode 100644 index 00000000000..657cc8cd1aa --- /dev/null +++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue @@ -0,0 +1,92 @@ +<script> +import GlModal from '~/vue_shared/components/gl_modal.vue'; +import { s__, sprintf } from '~/locale'; +import tooltip from '~/vue_shared/directives/tooltip'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import eventHub from '../event_hub'; + +export default { + id: 'stop-environment-modal', + name: 'StopEnvironmentModal', + + components: { + GlModal, + LoadingButton, + }, + + directives: { + tooltip, + }, + + props: { + environment: { + type: Object, + required: true, + }, + }, + + computed: { + noStopActionMessage() { + return sprintf( + s__( + `Environments|Note that this action will stop the environment, + but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment + due to no “stop environment action” being defined + in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`, + ), + { + emphasisStart: '<strong>', + emphasisEnd: '</strong>', + ciConfigLinkStart: + '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">', + ciConfigLinkEnd: '</a>', + }, + false, + ); + }, + }, + + methods: { + onSubmit() { + eventHub.$emit('stopEnvironment', this.environment); + }, + }, +}; +</script> + +<template> + <gl-modal + :id="$options.id" + :footer-primary-button-text="s__('Environments|Stop environment')" + footer-primary-button-variant="danger" + @submit="onSubmit" + > + <template slot="header"> + <h4 + class="modal-title d-flex mw-100" + > + Stopping + <span + v-tooltip + :title="environment.name" + class="text-truncate ml-1 mr-1 flex-fill" + >{{ environment.name }}</span> + ? + </h4> + </template> + + <p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p> + + <div + v-if="!environment.has_stop_action" + class="warning_message" + > + <p v-html="noStopActionMessage"></p> + <a + href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment" + target="_blank" + rel="noopener noreferrer" + >{{ s__('Environments|Learn more about stopping environments') }}</a> + </div> + </gl-modal> +</template> diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue index 5f72a39c5cb..e69bfa0b2cc 100644 --- a/app/assets/javascripts/environments/folder/environments_folder_view.vue +++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue @@ -1,12 +1,18 @@ <script> import environmentsMixin from '../mixins/environments_mixin'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin'; + import StopEnvironmentModal from '../components/stop_environment_modal.vue'; export default { + components: { + StopEnvironmentModal, + }, + mixins: [ environmentsMixin, CIPaginationMixin, ], + props: { endpoint: { type: String, @@ -38,6 +44,8 @@ </script> <template> <div :class="cssContainerClass"> + <stop-environment-modal :environment="environmentInStopModal" /> + <div v-if="!isLoading" class="top-area" diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js index a7a79dbca70..d88624f7f8d 100644 --- a/app/assets/javascripts/environments/mixins/environments_mixin.js +++ b/app/assets/javascripts/environments/mixins/environments_mixin.js @@ -40,6 +40,7 @@ export default { scope: getParameterByName('scope') || 'available', page: getParameterByName('page') || '1', requestData: {}, + environmentInStopModal: {}, }; }, @@ -85,7 +86,7 @@ export default { Flash(s__('Environments|An error occurred while fetching the environments.')); }, - postAction(endpoint) { + postAction({ endpoint, errorMessage }) { if (!this.isMakingRequest) { this.isLoading = true; @@ -93,7 +94,7 @@ export default { .then(() => this.fetchEnvironments()) .catch(() => { this.isLoading = false; - Flash(s__('Environments|An error occurred while making the request.')); + Flash(errorMessage || s__('Environments|An error occurred while making the request.')); }); } }, @@ -106,6 +107,15 @@ export default { .catch(this.errorCallback); }, + updateStopModal(environment) { + this.environmentInStopModal = environment; + }, + + stopEnvironment(environment) { + const endpoint = environment.stop_path; + const errorMessage = s__('Environments|An error occurred while stopping the environment, please try again'); + this.postAction({ endpoint, errorMessage }); + }, }, computed: { @@ -162,9 +172,13 @@ export default { }); eventHub.$on('postAction', this.postAction); + eventHub.$on('requestStopEnvironment', this.updateStopModal); + eventHub.$on('stopEnvironment', this.stopEnvironment); }, - beforeDestroyed() { - eventHub.$off('postAction'); + beforeDestroy() { + eventHub.$off('postAction', this.postAction); + eventHub.$off('requestStopEnvironment', this.updateStopModal); + eventHub.$off('stopEnvironment', this.stopEnvironment); }, }; diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js index 3b121551aca..4e07ccba91a 100644 --- a/app/assets/javascripts/environments/services/environments_service.js +++ b/app/assets/javascripts/environments/services/environments_service.js @@ -13,7 +13,7 @@ export default class EnvironmentsService { // eslint-disable-next-line class-methods-use-this postAction(endpoint) { - return axios.post(endpoint, {}, { emulateJSON: true }); + return axios.post(endpoint, {}); } getFolderContent(folderUrl) { diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 963e3a37b39..26482a02e00 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -200,7 +200,7 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea" class="btn btn-cancel note-edit-cancel js-close-discussion-note-form" type="button" @click="cancelHandler()"> - {{ __('Discard draft') }} + Cancel </button> </div> </form> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index b2bf86eea56..3eefbe11c37 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -15,6 +15,8 @@ let eTagPoll; export const expandDiscussion = ({ commit }, data) => commit(types.EXPAND_DISCUSSION, data); +export const collapseDiscussion = ({ commit }, data) => commit(types.COLLAPSE_DISCUSSION, data); + export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data); diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index a25098fbc06..6f374f78691 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -1,7 +1,6 @@ export const ADD_NEW_NOTE = 'ADD_NEW_NOTE'; export const ADD_NEW_REPLY_TO_DISCUSSION = 'ADD_NEW_REPLY_TO_DISCUSSION'; export const DELETE_NOTE = 'DELETE_NOTE'; -export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION'; export const REMOVE_PLACEHOLDER_NOTES = 'REMOVE_PLACEHOLDER_NOTES'; export const SET_NOTES_DATA = 'SET_NOTES_DATA'; export const SET_NOTEABLE_DATA = 'SET_NOTEABLE_DATA'; @@ -11,12 +10,16 @@ export const SET_LAST_FETCHED_AT = 'SET_LAST_FETCHED_AT'; export const SET_TARGET_NOTE_HASH = 'SET_TARGET_NOTE_HASH'; export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE'; export const TOGGLE_AWARD = 'TOGGLE_AWARD'; -export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; export const UPDATE_NOTE = 'UPDATE_NOTE'; export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION'; export const SET_DISCUSSION_DIFF_LINES = 'SET_DISCUSSION_DIFF_LINES'; export const SET_NOTES_FETCHED_STATE = 'SET_NOTES_FETCHED_STATE'; +// DISCUSSION +export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; +export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION'; +export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; + // Issue export const CLOSE_ISSUE = 'CLOSE_ISSUE'; export const REOPEN_ISSUE = 'REOPEN_ISSUE'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index a1849269010..ab6a95e2601 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -58,6 +58,11 @@ export default { discussion.expanded = true; }, + [types.COLLAPSE_DISCUSSION](state, { discussionId }) { + const discussion = utils.findNoteObjectById(state.discussions, discussionId); + discussion.expanded = false; + }, + [types.REMOVE_PLACEHOLDER_NOTES](state) { const { discussions } = state; diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js index 9aa83ce6269..ff19b9a9c30 100644 --- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js +++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js @@ -39,7 +39,6 @@ export default class Todos { } initFilters() { - this.initFilterDropdown($('.js-group-search'), 'group_id', ['text']); this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']); this.initFilterDropdown($('.js-type-search'), 'type'); this.initFilterDropdown($('.js-action-search'), 'action_id'); @@ -54,16 +53,7 @@ export default class Todos { filterable: searchFields ? true : false, search: { fields: searchFields }, data: $dropdown.data('data'), - clicked: () => { - const $formEl = $dropdown.closest('form.filter-form'); - const mutexDropdowns = { - group_id: 'project_id', - project_id: 'group_id', - }; - - $formEl.find(`input[name="${mutexDropdowns[fieldName]}"]`).remove(); - $formEl.submit(); - }, + clicked: () => $dropdown.closest('form.filter-form').submit(), }); } diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue deleted file mode 100644 index ffaed9c7193..00000000000 --- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue +++ /dev/null @@ -1,98 +0,0 @@ -<script> -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'); - -export default { - directives: { - tooltip, - }, - components: { - Icon, - LoadingIcon, - }, - props: { - issuableId: { - type: Number, - required: true, - }, - issuableType: { - type: String, - required: true, - }, - isTodo: { - type: Boolean, - required: false, - default: true, - }, - isActionActive: { - type: Boolean, - required: false, - default: false, - }, - collapsed: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - buttonClasses() { - return this.collapsed ? - 'btn-blank btn-todo sidebar-collapsed-icon dont-change-state' : - 'btn btn-default btn-todo issuable-header-btn float-right'; - }, - buttonLabel() { - return this.isTodo ? MARK_TEXT : TODO_TEXT; - }, - collapsedButtonIconClasses() { - return this.isTodo ? 'todo-undone' : ''; - }, - collapsedButtonIcon() { - return this.isTodo ? 'todo-done' : 'todo-add'; - }, - }, - methods: { - handleButtonClick() { - this.$emit('toggleTodo'); - }, - }, -}; -</script> - -<template> - <button - v-tooltip - :class="buttonClasses" - :title="buttonLabel" - :aria-label="buttonLabel" - :data-issuable-id="issuableId" - :data-issuable-type="issuableType" - type="button" - data-container="body" - data-placement="left" - data-boundary="viewport" - @click="handleButtonClick" - > - <icon - v-show="collapsed" - :css-classes="collapsedButtonIconClasses" - :name="collapsedButtonIcon" - /> - <span - v-show="!collapsed" - class="issuable-todo-inner" - > - {{ buttonLabel }} - </span> - <loading-icon - v-show="isActionActive" - :inline="true" - /> - </button> -</template> diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue index 80dc7d3557c..ac2e99abe77 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue @@ -12,11 +12,6 @@ export default { type: Boolean, required: true, }, - cssClasses: { - type: String, - required: false, - default: '', - }, }, computed: { tooltipLabel() { @@ -35,12 +30,10 @@ export default { <button v-tooltip :title="tooltipLabel" - :class="cssClasses" type="button" class="btn btn-blank gutter-toggle btn-sidebar-action" data-container="body" data-placement="left" - data-boundary="viewport" @click="toggle" > <i diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 199039f38f7..3144dcc4dc0 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -23,7 +23,7 @@ } .btn-group { - > a { + > .btn:not(.btn-danger) { color: $gl-text-color-secondary; } diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss index f6617380cc0..f9fd9f1ab8b 100644 --- a/app/assets/stylesheets/pages/issuable.scss +++ b/app/assets/stylesheets/pages/issuable.scss @@ -449,7 +449,6 @@ .todo-undone { color: $gl-link-color; - fill: $gl-link-color; } .author { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c32049e1b33..2af79c511ba 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -116,10 +116,8 @@ .modify-merge-commit-link { padding: 0; - background-color: transparent; border: 0; - color: $gl-text-color; &:hover, @@ -501,10 +499,6 @@ } } -.merge-request-details .content-block { - border-bottom: 0; -} - .mr-source-target { display: flex; flex-wrap: wrap; diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss index 010a2c05a1c..e5d7dd13915 100644 --- a/app/assets/stylesheets/pages/todos.scss +++ b/app/assets/stylesheets/pages/todos.scss @@ -174,18 +174,6 @@ } } -@include media-breakpoint-down(lg) { - .todos-filters { - .filter-categories { - width: 75%; - - .filter-item { - margin-bottom: 10px; - } - } - } -} - @include media-breakpoint-down(xs) { .todo { .avatar { @@ -211,10 +199,6 @@ } .todos-filters { - .filter-categories { - width: auto; - } - .dropdown-menu-toggle { width: 100%; } diff --git a/app/controllers/concerns/todos_actions.rb b/app/controllers/concerns/todos_actions.rb deleted file mode 100644 index c0acdb3498d..00000000000 --- a/app/controllers/concerns/todos_actions.rb +++ /dev/null @@ -1,12 +0,0 @@ -module TodosActions - extend ActiveSupport::Concern - - def create - todo = TodoService.new.mark_todo(issuable, current_user) - - render json: { - count: TodosFinder.new(current_user, state: :pending).execute.count, - delete_path: dashboard_todo_path(todo) - } - end -end diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb index bd7111e28bc..f9e8fe624e8 100644 --- a/app/controllers/dashboard/todos_controller.rb +++ b/app/controllers/dashboard/todos_controller.rb @@ -70,7 +70,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController end def todo_params - params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id) + params.permit(:action_id, :author_id, :project_id, :type, :sort, :state) end def redirect_out_of_range(todos) diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index 395c5336ad5..68353e6a210 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -2,7 +2,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController layout 'project' before_action :authorize_read_environment! before_action :authorize_create_environment!, only: [:new, :create] - before_action :authorize_create_deployment!, only: [:stop] + before_action :authorize_stop_environment!, only: [:stop] before_action :authorize_update_environment!, only: [:edit, :update] before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics] @@ -175,4 +175,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController def environment @environment ||= project.environments.find(params[:id]) end + + def authorize_stop_environment! + access_denied! unless can?(current_user, :stop_environment, environment) + end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 1ad2e93c85f..dc6551fc761 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -192,7 +192,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo deployment = environment.first_deployment_for(@merge_request.diff_head_sha) stop_url = - if environment.stop_action? && can?(current_user, :create_deployment, environment) + if can?(current_user, :stop_environment, environment) stop_project_environment_path(project, environment) end diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb index 93fb9da6510..a41fcb85c40 100644 --- a/app/controllers/projects/todos_controller.rb +++ b/app/controllers/projects/todos_controller.rb @@ -1,13 +1,19 @@ class Projects::TodosController < Projects::ApplicationController - include Gitlab::Utils::StrongMemoize - include TodosActions - before_action :authenticate_user!, only: [:create] + def create + todo = TodoService.new.mark_todo(issuable, current_user) + + render json: { + count: TodosFinder.new(current_user, state: :pending).execute.count, + delete_path: dashboard_todo_path(todo) + } + end + private def issuable - strong_memoize(:issuable) do + @issuable ||= begin case params[:issuable_type] when "issue" IssuesFinder.new(current_user, project_id: @project.id).find(params[:issuable_id]) diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb index 2156413fb26..09e2c586f2a 100644 --- a/app/finders/todos_finder.rb +++ b/app/finders/todos_finder.rb @@ -15,7 +15,6 @@ class TodosFinder prepend FinderWithCrossProjectAccess include FinderMethods - include Gitlab::Utils::StrongMemoize requires_cross_project_access unless: -> { project? } @@ -35,11 +34,9 @@ class TodosFinder items = by_author(items) items = by_state(items) items = by_type(items) - items = by_group(items) # Filtering by project HAS TO be the last because we use # the project IDs yielded by the todos query thus far items = by_project(items) - items = visible_to_user(items) sort(items) end @@ -85,10 +82,6 @@ class TodosFinder params[:project_id].present? end - def group? - params[:group_id].present? - end - def project return @project if defined?(@project) @@ -107,14 +100,18 @@ class TodosFinder @project end - def group - strong_memoize(:group) do - Group.find(params[:group_id]) + def project_ids(items) + ids = items.except(:order).select(:project_id) + if Gitlab::Database.mysql? + # To make UPDATE work on MySQL, wrap it in a SELECT with an alias + ids = Todo.except(:order).select('*').from("(#{ids.to_sql}) AS t") end + + ids end def type? - type.present? && %w(Issue MergeRequest Epic).include?(type) + type.present? && %w(Issue MergeRequest).include?(type) end def type @@ -151,37 +148,12 @@ class TodosFinder def by_project(items) if project? - items = items.where(project: project) - end - - items - end + items.where(project: project) + else + projects = Project.public_or_visible_to_user(current_user) - def by_group(items) - if group? - groups = group.self_and_descendants - items = items.where( - 'project_id IN (?) OR group_id IN (?)', - Project.where(group: groups).select(:id), - groups.select(:id) - ) + items.joins(:project).merge(projects) end - - items - end - - def visible_to_user(items) - projects = Project.public_or_visible_to_user(current_user) - groups = Group.public_or_visible_to_user(current_user) - - items - .joins('LEFT JOIN namespaces ON namespaces.id = todos.group_id') - .joins('LEFT JOIN projects ON projects.id = todos.project_id') - .where( - 'project_id IN (?) OR group_id IN (?)', - projects.select(:id), - groups.select(:id) - ) end def by_state(items) diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index ef1bf283d0c..358b896702b 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -251,6 +251,7 @@ module ApplicationSettingsHelper :user_oauth_applications, :version_check_enabled, :allow_local_requests_from_hooks_and_services, + :hide_third_party_offers, :enforce_terms, :terms, :mirror_available diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index c24d340d184..8fd0b6f14c6 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -4,6 +4,7 @@ module ClustersHelper end def render_gcp_signup_offer + return if Gitlab::CurrentSettings.current_application_settings.hide_third_party_offers? return unless show_gcp_signup_offer? content_tag :section, class: 'no-animate expanded' do diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index 7bbdc798ddd..8766bb43cac 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -131,19 +131,6 @@ module IssuablesHelper end end - def group_dropdown_label(group_id, default_label) - return default_label if group_id.nil? - return "Any group" if group_id == "0" - - group = ::Group.find_by(id: group_id) - - if group - group.full_name - else - default_label - end - end - def milestone_dropdown_label(milestone_title, default_label = "Milestone") title = case milestone_title diff --git a/app/helpers/pipeline_schedules_helper.rb b/app/helpers/pipeline_schedules_helper.rb index 6edaf78de1b..4b9f6bd2caf 100644 --- a/app/helpers/pipeline_schedules_helper.rb +++ b/app/helpers/pipeline_schedules_helper.rb @@ -3,7 +3,7 @@ module PipelineSchedulesHelper ActiveSupport::TimeZone.all.map do |timezone| { name: timezone.name, - offset: timezone.utc_offset, + offset: timezone.now.utc_offset, identifier: timezone.tzinfo.identifier } end diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb index 271e839692a..336385f6798 100644 --- a/app/helpers/time_helper.rb +++ b/app/helpers/time_helper.rb @@ -5,9 +5,13 @@ module TimeHelper seconds = interval_in_seconds - minutes * 60 if minutes >= 1 - "#{pluralize(minutes, "minute")} #{pluralize(seconds, "second")}" + if seconds % 60 == 0 + pluralize(minutes, "minute") + else + [pluralize(minutes, "minute"), pluralize(seconds, "second")].to_sentence + end else - "#{pluralize(seconds, "second")}" + pluralize(seconds, "second") end end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 7cd74358168..f7620e0b6b8 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -43,7 +43,7 @@ module TodosHelper project_commit_path(todo.project, todo.target, anchor: anchor) else - path = [todo.parent, todo.target] + path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] path.unshift(:pipelines) if todo.build_failed? @@ -167,12 +167,4 @@ module TodosHelper def show_todo_state?(todo) (todo.target.is_a?(MergeRequest) || todo.target.is_a?(Issue)) && %w(closed merged).include?(todo.target.state) end - - def todo_group_options - groups = current_user.authorized_groups.map do |group| - { id: group.id, text: group.full_name } - end - - groups.unshift({ id: '', text: 'Any Group' }).to_json - end end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index bddeb8b0352..f770b219422 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -294,6 +294,7 @@ class ApplicationSetting < ActiveRecord::Base gitaly_timeout_medium: 30, gitaly_timeout_default: 55, allow_local_requests_from_hooks_and_services: false, + hide_third_party_offers: false, mirror_available: true } end diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 44103e3bc4f..d8ddb4bc667 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -437,9 +437,9 @@ module Ci end def artifacts_metadata_entry(path, **options) - artifacts_metadata.use_file do |metadata_path| + artifacts_metadata.open do |metadata_stream| metadata = Gitlab::Ci::Build::Artifacts::Metadata.new( - metadata_path, + metadata_stream, path, **options) diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index 7a459078151..b93c1145f82 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -243,12 +243,6 @@ module Issuable opened? end - def overdue? - return false unless respond_to?(:due_date) - - due_date.try(:past?) || false - end - def user_notes_count if notes.loaded? # Use the in-memory association to select and count to avoid hitting the db diff --git a/app/models/concerns/protected_ref_access.rb b/app/models/concerns/protected_ref_access.rb index e3a7f2d5498..71b0c3468b9 100644 --- a/app/models/concerns/protected_ref_access.rb +++ b/app/models/concerns/protected_ref_access.rb @@ -2,19 +2,20 @@ module ProtectedRefAccess extend ActiveSupport::Concern ALLOWED_ACCESS_LEVELS = [ - Gitlab::Access::MASTER, + Gitlab::Access::MAINTAINER, Gitlab::Access::DEVELOPER, Gitlab::Access::NO_ACCESS ].freeze HUMAN_ACCESS_LEVELS = { - Gitlab::Access::MASTER => "Maintainers".freeze, + Gitlab::Access::MAINTAINER => "Maintainers".freeze, Gitlab::Access::DEVELOPER => "Developers + Maintainers".freeze, Gitlab::Access::NO_ACCESS => "No one".freeze }.freeze included do - scope :master, -> { where(access_level: Gitlab::Access::MASTER) } + scope :master, -> { maintainer } # @deprecated + scope :maintainer, -> { where(access_level: Gitlab::Access::MAINTAINER) } scope :developer, -> { where(access_level: Gitlab::Access::DEVELOPER) } validates :access_level, presence: true, if: :role?, inclusion: { diff --git a/app/models/concerns/select_for_project_authorization.rb b/app/models/concerns/select_for_project_authorization.rb index 58194b0ea13..7af0fdbd618 100644 --- a/app/models/concerns/select_for_project_authorization.rb +++ b/app/models/concerns/select_for_project_authorization.rb @@ -6,8 +6,11 @@ module SelectForProjectAuthorization select("projects.id AS project_id, members.access_level") end - def select_as_master_for_project_authorization - select(["projects.id AS project_id", "#{Gitlab::Access::MASTER} AS access_level"]) + def select_as_maintainer_for_project_authorization + select(["projects.id AS project_id", "#{Gitlab::Access::MAINTAINER} AS access_level"]) end + + # @deprecated + alias_method :select_as_master_for_project_authorization, :select_as_maintainer_for_project_authorization end end diff --git a/app/models/group.rb b/app/models/group.rb index b0392774379..ddebaff50b0 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -39,8 +39,6 @@ class Group < Namespace has_many :boards has_many :badges, class_name: 'GroupBadge' - has_many :todos - accepts_nested_attributes_for :variables, allow_destroy: true validate :visibility_level_allowed_by_projects @@ -84,12 +82,6 @@ class Group < Namespace where(id: user.authorized_groups.select(:id).reorder(nil)) end - def public_or_visible_to_user(user) - where('id IN (?) OR namespaces.visibility_level IN (?)', - user.authorized_groups.select(:id), - Gitlab::VisibilityLevel.levels_for_user(user)) - end - def select_for_project_authorization if current_scope.joins_values.include?(:shared_projects) joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id') @@ -186,10 +178,13 @@ class Group < Namespace add_user(user, :developer, current_user: current_user) end - def add_master(user, current_user = nil) - add_user(user, :master, current_user: current_user) + def add_maintainer(user, current_user = nil) + add_user(user, :maintainer, current_user: current_user) end + # @deprecated + alias_method :add_master, :add_maintainer + def add_owner(user, current_user = nil) add_user(user, :owner, current_user: current_user) end @@ -206,12 +201,15 @@ class Group < Namespace members_with_parents.owners.where(user_id: user).any? end - def has_master?(user) + def has_maintainer?(user) return false unless user - members_with_parents.masters.where(user_id: user).any? + members_with_parents.maintainers.where(user_id: user).any? end + # @deprecated + alias_method :has_master?, :has_maintainer? + # Check if user is a last owner of the group. # Parent owners are ignored for nested groups. def last_owner?(user) diff --git a/app/models/issue.rb b/app/models/issue.rb index 983684a5e05..4715d942c8d 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -275,6 +275,10 @@ class Issue < ActiveRecord::Base user ? readable_by?(user) : publicly_visible? end + def overdue? + due_date.try(:past?) || false + end + def check_for_spam? project.public? && (title_changed? || description_changed?) end diff --git a/app/models/member.rb b/app/models/member.rb index 68572f2e33a..00a13a279a9 100644 --- a/app/models/member.rb +++ b/app/models/member.rb @@ -69,9 +69,11 @@ class Member < ActiveRecord::Base scope :guests, -> { active.where(access_level: GUEST) } scope :reporters, -> { active.where(access_level: REPORTER) } scope :developers, -> { active.where(access_level: DEVELOPER) } - scope :masters, -> { active.where(access_level: MASTER) } + scope :maintainers, -> { active.where(access_level: MAINTAINER) } + scope :masters, -> { maintainers } # @deprecated scope :owners, -> { active.where(access_level: OWNER) } - scope :owners_and_masters, -> { active.where(access_level: [OWNER, MASTER]) } + scope :owners_and_maintainers, -> { active.where(access_level: [OWNER, MAINTAINER]) } + scope :owners_and_masters, -> { owners_and_maintainers } # @deprecated scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) } scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) } diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb index 024106056b4..4f27d0aeaf8 100644 --- a/app/models/members/project_member.rb +++ b/app/models/members/project_member.rb @@ -17,19 +17,19 @@ class ProjectMember < Member # Add users to projects with passed access option # # access can be an integer representing a access code - # or symbol like :master representing role + # or symbol like :maintainer representing role # # Ex. # add_users_to_projects( # project_ids, # user_ids, - # ProjectMember::MASTER + # ProjectMember::MAINTAINER # ) # # add_users_to_projects( # project_ids, # user_ids, - # :master + # :maintainer # ) # def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil) diff --git a/app/models/note.rb b/app/models/note.rb index 3918bbee194..abc40d9016e 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -229,10 +229,6 @@ class Note < ActiveRecord::Base !for_personal_snippet? end - def for_issuable? - for_issue? || for_merge_request? - end - def skip_project_check? !for_project_noteable? end diff --git a/app/models/project.rb b/app/models/project.rb index 770262f6193..1894de6ceed 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -269,7 +269,8 @@ class Project < ActiveRecord::Base delegate :name, to: :owner, allow_nil: true, prefix: true delegate :members, to: :team, prefix: true delegate :add_user, :add_users, to: :team - delegate :add_guest, :add_reporter, :add_developer, :add_master, :add_role, to: :team + delegate :add_guest, :add_reporter, :add_developer, :add_maintainer, :add_role, to: :team + delegate :add_master, to: :team # @deprecated delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings # Validations @@ -1647,10 +1648,10 @@ class Project < ActiveRecord::Base params = { name: default_branch, push_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_PUSH ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER }], merge_access_levels_attributes: [{ - access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MASTER + access_level: Gitlab::CurrentSettings.default_branch_protection == Gitlab::Access::PROTECTION_DEV_CAN_MERGE ? Gitlab::Access::DEVELOPER : Gitlab::Access::MAINTAINER }] } diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb index ac1e9ab2b0b..cf8fc41e870 100644 --- a/app/models/project_group_link.rb +++ b/app/models/project_group_link.rb @@ -4,7 +4,8 @@ class ProjectGroupLink < ActiveRecord::Base GUEST = 10 REPORTER = 20 DEVELOPER = 30 - MASTER = 40 + MAINTAINER = 40 + MASTER = MAINTAINER # @deprecated belongs_to :project belongs_to :group diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 9a38806baab..c7d0f49d837 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -19,10 +19,13 @@ class ProjectTeam add_user(user, :developer, current_user: current_user) end - def add_master(user, current_user: nil) - add_user(user, :master, current_user: current_user) + def add_maintainer(user, current_user: nil) + add_user(user, :maintainer, current_user: current_user) end + # @deprecated + alias_method :add_master, :add_maintainer + def add_role(user, role, current_user: nil) public_send(:"add_#{role}", user, current_user: current_user) # rubocop:disable GitlabSecurity/PublicSend end @@ -81,10 +84,13 @@ class ProjectTeam @developers ||= fetch_members(Gitlab::Access::DEVELOPER) end - def masters - @masters ||= fetch_members(Gitlab::Access::MASTER) + def maintainers + @maintainers ||= fetch_members(Gitlab::Access::MAINTAINER) end + # @deprecated + alias_method :masters, :maintainers + def owners @owners ||= if group @@ -136,10 +142,13 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::DEVELOPER end - def master?(user) - max_member_access(user.id) == Gitlab::Access::MASTER + def maintainer?(user) + max_member_access(user.id) == Gitlab::Access::MAINTAINER end + # @deprecated + alias_method :master?, :maintainer? + # Checks if `user` is authorized for this project, with at least the # `min_access_level` (if given). def member?(user, min_access_level = Gitlab::Access::GUEST) diff --git a/app/models/todo.rb b/app/models/todo.rb index 942cbb754e3..a2ab405fdbe 100644 --- a/app/models/todo.rb +++ b/app/models/todo.rb @@ -22,18 +22,15 @@ class Todo < ActiveRecord::Base belongs_to :author, class_name: "User" belongs_to :note belongs_to :project - belongs_to :group belongs_to :target, polymorphic: true, touch: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :user delegate :name, :email, to: :author, prefix: true, allow_nil: true - validates :action, :target_type, :user, presence: true + validates :action, :project, :target_type, :user, presence: true validates :author, presence: true validates :target_id, presence: true, unless: :for_commit? validates :commit_id, presence: true, if: :for_commit? - validates :project, presence: true, unless: :group_id - validates :group, presence: true, unless: :project_id scope :pending, -> { with_state(:pending) } scope :done, -> { with_state(:done) } @@ -47,7 +44,7 @@ class Todo < ActiveRecord::Base state :done end - after_save :keep_around_commit, if: :commit_id + after_save :keep_around_commit class << self # Priority sorting isn't displayed in the dropdown, because we don't show @@ -82,10 +79,6 @@ class Todo < ActiveRecord::Base end end - def parent - project - end - def unmergeable? action == UNMERGEABLE end diff --git a/app/models/user.rb b/app/models/user.rb index 1c5d39db118..4987d01aac6 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -99,7 +99,8 @@ class User < ActiveRecord::Base has_many :group_members, -> { where(requested_at: nil) }, source: 'GroupMember' has_many :groups, through: :group_members has_many :owned_groups, -> { where(members: { access_level: Gitlab::Access::OWNER }) }, through: :group_members, source: :group - has_many :masters_groups, -> { where(members: { access_level: Gitlab::Access::MASTER }) }, through: :group_members, source: :group + has_many :maintainers_groups, -> { where(members: { access_level: Gitlab::Access::MAINTAINER }) }, through: :group_members, source: :group + alias_attribute :masters_groups, :maintainers_groups # Projects has_many :groups_projects, through: :groups, source: :projects @@ -728,7 +729,7 @@ class User < ActiveRecord::Base end def several_namespaces? - owned_groups.any? || masters_groups.any? + owned_groups.any? || maintainers_groups.any? end def namespace_id @@ -974,15 +975,15 @@ class User < ActiveRecord::Base end def manageable_groups - union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), masters_groups.select(:id)]).to_sql + union_sql = Gitlab::SQL::Union.new([owned_groups.select(:id), maintainers_groups.select(:id)]).to_sql # Update this line to not use raw SQL when migrated to Rails 5.2. # Either ActiveRecord or Arel constructions are fine. # This was replaced with the raw SQL construction because of bugs in the arel gem. # Bugs were fixed in arel 9.0.0 (Rails 5.2). - owned_and_master_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection + owned_and_maintainer_groups = Group.where("namespaces.id IN (#{union_sql})") # rubocop:disable GitlabSecurity/SqlInjection - Gitlab::GroupHierarchy.new(owned_and_master_groups).base_and_descendants + Gitlab::GroupHierarchy.new(owned_and_maintainer_groups).base_and_descendants end def namespaces @@ -1023,11 +1024,11 @@ class User < ActiveRecord::Base def ci_owned_runners @ci_owned_runners ||= begin project_runner_ids = Ci::RunnerProject - .where(project: authorized_projects(Gitlab::Access::MASTER)) + .where(project: authorized_projects(Gitlab::Access::MAINTAINER)) .select(:runner_id) group_runner_ids = Ci::RunnerNamespace - .where(namespace_id: owned_or_masters_groups.select(:id)) + .where(namespace_id: owned_or_maintainers_groups.select(:id)) .select(:runner_id) union = Gitlab::SQL::Union.new([project_runner_ids, group_runner_ids]) @@ -1236,11 +1237,14 @@ class User < ActiveRecord::Base !terms_accepted? end - def owned_or_masters_groups - union = Gitlab::SQL::Union.new([owned_groups, masters_groups]) + def owned_or_maintainers_groups + union = Gitlab::SQL::Union.new([owned_groups, maintainers_groups]) Group.from("(#{union.to_sql}) namespaces") end + # @deprecated + alias_method :owned_or_masters_groups, :owned_or_maintainers_groups + protected # override, from Devise::Validatable diff --git a/app/policies/clusters/cluster_policy.rb b/app/policies/clusters/cluster_policy.rb index 1f7c13072b9..b5b24491655 100644 --- a/app/policies/clusters/cluster_policy.rb +++ b/app/policies/clusters/cluster_policy.rb @@ -4,7 +4,7 @@ module Clusters delegate { cluster.project } - rule { can?(:master_access) }.policy do + rule { can?(:maintainer_access) }.policy do enable :update_cluster enable :admin_cluster end diff --git a/app/policies/deploy_token_policy.rb b/app/policies/deploy_token_policy.rb index 7aa9106e8b1..d1b459cfc90 100644 --- a/app/policies/deploy_token_policy.rb +++ b/app/policies/deploy_token_policy.rb @@ -1,10 +1,10 @@ class DeployTokenPolicy < BasePolicy with_options scope: :subject, score: 0 - condition(:master) { @subject.project.team.master?(@user) } + condition(:maintainer) { @subject.project.team.maintainer?(@user) } rule { anonymous }.prevent_all - rule { master }.policy do + rule { maintainer }.policy do enable :create_deploy_token enable :update_deploy_token end diff --git a/app/policies/environment_policy.rb b/app/policies/environment_policy.rb index 375a5535359..978dc3a7c81 100644 --- a/app/policies/environment_policy.rb +++ b/app/policies/environment_policy.rb @@ -1,9 +1,13 @@ class EnvironmentPolicy < BasePolicy delegate { @subject.project } - condition(:stop_action_allowed) do - @subject.stop_action? && can?(:update_build, @subject.stop_action) + condition(:stop_with_deployment_allowed) do + @subject.stop_action? && can?(:create_deployment) && can?(:update_build, @subject.stop_action) end - rule { can?(:create_deployment) & stop_action_allowed }.enable :stop_environment + condition(:stop_with_update_allowed) do + !@subject.stop_action? && can?(:update_environment, @subject) + end + + rule { stop_with_deployment_allowed | stop_with_update_allowed }.enable :stop_environment end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index ded9fe30eff..dc339b71ec7 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -11,7 +11,7 @@ class GroupPolicy < BasePolicy condition(:guest) { access_level >= GroupMember::GUEST } condition(:developer) { access_level >= GroupMember::DEVELOPER } condition(:owner) { access_level >= GroupMember::OWNER } - condition(:master) { access_level >= GroupMember::MASTER } + condition(:maintainer) { access_level >= GroupMember::MAINTAINER } condition(:reporter) { access_level >= GroupMember::REPORTER } condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? } @@ -59,7 +59,7 @@ class GroupPolicy < BasePolicy enable :admin_issue end - rule { master }.policy do + rule { maintainer }.policy do enable :create_projects enable :admin_pipeline enable :admin_build diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index 199bcf92b21..bc49092633f 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -46,7 +46,7 @@ class ProjectPolicy < BasePolicy condition(:developer) { team_access_level >= Gitlab::Access::DEVELOPER } desc "User has maintainer access" - condition(:master) { team_access_level >= Gitlab::Access::MASTER } + condition(:maintainer) { team_access_level >= Gitlab::Access::MAINTAINER } desc "Project is public" condition(:public_project, scope: :subject, score: 0) { project.public? } @@ -123,14 +123,14 @@ class ProjectPolicy < BasePolicy rule { guest }.enable :guest_access rule { reporter }.enable :reporter_access rule { developer }.enable :developer_access - rule { master }.enable :master_access + rule { maintainer }.enable :maintainer_access rule { owner | admin }.enable :owner_access rule { can?(:owner_access) }.policy do enable :guest_access enable :reporter_access enable :developer_access - enable :master_access + enable :maintainer_access enable :change_namespace enable :change_visibility_level @@ -228,7 +228,7 @@ class ProjectPolicy < BasePolicy enable :create_deployment end - rule { can?(:master_access) }.policy do + rule { can?(:maintainer_access) }.policy do enable :push_to_delete_protected_branch enable :update_project_snippet enable :update_environment diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index ba0ae6ba8a0..0fc3f92b151 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -7,7 +7,7 @@ class EnvironmentEntity < Grape::Entity expose :external_url expose :environment_type expose :last_deployment, using: DeploymentEntity - expose :stop_action? + expose :stop_action?, as: :has_stop_action expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| metrics_project_environment_path(environment.project, environment) @@ -31,4 +31,14 @@ class EnvironmentEntity < Grape::Entity end expose :created_at, :updated_at + + expose :can_stop do |environment| + environment.available? && can?(current_user, :stop_environment, environment) + end + + private + + def current_user + request.current_user + end end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 8c6221af788..d7be9a925b5 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -274,9 +274,9 @@ class NotificationService def new_access_request(member) return true unless member.notifiable?(:subscription) - recipients = member.source.members.active_without_invites_and_requests.owners_and_masters - if fallback_to_group_owners_masters?(recipients, member) - recipients = member.source.group.members.active_without_invites_and_requests.owners_and_masters + recipients = member.source.members.active_without_invites_and_requests.owners_and_maintainers + if fallback_to_group_owners_maintainers?(recipients, member) + recipients = member.source.group.members.active_without_invites_and_requests.owners_and_maintainers end recipients.each { |recipient| deliver_access_request_email(recipient, member) } @@ -519,7 +519,7 @@ class NotificationService return [] unless project - notifiable_users(project.team.masters, :watch, target: project) + notifiable_users(project.team.maintainers, :watch, target: project) end def notifiable?(*args) @@ -534,7 +534,7 @@ class NotificationService mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.notification_email).deliver_later end - def fallback_to_group_owners_masters?(recipients, member) + def fallback_to_group_owners_maintainers?(recipients, member) return false if recipients.present? member.source.respond_to?(:group) && member.source.group diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb index 172497b8e67..85491089d8e 100644 --- a/app/services/projects/create_service.rb +++ b/app/services/projects/create_service.rb @@ -115,7 +115,7 @@ module Projects @project.group.refresh_members_authorized_projects(blocking: false) current_user.refresh_authorized_projects else - @project.add_master(@project.namespace.owner, current_user: current_user) + @project.add_maintainer(@project.namespace.owner, current_user: current_user) end end diff --git a/app/services/protected_branches/access_level_params.rb b/app/services/protected_branches/access_level_params.rb index 253ae8b0124..4658b0e850d 100644 --- a/app/services/protected_branches/access_level_params.rb +++ b/app/services/protected_branches/access_level_params.rb @@ -14,7 +14,7 @@ module ProtectedBranches private def params_with_default(params) - params[:"#{type}_access_level"] ||= Gitlab::Access::MASTER if use_default_access_level?(params) + params[:"#{type}_access_level"] ||= Gitlab::Access::MAINTAINER if use_default_access_level?(params) params end diff --git a/app/services/protected_branches/legacy_api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb index e358fd0374e..bb7656489c5 100644 --- a/app/services/protected_branches/legacy_api_create_service.rb +++ b/app/services/protected_branches/legacy_api_create_service.rb @@ -9,14 +9,14 @@ module ProtectedBranches if params.delete(:developers_can_push) Gitlab::Access::DEVELOPER else - Gitlab::Access::MASTER + Gitlab::Access::MAINTAINER end merge_access_level = if params.delete(:developers_can_merge) Gitlab::Access::DEVELOPER else - Gitlab::Access::MASTER + Gitlab::Access::MAINTAINER end @params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }], diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb index 33176253ca2..1df38de0e4a 100644 --- a/app/services/protected_branches/legacy_api_update_service.rb +++ b/app/services/protected_branches/legacy_api_update_service.rb @@ -17,14 +17,14 @@ module ProtectedBranches when true params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false - params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }] + params[:push_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }] end case @developers_can_merge when true params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::DEVELOPER }] when false - params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MASTER }] + params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }] end service = ProtectedBranches::UpdateService.new(@project, @current_user, @params) diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb index 46f12086555..f91cd03bf5c 100644 --- a/app/services/todo_service.rb +++ b/app/services/todo_service.rb @@ -260,15 +260,15 @@ class TodoService end end - def create_mention_todos(parent, target, author, note = nil, skip_users = []) + def create_mention_todos(project, target, author, note = nil, skip_users = []) # Create Todos for directly addressed users - directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users) - attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note) + directly_addressed_users = filter_directly_addressed_users(project, note || target, author, skip_users) + attributes = attributes_for_todo(project, target, author, Todo::DIRECTLY_ADDRESSED, note) create_todos(directly_addressed_users, attributes) # Create Todos for mentioned users - mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users) - attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note) + mentioned_users = filter_mentioned_users(project, note || target, author, skip_users) + attributes = attributes_for_todo(project, target, author, Todo::MENTIONED, note) create_todos(mentioned_users, attributes) end @@ -299,36 +299,36 @@ class TodoService def attributes_for_todo(project, target, author, action, note = nil) attributes_for_target(target).merge!( - project_id: project&.id, + project_id: project.id, author_id: author.id, action: action, note: note ) end - def filter_todo_users(users, parent, target) - reject_users_without_access(users, parent, target).uniq + def filter_todo_users(users, project, target) + reject_users_without_access(users, project, target).uniq end - def filter_mentioned_users(parent, target, author, skip_users = []) + def filter_mentioned_users(project, target, author, skip_users = []) mentioned_users = target.mentioned_users(author) - skip_users - filter_todo_users(mentioned_users, parent, target) + filter_todo_users(mentioned_users, project, target) end - def filter_directly_addressed_users(parent, target, author, skip_users = []) + def filter_directly_addressed_users(project, target, author, skip_users = []) directly_addressed_users = target.directly_addressed_users(author) - skip_users - filter_todo_users(directly_addressed_users, parent, target) + filter_todo_users(directly_addressed_users, project, target) end - def reject_users_without_access(users, parent, target) - if target.is_a?(Note) && target.for_issuable? + def reject_users_without_access(users, project, target) + if target.is_a?(Note) && (target.for_issue? || target.for_merge_request?) target = target.noteable end if target.is_a?(Issuable) select_users(users, :"read_#{target.to_ability_name}", target) else - select_users(users, :read_project, parent) + select_users(users, :read_project, project) end end diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb index 7919f126075..719bd6ef418 100644 --- a/app/uploaders/gitlab_uploader.rb +++ b/app/uploaders/gitlab_uploader.rb @@ -71,6 +71,28 @@ class GitlabUploader < CarrierWave::Uploader::Base File.join('/', self.class.base_dir, dynamic_segment, filename) end + def cached_size + size + end + + def open + stream = + if file_storage? + File.open(path, "rb") if path + else + ::Gitlab::HttpIO.new(url, cached_size) if url + end + + return unless stream + return stream unless block_given? + + begin + yield(stream) + ensure + stream.close + end + end + private # Designed to be overridden by child uploaders that have a dynamic path diff --git a/app/uploaders/job_artifact_uploader.rb b/app/uploaders/job_artifact_uploader.rb index 855cf2fc21c..f6af023e0f9 100644 --- a/app/uploaders/job_artifact_uploader.rb +++ b/app/uploaders/job_artifact_uploader.rb @@ -18,14 +18,6 @@ class JobArtifactUploader < GitlabUploader dynamic_segment end - def open - if file_storage? - File.open(path, "rb") if path - else - ::Gitlab::Ci::Trace::HttpIO.new(url, cached_size) if url - end - end - private def dynamic_segment diff --git a/app/views/admin/application_settings/_third_party_offers.html.haml b/app/views/admin/application_settings/_third_party_offers.html.haml new file mode 100644 index 00000000000..c5d775d4bf5 --- /dev/null +++ b/app/views/admin/application_settings/_third_party_offers.html.haml @@ -0,0 +1,13 @@ +- application_setting = local_assigns.fetch(:application_setting) + += form_for application_setting, url: admin_application_settings_path, html: { class: 'fieldset-form' } do |f| + = form_errors(application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :hide_third_party_offers, class: 'form-check-input' + = f.label :hide_third_party_offers, class: 'form-check-label' do + Do not display offers from third parties within GitLab + + = f.submit 'Save changes', class: "btn btn-success" diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml index bd43504dd37..5cb8001a364 100644 --- a/app/views/admin/application_settings/show.html.haml +++ b/app/views/admin/application_settings/show.html.haml @@ -325,5 +325,16 @@ .settings-content = render partial: 'repository_mirrors_form' +%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/pseudonymizer_settings', expanded: expanded 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 37fb8fbab26..3ae9ce6c11f 100644 --- a/app/views/ci/runner/_how_to_setup_runner.html.haml +++ b/app/views/ci/runner/_how_to_setup_runner.html.haml @@ -5,7 +5,7 @@ %ol %li = _("Install a Runner compatible with GitLab CI") - = (_("(checkout the %{link} for information on how to install it).") % { link: link }).html_safe + = (_("(check out the %{link} for information on how to install it).") % { link: link }).html_safe %li = _("Specify the following URL during the Runner setup:") %code#coordinator_address= root_url(only_path: false) diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml index 8b3974d97f8..d5a9cc646a6 100644 --- a/app/views/dashboard/todos/index.html.haml +++ b/app/views/dashboard/todos/index.html.haml @@ -30,33 +30,27 @@ .todos-filters .row-content-block.second-block - = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form d-sm-flex' do - .filter-categories.flex-fill - .filter-item.inline - - if params[:group_id].present? - = hidden_field_tag(:group_id, params[:group_id]) - = dropdown_tag(group_dropdown_label(params[:group_id], 'Group'), options: { toggle_class: 'js-group-search js-filter-submit', title: 'Filter by group', filter: true, filterInput: 'input#group-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-group js-filter-submit', - placeholder: 'Search groups', data: { data: todo_group_options, default_label: 'Group', display: 'static' } }) - .filter-item.inline - - if params[:project_id].present? - = hidden_field_tag(:project_id, params[:project_id]) - = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', - placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } }) - .filter-item.inline - - if params[:author_id].present? - = hidden_field_tag(:author_id, params[:author_id]) - = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', - placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } }) - .filter-item.inline - - if params[:type].present? - = hidden_field_tag(:type, params[:type]) - = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', - data: { data: todo_types_options, default_label: 'Type' } }) - .filter-item.inline.actions-filter - - if params[:action_id].present? - = hidden_field_tag(:action_id, params[:action_id]) - = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', - data: { data: todo_actions_options, default_label: 'Action' } }) + = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do + .filter-item.inline + - if params[:project_id].present? + = hidden_field_tag(:project_id, params[:project_id]) + = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit', + placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project', display: 'static' } }) + .filter-item.inline + - if params[:author_id].present? + = hidden_field_tag(:author_id, params[:author_id]) + = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit', + placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author', todo_filter: true, todo_state_filter: params[:state] || 'pending' } }) + .filter-item.inline + - if params[:type].present? + = hidden_field_tag(:type, params[:type]) + = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit', + data: { data: todo_types_options, default_label: 'Type' } }) + .filter-item.inline.actions-filter + - if params[:action_id].present? + = hidden_field_tag(:action_id, params[:action_id]) + = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit', + data: { data: todo_actions_options, default_label: 'Action' } }) .filter-item.sort-filter .dropdown %button.dropdown-menu-toggle.dropdown-menu-toggle-sort{ type: 'button', 'data-toggle' => 'dropdown' } diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml index 62402c32e08..4c73da4c75b 100644 --- a/app/views/layouts/nav/sidebar/_admin.html.haml +++ b/app/views/layouts/nav/sidebar/_admin.html.haml @@ -6,14 +6,14 @@ = sprite_icon('admin', size: 24) .sidebar-context-title Admin Area %ul.sidebar-top-level-items - = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do + = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers cohorts conversational_development_index), 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 cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do + = nav_link(controller: %w(dashboard admin projects users groups jobs runners gitaly_servers cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do = link_to admin_root_path do %strong.fly-out-top-item-name #{ _('Overview') } @@ -42,6 +42,10 @@ = link_to admin_runners_path, title: 'Runners' do %span Runners + = nav_link(controller: :gitaly_servers) do + = link_to admin_gitaly_servers_path, title: 'Gitaly Servers' do + %span + Gitaly Servers = nav_link path: 'cohorts#index' do = link_to admin_cohorts_path, title: 'Cohorts' do %span diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index 00d75b3399b..33de74dbaa2 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -122,7 +122,7 @@ = render_if_exists 'projects/sidebar/issues_service_desk' = nav_link(controller: :milestones) do - = link_to project_milestones_path(@project), title: 'Milestones' do + = link_to project_milestones_path(@project), title: 'Milestones', class: 'qa-milestones-link' do %span = _('Milestones') - if project_nav_tab? :external_issue_tracker diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml index e0ecf56525a..f4c91377ecb 100644 --- a/app/views/projects/deployments/_actions.haml +++ b/app/views/projects/deployments/_actions.haml @@ -3,13 +3,12 @@ - if actions.present? .btn-group .dropdown - %button.dropdown.dropdown-new.btn.btn-default{ type: 'button', 'data-toggle' => 'dropdown' } - = custom_icon('icon_play') + %button.dropdown.dropdown-new.btn.btn-default.has-tooltip{ type: 'button', 'data-toggle' => 'dropdown', title: s_('Environments|Deploy to...') } + = sprite_icon('play') = icon('caret-down') %ul.dropdown-menu.dropdown-menu-right - actions.each do |action| - next unless can?(current_user, :update_build, action) %li - = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow' do - = custom_icon('icon_play') + = link_to [:play, @project.namespace.becomes(Namespace), @project, action], method: :post, rel: 'nofollow', class: 'btn' do %span= action.name.humanize diff --git a/app/views/projects/deployments/_rollback.haml b/app/views/projects/deployments/_rollback.haml index 95f950948ab..281e042c915 100644 --- a/app/views/projects/deployments/_rollback.haml +++ b/app/views/projects/deployments/_rollback.haml @@ -1,6 +1,7 @@ - if can?(current_user, :create_deployment, deployment) && deployment.deployable - = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build' do + - tooltip = deployment.last? ? s_('Environments|Re-deploy to environment') : s_('Environments|Rollback environment') + = link_to [:retry, @project.namespace.becomes(Namespace), @project, deployment.deployable], method: :post, class: 'btn btn-build has-tooltip', title: tooltip do - if deployment.last? - = _("Re-deploy") + = sprite_icon('repeat') - else - = _("Rollback") + = sprite_icon('redo') diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml index a264252e095..4694bc39d54 100644 --- a/app/views/projects/environments/_external_url.html.haml +++ b/app/views/projects/environments/_external_url.html.haml @@ -1,4 +1,4 @@ - if environment.external_url && can?(current_user, :read_environment, environment) - = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do + = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url has-tooltip', title: s_('Environments|Open live environment') do = sprite_icon('external-link') View deployment diff --git a/app/views/projects/environments/_stop.html.haml b/app/views/projects/environments/_stop.html.haml deleted file mode 100644 index c35f9af2873..00000000000 --- a/app/views/projects/environments/_stop.html.haml +++ /dev/null @@ -1,5 +0,0 @@ -- if can?(current_user, :create_deployment, environment) && environment.stop_action? - .inline - = link_to stop_project_environment_path(@project, environment), method: :post, - class: 'btn stop-env-link', rel: 'nofollow', data: { confirm: 'Are you sure you want to stop this environment?' } do - = icon('stop', class: 'stop-env-icon') diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml index add394a6356..a33bc9d4ce6 100644 --- a/app/views/projects/environments/show.html.haml +++ b/app/views/projects/environments/show.html.haml @@ -4,6 +4,33 @@ - page_title "Environments" %div{ class: container_class } + - if can?(current_user, :stop_environment, @environment) + #stop-environment-modal.modal.fade{ tabindex: -1 } + .modal-dialog + .modal-content + .modal-header + %h4.modal-title.d-flex.mw-100 + Stopping + %span.has-tooltip.text-truncate.ml-1.mr-1.flex-fill{ title: @environment.name, data: { container: '#stop-environment-modal' } } + = @environment.name + ? + .modal-body + %p= s_('Environments|Are you sure you want to stop this environment?') + - unless @environment.stop_action? + .warning_message + %p= s_('Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file.').html_safe % { emphasis_start: '<strong>'.html_safe, + emphasis_end: '</strong>'.html_safe, + ci_config_link_start: '<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">'.html_safe, + ci_config_link_end: '</a>'.html_safe } + %a{ href: 'https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment', + target: '_blank', + rel: 'noopener noreferrer' } + = s_('Environments|Learn more about stopping environments') + .modal-footer + = button_tag _('Cancel'), type: 'button', class: 'btn btn-cancel', data: { dismiss: 'modal' } + = button_to stop_project_environment_path(@project, @environment), class: 'btn btn-danger has-tooltip', method: :post do + = s_('Environments|Stop environment') + .row.top-area.adjust .col-md-7 %h3.page-title= @environment.name @@ -15,7 +42,10 @@ - if can?(current_user, :update_environment, @environment) = link_to 'Edit', edit_project_environment_path(@project, @environment), class: 'btn' - if can?(current_user, :stop_environment, @environment) - = link_to 'Stop', stop_project_environment_path(@project, @environment), data: { confirm: 'Are you sure you want to stop this environment?' }, class: 'btn btn-danger', method: :post + = button_tag class: 'btn btn-danger', type: 'button', data: { toggle: 'modal', + target: '#stop-environment-modal' } do + = sprite_icon('stop') + = s_('Environments|Stop') .environments-container - if @deployments.blank? diff --git a/app/views/projects/merge_requests/_mr_box.html.haml b/app/views/projects/merge_requests/_mr_box.html.haml index 8a390cf8700..1a9ab288683 100644 --- a/app/views/projects/merge_requests/_mr_box.html.haml +++ b/app/views/projects/merge_requests/_mr_box.html.haml @@ -1,4 +1,4 @@ -.detail-page-description.content-block +.detail-page-description %h2.title = markdown_field(@merge_request, :title) diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml index ace094a671a..28f0a167128 100644 --- a/app/views/projects/milestones/_form.html.haml +++ b/app/views/projects/milestones/_form.html.haml @@ -7,12 +7,12 @@ .form-group.row = f.label :title, "Title", class: "col-form-label col-sm-2" .col-sm-10 - = f.text_field :title, maxlength: 255, class: "form-control", required: true, autofocus: true + = f.text_field :title, maxlength: 255, class: "qa-milestone-title form-control", required: true, autofocus: true .form-group.row.milestone-description = f.label :description, "Description", class: "col-form-label col-sm-2" .col-sm-10 = render layout: 'projects/md_preview', locals: { url: preview_markdown_path(@project) } do - = render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: 'Write milestone description...' + = render 'projects/zen', f: f, attr: :description, classes: 'qa-milestone-description note-textarea', placeholder: 'Write milestone description...' = render 'shared/notes/hints' .clearfix .error-alert @@ -20,7 +20,7 @@ .form-actions - if @milestone.new_record? - = f.submit 'Create milestone', class: "btn-create btn" + = f.submit 'Create milestone', class: "btn-create 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" diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml index 5b0197ed58c..26d2ea8447b 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", title: 'New milestone' do + = link_to new_project_milestone_path(@project), class: "btn btn-new qa-new-project-milestone", title: 'New milestone' do New milestone .milestones diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml index f7b04c436a6..2a9e20c2caa 100644 --- a/app/views/projects/milestones/show.html.haml +++ b/app/views/projects/milestones/show.html.haml @@ -60,7 +60,7 @@ = icon('angle-double-left') .detail-page-description.milestone-detail - %h2.title + %h2.title.qa-milestone-title = markdown_field(@milestone, :title) %div diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml index ac2ebb701a5..d38d161047b 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{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } } + %a.btn.btn-new.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 + %button.btn.btn-new.new-project-item-select-button.qa-new-project-item-select-button = icon('caret-down') diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index 37625a4a163..c2da363b8c6 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -7,7 +7,7 @@ - dropdown_title = local_assigns.fetch(:dropdown_title, "Filter by milestone") - if selected.present? || params[:milestone_title].present? = hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) -= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", += dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do - if project %ul.dropdown-footer-list diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index bd87bb38e77..3b017c62a80 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -18,7 +18,7 @@ = form.label :milestone_id, "Milestone", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" .col-sm-10{ class: ("col-md-8" if has_due_date) } .issuable-form-select-holder - = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" + = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: "Select milestone" .form-group.row - has_labels = @labels && @labels.any? = form.label :label_ids, "Labels", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}" diff --git a/app/views/shared/notes/_form.html.haml b/app/views/shared/notes/_form.html.haml index 6b2715b47a7..c360f1ffe2a 100644 --- a/app/views/shared/notes/_form.html.haml +++ b/app/views/shared/notes/_form.html.haml @@ -40,5 +40,5 @@ = yield(:note_actions) - %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Discard draft" } } + %a.btn.btn-cancel.js-note-discard{ role: "button", data: {cancel_text: "Cancel" } } Discard draft diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb index fd49bc18161..2d381c6fd6c 100644 --- a/app/workers/git_garbage_collect_worker.rb +++ b/app/workers/git_garbage_collect_worker.rb @@ -65,10 +65,10 @@ class GitGarbageCollectWorker client.repack_incremental end rescue GRPC::NotFound => e - Gitlab::GitLogger.error("#{method} failed:\nRepository not found") + Gitlab::GitLogger.error("#{__method__} failed:\nRepository not found") raise Gitlab::Git::Repository::NoRepository.new(e) rescue GRPC::BadStatus => e - Gitlab::GitLogger.error("#{method} failed:\n#{e}") + Gitlab::GitLogger.error("#{__method__} failed:\n#{e}") raise Gitlab::Git::CommandError.new(e) end diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb index ed39b4a1ea8..c9f6df9b56d 100644 --- a/app/workers/process_commit_worker.rb +++ b/app/workers/process_commit_worker.rb @@ -79,9 +79,10 @@ class ProcessCommitWorker # Avoid reprocessing commits that already exist in the upstream # when project is forked. This will also prevent duplicated system notes. def commit_exists_in_upstream?(project, commit_hash) - return false unless project.forked? + upstream_project = project.fork_source + + return false unless upstream_project - upstream_project = project.forked_from_project commit_id = commit_hash.with_indifferent_access[:id] upstream_project.commit(commit_id).present? end |