diff options
Diffstat (limited to 'app')
56 files changed, 645 insertions, 363 deletions
diff --git a/app/assets/javascripts/dirty_submit/dirty_submit_form.js b/app/assets/javascripts/dirty_submit/dirty_submit_form.js index 5bea47f23c5..d8d0fa1fac4 100644 --- a/app/assets/javascripts/dirty_submit/dirty_submit_form.js +++ b/app/assets/javascripts/dirty_submit/dirty_submit_form.js @@ -31,7 +31,7 @@ class DirtySubmitForm { updateDirtyInput(event) { const input = event.target; - if (!input.dataset.dirtySubmitOriginalValue) return; + if (!input.dataset.isDirtySubmitInput) return; this.updateDirtyInputs(input); this.toggleSubmission(); @@ -65,6 +65,7 @@ class DirtySubmitForm { } static initInput(element) { + element.dataset.isDirtySubmitInput = true; element.dataset.dirtySubmitOriginalValue = DirtySubmitForm.inputCurrentValue(element); } diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue index a9534ac597e..aff483876f8 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job_app.vue @@ -3,9 +3,11 @@ import _ from 'underscore'; import { mapGetters, mapState, mapActions } from 'vuex'; import { GlLoadingIcon } from '@gitlab-org/gitlab-ui'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; +import { polyfillSticky } from '~/lib/utils/sticky'; import bp from '~/breakpoints'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; import Callout from '~/vue_shared/components/callout.vue'; +import Icon from '~/vue_shared/components/icon.vue'; import createStore from '../store'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; @@ -24,6 +26,7 @@ export default { EmptyState, EnvironmentsBlock, ErasedBlock, + Icon, Log, LogTopBar, StuckBlock, @@ -97,6 +100,14 @@ export default { if (_.isEmpty(oldVal) && !_.isEmpty(newVal.pipeline)) { this.fetchStages(); } + + if (newVal.archived) { + this.$nextTick(() => { + if (this.$refs.sticky) { + polyfillSticky(this.$refs.sticky); + } + }); + } }, }, created() { @@ -114,16 +125,13 @@ export default { window.addEventListener('resize', this.onResize); window.addEventListener('scroll', this.updateScroll); }, - mounted() { this.updateSidebar(); }, - destroyed() { window.removeEventListener('resize', this.onResize); window.removeEventListener('scroll', this.updateScroll); }, - methods: { ...mapActions([ 'setJobEndpoint', @@ -218,14 +226,28 @@ export default { :erased-at="job.erased_at" /> + <div + v-if="job.archived" + ref="sticky" + class="js-archived-job prepend-top-default archived-sticky sticky-top" + > + <icon + name="lock" + class="align-text-bottom" + /> + + {{ __('This job is archived. Only the complete pipeline can be retried.') }} + </div> <!--job log --> <div v-if="hasTrace" - class="build-trace-container prepend-top-default"> + class="build-trace-container" + > <log-top-bar :class="{ 'sidebar-expanded': isSidebarOpen, - 'sidebar-collapsed': !isSidebarOpen + 'sidebar-collapsed': !isSidebarOpen, + 'has-archived-block': job.archived }" :erase-path="job.erase_path" :size="traceSize" diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index eeefa33264f..8b506b124ec 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -69,7 +69,7 @@ export default { }; </script> <template> - <div class="top-bar affix"> + <div class="top-bar"> <!-- truncate information --> <div class="js-truncated-info truncated-info d-none d-sm-block float-left"> <template v-if="isTraceSizeVisible"> diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/reports/components/issues_list.vue index 3b425ee2fed..f4243522ef8 100644 --- a/app/assets/javascripts/reports/components/issues_list.vue +++ b/app/assets/javascripts/reports/components/issues_list.vue @@ -1,18 +1,31 @@ <script> -import IssuesBlock from '~/reports/components/report_issues.vue'; -import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; +import ReportItem from '~/reports/components/report_item.vue'; +import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants'; +import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue'; + +const wrapIssueWithState = (status, isNew = false) => issue => ({ + status: issue.status || status, + isNew, + issue, +}); /** * Renders block of issues */ - export default { components: { - IssuesBlock, + SmartVirtualList, + ReportItem, }, - success: STATUS_SUCCESS, - failed: STATUS_FAILED, - neutral: STATUS_NEUTRAL, + // Typical height of a report item in px + typicalReportItemHeight: 32, + /* + The maximum amount of shown issues. This is calculated by + ( max-height of report-block-list / typicalReportItemHeight ) + some safety margin + We will use VirtualList if we have more items than this number. + For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly. + */ + maxShownReportItems: 20, props: { newIssues: { type: Array, @@ -40,42 +53,34 @@ export default { default: '', }, }, + computed: { + issuesWithState() { + return [ + ...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)), + ...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)), + ...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)), + ...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)), + ]; + }, + }, }; </script> <template> - <div class="report-block-container"> - - <issues-block - v-if="newIssues.length" - :component="component" - :issues="newIssues" - class="js-mr-code-new-issues" - status="failed" - is-new - /> - - <issues-block - v-if="unresolvedIssues.length" - :component="component" - :issues="unresolvedIssues" - :status="$options.failed" - class="js-mr-code-new-issues" - /> - - <issues-block - v-if="neutralIssues.length" - :component="component" - :issues="neutralIssues" - :status="$options.neutral" - class="js-mr-code-non-issues" - /> - - <issues-block - v-if="resolvedIssues.length" + <smart-virtual-list + :length="issuesWithState.length" + :remain="$options.maxShownReportItems" + :size="$options.typicalReportItemHeight" + class="report-block-container" + wtag="ul" + wclass="report-block-list" + > + <report-item + v-for="(wrapped, index) in issuesWithState" + :key="index" + :issue="wrapped.issue" + :status="wrapped.status" :component="component" - :issues="resolvedIssues" - :status="$options.success" - class="js-mr-code-resolved-issues" + :is-new="wrapped.isNew" /> - </div> + </smart-virtual-list> </template> diff --git a/app/assets/javascripts/reports/components/report_issues.vue b/app/assets/javascripts/reports/components/report_item.vue index a2a03945ae3..01e6d357a21 100644 --- a/app/assets/javascripts/reports/components/report_issues.vue +++ b/app/assets/javascripts/reports/components/report_item.vue @@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; import { components, componentNames } from '~/reports/components/issue_body'; export default { - name: 'ReportIssues', + name: 'ReportItem', components: { IssueStatusIcon, ...components, }, props: { - issues: { - type: Array, + issue: { + type: Object, required: true, }, component: { @@ -33,27 +33,21 @@ export default { }; </script> <template> - <div> - <ul class="report-block-list"> - <li - v-for="(issue, index) in issues" - :key="index" - :class="{ 'is-dismissed': issue.isDismissed }" - class="report-block-list-issue" - > - <issue-status-icon - :status="issue.status || status" - class="append-right-5" - /> + <li + :class="{ 'is-dismissed': issue.isDismissed }" + class="report-block-list-issue" + > + <issue-status-icon + :status="status" + class="append-right-5" + /> - <component - :is="component" - v-if="component" - :issue="issue" - :status="issue.status || status" - :is-new="isNew" - /> - </li> - </ul> - </div> + <component + :is="component" + v-if="component" + :issue="issue" + :status="status" + :is-new="isNew" + /> + </li> </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue index 57c52a2016a..2a8380f5f2b 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/deployment.vue @@ -65,6 +65,14 @@ export default { deployedText() { return this.$options.deployedTextMap[this.deployment.status]; }, + isDeployInProgress() { + return this.deployment.status === 'running'; + }, + deployInProgressTooltip() { + return this.isDeployInProgress + ? __('Stopping this environment is currently not possible as a deployment is in progress') + : ''; + }, shouldRenderDropdown() { return ( this.enableCiEnvironmentsStatusChanges && @@ -183,15 +191,23 @@ export default { css-class="js-deploy-url js-deploy-url-feature-flag deploy-link btn btn-default btn-sm inlin" /> </template> - <loading-button + <span v-if="deployment.stop_url" - :loading="isStopping" - container-class="btn btn-default btn-sm inline prepend-left-4" - title="Stop environment" - @click="stopEnvironment" + v-tooltip + :title="deployInProgressTooltip" + class="d-inline-block" + tabindex="0" > - <icon name="stop" /> - </loading-button> + <loading-button + :loading="isStopping" + :disabled="isDeployInProgress" + :title="__('Stop environment')" + container-class="js-stop-env btn btn-default btn-sm inline prepend-left-4" + @click="stopEnvironment" + > + <icon name="stop" /> + </loading-button> + </span> </div> </div> </div> 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 8bcabc10225..53608838f2f 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 @@ -71,6 +71,7 @@ export default { linkStart: `<a href="${this.troubleshootingDocsPath}">`, linkEnd: '</a>', }, + false, ); }, }, diff --git a/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue new file mode 100644 index 00000000000..63034a45f77 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/smart_virtual_list.vue @@ -0,0 +1,42 @@ +<script> +import VirtualList from 'vue-virtual-scroll-list'; + +export default { + name: 'SmartVirtualList', + components: { VirtualList }, + props: { + size: { type: Number, required: true }, + length: { type: Number, required: true }, + remain: { type: Number, required: true }, + rtag: { type: String, default: 'div' }, + wtag: { type: String, default: 'div' }, + wclass: { type: String, default: null }, + }, +}; +</script> +<template> + <virtual-list + v-if="length > remain" + v-bind="$attrs" + :size="remain" + :remain="remain" + :rtag="rtag" + :wtag="wtag" + :wclass="wclass" + class="js-virtual-list" + > + <slot></slot> + </virtual-list> + <component + :is="rtag" + v-else + class="js-plain-element" + > + <component + :is="wtag" + :class="wclass" + > + <slot></slot> + </component> + </component> +</template> diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss index 1e93bf2b751..a20920e2503 100644 --- a/app/assets/stylesheets/framework/images.scss +++ b/app/assets/stylesheets/framework/images.scss @@ -39,7 +39,7 @@ svg { fill: currentColor; - $svg-sizes: 8 10 12 16 18 24 32 48 72; + $svg-sizes: 8 10 12 14 16 18 24 32 48 72; @each $svg-size in $svg-sizes { &.s#{$svg-size} { @include svg-size(#{$svg-size}px); diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index 19eee4e4aba..bfcac3f1c3f 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -269,6 +269,7 @@ $flash-height: 52px; $context-header-height: 60px; $breadcrumb-min-height: 48px; $project-title-row-height: 24px; +$gl-line-height: 16px; /* * Common component specific colors diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss index 1449723de52..81cb519883b 100644 --- a/app/assets/stylesheets/pages/builds.scss +++ b/app/assets/stylesheets/pages/builds.scss @@ -55,9 +55,29 @@ @include build-trace(); } + .archived-sticky { + top: $header-height; + border-radius: 2px 2px 0 0; + color: $orange-600; + background-color: $orange-100; + border: 1px solid $border-gray-normal; + border-bottom: 0; + padding: 3px 12px; + margin: auto; + align-items: center; + + .with-performance-bar & { + top: $header-height + $performance-bar-height; + } + } + .top-bar { @include build-trace-top-bar(35px); + &.has-archived-block { + top: $header-height + $performance-bar-height + 28px; + } + &.affix { top: $header-height; diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss index a91d44805ee..618f23d81b1 100644 --- a/app/assets/stylesheets/pages/events.scss +++ b/app/assets/stylesheets/pages/events.scss @@ -4,41 +4,29 @@ */ .event-item { font-size: $gl-font-size; - padding: $gl-padding-top 0 $gl-padding-top 40px; + padding: $gl-padding 0 $gl-padding 56px; border-bottom: 1px solid $white-normal; - color: $gl-text-color; + color: $gl-text-color-secondary; position: relative; - - &.event-inline { - .system-note-image { - top: 20px; - } - - .user-avatar { - top: 14px; - } - - .event-title, - .event-item-timestamp { - line-height: 40px; - } - } - - a { - color: $gl-text-color; - } + line-height: $gl-line-height; .system-note-image { position: absolute; left: 0; - top: 14px; svg { - width: 20px; - height: 20px; fill: $gl-text-color-secondary; } + } + + .system-note-image-inline { + svg { + fill: $gl-text-color-secondary; + } + } + .system-note-image, + .system-note-image-inline { &.opened-icon, &.created-icon { svg { @@ -53,16 +41,35 @@ &.accepted-icon svg { fill: $blue-300; } + + &.commented-on-icon svg { + fill: $blue-600; + } + } + + .event-user-info { + margin-bottom: $gl-padding-8; + + .author_name { + a { + color: $gl-text-color; + font-weight: $gl-font-weight-bold; + } + } } .event-title { - @include str-truncated(calc(100% - 174px)); - font-weight: $gl-font-weight-bold; - color: $gl-text-color; + .event-type { + &::first-letter { + text-transform: capitalize; + } + } } .event-body { + margin-top: $gl-padding-8; margin-right: 174px; + color: $gl-text-color; .event-note { word-wrap: break-word; @@ -92,7 +99,7 @@ } .note-image-attach { - margin-top: 4px; + margin-top: $gl-padding-4; margin-left: 0; max-width: 200px; float: none; @@ -107,7 +114,6 @@ color: $gl-gray-500; float: left; font-size: $gl-font-size; - line-height: 16px; margin-right: 5px; } } @@ -127,7 +133,9 @@ } } - &:last-child { border: 0; } + &:last-child { + border: 0; + } .event_commits { li { @@ -154,7 +162,6 @@ .event-item-timestamp { float: right; - line-height: 22px; } } @@ -177,10 +184,8 @@ .event-item { padding-left: 0; - &.event-inline { - .event-title { - line-height: 20px; - } + .event-user-info { + margin-bottom: $gl-padding-4; } .event-title { @@ -194,7 +199,8 @@ } .event-body { - margin: 0; + margin-top: $gl-padding-4; + margin-right: 0; padding-left: 0; } diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss index f084adaf5d3..1d691d1d8b8 100644 --- a/app/assets/stylesheets/pages/profile.scss +++ b/app/assets/stylesheets/pages/profile.scss @@ -240,6 +240,12 @@ left: 0; } + .activities-block { + .event-item { + padding-left: 40px; + } + } + @include media-breakpoint-down(xs) { .cover-block { padding-top: 20px; @@ -267,6 +273,12 @@ margin-right: 0; } } + + .activities-block { + .event-item { + padding-left: 0; + } + } } } diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb index 7f874687212..0dd7500623d 100644 --- a/app/controllers/boards/issues_controller.rb +++ b/app/controllers/boards/issues_controller.rb @@ -100,18 +100,12 @@ module Boards .merge(board_id: params[:board_id], list_id: params[:list_id], request: request) end + def serializer + IssueSerializer.new(current_user: current_user) + end + def serialize_as_json(resource) - resource.as_json( - only: [:id, :iid, :project_id, :title, :confidential, :due_date, :relative_position, :weight], - labels: true, - issue_endpoints: true, - include_full_project_path: board.group_board?, - include: { - project: { only: [:id, :path] }, - assignees: { only: [:id, :name, :username], methods: [:avatar_url] }, - milestone: { only: [:id, :title] } - } - ) + serializer.represent(resource, serializer: 'board', include_full_project_path: board.group_board?) end def whitelist_query_limiting diff --git a/app/controllers/concerns/members_presentation.rb b/app/controllers/concerns/members_presentation.rb index c6c3598a976..0a9d3d86245 100644 --- a/app/controllers/concerns/members_presentation.rb +++ b/app/controllers/concerns/members_presentation.rb @@ -12,12 +12,7 @@ module MembersPresentation ).fabricate! end - # rubocop: disable CodeReuse/ActiveRecord def preload_associations(members) - ActiveRecord::Associations::Preloader.new.preload(members, :user) - ActiveRecord::Associations::Preloader.new.preload(members, :source) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) - ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + MembersPreloader.new(members).preload_all end - # rubocop: enable CodeReuse/ActiveRecord end diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb index c94946a04e7..2adfc04deb8 100644 --- a/app/helpers/events_helper.rb +++ b/app/helpers/events_helper.rb @@ -163,14 +163,10 @@ module EventsHelper def event_note_title_html(event) if event.note_target - text = raw("#{event.note_target_type} ") + - if event.commit_note? - content_tag(:span, event.note_target_reference, class: 'commit-sha') - else - event.note_target_reference - end - - link_to(text, event_note_target_url(event), title: event.target_title, class: 'has-tooltip') + capture do + concat content_tag(:span, event.note_target_type, class: "event-target-type append-right-4") + concat link_to(event.note_target_reference, event_note_target_url(event), title: event.target_title, class: 'has-tooltip event-target-link append-right-4') + end else content_tag(:strong, '(deleted)') end @@ -183,17 +179,9 @@ module EventsHelper "--broken encoding" end - def event_row_class(event) - if event.body? - "event-block" - else - "event-inline" - end - end - - def icon_for_event(note) + def icon_for_event(note, size: 24) icon_name = ICON_NAMES_BY_EVENT_TYPE[note] - sprite_icon(icon_name) if icon_name + sprite_icon(icon_name, size: size) if icon_name end def icon_for_profile_event(event) @@ -203,8 +191,24 @@ module EventsHelper end else content_tag :div, class: 'system-note-image user-avatar' do - author_avatar(event, size: 32) + author_avatar(event, size: 40) + end + end + end + + def inline_event_icon(event) + unless current_path?('users#show') + content_tag :span, class: "system-note-image-inline d-none d-sm-flex append-right-4 #{event.action_name.parameterize}-icon align-self-center" do + icon_for_event(event.action_name, size: 14) end end end + + def event_user_info(event) + content_tag(:div, class: "event-user-info") do + concat content_tag(:span, link_to_author(event), class: "author_name") + concat " ".html_safe + concat content_tag(:span, event.author.to_reference, class: "username") + end + end end diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index bae01d476df..4aba48061ba 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -3,7 +3,6 @@ module UserCalloutsHelper GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze - CLUSTER_SECURITY_WARNING = 'cluster_security_warning'.freeze def show_gke_cluster_integration_callout?(project) can?(current_user, :create_cluster, project) && @@ -14,10 +13,6 @@ module UserCalloutsHelper !user_dismissed?(GCP_SIGNUP_OFFER) end - def show_cluster_security_warning? - !user_dismissed?(CLUSTER_SECURITY_WARNING) - end - private def user_dismissed?(feature_name) diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index d7eab57763e..360c9924a7d 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -9,19 +9,18 @@ module Ci include Presentable include Importable include Gitlab::Utils::StrongMemoize + include Deployable belongs_to :project, inverse_of: :builds belongs_to :runner belongs_to :trigger_request belongs_to :erased_by, class_name: 'User' - has_many :deployments, as: :deployable - RUNNER_FEATURES = { upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? } }.freeze - has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' + has_one :deployment, as: :deployable, class_name: 'Deployment' has_many :trace_sections, class_name: 'Ci::BuildTraceSection' has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id @@ -195,6 +194,8 @@ module Ci end after_transition pending: :running do |build| + build.deployment&.run + build.run_after_commit do BuildHooksWorker.perform_async(id) end @@ -207,14 +208,18 @@ module Ci end after_transition any => [:success] do |build| + build.deployment&.succeed + build.run_after_commit do - BuildSuccessWorker.perform_async(id) PagesWorker.perform_async(:deploy, id) if build.pages_generator? end end before_transition any => [:failed] do |build| next unless build.project + + build.deployment&.drop + next if build.retries_max.zero? if build.retries_count < build.retries_max @@ -233,6 +238,10 @@ module Ci after_transition running: any do |build| Ci::BuildRunnerSession.where(build: build).delete_all end + + after_transition any => [:skipped, :canceled] do |build| + build.deployment&.cancel + end end def ensure_metadata @@ -342,8 +351,12 @@ module Ci self.options.fetch(:environment, {}).fetch(:action, 'start') if self.options end + def has_deployment? + !!self.deployment + end + def outdated_deployment? - success? && !last_deployment.try(:last?) + success? && !deployment.try(:last?) end def depends_on_builds @@ -358,6 +371,10 @@ module Ci user == current_user end + def on_stop + options&.dig(:environment, :on_stop) + end + # A slugified version of the build ref, suitable for inclusion in URLs and # domain names. Rules: # @@ -725,7 +742,7 @@ module Ci if success? return successful_deployment_status - elsif complete? && !success? + elsif failed? return :failed end @@ -742,13 +759,11 @@ module Ci end def successful_deployment_status - if success? && last_deployment&.last? - return :last - elsif success? && last_deployment.present? - return :out_of_date + if deployment&.last? + :last + else + :out_of_date end - - :creating end def each_report(report_types) diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb index 34a889057ab..11c88200c37 100644 --- a/app/models/ci/job_artifact.rb +++ b/app/models/ci/job_artifact.rb @@ -15,7 +15,7 @@ module Ci metadata: nil, trace: nil, junit: 'junit.xml', - codequality: 'codequality.json', + codequality: 'gl-code-quality-report.json', sast: 'gl-sast-report.json', dependency_scanning: 'gl-dependency-scanning-report.json', container_scanning: 'gl-container-scanning-report.json', diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index aeee7f0a5d2..56010e899a4 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -181,22 +181,31 @@ module Ci # # ref - The name (or names) of the branch(es)/tag(s) to limit the list of # pipelines to. - def self.newest_first(ref = nil) + # limit - This limits a backlog search, default to 100. + def self.newest_first(ref: nil, limit: 100) relation = order(id: :desc) + relation = relation.where(ref: ref) if ref + + if limit + ids = relation.limit(limit).select(:id) + # MySQL does not support limit in subquery + ids = ids.pluck(:id) if Gitlab::Database.mysql? + relation = relation.where(id: ids) + end - ref ? relation.where(ref: ref) : relation + relation end def self.latest_status(ref = nil) - newest_first(ref).pluck(:status).first + newest_first(ref: ref).pluck(:status).first end def self.latest_successful_for(ref) - newest_first(ref).success.take + newest_first(ref: ref).success.take end def self.latest_successful_for_refs(refs) - relation = newest_first(refs).success + relation = newest_first(ref: refs).success relation.each_with_object({}) do |pipeline, hash| hash[pipeline.ref] ||= pipeline @@ -238,6 +247,10 @@ module Ci end end + def self.latest_successful_ids_per_project + success.group(:project_id).select('max(id) as id') + end + def self.truncate_sha(sha) sha[0...8] end diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 2bd373e0950..e80d35d0f3c 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -3,6 +3,7 @@ module Clusters class Cluster < ActiveRecord::Base include Presentable + include Gitlab::Utils::StrongMemoize self.table_name = 'clusters' @@ -24,9 +25,6 @@ module Clusters has_many :cluster_groups, class_name: 'Clusters::Group' has_many :groups, through: :cluster_groups, class_name: '::Group' - has_one :cluster_group, -> { order(id: :desc) }, class_name: 'Clusters::Group' - has_one :group, through: :cluster_group, class_name: '::Group' - # we force autosave to happen when we save `Cluster` model has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true @@ -119,12 +117,19 @@ module Clusters end def first_project - return @first_project if defined?(@first_project) - - @first_project = projects.first + strong_memoize(:first_project) do + projects.first + end end alias_method :project, :first_project + def first_group + strong_memoize(:first_group) do + groups.first + end + end + alias_method :group, :first_group + def kubeclient platform_kubernetes.kubeclient if kubernetes? end diff --git a/app/models/concerns/deployable.rb b/app/models/concerns/deployable.rb new file mode 100644 index 00000000000..f4f1989f0a9 --- /dev/null +++ b/app/models/concerns/deployable.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module Deployable + extend ActiveSupport::Concern + + included do + after_create :create_deployment + + def create_deployment + return unless starts_environment? && !has_deployment? + + environment = project.environments.find_or_create_by( + name: expanded_environment_name + ) + + environment.deployments.create!( + project_id: environment.project_id, + environment: environment, + ref: ref, + tag: tag, + sha: sha, + user: user, + deployable: self, + on_stop: on_stop).tap do |_| + self.reload # Reload relationships + end + end + end +end diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb index 0b2eedf3631..e3524305346 100644 --- a/app/models/deploy_token.rb +++ b/app/models/deploy_token.rb @@ -4,6 +4,7 @@ class DeployToken < ActiveRecord::Base include Expirable include TokenAuthenticatable include PolicyActor + include Gitlab::Utils::StrongMemoize add_authentication_token_field :token AVAILABLE_SCOPES = %i(read_repository read_registry).freeze @@ -49,7 +50,9 @@ class DeployToken < ActiveRecord::Base # to a single project, later we're going to extend # that to be for multiple projects and namespaces. def project - projects.first + strong_memoize(:project) do + projects.first + end end def expires_at diff --git a/app/models/deployment.rb b/app/models/deployment.rb index 37efbb04fce..54a900a3b85 100644 --- a/app/models/deployment.rb +++ b/app/models/deployment.rb @@ -3,6 +3,7 @@ class Deployment < ActiveRecord::Base include AtomicInternalId include IidRoutes + include AfterCommitQueue belongs_to :project, required: true belongs_to :environment, required: true @@ -16,11 +17,44 @@ class Deployment < ActiveRecord::Base delegate :name, to: :environment, prefix: true - after_create :create_ref - after_create :invalidate_cache - scope :for_environment, -> (environment) { where(environment_id: environment) } + state_machine :status, initial: :created do + event :run do + transition created: :running + end + + event :succeed do + transition any - [:success] => :success + end + + event :drop do + transition any - [:failed] => :failed + end + + event :cancel do + transition any - [:canceled] => :canceled + end + + before_transition any => [:success, :failed, :canceled] do |deployment| + deployment.finished_at = Time.now + end + + after_transition any => :success do |deployment| + deployment.run_after_commit do + Deployments::SuccessWorker.perform_async(id) + end + end + end + + enum status: { + created: 0, + running: 1, + success: 2, + failed: 3, + canceled: 4 + } + def self.last_for_environment(environment) ids = self .for_environment(environment) @@ -69,15 +103,15 @@ class Deployment < ActiveRecord::Base end def update_merge_request_metrics! - return unless environment.update_merge_request_metrics? + return unless environment.update_merge_request_metrics? && success? merge_requests = project.merge_requests .joins(:metrics) .where(target_branch: self.ref, merge_request_metrics: { first_deployed_to_production_at: nil }) - .where("merge_request_metrics.merged_at <= ?", self.created_at) + .where("merge_request_metrics.merged_at <= ?", finished_at) if previous_deployment - merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.created_at) + merge_requests = merge_requests.where("merge_request_metrics.merged_at >= ?", previous_deployment.finished_at) end # Need to use `map` instead of `select` because MySQL doesn't allow `SELECT`ing from the same table @@ -91,7 +125,7 @@ class Deployment < ActiveRecord::Base MergeRequest::Metrics .where(merge_request_id: merge_request_ids, first_deployed_to_production_at: nil) - .update_all(first_deployed_to_production_at: self.created_at) + .update_all(first_deployed_to_production_at: finished_at) end def previous_deployment @@ -109,8 +143,18 @@ class Deployment < ActiveRecord::Base @stop_action ||= manual_actions.find_by(name: on_stop) end + def finished_at + read_attribute(:finished_at) || legacy_finished_at + end + + def deployed_at + return unless success? + + finished_at + end + def formatted_deployment_time - created_at.to_time.in_time_zone.to_s(:medium) + deployed_at&.to_time&.in_time_zone&.to_s(:medium) end def has_metrics? @@ -118,21 +162,17 @@ class Deployment < ActiveRecord::Base end def metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} + metrics&.merge(deployment_time: finished_at.to_i) || {} end def additional_metrics - return {} unless has_metrics? + return {} unless has_metrics? && success? metrics = prometheus_adapter.query(:additional_metrics_deployment, self) - metrics&.merge(deployment_time: created_at.to_i) || {} - end - - def status - 'success' + metrics&.merge(deployment_time: finished_at.to_i) || {} end private @@ -144,4 +184,8 @@ class Deployment < ActiveRecord::Base def ref_path File.join(environment.ref_path, 'deployments', iid.to_s) end + + def legacy_finished_at + self.created_at if success? && !read_attribute(:finished_at) + end end diff --git a/app/models/environment.rb b/app/models/environment.rb index 1c31c01eb9f..7d104bb0c25 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -8,9 +8,9 @@ class Environment < ActiveRecord::Base belongs_to :project, required: true - has_many :deployments, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent + has_many :deployments, -> { success }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent - has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' + has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' before_validation :nullify_external_url before_validation :generate_slug, if: ->(env) { env.slug.blank? } diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb index a84871f7253..7efc8da09ad 100644 --- a/app/models/environment_status.rb +++ b/app/models/environment_status.rb @@ -8,8 +8,8 @@ class EnvironmentStatus delegate :id, to: :environment delegate :name, to: :environment delegate :project, to: :environment + delegate :status, to: :deployment, allow_nil: true delegate :deployed_at, to: :deployment, allow_nil: true - delegate :status, to: :deployment def self.for_merge_request(mr, user) build_environments_status(mr, user, mr.head_pipeline) @@ -33,10 +33,6 @@ class EnvironmentStatus end end - def deployed_at - deployment&.created_at - end - def changes return [] if project.route_map_for(sha).nil? diff --git a/app/models/issue.rb b/app/models/issue.rb index 0de5e434b02..abdb3448d4e 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -231,20 +231,6 @@ class Issue < ActiveRecord::Base def as_json(options = {}) super(options).tap do |json| - if options.key?(:issue_endpoints) && project - url_helper = Gitlab::Routing.url_helpers - - issue_reference = options[:include_full_project_path] ? to_reference(full: true) : to_reference - - json.merge!( - reference_path: issue_reference, - real_path: url_helper.project_issue_path(project, self), - issue_sidebar_endpoint: url_helper.project_issue_path(project, self, format: :json, serializer: 'sidebar'), - toggle_subscription_endpoint: url_helper.toggle_subscription_project_issue_path(project, self), - assignable_labels_endpoint: url_helper.project_labels_path(project, format: :json, include_ancestor_groups: true) - ) - end - if options.key?(:labels) json[:labels] = labels.as_json( project: project, diff --git a/app/models/label.rb b/app/models/label.rb index 43b49445765..165e4a8f3e5 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -41,8 +41,8 @@ class Label < ActiveRecord::Base scope :templates, -> { where(template: true) } scope :with_title, ->(title) { where(title: title) } scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) } - scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) } scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) } + scope :on_board, ->(board_id) { with_lists_and_board.where(boards: { id: board_id }) } scope :order_name_asc, -> { reorder(title: :asc) } scope :order_name_desc, -> { reorder(title: :desc) } scope :subscribed_by, ->(user_id) { joins(:subscriptions).where(subscriptions: { user_id: user_id, subscribed: true }) } diff --git a/app/models/members_preloader.rb b/app/models/members_preloader.rb new file mode 100644 index 00000000000..33855191ca8 --- /dev/null +++ b/app/models/members_preloader.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class MembersPreloader + attr_reader :members + + def initialize(members) + @members = members + end + + def preload_all + ActiveRecord::Associations::Preloader.new.preload(members, :user) + ActiveRecord::Associations::Preloader.new.preload(members, :source) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :status) + ActiveRecord::Associations::Preloader.new.preload(members.map(&:user), :u2f_registrations) + end +end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 74d48d0a9af..4a6627d3ca1 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -232,6 +232,12 @@ class Namespace < ActiveRecord::Base Project.inside_path(full_path) end + # Includes pipelines from this namespace and pipelines from all subgroups + # that belongs to this namespace + def all_pipelines + Ci::Pipeline.where(project: all_projects) + end + def has_parent? parent.present? end diff --git a/app/models/project.rb b/app/models/project.rb index 872bea46e7c..d5a4ae79c47 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -95,8 +95,7 @@ class Project < ActiveRecord::Base unless: :ci_cd_settings, if: proc { ProjectCiCdSetting.available? } - after_create :set_last_activity_at - after_create :set_last_repository_updated_at + after_create :set_timestamps_for_create after_update :update_forks_visibility_level before_destroy :remove_private_deploy_keys @@ -255,7 +254,7 @@ class Project < ActiveRecord::Base has_many :variables, class_name: 'Ci::Variable' has_many :triggers, class_name: 'Ci::Trigger' has_many :environments - has_many :deployments + has_many :deployments, -> { success } has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :project_deploy_tokens has_many :deploy_tokens, through: :project_deploy_tokens @@ -2103,13 +2102,8 @@ class Project < ActiveRecord::Base gitlab_shell.exists?(repository_storage, "#{disk_path}.git") end - # set last_activity_at to the same as created_at - def set_last_activity_at - update_column(:last_activity_at, self.created_at) - end - - def set_last_repository_updated_at - update_column(:last_repository_updated_at, self.created_at) + def set_timestamps_for_create + update_columns(last_activity_at: self.created_at, last_repository_updated_at: self.created_at) end def cross_namespace_reference?(from) diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb index 7cff0e30e8d..a399982e5ec 100644 --- a/app/models/project_services/issue_tracker_service.rb +++ b/app/models/project_services/issue_tracker_service.rb @@ -12,9 +12,9 @@ class IssueTrackerService < Service # overridden patterns. See ReferenceRegexes::EXTERNAL_PATTERN def self.reference_pattern(only_long: false) if only_long - /(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)/ + /(\b[A-Z][A-Z0-9_]*-)(?<issue>\d+)/ else - /(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)/ + /(\b[A-Z][A-Z0-9_]*-|#{Issue.reference_prefix})(?<issue>\d+)/ end end diff --git a/app/models/repository.rb b/app/models/repository.rb index 37a1dd64052..ee5579329a8 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -912,10 +912,6 @@ class Repository async_remove_remote(remote_name) if tmp_remote_name end - def fetch_remote(remote, forced: false, ssh_auth: nil, no_tags: false, prune: true) - gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune) - end - def async_remove_remote(remote_name) return unless remote_name diff --git a/app/serializers/README.md b/app/serializers/README.md index 0337f88db5f..bb94745b0b5 100644 --- a/app/serializers/README.md +++ b/app/serializers/README.md @@ -180,7 +180,7 @@ def index render json: MyResourceSerializer .new(current_user: @current_user) .represent_details(@project.resources) - nd + end end ``` @@ -196,7 +196,7 @@ def index .represent_details(@project.resources), count: @project.resources.count } - nd + end end ``` diff --git a/app/serializers/issue_board_entity.rb b/app/serializers/issue_board_entity.rb new file mode 100644 index 00000000000..6a9e9638e70 --- /dev/null +++ b/app/serializers/issue_board_entity.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +class IssueBoardEntity < Grape::Entity + include RequestAwareEntity + + expose :id + expose :iid + expose :title + + expose :confidential + expose :due_date + expose :project_id + expose :relative_position + expose :weight, if: -> (*) { respond_to?(:weight) } + + expose :project do |issue| + API::Entities::Project.represent issue.project, only: [:id, :path] + end + + expose :milestone, expose_nil: false do |issue| + API::Entities::Project.represent issue.milestone, only: [:id, :title] + end + + expose :assignees do |issue| + API::Entities::UserBasic.represent issue.assignees, only: [:id, :name, :username, :avatar_url] + end + + expose :labels do |issue| + LabelEntity.represent issue.labels, project: issue.project, only: [:id, :title, :description, :color, :priority, :text_color] + end + + expose :reference_path, if: -> (issue) { issue.project } do |issue, options| + options[:include_full_project_path] ? issue.to_reference(full: true) : issue.to_reference + end + + expose :real_path, if: -> (issue) { issue.project } do |issue| + project_issue_path(issue.project, issue) + end + + expose :issue_sidebar_endpoint, if: -> (issue) { issue.project } do |issue| + project_issue_path(issue.project, issue, format: :json, serializer: 'sidebar') + end + + expose :toggle_subscription_endpoint, if: -> (issue) { issue.project } do |issue| + toggle_subscription_project_issue_path(issue.project, issue) + end + + expose :assignable_labels_endpoint, if: -> (issue) { issue.project } do |issue| + project_labels_path(issue.project, format: :json, include_ancestor_groups: true) + end +end diff --git a/app/serializers/issue_serializer.rb b/app/serializers/issue_serializer.rb index 37cf5e28396..d66f0a5acb7 100644 --- a/app/serializers/issue_serializer.rb +++ b/app/serializers/issue_serializer.rb @@ -4,15 +4,17 @@ class IssueSerializer < BaseSerializer # This overrided method takes care of which entity should be used # to serialize the `issue` based on `basic` key in `opts` param. # Hence, `entity` doesn't need to be declared on the class scope. - def represent(merge_request, opts = {}) + def represent(issue, opts = {}) entity = case opts[:serializer] when 'sidebar' IssueSidebarEntity + when 'board' + IssueBoardEntity else IssueEntity end - super(merge_request, opts, entity) + super(issue, opts, entity) end end diff --git a/app/serializers/label_entity.rb b/app/serializers/label_entity.rb index 98743d62b50..5082245dda9 100644 --- a/app/serializers/label_entity.rb +++ b/app/serializers/label_entity.rb @@ -12,4 +12,8 @@ class LabelEntity < Grape::Entity expose :text_color expose :created_at expose :updated_at + + expose :priority, if: -> (*) { options.key?(:project) } do |label| + label.priority(options[:project]) + end end diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 7dd87034410..43a26f4264e 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -70,10 +70,8 @@ module Boards label_ids = if moving_to_list.movable? moving_from_list.label_id - elsif board.group_board? - ::Label.on_group_boards(parent.id).pluck(:label_id) else - ::Label.on_project_boards(parent.id).pluck(:label_id) + ::Label.on_board(board.id).pluck(:label_id) end Array(label_ids).compact diff --git a/app/services/create_deployment_service.rb b/app/services/create_deployment_service.rb deleted file mode 100644 index bb3f605da28..00000000000 --- a/app/services/create_deployment_service.rb +++ /dev/null @@ -1,74 +0,0 @@ -# frozen_string_literal: true - -class CreateDeploymentService - attr_reader :job - - delegate :expanded_environment_name, - :variables, - :project, - to: :job - - def initialize(job) - @job = job - end - - def execute - return unless executable? - - ActiveRecord::Base.transaction do - environment.external_url = expanded_environment_url if - expanded_environment_url - - environment.fire_state_event(action) - - break unless environment.save - break if environment.stopped? - - deploy.tap(&:update_merge_request_metrics!) - end - end - - private - - def executable? - project && job.environment.present? && environment - end - - def deploy - project.deployments.create( - environment: environment, - ref: job.ref, - tag: job.tag, - sha: job.sha, - user: job.user, - deployable: job, - on_stop: on_stop) - end - - def environment - @environment ||= job.persisted_environment - end - - def environment_options - @environment_options ||= job.options&.dig(:environment) || {} - end - - def expanded_environment_url - return @expanded_environment_url if defined?(@expanded_environment_url) - - @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) if environment_url - end - - def environment_url - environment_options[:url] - end - - def on_stop - environment_options[:on_stop] - end - - def action - environment_options[:action] || 'start' - end -end diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb new file mode 100644 index 00000000000..aa7fcca1e2a --- /dev/null +++ b/app/services/update_deployment_service.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class UpdateDeploymentService + attr_reader :deployment + attr_reader :deployable + + delegate :environment, to: :deployment + delegate :variables, to: :deployable + + def initialize(deployment) + @deployment = deployment + @deployable = deployment.deployable + end + + def execute + deployment.create_ref + deployment.invalidate_cache + + ActiveRecord::Base.transaction do + environment.external_url = expanded_environment_url if + expanded_environment_url + + environment.fire_state_event(action) + + break unless environment.save + break if environment.stopped? + + deployment.tap(&:update_merge_request_metrics!) + end + end + + private + + def environment_options + @environment_options ||= deployable.options&.dig(:environment) || {} + end + + def expanded_environment_url + return @expanded_environment_url if defined?(@expanded_environment_url) + return unless environment_url + + @expanded_environment_url = + ExpandVariables.expand(environment_url, variables) + end + + def environment_url + environment_options[:url] + end + + def action + environment_options[:action] || 'start' + end +end diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml index 73cfea0ef92..160c5f009a7 100644 --- a/app/views/clusters/clusters/_banner.html.haml +++ b/app/views/clusters/clusters/_banner.html.haml @@ -7,9 +7,3 @@ .hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' } = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details") - -- if show_cluster_security_warning? - .js-cluster-security-warning.alert.alert-block.alert-dismissable.bs-callout.bs-callout-warning - %button.close{ type: "button", data: { feature_id: UserCalloutsHelper::CLUSTER_SECURITY_WARNING, dismiss_endpoint: user_callouts_path } } × - = s_("ClusterIntegration|The default cluster configuration grants access to many functionalities needed to successfully build and deploy a containerised application.") - = link_to s_("More information"), help_page_path('user/project/clusters/index.md', anchor: 'security-implications') diff --git a/app/views/clusters/clusters/gcp/_form.html.haml b/app/views/clusters/clusters/gcp/_form.html.haml index ad842036a62..8ed4666e79a 100644 --- a/app/views/clusters/clusters/gcp/_form.html.haml +++ b/app/views/clusters/clusters/gcp/_form.html.haml @@ -64,7 +64,7 @@ .form-group .form-check = provider_gcp_field.check_box :legacy_abac, { class: 'form-check-input' }, false, true - = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = provider_gcp_field.label :legacy_abac, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/gcp/_show.html.haml b/app/views/clusters/clusters/gcp/_show.html.haml index 6021b220285..ca55ccb8fdf 100644 --- a/app/views/clusters/clusters/gcp/_show.html.haml +++ b/app/views/clusters/clusters/gcp/_show.html.haml @@ -40,7 +40,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/user/_form.html.haml b/app/views/clusters/clusters/user/_form.html.haml index 4e6232b69de..e4758938059 100644 --- a/app/views/clusters/clusters/user/_form.html.haml +++ b/app/views/clusters/clusters/user/_form.html.haml @@ -28,7 +28,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input qa-rbac-checkbox' }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/clusters/clusters/user/_show.html.haml b/app/views/clusters/clusters/user/_show.html.haml index a871fef0240..ad8c35e32e3 100644 --- a/app/views/clusters/clusters/user/_show.html.haml +++ b/app/views/clusters/clusters/user/_show.html.haml @@ -29,7 +29,7 @@ .form-group .form-check = platform_kubernetes_field.check_box :authorization_type, { class: 'form-check-input', disabled: true }, 'rbac', 'abac' - = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster (experimental)'), class: 'form-check-label label-bold' + = platform_kubernetes_field.label :authorization_type, s_('ClusterIntegration|RBAC-enabled cluster'), class: 'form-check-label label-bold' .form-text.text-muted = s_('ClusterIntegration|Enable this setting if using role-based access control (RBAC).') = s_('ClusterIntegration|This option will allow you to install applications on RBAC clusters.') diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml index 78a1d1a0553..2fcb1d1fd2b 100644 --- a/app/views/events/_event.html.haml +++ b/app/views/events/_event.html.haml @@ -1,5 +1,5 @@ - if event.visible_to_user?(current_user) - .event-item{ class: event_row_class(event) } + .event-item .event-item-timestamp #{time_ago_with_tooltip(event.created_at)} diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml index 829a3da1558..96d6553a2ac 100644 --- a/app/views/events/event/_common.html.haml +++ b/app/views/events/event/_common.html.haml @@ -1,20 +1,19 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) - if event.target - = event.action_name - %strong - = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title do - = event.target_type.titleize.downcase - = event.target.reference_link_text + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name + %span.event-target-type.append-right-4= event.target_type.titleize.downcase + = link_to [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip event-target-link append-right-4', title: event.target_title do + = event.target.reference_link_text + - unless event.milestone? + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe - else - = event_action_name(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event_action_name(event) = render "events/event_scope", event: event - -- if event.target.respond_to?(:title) - .event-body - .event-note - = event.target.title diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml index 6ad7e157131..2f156603414 100644 --- a/app/views/events/event/_created_project.html.haml +++ b/app/views/events/event/_created_project.html.haml @@ -1,8 +1,10 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span{ class: event.action_name } += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } = event_action_name(event) - if event.project diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml index cdacd998a69..fb0d2c3b8b0 100644 --- a/app/views/events/event/_note.html.haml +++ b/app/views/events/event/_note.html.haml @@ -1,9 +1,13 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - = event.action_name += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4{ class: event.action_name } + = event.action_name = event_note_title_html(event) + %span.event-target-title.append-right-4= """.html_safe + event.target.title + """.html_safe = render "events/event_scope", event: event diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml index ccd2aacb4ea..d91f30c07cb 100644 --- a/app/views/events/event/_private.html.haml +++ b/app/views/events/event/_private.html.haml @@ -1,10 +1,11 @@ -.event-inline.event-item +.event-item .event-item-timestamp = time_ago_with_tooltip(event.created_at) - .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon') + .system-note-image= sprite_icon('eye-slash', size: 24, css_class: 'icon') - .event-title - - author_name = capture do - %span.author_name= link_to_author(event) - = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name } + = event_user_info(event) + + .event-title.d-flex.flex-wrap + = inline_event_icon(event) + = s_('Profiles|Made a private contribution') diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml index 5f0ee79cd9b..82693ec832e 100644 --- a/app/views/events/event/_push.html.haml +++ b/app/views/events/event/_push.html.haml @@ -2,13 +2,15 @@ = icon_for_profile_event(event) -.event-title - %span.author_name= link_to_author(event) - %span.pushed #{event.action_name} #{event.ref_type} - %strong += event_user_info(event) + +.event-title.d-flex.flex-wrap + = inline_event_icon(event) + %span.event-type.d-inline-block.append-right-4.pushed #{event.action_name} #{event.ref_type} + %span - commits_link = project_commits_path(project, event.ref_name) - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name) - = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name' + = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name append-right-4' = render "events/event_scope", event: event diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml index 2682d92fc56..b4b3f4a6b7e 100644 --- a/app/views/shared/members/_member.html.haml +++ b/app/views/shared/members/_member.html.haml @@ -14,6 +14,8 @@ = user_status(user) %span.cgray= user.to_reference + = render_if_exists 'shared/members/ee/sso_badge', member: member + - if user == current_user %span.badge.badge-success.prepend-left-5 It's you diff --git a/app/views/users/_overview.html.haml b/app/views/users/_overview.html.haml index f8b3754840d..cf525f2bb2d 100644 --- a/app/views/users/_overview.html.haml +++ b/app/views/users/_overview.html.haml @@ -11,8 +11,8 @@ - if can?(current_user, :read_cross_project) .activities-block - .content-block - %h5.prepend-top-10 + .border-bottom.prepend-top-16 + %h5 = s_('UserProfile|Recent contributions') .overview-content-list{ data: { href: user_path } } .center.light.loading @@ -22,7 +22,7 @@ .col-md-12.col-lg-6 .projects-block - .content-block + .border-bottom.prepend-top-16 %h4 = s_('UserProfile|Personal projects') .overview-content-list{ data: { href: user_projects_path } } diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index a66a6f4c777..953ab95735b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -73,6 +73,8 @@ - pipeline_processing:update_head_pipeline_for_merge_request - pipeline_processing:ci_build_schedule +- deployment:deployments_success + - repository_check:repository_check_clear - repository_check:repository_check_batch - repository_check:repository_check_single_repository diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb index c17608f7378..9a865fea621 100644 --- a/app/workers/build_success_worker.rb +++ b/app/workers/build_success_worker.rb @@ -10,13 +10,27 @@ class BuildSuccessWorker def perform(build_id) Ci::Build.find_by(id: build_id).try do |build| create_deployment(build) if build.has_environment? + stop_environment(build) if build.stops_environment? end end # rubocop: enable CodeReuse/ActiveRecord private + ## + # Deprecated: + # As of 11.5, we started creating a deployment record when ci_builds record is created. + # Therefore we no longer need to create a deployment, after a build succeeded. + # We're leaving this code for the transition period, but we can remove this code in 11.6. def create_deployment(build) - CreateDeploymentService.new(build).execute + build.create_deployment.try do |deployment| + deployment.succeed + end + end + + ## + # TODO: This should be processed in DeploymentSuccessWorker once we started storing `action` value in `deployments` records + def stop_environment(build) + build.persisted_environment.fire_state_event(:stop) end end diff --git a/app/workers/deployments/success_worker.rb b/app/workers/deployments/success_worker.rb new file mode 100644 index 00000000000..da517f3fb26 --- /dev/null +++ b/app/workers/deployments/success_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Deployments + class SuccessWorker + include ApplicationWorker + + queue_namespace :deployment + + def perform(deployment_id) + Deployment.find_by_id(deployment_id).try do |deployment| + break unless deployment.success? + + UpdateDeploymentService.new(deployment).execute + end + end + end +end |