diff options
Diffstat (limited to 'app')
86 files changed, 758 insertions, 207 deletions
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js index aeb88715c11..3826ecd1ac1 100644 --- a/app/assets/javascripts/awards_handler.js +++ b/app/assets/javascripts/awards_handler.js @@ -8,6 +8,7 @@ import { updateTooltipTitle } from './lib/utils/common_utils'; import { isInVueNoteablePage } from './lib/utils/dom_utils'; import flash from './flash'; import axios from './lib/utils/axios_utils'; +import bp from './breakpoints'; const animationEndEventString = 'animationend webkitAnimationEnd MSAnimationEnd oAnimationEnd'; const transitionEndEventString = 'transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd'; @@ -264,7 +265,10 @@ export class AwardsHandler { const css = { top: `${$addBtn.offset().top + $addBtn.outerHeight()}px`, }; - if (position === 'right') { + // for xs screen we position the element on center + if (bp.getBreakpointSize() === 'xs') { + css.left = '5%'; + } else if (position === 'right') { css.left = `${$addBtn.offset().left - $menu.outerWidth() + 20}px`; $menu.addClass('is-aligned-right'); } else { diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js index fc9286d15e6..bfb073fdcdc 100644 --- a/app/assets/javascripts/behaviors/markdown/render_gfm.js +++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js @@ -4,6 +4,7 @@ import renderMath from './render_math'; import renderMermaid from './render_mermaid'; import highlightCurrentUser from './highlight_current_user'; import initUserPopovers from '../../user_popovers'; +import initMRPopovers from '../../mr_popover'; // Render GitLab flavoured Markdown // @@ -15,6 +16,7 @@ $.fn.renderGFM = function renderGFM() { renderMermaid(this.find('.js-render-mermaid')); highlightCurrentUser(this.find('.gfm-project_member').get()); initUserPopovers(this.find('.gfm-project_member').get()); + initMRPopovers(this.find('.gfm-merge_request').get()); return this; }; diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 388f674f643..c95d7608e37 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -12,6 +12,8 @@ import { REQUEST_FAILURE, UPGRADE_REQUESTED, UPGRADE_REQUEST_FAILURE, + INGRESS, + INGRESS_DOMAIN_SUFFIX, } from './constants'; import ClustersService from './services/clusters_service'; import ClustersStore from './stores/clusters_store'; @@ -76,6 +78,10 @@ export default class Clusters { this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); this.showTokenButton = document.querySelector('.js-show-cluster-token'); this.tokenField = document.querySelector('.js-cluster-token'); + this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text'); + this.ingressDomainSnippet = this.ingressDomainHelpText.querySelector( + '.js-ingress-domain-snippet', + ); Clusters.initDismissableCallout(); initSettingsPanels(); @@ -182,6 +188,10 @@ export default class Clusters { this.checkForNewInstalls(prevApplicationMap, this.store.state.applications); this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason); + this.toggleIngressDomainHelpText( + prevApplicationMap[INGRESS], + this.store.state.applications[INGRESS], + ); } showToken() { @@ -277,6 +287,16 @@ export default class Clusters { this.store.updateAppProperty(appId, 'requestStatus', null); } + toggleIngressDomainHelpText(ingressPreviousState, ingressNewState) { + const helpTextHidden = ingressNewState.status !== APPLICATION_STATUS.INSTALLED; + const domainSnippetText = `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`; + + if (ingressPreviousState.status !== ingressNewState.status) { + this.ingressDomainHelpText.classList.toggle('hide', helpTextHidden); + this.ingressDomainSnippet.textContent = domainSnippetText; + } + } + saveKnativeDomain(data) { const appId = data.id; this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 5b206b82fe0..d54f9ce552c 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -393,7 +393,6 @@ export default { <div slot="description" v-html="prometheusDescription"></div> </application-row> <application-row - v-if="isProjectCluster" id="runner" :logo-url="gitlabLogo" :title="applications.runner.title" @@ -409,9 +408,9 @@ export default { > <div slot="description"> {{ - s__(`ClusterIntegration|GitLab Runner connects to this - project's repository and executes CI/CD jobs, - pushing results back and deploying, + s__(`ClusterIntegration|GitLab Runner connects to the + repository and executes CI/CD jobs, + pushing results back and deploying applications to production.`) }} </div> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js index 39022879d91..67f481f2afb 100644 --- a/app/assets/javascripts/clusters/constants.js +++ b/app/assets/javascripts/clusters/constants.js @@ -28,3 +28,4 @@ export const JUPYTER = 'jupyter'; export const KNATIVE = 'knative'; export const RUNNER = 'runner'; export const CERT_MANAGER = 'cert_manager'; +export const INGRESS_DOMAIN_SUFFIX = '.nip.io'; diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue index caf0df8a4e3..c60246bf8ef 100644 --- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue +++ b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue @@ -140,7 +140,7 @@ export default { :id="line.left.line_code" :class="parallelViewLeftLineType" class="line_content parallel left-side" - @mousedown.native="handleParallelLineMouseDown" + @mousedown="handleParallelLineMouseDown" v-html="line.left.rich_text" ></td> </template> @@ -171,7 +171,7 @@ export default { }, ]" class="line_content parallel right-side" - @mousedown.native="handleParallelLineMouseDown" + @mousedown="handleParallelLineMouseDown" v-html="line.right.rich_text" ></td> </template> diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 29dc2d6a8a3..aa50fd8ff62 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -244,7 +244,7 @@ export default { <gl-loading-icon v-if="isLoading" :label="s__('GroupsTree|Loading groups')" - :size="2" + size="md" class="loading-animation prepend-top-20" /> <groups-component diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 2b44438f849..9161eb3d9b1 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -38,8 +38,8 @@ export default { }, }, computed: { - ...mapState('commit', ['commitAction']), - ...mapGetters('commit', ['newBranchName']), + ...mapState('commit', ['commitAction', 'newBranchName']), + ...mapGetters('commit', ['placeholderBranchName']), tooltipTitle() { return this.disabled ? this.title : ''; }, @@ -73,7 +73,8 @@ export default { </label> <div v-if="commitAction === value && showInput" class="ide-commit-new-branch"> <input - :placeholder="newBranchName" + :placeholder="placeholderBranchName" + :value="newBranchName" type="text" class="form-control monospace" @input="updateBranchName($event.target.value)" diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue index ba6bbdfef4b..412b07553dc 100644 --- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue +++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue @@ -29,7 +29,7 @@ export default { return this.name || (entryPath ? `${entryPath}/` : ''); }, set(val) { - this.name = val; + this.name = val.trim(); }, }, modalTitle() { diff --git a/app/assets/javascripts/ide/stores/modules/commit/getters.js b/app/assets/javascripts/ide/stores/modules/commit/getters.js index 03777e6c10b..bbe40b2ec2f 100644 --- a/app/assets/javascripts/ide/stores/modules/commit/getters.js +++ b/app/assets/javascripts/ide/stores/modules/commit/getters.js @@ -14,7 +14,7 @@ const createTranslatedTextForFiles = (files, text) => { export const discardDraftButtonDisabled = state => state.commitMessage === '' || state.submitCommitLoading; -export const newBranchName = (state, _, rootState) => +export const placeholderBranchName = (state, _, rootState) => `${gon.current_username}-${rootState.currentBranchId}-patch-${`${new Date().getTime()}`.substr( -BRANCH_SUFFIX_COUNT, )}`; @@ -25,7 +25,7 @@ export const branchName = (state, getters, rootState) => { state.commitAction === consts.COMMIT_TO_NEW_BRANCH_MR ) { if (state.newBranchName === '') { - return getters.newBranchName; + return getters.placeholderBranchName; } return state.newBranchName; diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index 7076a79dd5d..b651a6e4bfb 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -39,7 +39,7 @@ export default { </gl-link> <clipboard-button - :text="commit.short_id" + :text="commit.id" :title="__('Copy commit SHA to clipboard')" css-class="btn btn-clipboard btn-transparent" /> diff --git a/app/assets/javascripts/lib/utils/autosave.js b/app/assets/javascripts/lib/utils/autosave.js new file mode 100644 index 00000000000..023c336db02 --- /dev/null +++ b/app/assets/javascripts/lib/utils/autosave.js @@ -0,0 +1,32 @@ +import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; + +export const clearDraft = autosaveKey => { + try { + window.localStorage.removeItem(`autosave/${autosaveKey}`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +export const getDraft = autosaveKey => { + try { + return window.localStorage.getItem(`autosave/${autosaveKey}`); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + return null; + } +}; + +export const updateDraft = (autosaveKey, text) => { + try { + window.localStorage.setItem(`autosave/${autosaveKey}`, text); + } catch (e) { + // eslint-disable-next-line no-console + console.error(e); + } +}; + +export const getDiscussionReplyKey = (noteableType, discussionId) => + ['Note', capitalizeFirstCharacter(noteableType), discussionId, 'Reply'].join('/'); diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 895a57785bc..7883a3f9abc 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -1,4 +1,5 @@ <script> +import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { s__ } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import Flash from '../../flash'; @@ -17,6 +18,8 @@ export default { GraphGroup, EmptyState, Icon, + GlDropdown, + GlDropdownItem, }, props: { hasMetrics: { @@ -157,28 +160,21 @@ export default { <template> <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default"> <div class="environments d-flex align-items-center"> - {{ s__('Metrics|Environment') }} - <div class="dropdown prepend-left-10"> - <button class="dropdown-menu-toggle" data-toggle="dropdown" type="button"> - <span>{{ currentEnvironmentName }}</span> - <icon name="chevron-down" /> - </button> - <div - v-if="store.environmentsData.length > 0" - class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up" + <strong>{{ s__('Metrics|Environment') }}</strong> + <gl-dropdown + class="prepend-left-10 js-environments-dropdown" + toggle-class="dropdown-menu-toggle" + :text="currentEnvironmentName" + :disabled="store.environmentsData.length === 0" + > + <gl-dropdown-item + v-for="environment in store.environmentsData" + :key="environment.id" + :active="environment.name === currentEnvironmentName" + active-class="is-active" + >{{ environment.name }}</gl-dropdown-item > - <ul> - <li v-for="environment in store.environmentsData" :key="environment.id"> - <a - :href="environment.metrics_path" - :class="{ 'is-active': environment.name == currentEnvironmentName }" - class="dropdown-item" - >{{ environment.name }}</a - > - </li> - </ul> - </div> - </div> + </gl-dropdown> </div> <graph-group v-for="(groupData, index) in store.groups" diff --git a/app/assets/javascripts/mr_popover/components/mr_popover.vue b/app/assets/javascripts/mr_popover/components/mr_popover.vue new file mode 100644 index 00000000000..8e2d8fa816a --- /dev/null +++ b/app/assets/javascripts/mr_popover/components/mr_popover.vue @@ -0,0 +1,110 @@ +<script> +import { GlPopover, GlSkeletonLoading } from '@gitlab/ui'; +import Icon from '../../vue_shared/components/icon.vue'; +import CiIcon from '../../vue_shared/components/ci_icon.vue'; +import timeagoMixin from '../../vue_shared/mixins/timeago'; +import query from '../queries/merge_request.graphql'; +import { mrStates, humanMRStates } from '../constants'; + +export default { + name: 'MRPopover', + components: { + GlPopover, + GlSkeletonLoading, + Icon, + CiIcon, + }, + mixins: [timeagoMixin], + props: { + target: { + type: HTMLAnchorElement, + required: true, + }, + projectPath: { + type: String, + required: true, + }, + mergeRequestIID: { + type: String, + required: true, + }, + mergeRequestTitle: { + type: String, + required: true, + }, + }, + data() { + return { + mergeRequest: {}, + }; + }, + computed: { + detailedStatus() { + return this.mergeRequest.headPipeline && this.mergeRequest.headPipeline.detailedStatus; + }, + formattedTime() { + return this.timeFormated(this.mergeRequest.createdAt); + }, + statusBoxClass() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return 'status-box-mr-merged'; + case mrStates.closed: + return 'status-box-closed'; + default: + return 'status-box-open'; + } + }, + stateHumanName() { + switch (this.mergeRequest.state) { + case mrStates.merged: + return humanMRStates.merged; + case mrStates.closed: + return humanMRStates.closed; + default: + return humanMRStates.open; + } + }, + showDetails() { + return Object.keys(this.mergeRequest).length > 0; + }, + }, + apollo: { + mergeRequest: { + query, + update: data => data.project.mergeRequest, + variables() { + const { projectPath, mergeRequestIID } = this; + + return { + projectPath, + mergeRequestIID, + }; + }, + }, + }, +}; +</script> + +<template> + <gl-popover :target="target" boundary="viewport" placement="top" show> + <div class="mr-popover"> + <div v-if="$apollo.loading"> + <gl-skeleton-loading :lines="1" class="animation-container-small mt-1" /> + </div> + <div v-else-if="showDetails" class="d-flex align-items-center justify-content-between"> + <div class="d-inline-flex align-items-center"> + <div :class="`issuable-status-box status-box ${statusBoxClass}`"> + {{ stateHumanName }} + </div> + <span class="text-secondary">Opened <time v-text="formattedTime"></time></span> + </div> + <ci-icon v-if="detailedStatus" :status="detailedStatus" /> + </div> + <h5 class="my-2">{{ mergeRequestTitle }}</h5> + <div class="text-secondary"> + {{ `${projectPath}!${mergeRequestIID}` }} + </div> + </div> + </gl-popover> +</template> diff --git a/app/assets/javascripts/mr_popover/constants.js b/app/assets/javascripts/mr_popover/constants.js new file mode 100644 index 00000000000..433df844c80 --- /dev/null +++ b/app/assets/javascripts/mr_popover/constants.js @@ -0,0 +1,10 @@ +export const mrStates = { + merged: 'merged', + closed: 'closed', +}; + +export const humanMRStates = { + merged: 'Merged', + closed: 'Closed', + open: 'Open', +}; diff --git a/app/assets/javascripts/mr_popover/index.js b/app/assets/javascripts/mr_popover/index.js new file mode 100644 index 00000000000..cc686b401d2 --- /dev/null +++ b/app/assets/javascripts/mr_popover/index.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import MRPopover from './components/mr_popover.vue'; +import createDefaultClient from '~/lib/graphql'; + +let renderedPopover; +let renderFn; + +const handleUserPopoverMouseOut = ({ target }) => { + target.removeEventListener('mouseleave', handleUserPopoverMouseOut); + + if (renderFn) { + clearTimeout(renderFn); + } + if (renderedPopover) { + renderedPopover.$destroy(); + renderedPopover = null; + } +}; + +/** + * Adds a MergeRequestPopover component to the body, hands over as much data as the target element has in data attributes. + * loads based on data-project-path and data-iid more data about an MR from the API and sets it on the popover + */ +const handleMRPopoverMount = apolloProvider => ({ target }) => { + // Add listener to actually remove it again + target.addEventListener('mouseleave', handleUserPopoverMouseOut); + + const { projectPath, mrTitle, iid } = target.dataset; + const mergeRequest = {}; + + renderFn = setTimeout(() => { + const MRPopoverComponent = Vue.extend(MRPopover); + renderedPopover = new MRPopoverComponent({ + propsData: { + target, + projectPath, + mergeRequestIID: iid, + mergeRequest, + mergeRequestTitle: mrTitle, + }, + apolloProvider, + }); + + renderedPopover.$mount(); + }, 200); // 200ms delay so not every mouseover triggers Popover + API Call +}; + +export default elements => { + const mrLinks = elements || [...document.querySelectorAll('.gfm-merge_request')]; + if (mrLinks.length > 0) { + Vue.use(VueApollo); + + const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), + }); + + mrLinks.forEach(el => { + el.addEventListener('mouseenter', handleMRPopoverMount(apolloProvider)); + }); + } +}; diff --git a/app/assets/javascripts/mr_popover/queries/merge_request.graphql b/app/assets/javascripts/mr_popover/queries/merge_request.graphql new file mode 100644 index 00000000000..0bb9bc03bc7 --- /dev/null +++ b/app/assets/javascripts/mr_popover/queries/merge_request.graphql @@ -0,0 +1,14 @@ +query mergeRequest($projectPath: ID!, $mergeRequestIID: ID!) { + project(fullPath: $projectPath) { + mergeRequest(iid: $mergeRequestIID) { + createdAt + state + headPipeline { + detailedStatus { + icon + group + } + } + } + } +} diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue index 92258a25438..57d6b181bd7 100644 --- a/app/assets/javascripts/notes/components/note_form.vue +++ b/app/assets/javascripts/notes/components/note_form.vue @@ -7,6 +7,7 @@ import markdownField from '../../vue_shared/components/markdown/field.vue'; import issuableStateMixin from '../mixins/issuable_state'; import resolvable from '../mixins/resolvable'; import { __ } from '~/locale'; +import { getDraft, updateDraft } from '~/lib/utils/autosave'; export default { name: 'NoteForm', @@ -65,10 +66,21 @@ export default { required: false, default: '', }, + autosaveKey: { + type: String, + required: false, + default: '', + }, }, data() { + let updatedNoteBody = this.noteBody; + + if (!updatedNoteBody && this.autosaveKey) { + updatedNoteBody = getDraft(this.autosaveKey) || ''; + } + return { - updatedNoteBody: this.noteBody, + updatedNoteBody, conflictWhileEditing: false, isSubmitting: false, isResolving: this.resolveDiscussion, @@ -175,6 +187,12 @@ export default { // Sends information about confirm message and if the textarea has changed this.$emit('cancelForm', shouldConfirm, this.noteBody !== this.updatedNoteBody); }, + onInput() { + if (this.autosaveKey) { + const { autosaveKey, updatedNoteBody: text } = this; + updateDraft(autosaveKey, text); + } + }, }, }; </script> @@ -218,6 +236,7 @@ export default { @keydown.ctrl.enter="handleKeySubmit()" @keydown.up="editMyLastNote()" @keydown.esc="cancelHandler(true)" + @input="onInput" ></textarea> </markdown-field> <div class="note-form-actions clearfix"> diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue index fc51998935d..a3d664a738f 100644 --- a/app/assets/javascripts/notes/components/noteable_discussion.vue +++ b/app/assets/javascripts/notes/components/noteable_discussion.vue @@ -4,6 +4,7 @@ import { mapActions, mapGetters } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, __, sprintf } from '~/locale'; +import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave'; import systemNote from '~/vue_shared/components/notes/system_note.vue'; import icon from '~/vue_shared/components/icon.vue'; import diffLineNoteFormMixin from 'ee_else_ce/notes/mixins/diff_line_note_form'; @@ -21,7 +22,6 @@ import noteForm from './note_form.vue'; import diffWithNote from './diff_with_note.vue'; import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue'; import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue'; -import autosave from '../mixins/autosave'; import noteable from '../mixins/noteable'; import resolvable from '../mixins/resolvable'; import discussionNavigation from '../mixins/discussion_navigation'; @@ -54,7 +54,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, - mixins: [autosave, noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], + mixins: [noteable, resolvable, discussionNavigation, diffLineNoteFormMixin], props: { discussion: { type: Object, @@ -87,13 +87,10 @@ export default { }, }, data() { - const { diff_discussion: isDiffDiscussion, resolved } = this.discussion; - return { isReplying: false, isResolving: false, resolveAsThread: true, - isRepliesCollapsed: Boolean(!isDiffDiscussion && resolved), }; }, computed: { @@ -106,7 +103,10 @@ export default { 'showJumpToNextDiscussion', ]), author() { - return this.initialDiscussion.author; + return this.firstNote.author; + }, + autosaveKey() { + return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id); }, canReply() { return this.getNoteableData.current_user.can_create_note; @@ -117,7 +117,7 @@ export default { hasReplies() { return this.discussion.notes.length > 1; }, - initialDiscussion() { + firstNote() { return this.discussion.notes.slice(0, 1)[0]; }, replies() { @@ -175,11 +175,11 @@ export default { return ''; }, - shouldShowDiscussions() { - const { expanded, resolved } = this.discussion; - const isResolvedNonDiffDiscussion = !this.discussion.diff_discussion && resolved; - - return expanded || this.alwaysExpanded || isResolvedNonDiffDiscussion; + isExpanded() { + return this.discussion.expanded || this.alwaysExpanded; + }, + shouldHideDiscussionBody() { + return this.shouldRenderDiffs && !this.isExpanded; }, actionText() { const linkStart = `<a href="${_.escape(this.discussion.discussion_path)}">`; @@ -242,18 +242,6 @@ export default { return !this.discussionResolved && this.discussion.resolve_with_issue_path; }, }, - watch: { - isReplying() { - if (this.isReplying) { - this.$nextTick(() => { - // Pass an extra key to separate reply and note edit forms - this.initAutoSave({ ...this.initialDiscussion, ...this.discussion }, ['Reply']); - }); - } else { - this.disposeAutoSave(); - } - }, - }, created() { eventHub.$on('startReplying', this.onStartReplying); }, @@ -291,9 +279,6 @@ export default { toggleDiscussionHandler() { this.toggleDiscussion({ discussionId: this.discussion.id }); }, - toggleReplies() { - this.isRepliesCollapsed = !this.isRepliesCollapsed; - }, showReplyForm() { this.isReplying = true; }, @@ -312,7 +297,7 @@ export default { } this.isReplying = false; - this.resetAutoSave(); + clearDraft(this.autosaveKey); }, saveReply(noteText, form, callback) { const postData = { @@ -338,7 +323,7 @@ export default { this.isReplying = false; this.saveNote(replyData) .then(() => { - this.resetAutoSave(); + clearDraft(this.autosaveKey); callback(); }) .catch(err => { @@ -390,8 +375,8 @@ Please check your network connection and try again.`; <div class="timeline-content"> <note-header :author="author" - :created-at="initialDiscussion.created_at" - :note-id="initialDiscussion.id" + :created-at="firstNote.created_at" + :note-id="firstNote.id" :include-toggle="true" :expanded="discussion.expanded" @toggleHandler="toggleDiscussionHandler" @@ -414,7 +399,7 @@ Please check your network connection and try again.`; /> </div> </div> - <div v-if="shouldShowDiscussions" class="discussion-body"> + <div v-if="!shouldHideDiscussionBody" class="discussion-body"> <component :is="wrapperComponent" v-bind="wrapperComponentProps" @@ -424,8 +409,8 @@ Please check your network connection and try again.`; <ul class="notes"> <template v-if="shouldGroupReplies"> <component - :is="componentName(initialDiscussion)" - :note="componentData(initialDiscussion)" + :is="componentName(firstNote)" + :note="componentData(firstNote)" :line="line" :commit="commit" :help-page-path="helpPagePath" @@ -445,11 +430,11 @@ Please check your network connection and try again.`; </component> <toggle-replies-widget v-if="hasReplies" - :collapsed="isRepliesCollapsed" + :collapsed="!isExpanded" :replies="replies" - @toggle="toggleReplies" + @toggle="toggleDiscussionHandler" /> - <template v-if="!isRepliesCollapsed"> + <template v-if="isExpanded"> <component :is="componentName(note)" v-for="note in replies" @@ -476,7 +461,7 @@ Please check your network connection and try again.`; </template> </ul> <div - v-if="!isRepliesCollapsed || !hasReplies" + v-if="isExpanded || !hasReplies" :class="{ 'is-replying': isReplying }" class="discussion-reply-holder" > @@ -512,6 +497,7 @@ Please check your network connection and try again.`; :is-editing="false" :line="diffLine" save-button-title="Comment" + :autosave-key="autosaveKey" @handleFormUpdateAddToReview="addReplyToReview" @handleFormUpdate="saveReply" @cancelForm="cancelReplyForm" diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue index 9e63aa00341..f5a1ff2f6fd 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue @@ -1,5 +1,6 @@ <script> /* eslint-disable vue/require-default-prop */ +import { GlTooltipDirective, GlLink } from '@gitlab/ui'; import { sprintf, __ } from '~/locale'; import PipelineStage from '~/pipelines/components/stage.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -14,9 +15,13 @@ export default { CiIcon, Icon, TooltipOnTruncate, + GlLink, LinkedPipelinesMiniList: () => import('ee_component/vue_shared/components/linked_pipelines_mini_list.vue'), }, + directives: { + GlTooltip: GlTooltipDirective, + }, mixins: [mrWidgetPipelineMixin], props: { pipeline: { @@ -78,12 +83,18 @@ export default { false, ); }, + isTriggeredByMergeRequest() { + return Boolean(this.pipeline.merge_request); + }, + isMergeRequestPipeline() { + return Boolean(this.pipeline.flags && this.pipeline.flags.merge_request_pipeline); + }, }, }; </script> <template> - <div v-if="hasPipeline || hasCIError" class="ci-widget media"> + <div v-if="hasPipeline || hasCIError" class="ci-widget media js-ci-widget"> <template v-if="hasCIError"> <div class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default" @@ -99,21 +110,58 @@ export default { <div class="ci-widget-container d-flex"> <div class="ci-widget-content"> <div class="media-body"> - <div class="font-weight-bold"> - Pipeline - <a :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" - >#{{ pipeline.id }}</a + <div class="font-weight-bold js-pipeline-info-container"> + {{ s__('Pipeline|Pipeline') }} + <gl-link :href="pipeline.path" class="pipeline-id font-weight-normal pipeline-number" + >#{{ pipeline.id }}</gl-link > {{ pipeline.details.status.label }} <template v-if="hasCommitInfo"> - for - <a + {{ s__('Pipeline|for') }} + <gl-link :href="pipeline.commit.commit_path" class="commit-sha js-commit-link font-weight-normal" - >{{ pipeline.commit.short_id }}</a + >{{ pipeline.commit.short_id }}</gl-link > - on + {{ s__('Pipeline|on') }} + <template v-if="isTriggeredByMergeRequest"> + <gl-link + v-gl-tooltip + :href="pipeline.merge_request.path" + :title="pipeline.merge_request.title" + class="font-weight-normal" + >!{{ pipeline.merge_request.iid }}</gl-link + > + {{ s__('Pipeline|with') }} + <tooltip-on-truncate + :title="pipeline.merge_request.source_branch" + truncate-target="child" + class="label-branch label-truncate" + > + <gl-link + :href="pipeline.merge_request.source_branch_path" + class="font-weight-normal" + >{{ pipeline.merge_request.source_branch }}</gl-link + > + </tooltip-on-truncate> + + <template v-if="isMergeRequestPipeline"> + {{ s__('Pipeline|into') }} + <tooltip-on-truncate + :title="pipeline.merge_request.target_branch" + truncate-target="child" + class="label-branch label-truncate" + > + <gl-link + :href="pipeline.merge_request.target_branch_path" + class="font-weight-normal" + >{{ pipeline.merge_request.target_branch }}</gl-link + > + </tooltip-on-truncate> + </template> + </template> <tooltip-on-truncate + v-else :title="sourceBranch" truncate-target="child" class="label-branch label-truncate" @@ -121,7 +169,9 @@ export default { /> </template> </div> - <div v-if="pipeline.coverage" class="coverage">Coverage {{ pipeline.coverage }}%</div> + <div v-if="pipeline.coverage" class="coverage"> + {{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}% + </div> </div> </div> <div> diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue index e6f0a1c69cd..25f80219993 100644 --- a/app/assets/javascripts/vue_shared/components/ci_icon.vue +++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue @@ -22,6 +22,7 @@ import Icon from '../../vue_shared/components/icon.vue'; * - Jobs show view header * - Jobs show view sidebar * - Linked pipelines + * - Extended MR Popover */ const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 7a53d053eec..216f6c62e69 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -53,7 +53,7 @@ export default { <template> <button :class="containerClass" :disabled="loading || disabled" type="button" @click="onClick"> - <transition name="fade"> + <transition name="fade-in"> <gl-loading-icon v-if="loading" :inline="true" @@ -63,7 +63,7 @@ export default { class="js-loading-button-icon" /> </transition> - <transition name="fade"> + <transition name="fade-in"> <slot> <span v-if="label" class="js-loading-button-label"> {{ label }} </span> </slot> diff --git a/app/assets/stylesheets/components/popover.scss b/app/assets/stylesheets/components/popover.scss index 2f4d30fe923..7d46b262a69 100644 --- a/app/assets/stylesheets/components/popover.scss +++ b/app/assets/stylesheets/components/popover.scss @@ -7,3 +7,10 @@ line-height: $gl-line-height; } } + +.mr-popover { + .text-secondary { + font-size: 12px; + line-height: 1.33; + } +} diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index af79a4d9392..37a729c7a63 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -61,6 +61,10 @@ border: 0; } + &.avatar-placeholder { + border: 0; + } + &:not([href]):hover { border-color: darken($gray-normal, 10%); } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index a4af84f8d27..695ce014659 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -443,7 +443,8 @@ border-color: transparent; } - &.btn-secondary-hover-link { + &.btn-secondary-hover-link, + &.btn-default-hover-link { color: $gl-text-color-secondary; &:hover, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index f5ed6621c55..1e025b3a67d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -304,7 +304,9 @@ } } -.caret-down { +.caret-down, +.btn .caret-down { + top: 0; height: 11px; width: 11px; margin-left: 4px; diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss index 1a74e06a75d..298610a0631 100644 --- a/app/assets/stylesheets/framework/lists.scss +++ b/app/assets/stylesheets/framework/lists.scss @@ -156,6 +156,12 @@ ul.content-list { margin-top: 3px; margin-bottom: 4px; + &.btn-ldap-override { + @include media-breakpoint-up(sm) { + margin-bottom: 0; + } + } + &.has-tooltip, &:last-child { margin-right: 0; diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss index d6c4e68f68f..de2cd600623 100644 --- a/app/assets/stylesheets/framework/markdown_area.scss +++ b/app/assets/stylesheets/framework/markdown_area.scss @@ -61,6 +61,10 @@ padding-top: 0; line-height: 19px; + &.btn.btn-sm { + padding: 2px 5px; + } + &:focus { margin-top: -10px; padding-top: 10px; @@ -150,7 +154,7 @@ } - table:not(.js-syntax-highlight) { + table { @include markdown-table; } } diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss index 955ae80cd58..9e192cbe3fc 100644 --- a/app/assets/stylesheets/framework/mixins.scss +++ b/app/assets/stylesheets/framework/mixins.scss @@ -29,15 +29,14 @@ display: block; overflow-x: auto; border: 0; - border-color: $gl-gray-100; tr { th { - border-bottom: solid 2px $gl-gray-100; + border-bottom: solid 2px $gl-gray-200; } td { - border-color: $gl-gray-100; + border-color: $gl-gray-200; } } } diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss index 3a117106cff..cd3d6f8297e 100644 --- a/app/assets/stylesheets/framework/panels.scss +++ b/app/assets/stylesheets/framework/panels.scss @@ -7,7 +7,6 @@ margin-bottom: $gl-vert-padding; } - .card-header { padding: $gl-vert-padding $gl-padding; line-height: 36px; diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss index 19640ab5986..31297b9d20c 100644 --- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss +++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss @@ -181,6 +181,33 @@ margin: 0; width: 100%; } + + &.inline { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + + > .btn, + > .btn-container, + > .dropdown, + > input, + > form { + flex: 1 1 auto; + margin: 0 0 10px; + margin-left: $gl-padding-top; + width: auto; + + &:first-child { + margin-left: 0; + float: none; + } + } + + .btn-full { + flex: 1 1 100%; + margin-left: 0; + } + } } } diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss index ac673eafdc7..81ccea1e01f 100644 --- a/app/assets/stylesheets/framework/selects.scss +++ b/app/assets/stylesheets/framework/selects.scss @@ -264,6 +264,16 @@ } } +.project-result { + .project-name { + font-weight: $gl-font-weight-bold; + } + + .project-path { + color: $gl-gray-400; + } +} + .user-result { min-height: 24px; display: flex; diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss index 55ce0d7004e..5e5e8bcc3d6 100644 --- a/app/assets/stylesheets/framework/typography.scss +++ b/app/assets/stylesheets/framework/typography.scss @@ -136,15 +136,21 @@ margin: 0 0 16px; } - table:not(.js-syntax-highlight) { + table { @extend .table; @extend .table-bordered; margin: 16px 0; color: $gl-text-color; border: 0; - th { - background: $label-gray-bg; + tr { + th { + border-bottom: solid 2px $gl-gray-200; + } + + td { + border-color: $gl-gray-200; + } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 08dbe3d5b0f..efebbd124d0 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -23,6 +23,7 @@ $darken-border-dashed-factor: 25%; $white-light: #fff; $white-normal: #f0f0f0; $white-dark: #eaeaea; +$white-transparent: rgba(255, 255, 255, 0.8); $gray-lightest: #fdfdfd; $gray-light: #fafafa; @@ -288,6 +289,10 @@ $gl-line-height: 16px; $gl-line-height-24: 24px; $gl-line-height-14: 14px; +$system-header-height: 35px; +$issue-box-upcoming-bg: #8f8f8f; +$pages-group-name-color: #4c4e54; + /* * Common component specific colors */ @@ -410,7 +415,7 @@ $award-emoji-menu-shadow: rgba(0, 0, 0, 0.175); $award-emoji-positive-add-bg: #fed159; $award-emoji-positive-add-lines: #bb9c13; $award-emoji-width: 376px; -$award-emoji-width-xs: 300px; +$award-emoji-width-xs: 90%; /* * Search Box @@ -626,6 +631,18 @@ Animation Functions $dropdown-animation-timing: cubic-bezier(0.23, 1, 0.32, 1); /* +GitLab Plans +*/ +$gl-gold-plan: #d4af37; +$gl-silver-plan: #91a1ab; +$gl-bronze-plan: #cd7f32; + +/* +Cross-project Pipelines + */ +$linked-project-column-margin: 60px; + +/* Performance Bar */ $perf-bar-production: #222; @@ -649,6 +666,17 @@ $image-comment-cursor-left-offset: 12; $image-comment-cursor-top-offset: 12; /* +Add GitLab Slack Application +*/ +$add-to-slack-popup-max-width: 400px; +$add-to-slack-gif-max-width: 850px; +$add-to-slack-well-max-width: 750px; +$add-to-slack-logo-size: 100px; +$double-headed-arrow-width: 100px; +$double-headed-arrow-height: 25px; +$right-arrow-size: 16px; + +/* Popup */ $popup-triangle-size: 15px; diff --git a/app/assets/stylesheets/framework/vue_transitions.scss b/app/assets/stylesheets/framework/vue_transitions.scss index e07a177e153..e3bdc0b0199 100644 --- a/app/assets/stylesheets/framework/vue_transitions.scss +++ b/app/assets/stylesheets/framework/vue_transitions.scss @@ -1,9 +1,13 @@ .fade-enter-active, -.fade-leave-active { +.fade-leave-active, +.fade-in-enter-active, +.fade-out-leave-active { transition: opacity $sidebar-transition-duration $general-hover-transition-curve; } .fade-enter, +.fade-in-enter, +.fade-out-leave-to, .fade-leave-to { opacity: 0; } diff --git a/app/assets/stylesheets/notify.scss b/app/assets/stylesheets/notify.scss index f24c80bd81c..d77b7dfad68 100644 --- a/app/assets/stylesheets/notify.scss +++ b/app/assets/stylesheets/notify.scss @@ -1,4 +1,4 @@ -@import "framework/variables"; +@import 'framework/variables'; img { max-width: 100%; diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss index 8ade995525a..0a07747e0d4 100644 --- a/app/assets/stylesheets/pages/groups.scss +++ b/app/assets/stylesheets/pages/groups.scss @@ -15,6 +15,11 @@ word-wrap: nowrap; } +.content-list .group-name { + font-weight: $gl-font-weight-bold; + color: $pages-group-name-color; +} + .group-row { @include basic-list-stats; @@ -172,6 +177,50 @@ } } +.card { + .shared_runners_limit_under_quota { + color: $green-500; + } + + .shared_runners_limit_over_quota { + color: $red-500; + } +} + +.pipeline-quota { + border-top: 1px solid $table-border-color; + border-bottom: 1px solid $table-border-color; + margin: 0 0 $gl-padding; + + .row { + padding-top: 10px; + padding-bottom: 10px; + } + + .right { + text-align: right; + } + + .progress { + height: 6px; + width: 100%; + margin-bottom: 0; + margin-top: 4px; + } +} + +.user-settings-pipeline-quota { + margin-top: $gl-padding; + + .pipeline-quota { + border-top: 0; + } +} + +table.pipeline-project-metrics tr td { + padding: $gl-padding; +} + .mattermost-icon svg { width: 16px; height: 16px; diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss index 75d219320ef..6f98b4f7f13 100644 --- a/app/assets/stylesheets/pages/labels.scss +++ b/app/assets/stylesheets/pages/labels.scss @@ -34,7 +34,7 @@ .dropdown-new-label { .dropdown-content { - max-height: 136px; + max-height: initial; } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index 3ca8e943a3a..49608a3964f 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -235,6 +235,7 @@ $status-box-line-height: 26px; padding: 0; } + .popover-body, .popover-content { padding: 0; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index faf85e151e3..9c72dcbc54c 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -44,6 +44,7 @@ $note-form-margin-left: 72px; border: 1px solid $border-color; border-radius: $border-radius-default; margin: $gl-padding 0; + overflow: auto; &.system-note, &.note-form { @@ -224,14 +225,9 @@ $note-form-margin-left: 72px; overflow-y: hidden; .note-text { - @include md-typography; // Reset ul style types since we're nested inside a ul already @include bulleted-list; word-wrap: break-word; - - table { - @include markdown-table; - } } } diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss index 8e53876eb4f..bcb306d97d5 100644 --- a/app/assets/stylesheets/pages/projects.scss +++ b/app/assets/stylesheets/pages/projects.scss @@ -18,12 +18,9 @@ } .input-group { - display: flex; - .select2-container { display: unset; max-width: unset; - width: unset !important; flex-grow: 1; } diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss index 54126577f93..e4ed685bd1b 100644 --- a/app/assets/stylesheets/pages/settings.scss +++ b/app/assets/stylesheets/pages/settings.scss @@ -216,6 +216,31 @@ } } +.nested-settings { + padding-left: 20px; +} + +.input-btn-group { + display: flex; + + .input-large { + flex: 1; + } + + .btn { + margin-left: 10px; + } +} + +.settings-flex-row { + display: flex; + align-items: center; + + .float-right { + margin-left: auto; + } +} + .prometheus-metrics-monitoring { .card { .card-toggle { @@ -246,6 +271,27 @@ } } + .custom-monitored-metrics { + .card-title { + display: flex; + align-items: center; + + > .btn-success { + margin-left: auto; + } + } + + .custom-metric { + display: flex; + align-items: center; + } + + .custom-metric-link-bold { + font-weight: $gl-font-weight-bold; + text-decoration: none; + } + } + .loading-metrics, .empty-metrics { padding: 30px 10px; @@ -280,6 +326,12 @@ } } +.saml-settings.info-well { + .form-control[readonly] { + background: $white-light; + } +} + .modal-doorkeepr-auth { .modal-body { padding: $gl-padding; diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb index 189fee98aa0..383ec2a7d16 100644 --- a/app/controllers/admin/appearances_controller.rb +++ b/app/controllers/admin/appearances_controller.rb @@ -14,7 +14,7 @@ class Admin::AppearancesController < Admin::ApplicationController @appearance = Appearance.new(appearance_params) if @appearance.save - redirect_to admin_appearances_path, notice: 'Appearance was successfully created.' + redirect_to admin_appearances_path, notice: _('Appearance was successfully created.') else render action: 'show' end @@ -22,7 +22,7 @@ class Admin::AppearancesController < Admin::ApplicationController def update if @appearance.update(appearance_params) - redirect_to admin_appearances_path, notice: 'Appearance was successfully updated.' + redirect_to admin_appearances_path, notice: _('Appearance was successfully updated.') else render action: 'show' end @@ -33,21 +33,21 @@ class Admin::AppearancesController < Admin::ApplicationController @appearance.save - redirect_to admin_appearances_path, notice: 'Logo was successfully removed.' + redirect_to admin_appearances_path, notice: _('Logo was successfully removed.') end def header_logos @appearance.remove_header_logo! @appearance.save - redirect_to admin_appearances_path, notice: 'Header logo was successfully removed.' + redirect_to admin_appearances_path, notice: _('Header logo was successfully removed.') end def favicon @appearance.remove_favicon! @appearance.save - redirect_to admin_appearances_path, notice: 'Favicon was successfully removed.' + redirect_to admin_appearances_path, notice: _('Favicon was successfully removed.') end private diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index 8f267eccc8a..ab792cf7403 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -48,7 +48,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController respond_to do |format| if successful format.json { head :ok } - format.html { redirect_to redirect_path, notice: 'Application settings saved successfully' } + format.html { redirect_to redirect_path, notice: _('Application settings saved successfully') } else format.json { head :bad_request } format.html { render :show } @@ -70,13 +70,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController def reset_registration_token @application_setting.reset_runners_registration_token! - flash[:notice] = 'New runners registration token has been generated!' + flash[:notice] = _('New runners registration token has been generated!') redirect_to admin_runners_path end def reset_health_check_token @application_setting.reset_health_check_access_token! - flash[:notice] = 'New health check access token has been generated!' + flash[:notice] = _('New health check access token has been generated!') redirect_back_or_default end @@ -85,7 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController redirect_to( admin_application_settings_path, - notice: 'Started asynchronous removal of all repository check states.' + notice: _('Started asynchronous removal of all repository check states.') ) end diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb index 6fc336714b6..3648c8be426 100644 --- a/app/controllers/admin/applications_controller.rb +++ b/app/controllers/admin/applications_controller.rb @@ -34,7 +34,7 @@ class Admin::ApplicationsController < Admin::ApplicationController def update if @application.update(application_params) - redirect_to admin_application_path(@application), notice: 'Application was successfully updated.' + redirect_to admin_application_path(@application), notice: _('Application was successfully updated.') else render :edit end @@ -42,7 +42,7 @@ class Admin::ApplicationsController < Admin::ApplicationController def destroy @application.destroy - redirect_to admin_applications_url, status: 302, notice: 'Application was successfully destroyed.' + redirect_to admin_applications_url, status: 302, notice: _('Application was successfully destroyed.') end private diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb index a91d9a534cd..6e5dd1a1f55 100644 --- a/app/controllers/admin/broadcast_messages_controller.rb +++ b/app/controllers/admin/broadcast_messages_controller.rb @@ -19,7 +19,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController @broadcast_message = BroadcastMessage.new(broadcast_message_params) if @broadcast_message.save - redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully created.' + redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully created.') else render :index end @@ -27,7 +27,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController def update if @broadcast_message.update(broadcast_message_params) - redirect_to admin_broadcast_messages_path, notice: 'Broadcast Message was successfully updated.' + redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully updated.') else render :edit end diff --git a/app/controllers/admin/deploy_keys_controller.rb b/app/controllers/admin/deploy_keys_controller.rb index 49ce275ad14..180f7d4c803 100644 --- a/app/controllers/admin/deploy_keys_controller.rb +++ b/app/controllers/admin/deploy_keys_controller.rb @@ -25,7 +25,7 @@ class Admin::DeployKeysController < Admin::ApplicationController def update if deploy_key.update(update_params) - flash[:notice] = 'Deploy key was successfully updated.' + flash[:notice] = _('Deploy key was successfully updated.') redirect_to admin_deploy_keys_path else render 'edit' diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 46e85e1424f..e0ecdb0c0e9 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -36,7 +36,7 @@ class Admin::GroupsController < Admin::ApplicationController if @group.save @group.add_owner(current_user) - redirect_to [:admin, @group], notice: "Group '#{@group.name}' was successfully created." + redirect_to [:admin, @group], notice: _('Group %{group_name} was successfully created.') % { group_name: @group.name } else render "new" end @@ -44,7 +44,7 @@ class Admin::GroupsController < Admin::ApplicationController def update if @group.update(group_params) - redirect_to [:admin, @group], notice: 'Group was successfully updated.' + redirect_to [:admin, @group], notice: _('Group was successfully updated.') else render "edit" end @@ -55,7 +55,7 @@ class Admin::GroupsController < Admin::ApplicationController result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group) if result[:status] == :success - redirect_to [:admin, @group], notice: 'Users were successfully added.' + redirect_to [:admin, @group], notice: _('Users were successfully added.') else redirect_to [:admin, @group], alert: result[:message] end @@ -66,7 +66,7 @@ class Admin::GroupsController < Admin::ApplicationController redirect_to admin_groups_path, status: 302, - alert: "Group '#{@group.name}' was scheduled for deletion." + alert: _('Group %{group_name} was scheduled for deletion.') % { group_name: @group.name } end private diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb index d0abdec50ae..51b0f45c5be 100644 --- a/app/controllers/admin/hooks_controller.rb +++ b/app/controllers/admin/hooks_controller.rb @@ -14,7 +14,7 @@ class Admin::HooksController < Admin::ApplicationController @hook = SystemHook.new(hook_params.to_h) if @hook.save - redirect_to admin_hooks_path, notice: 'Hook was successfully created.' + redirect_to admin_hooks_path, notice: _('Hook was successfully created.') else @hooks = SystemHook.all render :index @@ -26,7 +26,7 @@ class Admin::HooksController < Admin::ApplicationController def update if hook.update(hook_params) - flash[:notice] = 'System hook was successfully updated.' + flash[:notice] = _('System hook was successfully updated.') redirect_to admin_hooks_path else render 'edit' diff --git a/app/controllers/admin/identities_controller.rb b/app/controllers/admin/identities_controller.rb index b51c2f678ca..f518f7a657f 100644 --- a/app/controllers/admin/identities_controller.rb +++ b/app/controllers/admin/identities_controller.rb @@ -13,7 +13,7 @@ class Admin::IdentitiesController < Admin::ApplicationController @identity.user_id = user.id if @identity.save - redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully created.' + redirect_to admin_user_identities_path(@user), notice: _('User identity was successfully created.') else render :new end @@ -29,7 +29,7 @@ class Admin::IdentitiesController < Admin::ApplicationController def update if @identity.update(identity_params) RepairLdapBlockedUserService.new(@user).execute - redirect_to admin_user_identities_path(@user), notice: 'User identity was successfully updated.' + redirect_to admin_user_identities_path(@user), notice: _('User identity was successfully updated.') else render :edit end @@ -38,9 +38,9 @@ class Admin::IdentitiesController < Admin::ApplicationController def destroy if @identity.destroy RepairLdapBlockedUserService.new(@user).execute - redirect_to admin_user_identities_path(@user), status: 302, notice: 'User identity was successfully removed.' + redirect_to admin_user_identities_path(@user), status: 302, notice: _('User identity was successfully removed.') else - redirect_to admin_user_identities_path(@user), status: 302, alert: 'Failed to remove user identity.' + redirect_to admin_user_identities_path(@user), status: 302, alert: _('Failed to remove user identity.') end end diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb index 706bcc1e549..cfe29d734b7 100644 --- a/app/controllers/admin/impersonation_tokens_controller.rb +++ b/app/controllers/admin/impersonation_tokens_controller.rb @@ -12,7 +12,7 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController if @impersonation_token.save PersonalAccessToken.redis_store!(current_user.id, @impersonation_token.token) - redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created." + redirect_to admin_user_impersonation_tokens_path, notice: _("A new impersonation token has been created.") else set_index_vars render :index @@ -23,9 +23,9 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController @impersonation_token = finder.find(params[:id]) if @impersonation_token.revoke! - flash[:notice] = "Revoked impersonation token #{@impersonation_token.name}!" + flash[:notice] = _("Revoked impersonation token %{token_name}!") % { token_name: @impersonation_token.name } else - flash[:alert] = "Could not revoke impersonation token #{@impersonation_token.name}." + flash[:alert] = _("Could not revoke impersonation token %{token_name}.") % { token_name: @impersonation_token.name } end redirect_to admin_user_impersonation_tokens_path diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb index 4e9262ccc96..340eecd7632 100644 --- a/app/controllers/admin/keys_controller.rb +++ b/app/controllers/admin/keys_controller.rb @@ -17,9 +17,9 @@ class Admin::KeysController < Admin::ApplicationController respond_to do |format| if key.destroy - format.html { redirect_to keys_admin_user_path(user), status: 302, notice: 'User key was successfully removed.' } + format.html { redirect_to keys_admin_user_path(user), status: 302, notice: _('User key was successfully removed.') } else - format.html { redirect_to keys_admin_user_path(user), status: 302, alert: 'Failed to remove user key.' } + format.html { redirect_to keys_admin_user_path(user), status: 302, alert: _('Failed to remove user key.') } end end end diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb index aa5eae7a474..90c1694fd2e 100644 --- a/app/controllers/admin/labels_controller.rb +++ b/app/controllers/admin/labels_controller.rb @@ -21,7 +21,7 @@ class Admin::LabelsController < Admin::ApplicationController @label = Labels::CreateService.new(label_params).execute(template: true) if @label.persisted? - redirect_to admin_labels_url, notice: "Label was created" + redirect_to admin_labels_url, notice: _("Label was created") else render :new end @@ -31,7 +31,7 @@ class Admin::LabelsController < Admin::ApplicationController @label = Labels::UpdateService.new(label_params).execute(@label) if @label.valid? - redirect_to admin_labels_path, notice: 'Label was successfully updated.' + redirect_to admin_labels_path, notice: _('Label was successfully updated.') else render :edit end @@ -43,7 +43,7 @@ class Admin::LabelsController < Admin::ApplicationController respond_to do |format| format.html do - redirect_to admin_labels_path, status: 302, notice: 'Label was removed' + redirect_to admin_labels_path, status: 302, notice: _('Label was removed') end format.js end diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb index 3fa61c7b117..fb135d1a32c 100644 --- a/app/controllers/admin/projects_controller.rb +++ b/app/controllers/admin/projects_controller.rb @@ -50,7 +50,7 @@ class Admin::ProjectsController < Admin::ApplicationController redirect_to( admin_project_path(@project), - notice: 'Repository check was triggered.' + notice: _('Repository check was triggered.') ) end diff --git a/app/controllers/admin/runners_controller.rb b/app/controllers/admin/runners_controller.rb index 8a00408001e..783c59822f1 100644 --- a/app/controllers/admin/runners_controller.rb +++ b/app/controllers/admin/runners_controller.rb @@ -34,17 +34,17 @@ class Admin::RunnersController < Admin::ApplicationController def resume if Ci::UpdateRunnerService.new(@runner).update(active: true) - redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + redirect_to admin_runners_path, notice: _('Runner was successfully updated.') else - redirect_to admin_runners_path, alert: 'Runner was not updated.' + redirect_to admin_runners_path, alert: _('Runner was not updated.') end end def pause if Ci::UpdateRunnerService.new(@runner).update(active: false) - redirect_to admin_runners_path, notice: 'Runner was successfully updated.' + redirect_to admin_runners_path, notice: _('Runner was successfully updated.') else - redirect_to admin_runners_path, alert: 'Runner was not updated.' + redirect_to admin_runners_path, alert: _('Runner was not updated.') end end diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb index 18d22c95b61..45cf0d3207e 100644 --- a/app/controllers/admin/spam_logs_controller.rb +++ b/app/controllers/admin/spam_logs_controller.rb @@ -14,7 +14,7 @@ class Admin::SpamLogsController < Admin::ApplicationController spam_log.remove_user(deleted_by: current_user) redirect_to admin_spam_logs_path, status: 302, - notice: "User #{spam_log.user.username} was successfully removed." + notice: _('User %{username} was successfully removed.') % { username: spam_log.user.username } else spam_log.destroy head :ok @@ -25,9 +25,9 @@ class Admin::SpamLogsController < Admin::ApplicationController spam_log = SpamLog.find(params[:id]) if HamService.new(spam_log).mark_as_ham! - redirect_to admin_spam_logs_path, notice: 'Spam log successfully submitted as ham.' + redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.') else - redirect_to admin_spam_logs_path, alert: 'Error with Akismet. Please check the logs for more info.' + redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.') end end end diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index bfa7c7d0109..a02d0843615 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -39,19 +39,19 @@ class Admin::UsersController < Admin::ApplicationController warden.set_user(user, scope: :user) - Gitlab::AppLogger.info("User #{current_user.username} has started impersonating #{user.username}") + Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username }) - flash[:alert] = "You are now impersonating #{user.username}" + flash[:alert] = _("You are now impersonating %{username}") % { username: user.username } redirect_to root_path else flash[:alert] = if user.blocked? - "You cannot impersonate a blocked user" + _("You cannot impersonate a blocked user") elsif user.internal? - "You cannot impersonate an internal user" + _("You cannot impersonate an internal user") else - "You cannot impersonate a user who cannot log in" + _("You cannot impersonate a user who cannot log in") end redirect_to admin_user_path(user) @@ -60,35 +60,35 @@ class Admin::UsersController < Admin::ApplicationController def block if update_user { |user| user.block } - redirect_back_or_admin_user(notice: "Successfully blocked") + redirect_back_or_admin_user(notice: _("Successfully blocked")) else - redirect_back_or_admin_user(alert: "Error occurred. User was not blocked") + redirect_back_or_admin_user(alert: _("Error occurred. User was not blocked")) end end def unblock if user.ldap_blocked? - redirect_back_or_admin_user(alert: "This user cannot be unlocked manually from GitLab") + redirect_back_or_admin_user(alert: _("This user cannot be unlocked manually from GitLab")) elsif update_user { |user| user.activate } - redirect_back_or_admin_user(notice: "Successfully unblocked") + redirect_back_or_admin_user(notice: _("Successfully unblocked")) else - redirect_back_or_admin_user(alert: "Error occurred. User was not unblocked") + redirect_back_or_admin_user(alert: _("Error occurred. User was not unblocked")) end end def unlock if update_user { |user| user.unlock_access! } - redirect_back_or_admin_user(alert: "Successfully unlocked") + redirect_back_or_admin_user(alert: _("Successfully unlocked")) else - redirect_back_or_admin_user(alert: "Error occurred. User was not unlocked") + redirect_back_or_admin_user(alert: _("Error occurred. User was not unlocked")) end end def confirm if update_user { |user| user.confirm } - redirect_back_or_admin_user(notice: "Successfully confirmed") + redirect_back_or_admin_user(notice: _("Successfully confirmed")) else - redirect_back_or_admin_user(alert: "Error occurred. User was not confirmed") + redirect_back_or_admin_user(alert: _("Error occurred. User was not confirmed")) end end @@ -96,7 +96,7 @@ class Admin::UsersController < Admin::ApplicationController update_user { |user| user.disable_two_factor! } redirect_to admin_user_path(user), - notice: 'Two-factor Authentication has been disabled for this user' + notice: _('Two-factor Authentication has been disabled for this user') end def create @@ -109,7 +109,7 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| if @user.persisted? - format.html { redirect_to [:admin, @user], notice: 'User was successfully created.' } + format.html { redirect_to [:admin, @user], notice: _('User was successfully created.') } format.json { render json: @user, status: :created, location: @user } else format.html { render "new" } @@ -138,7 +138,7 @@ class Admin::UsersController < Admin::ApplicationController end if result[:status] == :success - format.html { redirect_to [:admin, user], notice: 'User was successfully updated.' } + format.html { redirect_to [:admin, user], notice: _('User was successfully updated.') } format.json { head :ok } else # restore username to keep form action url. @@ -153,7 +153,7 @@ class Admin::UsersController < Admin::ApplicationController user.delete_async(deleted_by: current_user, params: params.permit(:hard_delete)) respond_to do |format| - format.html { redirect_to admin_users_path, status: 302, notice: "The user is being deleted." } + format.html { redirect_to admin_users_path, status: 302, notice: _("The user is being deleted.") } format.json { head :ok } end end @@ -164,11 +164,11 @@ class Admin::UsersController < Admin::ApplicationController respond_to do |format| if success - format.html { redirect_back_or_admin_user(notice: 'Successfully removed email.') } + format.html { redirect_back_or_admin_user(notice: _('Successfully removed email.')) } format.json { head :ok } else - format.html { redirect_back_or_admin_user(alert: 'There was an error removing the e-mail.') } - format.json { render json: 'There was an error removing the e-mail.', status: :bad_request } + format.html { redirect_back_or_admin_user(alert: _('There was an error removing the e-mail.')) } + format.json { render json: _('There was an error removing the e-mail.'), status: :bad_request } end end end diff --git a/app/graphql/types/ci/detailed_status_type.rb b/app/graphql/types/ci/detailed_status_type.rb new file mode 100644 index 00000000000..2987354b556 --- /dev/null +++ b/app/graphql/types/ci/detailed_status_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +module Types + module Ci + class DetailedStatusType < BaseObject + graphql_name 'DetailedStatus' + + field :group, GraphQL::STRING_TYPE, null: false + field :icon, GraphQL::STRING_TYPE, null: false + field :favicon, GraphQL::STRING_TYPE, null: false + field :details_path, GraphQL::STRING_TYPE, null: false + field :has_details, GraphQL::BOOLEAN_TYPE, null: false, method: :has_details? + field :label, GraphQL::STRING_TYPE, null: false + field :text, GraphQL::STRING_TYPE, null: false + field :tooltip, GraphQL::STRING_TYPE, null: false, method: :status_tooltip + end + end +end diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 2bbffad4563..18696293b97 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -13,6 +13,10 @@ module Types field :sha, GraphQL::STRING_TYPE, null: false field :before_sha, GraphQL::STRING_TYPE, null: true field :status, PipelineStatusEnum, null: false + field :detailed_status, + Types::Ci::DetailedStatusType, + null: false, + resolve: -> (obj, _args, ctx) { obj.detailed_status(ctx[:current_user]) } field :duration, GraphQL::INT_TYPE, null: true, diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb index 916dcb1a308..769f75f57c4 100644 --- a/app/helpers/clusters_helper.rb +++ b/app/helpers/clusters_helper.rb @@ -14,4 +14,10 @@ module ClustersHelper render 'clusters/clusters/gcp_signup_offer_banner' end end + + def has_rbac_enabled?(cluster) + return cluster.platform_kubernetes_rbac? if cluster.platform_kubernetes + + !cluster.provider.legacy_abac? + end end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index ae74f569415..826b3f82bbf 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -746,6 +746,10 @@ module Ci triggered_by_merge_request? && target_sha == merge_request.target_branch_sha end + def matches_sha_or_source_sha?(sha) + self.sha == sha || self.source_sha == sha + end + private def ci_yaml_from_repo diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb index 941551dadaa..ec8f5cc40c0 100644 --- a/app/models/clusters/applications/runner.rb +++ b/app/models/clusters/applications/runner.rb @@ -3,7 +3,7 @@ module Clusters module Applications class Runner < ActiveRecord::Base - VERSION = '0.2.0'.freeze + VERSION = '0.3.0'.freeze self.table_name = 'clusters_applications_runners' @@ -13,7 +13,7 @@ module Clusters include ::Clusters::Concerns::ApplicationData belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id - delegate :project, to: :cluster + delegate :project, :group, to: :cluster default_value_for :version, VERSION @@ -55,12 +55,17 @@ module Clusters end def runner_create_params - { + attributes = { name: 'kubernetes-cluster', - runner_type: :project_type, - tag_list: %w(kubernetes cluster), - projects: [project] + runner_type: cluster.cluster_type, + tag_list: %w[kubernetes cluster] } + + if cluster.group_type? + attributes.merge(groups: [group]) + elsif cluster.project_type? + attributes.merge(projects: [project]) + end end def gitlab_url diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 15d8d58b9b5..28ea51d6769 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -15,7 +15,7 @@ module CacheMarkdownField # Increment this number every time the renderer changes its output CACHE_COMMONMARK_VERSION_START = 10 - CACHE_COMMONMARK_VERSION = 14 + CACHE_COMMONMARK_VERSION = 15 # changes to these attributes cause the cache to be invalidates INVALIDATED_BY = %w[author project].freeze diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index aeb6acf0ac0..5f6d5095bcc 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -232,7 +232,7 @@ class MergeRequest < ActiveRecord::Base # branch head commit, for example checking if a merge request can be merged. # For more information check: https://gitlab.com/gitlab-org/gitlab-ce/issues/40004 def actual_head_pipeline - head_pipeline&.sha == diff_head_sha ? head_pipeline : nil + head_pipeline&.matches_sha_or_source_sha?(diff_head_sha) ? head_pipeline : nil end def merge_pipeline diff --git a/app/models/project.rb b/app/models/project.rb index 14fc158ede1..611c64c8f49 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1384,6 +1384,7 @@ class Project < ActiveRecord::Base repository.raw_repository.write_ref('HEAD', "refs/heads/#{branch}") repository.copy_gitattributes(branch) repository.after_change_head + ProjectCacheWorker.perform_async(self.id, [], [:commit_count]) reload_default_branch else errors.add(:base, "Could not change HEAD: branch '#{branch}' does not exist") diff --git a/app/models/user.rb b/app/models/user.rb index 0ebfb9a0ccb..d2be26370ff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -917,6 +917,10 @@ class User < ApplicationRecord DeployKey.unscoped.in_projects(authorized_projects.pluck(:id)).distinct(:id) end + def highest_role + members.maximum(:access_level) || Gitlab::Access::NO_ACCESS + end + def accessible_deploy_keys @accessible_deploy_keys ||= begin key_ids = project_deploy_keys.pluck(:id) diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb index 13711070a46..066e30cd3bb 100644 --- a/app/serializers/diff_file_entity.rb +++ b/app/serializers/diff_file_entity.rb @@ -57,7 +57,7 @@ class DiffFileEntity < DiffFileBaseEntity diff_file.diff_lines_for_serializer end - expose :is_fully_expanded, if: -> (diff_file, _) { Feature.enabled?(:expand_diff_full_file) && diff_file.text? } do |diff_file| + expose :is_fully_expanded, if: -> (diff_file, _) { Feature.enabled?(:expand_diff_full_file, default_enabled: true) && diff_file.text? } do |diff_file| diff_file.fully_expanded? end diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb index 76248e6470e..8258135da4e 100644 --- a/app/serializers/environment_entity.rb +++ b/app/serializers/environment_entity.rb @@ -12,7 +12,7 @@ class EnvironmentEntity < Grape::Entity expose :last_deployment, using: DeploymentEntity expose :stop_action_available?, as: :has_stop_action - expose :metrics_path, if: -> (environment, _) { environment.has_metrics? } do |environment| + expose :metrics_path, if: -> (*) { environment.has_metrics? } do |environment| metrics_project_environment_path(environment.project, environment) end diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb index 5c4a34043c1..2292ec42b16 100644 --- a/app/services/ci/destroy_pipeline_service.rb +++ b/app/services/ci/destroy_pipeline_service.rb @@ -6,6 +6,8 @@ module Ci raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_pipeline, pipeline) pipeline.destroy! + + Gitlab::Cache::Ci::ProjectPipelineStatus.new(pipeline.project).delete_from_cache end end end diff --git a/app/services/clusters/applications/create_service.rb b/app/services/clusters/applications/create_service.rb index bd7c31bb981..c6f729aaa8a 100644 --- a/app/services/clusters/applications/create_service.rb +++ b/app/services/clusters/applications/create_service.rb @@ -13,7 +13,8 @@ module Clusters { "helm" => -> (cluster) { cluster.application_helm || cluster.build_application_helm }, "ingress" => -> (cluster) { cluster.application_ingress || cluster.build_application_ingress }, - "cert_manager" => -> (cluster) { cluster.application_cert_manager || cluster.build_application_cert_manager } + "cert_manager" => -> (cluster) { cluster.application_cert_manager || cluster.build_application_cert_manager }, + "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner } }.tap do |hash| hash.merge!(project_builders) if cluster.project_type? end @@ -24,7 +25,6 @@ module Clusters def project_builders { "prometheus" => -> (cluster) { cluster.application_prometheus || cluster.build_application_prometheus }, - "runner" => -> (cluster) { cluster.application_runner || cluster.build_application_runner }, "jupyter" => -> (cluster) { cluster.application_jupyter || cluster.build_application_jupyter }, "knative" => -> (cluster) { cluster.application_knative || cluster.build_application_knative } } diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml index 2a1d2c2aeab..581f6ae0714 100644 --- a/app/views/admin/dashboard/index.html.haml +++ b/app/views/admin/dashboard/index.html.haml @@ -163,7 +163,7 @@ %span.float-right #{Rails::VERSION::STRING} %p - = Gitlab::Database.adapter_name + = Gitlab::Database.human_adapter_name %span.float-right = Gitlab::Database.version %p diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a74e052707f..4ffa8d89504 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -117,6 +117,11 @@ %strong = @user.sign_in_count + %li + %span.light= _("Highest role:") + %strong + = Gitlab::Access.human_access_with_none(@user.highest_role) + - if @user.ldap_user? %li %span.light LDAP uid: diff --git a/app/views/clusters/clusters/_form.html.haml b/app/views/clusters/clusters/_form.html.haml index c08b41e2f23..455322b2089 100644 --- a/app/views/clusters/clusters/_form.html.haml +++ b/app/views/clusters/clusters/_form.html.haml @@ -33,9 +33,9 @@ - auto_devops_url = help_page_path('topics/autodevops/index') - auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url } = s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe } - - if @cluster.application_ingress_external_ip.present? + %span{ :class => ["js-ingress-domain-help-text", ("hide" unless @cluster.application_ingress_external_ip.present?)] } = s_('ClusterIntegration|Alternatively') - %code #{@cluster.application_ingress_external_ip}.nip.io + %code{ :class => "js-ingress-domain-snippet" } #{@cluster.application_ingress_external_ip}.nip.io = s_('ClusterIntegration| can be used instead of a custom domain.') - custom_domain_url = help_page_path('user/project/clusters/index', anchor: 'pointing-your-dns-at-the-external-endpoint') - custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url } diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index 8ed4666e79a..00582e19662 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -17,9 +17,11 @@ .form-group = field.label :name, s_('ClusterIntegration|Kubernetes cluster name'), class: 'label-bold' = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name') - .form-group - = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' - = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?, placeholder: s_('ClusterIntegration|Environment scope') + - if has_multiple_clusters? + .form-group + = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' + = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') + .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :provider_gcp, @gcp_cluster.provider_gcp do |provider_gcp_field| .form-group diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml index 68d9510e1bf..61188c6fa0b 100644 --- a/app/views/clusters/clusters/show.html.haml +++ b/app/views/clusters/clusters/show.html.haml @@ -17,7 +17,7 @@ install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative), update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative), toggle_status: @cluster.enabled? ? 'true': 'false', - has_rbac: @cluster.platform_kubernetes_rbac? ? 'true': 'false', + has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false', cluster_type: @cluster.cluster_type, cluster_status: @cluster.status_name, cluster_status_reason: @cluster.status_reason, diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 9793c77fc2b..136f98d0126 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -7,6 +7,7 @@ .form-group = field.label :environment_scope, s_('ClusterIntegration|Environment scope'), class: 'label-bold' = field.text_field :environment_scope, class: 'form-control', placeholder: s_('ClusterIntegration|Environment scope') + .form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster.") = field.fields_for :platform_kubernetes, @user_cluster.platform_kubernetes do |platform_kubernetes_field| .form-group diff --git a/app/views/dashboard/groups/_groups.html.haml b/app/views/dashboard/groups/_groups.html.haml index db856ef7d7b..2f9dbf87d95 100644 --- a/app/views/dashboard/groups/_groups.html.haml +++ b/app/views/dashboard/groups/_groups.html.haml @@ -1,4 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: dashboard_groups_path(format: :json), path: dashboard_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') + .loading-container.text-center.prepend-top-20 + .spinner.spinner-md diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml index ff57b39e947..a3249275d5e 100644 --- a/app/views/explore/groups/_groups.html.haml +++ b/app/views/explore/groups/_groups.html.haml @@ -1,4 +1,4 @@ .js-groups-list-holder #js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } } - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') + .loading-container.text-center.prepend-top-20 + .spinner.spinner-md diff --git a/app/views/groups/_archived_projects.html.haml b/app/views/groups/_archived_projects.html.haml index ed79f5790f0..48e9f630050 100644 --- a/app/views/groups/_archived_projects.html.haml +++ b/app/views/groups/_archived_projects.html.haml @@ -4,5 +4,5 @@ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } .js-groups-list-holder - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') + .loading-container.text-center.prepend-top-20 + .spinner.spinner-md diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index dbd01c3c61a..4daf3683eaf 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -47,7 +47,7 @@ %strong= new_subgroup_label %span= s_("GroupsTree|Create a subgroup in this group.") - else - = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success" + = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success prepend-top-default" - if @group.description.present? .group-home-desc.mt-1 diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml index 4eb8367f633..2769b69add3 100644 --- a/app/views/groups/_shared_projects.html.haml +++ b/app/views/groups/_shared_projects.html.haml @@ -4,5 +4,5 @@ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } .js-groups-list-holder - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') + .loading-container.text-center.prepend-top-20 + .spinner.spinner-md diff --git a/app/views/groups/_subgroups_and_projects.html.haml b/app/views/groups/_subgroups_and_projects.html.haml index d53c8026df8..784f5ac233e 100644 --- a/app/views/groups/_subgroups_and_projects.html.haml +++ b/app/views/groups/_subgroups_and_projects.html.haml @@ -4,5 +4,5 @@ %ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } } .js-groups-list-holder - .loading-container.text-center - = icon('spinner spin 2x', class: 'loading-animation prepend-top-20') + .loading-container.text-center.prepend-top-20 + .spinner.spinner-md diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml index 1ec368f8910..5a27237bf76 100644 --- a/app/views/layouts/nav/_dashboard.html.haml +++ b/app/views/layouts/nav/_dashboard.html.haml @@ -3,7 +3,7 @@ %ul.list-unstyled.navbar-sub-nav - if dashboard_nav_link?(:projects) = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects qa-projects-dropdown", data: { track_label: "projects_dropdown", track_event: "click_dropdown" } }) do - %button{ type: 'button', data: { toggle: "dropdown" } } + %button.btn{ type: 'button', data: { toggle: "dropdown" } } = _('Projects') = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.frequent-items-dropdown-menu @@ -11,7 +11,7 @@ - if dashboard_nav_link?(:groups) = nav_link(controller: ['dashboard/groups', 'explore/groups'], html_options: { id: 'nav-groups-dropdown', class: "home dropdown header-groups qa-groups-dropdown", data: { track_label: "groups_dropdown", track_event: "click_dropdown" } }) do - %button{ type: 'button', data: { toggle: "dropdown" } } + %button.btn{ type: 'button', data: { toggle: "dropdown" } } = _('Groups') = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.frequent-items-dropdown-menu diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml index 276363df7da..5129f11875c 100644 --- a/app/views/projects/_new_project_fields.html.haml +++ b/app/views/projects/_new_project_fields.html.haml @@ -12,9 +12,9 @@ .form-group.project-path.col-sm-6 = f.label :namespace_id, class: 'label-bold' do %span= s_("Project URL") - .input-group + .input-group.flex-nowrap - if current_user.can_select_namespace? - .input-group-prepend.has-tooltip{ title: root_url } + .input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url } .input-group-text = root_url - namespace_id = namespace_id_from(params) @@ -23,10 +23,10 @@ display_path: true, extra_group: namespace_id), {}, - { class: 'select2 js-select-namespace qa-project-namespace-select', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) + { class: 'select2 js-select-namespace qa-project-namespace-select block-truncated', tabindex: 1, data: { track_label: "#{track_label}", track_event: "activate_form_input", track_property: "project_path", track_value: "" }}) - else - .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' } + .input-group-prepend.static-namespace.flex-shrink-0.has-tooltip{ title: user_url(current_user.username) + '/' } .input-group-text.border-0 #{user_url(current_user.username)}/ = f.hidden_field :namespace_id, value: current_user.namespace_id diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml index 4997770321e..539b184e5c2 100644 --- a/app/views/projects/protected_branches/shared/_index.html.haml +++ b/app/views/projects/protected_branches/shared/_index.html.haml @@ -12,7 +12,7 @@ %p By default, protected branches are designed to: %ul - %li prevent their creation, if not already created, from everybody except users who are allowed to merge + %li prevent their creation, if not already created, from everybody except Maintainers %li prevent pushes from everybody except Maintainers %li prevent <strong>anyone</strong> from force pushing to the branch %li prevent <strong>anyone</strong> from deleting the branch diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb index d27b5e62574..b31099bc670 100644 --- a/app/workers/project_cache_worker.rb +++ b/app/workers/project_cache_worker.rb @@ -27,6 +27,7 @@ class ProjectCacheWorker # rubocop: enable CodeReuse/ActiveRecord def update_statistics(project, statistics = []) + return if Gitlab::Database.read_only? return unless try_obtain_lease_for(project.id, :update_statistics) Rails.logger.info("Updating statistics for project #{project.id}") |