summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/dirty_submit/dirty_submit_form.js3
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue32
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue2
-rw-r--r--app/assets/javascripts/reports/components/issues_list.vue85
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_issues.vue)44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/smart_virtual_list.vue42
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss20
-rw-r--r--app/assets/stylesheets/pages/events.scss78
-rw-r--r--app/assets/stylesheets/pages/profile.scss12
-rw-r--r--app/controllers/boards/issues_controller.rb16
-rw-r--r--app/controllers/concerns/members_presentation.rb7
-rw-r--r--app/helpers/events_helper.rb42
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/ci/build.rb39
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/ci/pipeline.rb23
-rw-r--r--app/models/clusters/cluster.rb17
-rw-r--r--app/models/concerns/deployable.rb29
-rw-r--r--app/models/deploy_token.rb5
-rw-r--r--app/models/deployment.rb76
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/environment_status.rb6
-rw-r--r--app/models/issue.rb14
-rw-r--r--app/models/label.rb2
-rw-r--r--app/models/members_preloader.rb16
-rw-r--r--app/models/namespace.rb6
-rw-r--r--app/models/project.rb14
-rw-r--r--app/models/project_services/issue_tracker_service.rb4
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/serializers/README.md4
-rw-r--r--app/serializers/issue_board_entity.rb51
-rw-r--r--app/serializers/issue_serializer.rb6
-rw-r--r--app/serializers/label_entity.rb4
-rw-r--r--app/services/boards/issues/move_service.rb4
-rw-r--r--app/services/create_deployment_service.rb74
-rw-r--r--app/services/update_deployment_service.rb53
-rw-r--r--app/views/clusters/clusters/_banner.html.haml6
-rw-r--r--app/views/clusters/clusters/gcp/_form.html.haml2
-rw-r--r--app/views/clusters/clusters/gcp/_show.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_form.html.haml2
-rw-r--r--app/views/clusters/clusters/user/_show.html.haml2
-rw-r--r--app/views/events/_event.html.haml2
-rw-r--r--app/views/events/event/_common.html.haml27
-rw-r--r--app/views/events/event/_created_project.html.haml8
-rw-r--r--app/views/events/event/_note.html.haml10
-rw-r--r--app/views/events/event/_private.html.haml13
-rw-r--r--app/views/events/event/_push.html.haml12
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/users/_overview.html.haml6
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/build_success_worker.rb16
-rw-r--r--app/workers/deployments/success_worker.rb17
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 "&nbsp;".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 } } &times;
- = 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= "&quot;".html_safe + event.target.title + "&quot".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= "&quot;".html_safe + event.target.title + "&quot".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