Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 15:10:33 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-27 15:10:33 +0300
commitfdf32113c3924f7faec91101282fc28ec42fc869 (patch)
tree388fdb9982d5ae80c8bc9b9bdcc0dde98cd6ead9 /app
parent5add82515889cf332b65bbf59394079222dc66b3 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue23
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/repository/constants.js2
-rw-r--r--app/assets/javascripts/search/sidebar/components/checkbox_filter.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter/index.vue2
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/new_card.scss15
-rw-r--r--app/models/ci/build.rb141
-rw-r--r--app/models/concerns/ci/deployable.rb159
-rw-r--r--app/views/admin/applications/index.html.haml87
-rw-r--r--app/workers/all_queues.yml9
-rw-r--r--app/workers/build_success_worker.rb2
-rw-r--r--app/workers/environments/stop_job_success_worker.rb23
20 files changed, 299 insertions, 206 deletions
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index d1e5e4eea13..aceae188b73 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -15,6 +15,7 @@ export const DATETIME_RANGE_TYPES = {
export const BV_SHOW_MODAL = 'bv::show::modal';
export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
+export const BV_SHOW_TOOLTIP = 'bv::show::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index a34ed065323..bdb78bab909 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -21,7 +21,13 @@ import projectInfoQuery from '../queries/project_info.query.graphql';
import getRefMixin from '../mixins/get_ref';
import userInfoQuery from '../queries/user_info.query.graphql';
import applicationInfoQuery from '../queries/application_info.query.graphql';
-import { DEFAULT_BLOB_INFO, TEXT_FILE_TYPE, LFS_STORAGE, LEGACY_FILE_TYPES } from '../constants';
+import {
+ DEFAULT_BLOB_INFO,
+ TEXT_FILE_TYPE,
+ LFS_STORAGE,
+ LEGACY_FILE_TYPES,
+ CODEOWNERS_FILE_NAME,
+} from '../constants';
import BlobButtonGroup from './blob_button_group.vue';
import ForkSuggestion from './fork_suggestion.vue';
import { loadViewer } from './blob_viewers';
@@ -32,6 +38,7 @@ export default {
BlobButtonGroup,
BlobContent,
GlLoadingIcon,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
GlButton,
ForkSuggestion,
WebIdeLink,
@@ -79,7 +86,7 @@ export default {
const queryVariables = {
projectPath: this.projectPath,
filePath: this.path,
- ref: this.originalBranch || this.ref,
+ ref: this.currentRef,
refType: this.refType?.toUpperCase() || null,
shouldFetchRawText: true,
};
@@ -171,6 +178,12 @@ export default {
return nodes[0] || {};
},
+ currentRef() {
+ return this.originalBranch || this.ref;
+ },
+ isCodeownersFile() {
+ return this.path.includes(CODEOWNERS_FILE_NAME);
+ },
viewer() {
const { richViewer, simpleViewer } = this.blobInfo;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
@@ -402,6 +415,12 @@ export default {
:fork-path="forkPath"
@cancel="setForkTarget(null)"
/>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="path"
+ />
<blob-content
v-if="!blobViewer"
class="js-syntax-highlight"
diff --git a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
index 014f1abc121..9a8bb8e4aa6 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_viewers/image_viewer.vue
@@ -8,7 +8,7 @@ export default {
},
data() {
return {
- url: this.blob.rawPath,
+ url: this.blob.externalStorageUrl || this.blob.rawPath,
alt: this.blob.name,
};
},
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index b711f671850..4327b237c8c 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -114,3 +114,5 @@ export const POLLING_INTERVAL_BACKOFF = 2;
export const CONFLICTS_MODAL_ID = 'fork-sync-conflicts-modal';
export const FORK_UPDATED_EVENT = 'fork:updated';
+
+export const CODEOWNERS_FILE_NAME = 'CODEOWNERS';
diff --git a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
index feff3f77dd2..bca049e56c7 100644
--- a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
@@ -27,7 +27,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['queryLanguageFilters']),
dataFilters() {
return Object.values(this.filtersData?.filters || []);
@@ -69,7 +69,9 @@ export default {
<template>
<div class="gl-mx-5">
- <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useNewNavigation }">{{ filtersData.header }}</h5>
+ <h5 class="gl-mt-0" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ {{ filtersData.header }}
+ </h5>
<gl-form-checkbox-group v-model="selectedFilter">
<gl-form-checkbox
v-for="f in dataFilters"
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
index 7909aa9234b..312092b9904 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
@@ -10,7 +10,7 @@ export default {
RadioFilter,
},
computed: {
- ...mapState(['useNewNavigation']),
+ ...mapState(['useSidebarNavigation']),
},
confidentialFilterData,
HR_DEFAULT_CLASSES,
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index 3eb025327a2..5fd8b6418d7 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -28,7 +28,7 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
showReset() {
return this.urlQuery.state || this.urlQuery.confidential || this.urlQuery.labels;
@@ -69,12 +69,12 @@ export default {
<template>
<form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
<hr
- v-if="!useNewNavigation && showConfidentialityFilter && showLabelFilter"
+ v-if="!useSidebarNavigation && showConfidentialityFilter && showLabelFilter"
:class="hrClasses"
/>
<label-filter v-if="showLabelFilter" />
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
index b820ca837bc..b42fe9185cb 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/checkbox_filter.vue
@@ -27,7 +27,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['queryLanguageFilters']),
dataFilters() {
return Object.values(this.filtersData?.filters || []);
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index c10b14bd116..e5560dd5b55 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -36,7 +36,7 @@ export default {
reset: s__('GlobalSearch|Reset filters'),
},
computed: {
- ...mapState(['aggregations', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['aggregations', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters([
'languageAggregationBuckets',
'currentUrlQueryHasLanguageFilters',
@@ -120,8 +120,8 @@ export default {
class="gl-m-5 gl-my-0 language-filter-checkbox"
@submit.prevent="submitQuery"
>
- <hr v-if="!useNewNavigation" :class="dividerClassesTop" />
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }">
+ <hr v-if="!useSidebarNavigation" :class="dividerClassesTop" />
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ $options.languageFilterData.header }}
</h5>
<div
@@ -153,7 +153,7 @@ export default {
</gl-button>
</div>
<div v-if="!aggregations.error">
- <hr v-if="!useNewNavigation" :class="dividerClassesBottom" />
+ <hr v-if="!useSidebarNavigation" :class="dividerClassesBottom" />
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-mt-4">
<gl-button
category="primary"
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index 10ece1b82eb..8ad403ab31b 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -16,7 +16,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useNewNavigation']),
+ ...mapState(['query', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
ANY() {
return this.filterData.filters.ANY;
@@ -56,7 +56,7 @@ export default {
<template>
<div>
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useNewNavigation }">
+ <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
{{ filterData.header }}
</h5>
<gl-form-radio-group v-model="selectedFilter">
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
index a9addb87f7b..88e434cf99e 100644
--- a/app/assets/javascripts/search/sidebar/components/results_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -16,7 +16,7 @@ export default {
ConfidentialityFilter,
},
computed: {
- ...mapState(['urlQuery', 'sidebarDirty', 'useNewNavigation']),
+ ...mapState(['urlQuery', 'sidebarDirty', 'useSidebarNavigation']),
...mapGetters(['currentScope']),
showReset() {
return this.urlQuery.state || this.urlQuery.confidential;
@@ -39,7 +39,7 @@ export default {
<template>
<form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery">
- <hr v-if="!useNewNavigation" :class="hrClasses" />
+ <hr v-if="!useSidebarNavigation" :class="hrClasses" />
<status-filter v-if="showStatusFilter" />
<confidentiality-filter v-if="showConfidentialityFilter" />
<div class="gl-display-flex gl-align-items-center gl-mt-4 gl-px-5">
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
index 494d75db6ce..010cfbad590 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
@@ -10,7 +10,7 @@ export default {
RadioFilter,
},
computed: {
- ...mapState(['useNewNavigation']),
+ ...mapState(['useSidebarNavigation']),
},
statusFilterData,
HR_DEFAULT_CLASSES,
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 2e88b45d646..613e504c771 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -263,6 +263,11 @@ span.idiff {
}
}
+.file-validation {
+ // we use $gray-light variable instead of utility class, because it's value is dynamic per color theme
+ background-color: $gray-light;
+}
+
.blob-content-holder .file-actions {
@include media-breakpoint-down(sm) {
.btn {
diff --git a/app/assets/stylesheets/framework/new_card.scss b/app/assets/stylesheets/framework/new_card.scss
index 48a834858c6..411f5300120 100644
--- a/app/assets/stylesheets/framework/new_card.scss
+++ b/app/assets/stylesheets/framework/new_card.scss
@@ -95,6 +95,10 @@
// Table adjustments
@mixin new-card-table-adjustments {
tbody > tr {
+ &:first-of-type > td[data-label] {
+ @include gl-border-t-0;
+ }
+
> td[data-label] {
@include gl-border-left-0;
@include gl-border-l-none;
@@ -119,8 +123,15 @@
table.b-table-stacked-sm,
table.b-table-stacked-md {
- @include gl-mt-n1;
- @include gl-mb-n2;
+ @include gl-mb-0;
+
+ tr:first-of-type th {
+ @include gl-border-t-0;
+ }
+
+ tr:last-of-type td {
+ @include gl-border-b-0;
+ }
}
table.gl-table.b-table.b-table-stacked-sm {
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index da70361743b..1ce852d4f71 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -5,6 +5,7 @@ module Ci
prepend Ci::BulkInsertableTags
include Ci::Metadatable
include Ci::Contextable
+ include Ci::Deployable
include TokenAuthenticatable
include AfterCommitQueue
include Presentable
@@ -34,7 +35,6 @@ module Ci
DEPLOYMENT_NAMES = %w[deploy release rollout].freeze
- has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable
has_one :pending_state, class_name: 'Ci::BuildPendingState', foreign_key: :build_id, inverse_of: :build
has_one :queuing_entry, class_name: 'Ci::PendingBuild', foreign_key: :build_id, inverse_of: :build
has_one :runtime_metadata, class_name: 'Ci::RunningBuild', foreign_key: :build_id, inverse_of: :build
@@ -327,7 +327,6 @@ module Ci
after_transition any => [:success] do |build|
build.run_after_commit do
- BuildSuccessWorker.perform_async(id)
PagesWorker.perform_async(:deploy, id) if build.pages_generator?
end
end
@@ -345,18 +344,6 @@ module Ci
end
end
end
-
- # Synchronize Deployment Status
- # Please note that the data integirty is not assured because we can't use
- # a database transaction due to DB decomposition.
- after_transition do |build, transition|
- next if transition.loopback?
- next unless build.project
-
- build.run_after_commit do
- build.deployment&.sync_status_with(build)
- end
- end
end
def self.build_matchers(project)
@@ -428,15 +415,6 @@ module Ci
action? && !archived? && (manual? || scheduled? || retryable?)
end
- def outdated_deployment?
- strong_memoize(:outdated_deployment) do
- deployment_job? &&
- project.ci_forward_deployment_enabled? &&
- (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
- deployment&.older_than_last_successful_deployment?
- end
- end
-
def schedulable?
self.when == 'delayed' && options[:start_in].present?
end
@@ -478,94 +456,10 @@ module Ci
Gitlab::Ci::Build::Prerequisite::Factory.new(self).unmet
end
- def persisted_environment
- return unless has_environment_keyword?
-
- strong_memoize(:persisted_environment) do
- # This code path has caused N+1s in the past, since environments are only indirectly
- # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
- # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
- BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
- Environment.where(name: names, project: args[:key]).find_each do |environment|
- loader.call(environment.name, environment)
- end
- end
- end
- end
-
- def persisted_environment=(environment)
- strong_memoize(:persisted_environment) { environment }
- end
-
- # If build.persisted_environment is a BatchLoader, we need to remove
- # the method proxy in order to clone into new item here
- # https://github.com/exAspArk/batch-loader/issues/31
- def actual_persisted_environment
- persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment
- end
-
- def expanded_environment_name
- return unless has_environment_keyword?
-
- strong_memoize(:expanded_environment_name) do
- # We're using a persisted expanded environment name in order to avoid
- # variable expansion per request.
- if metadata&.expanded_environment_name.present?
- metadata.expanded_environment_name
- else
- ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all })
- end
- end
- end
-
- def expanded_kubernetes_namespace
- return unless has_environment_keyword?
-
- namespace = options.dig(:environment, :kubernetes, :namespace)
-
- if namespace.present?
- strong_memoize(:expanded_kubernetes_namespace) do
- ExpandVariables.expand(namespace, -> { simple_variables })
- end
- end
- end
-
- def has_environment_keyword?
- environment.present?
- end
-
- def deployment_job?
- has_environment_keyword? && environment_action == 'start'
- end
-
- def stops_environment?
- has_environment_keyword? && environment_action == 'stop'
- end
-
- def environment_action
- options.fetch(:environment, {}).fetch(:action, 'start') if options
- end
-
- def environment_tier_from_options
- options.dig(:environment, :deployment_tier) if options
- end
-
- def environment_tier
- environment_tier_from_options || persisted_environment.try(:tier)
- end
-
def triggered_by?(current_user)
user == current_user
end
- def on_stop
- options&.dig(:environment, :on_stop)
- end
-
- def stop_action_successful?
- success?
- end
-
##
# All variables, including persisted environment variables.
#
@@ -1033,19 +927,6 @@ module Ci
job_artifacts.all_reports
end
- # Virtual deployment status depending on the environment status.
- def deployment_status
- return unless deployment_job?
-
- if success?
- return successful_deployment_status
- elsif failed?
- return :failed
- end
-
- :creating
- end
-
# Consider this object to have a structural integrity problems
def doom!
transaction do
@@ -1206,31 +1087,11 @@ module Ci
strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end
- def successful_deployment_status
- if deployment&.last?
- :last
- else
- :out_of_date
- end
- end
-
def job_artifacts_for_types(report_types)
# Use select to leverage cached associations and avoid N+1 queries
job_artifacts.select { |artifact| artifact.file_type.in?(report_types) }
end
- def environment_url
- options&.dig(:environment, :url) || persisted_environment&.external_url
- end
-
- def environment_status
- strong_memoize(:environment_status) do
- if has_environment_keyword? && merge_request
- EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
- end
- end
- end
-
def has_expiring_artifacts?
artifacts_expire_at.present? && artifacts_expire_at > Time.current
end
diff --git a/app/models/concerns/ci/deployable.rb b/app/models/concerns/ci/deployable.rb
new file mode 100644
index 00000000000..ffdb38f76ac
--- /dev/null
+++ b/app/models/concerns/ci/deployable.rb
@@ -0,0 +1,159 @@
+# frozen_string_literal: true
+
+# rubocop:disable Gitlab/StrongMemoizeAttr
+module Ci
+ module Deployable
+ extend ActiveSupport::Concern
+
+ included do
+ has_one :deployment, as: :deployable, class_name: 'Deployment', inverse_of: :deployable
+
+ state_machine :status do
+ after_transition any => [:success] do |job|
+ job.run_after_commit do
+ Environments::StopJobSuccessWorker.perform_async(id)
+ end
+ end
+
+ # Synchronize Deployment Status
+ # Please note that the data integirty is not assured because we can't use
+ # a database transaction due to DB decomposition.
+ after_transition do |job, transition|
+ next if transition.loopback?
+ next unless job.project
+
+ job.run_after_commit do
+ job.deployment&.sync_status_with(job)
+ end
+ end
+ end
+ end
+
+ def outdated_deployment?
+ strong_memoize(:outdated_deployment) do
+ deployment_job? &&
+ project.ci_forward_deployment_enabled? &&
+ (!project.ci_forward_deployment_rollback_allowed? || incomplete?) &&
+ deployment&.older_than_last_successful_deployment?
+ end
+ end
+
+ # Virtual deployment status depending on the environment status.
+ def deployment_status
+ return unless deployment_job?
+
+ if success?
+ return successful_deployment_status
+ elsif failed?
+ return :failed
+ end
+
+ :creating
+ end
+
+ def successful_deployment_status
+ if deployment&.last?
+ :last
+ else
+ :out_of_date
+ end
+ end
+
+ def persisted_environment
+ return unless has_environment_keyword?
+
+ strong_memoize(:persisted_environment) do
+ # This code path has caused N+1s in the past, since environments are only indirectly
+ # associated to builds and pipelines; see https://gitlab.com/gitlab-org/gitlab/-/issues/326445
+ # We therefore batch-load them to prevent dormant N+1s until we found a proper solution.
+ BatchLoader.for(expanded_environment_name).batch(key: project_id) do |names, loader, args|
+ Environment.where(name: names, project: args[:key]).find_each do |environment|
+ loader.call(environment.name, environment)
+ end
+ end
+ end
+ end
+
+ def persisted_environment=(environment)
+ strong_memoize(:persisted_environment) { environment }
+ end
+
+ # If build.persisted_environment is a BatchLoader, we need to remove
+ # the method proxy in order to clone into new item here
+ # https://github.com/exAspArk/batch-loader/issues/31
+ def actual_persisted_environment
+ persisted_environment.respond_to?(:__sync) ? persisted_environment.__sync : persisted_environment
+ end
+
+ def expanded_environment_name
+ return unless has_environment_keyword?
+
+ strong_memoize(:expanded_environment_name) do
+ # We're using a persisted expanded environment name in order to avoid
+ # variable expansion per request.
+ if metadata&.expanded_environment_name.present?
+ metadata.expanded_environment_name
+ else
+ ExpandVariables.expand(environment, -> { simple_variables.sort_and_expand_all })
+ end
+ end
+ end
+
+ def expanded_kubernetes_namespace
+ return unless has_environment_keyword?
+
+ namespace = options.dig(:environment, :kubernetes, :namespace)
+
+ if namespace.present? # rubocop:disable Style/GuardClause
+ strong_memoize(:expanded_kubernetes_namespace) do
+ ExpandVariables.expand(namespace, -> { simple_variables })
+ end
+ end
+ end
+
+ def has_environment_keyword?
+ environment.present?
+ end
+
+ def deployment_job?
+ has_environment_keyword? && environment_action == 'start'
+ end
+
+ def stops_environment?
+ has_environment_keyword? && environment_action == 'stop'
+ end
+
+ def environment_action
+ options.fetch(:environment, {}).fetch(:action, 'start') if options
+ end
+
+ def environment_tier_from_options
+ options.dig(:environment, :deployment_tier) if options
+ end
+
+ def environment_tier
+ environment_tier_from_options || persisted_environment.try(:tier)
+ end
+
+ def environment_url
+ options&.dig(:environment, :url) || persisted_environment&.external_url
+ end
+
+ def environment_status
+ strong_memoize(:environment_status) do
+ if has_environment_keyword? && merge_request
+ EnvironmentStatus.new(project, persisted_environment, merge_request, pipeline.sha)
+ end
+ end
+ end
+
+ def on_stop
+ options&.dig(:environment, :on_stop)
+ end
+
+ def stop_action_successful?
+ success?
+ end
+ end
+end
+# rubocop:enable Gitlab/StrongMemoizeAttr
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index e32a50e252d..063045033b6 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -1,52 +1,51 @@
- page_title s_('AdminArea|Instance OAuth applications')
-%h1.page-title.gl-font-size-h-display
- = s_('AdminArea|Instance OAuth applications')
-%p.light
- - docs_link_path = help_page_path('integration/oauth_provider')
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path }
- = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
+= render Pajamas::CardComponent.new(card_options: { class: 'gl-new-card js-toggle-container' }, header_options: { class: 'gl-new-card-header' }, body_options: { class: 'gl-new-card-body gl-px-0' }) do |c|
+ - c.with_header do
+ .gl-new-card-title-wrapper.gl-flex-direction-column
+ %h3.gl-new-card-title
+ = s_('AdminArea|Instance OAuth applications')
+ .gl-new-card-count
+ = sprite_icon('applications', css_class: 'gl-mr-2')
+ = @applications.size
+ %p.gl-new-card-description
+ - docs_link_path = help_page_path('integration/oauth_provider')
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: docs_link_path }
+ = s_('AdminArea|Manage applications for your instance that can use GitLab as an %{docs_link_start}OAuth provider%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: '</a>'.html_safe }
-- if @applications.empty?
- %section.empty-state.gl-text-center.gl-display-flex.gl-flex-direction-column
- .svg-content.svg-150
- = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full'
-
- .gl-max-w-full.gl-m-auto
- %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found')
- = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do
+ .gl-new-card-actions
+ = render Pajamas::ButtonComponent.new(size: :small, href: new_admin_application_path, button_options: { data: { qa_selector: 'new_application_button' } }) do
= _('New application')
+ - c.with_body do
+ - if @applications.empty?
+ %section.empty-state.gl-my-5.gl-text-center.gl-display-flex.gl-flex-direction-column
+ .svg-content.svg-150
+ = image_tag 'illustrations/empty-state/empty-admin-apps-md.svg', class: 'gl-max-w-full'
-- else
- %hr
- = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm, button_options: { data: { qa_selector: 'new_application_button' } }) do
- = _('New application')
-
- .table-responsive
- %table.b-table.gl-table.gl-w-full{ role: 'table' }
- %thead
- %tr
- %th
- = _('Name')
- %th
- = _('Callback URL')
- %th
- = _('Trusted')
- %th
- = _('Confidential')
- %th
- %th
- %tbody.oauth-applications
- - @applications.each do |application|
- %tr{ id: "application_#{application.id}" }
- %td= link_to application.name, admin_application_path(application)
- %td= application.redirect_uri
- %td= application.trusted? ? _('Yes'): _('No')
- %td= application.confidential? ? _('Yes'): _('No')
- %td
- = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), variant: :link) do
- = _('Edit')
- %td= render 'delete_form', application: application
+ .gl-max-w-full.gl-m-auto
+ %h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found')
+ %p.gl-text-secondary.gl-mt-3= s_('AdminArea|Manage applications for your instance that can use GitLab as an OAuth provider, start by creating a new one above.')
+ - else
+ .table-holder
+ %table.table.b-table.gl-table.b-table-stacked-md{ role: 'table' }
+ %thead
+ %tr
+ %th= _('Name')
+ %th= _('Callback URL')
+ %th= _('Trusted')
+ %th= _('Confidential')
+ %th= _('Actions')
+ %tbody.oauth-applications
+ - @applications.each do |application|
+ %tr{ id: "application_#{application.id}" }
+ %td{ data: { label: _('Name') } }= link_to application.name, admin_application_path(application)
+ %td{ data: { label: _('Callback URL') } }= application.redirect_uri
+ %td{ data: { label: _('Trusted') } }= application.trusted? ? _('Yes'): _('No')
+ %td{ data: { label: _('Confidential') } }= application.confidential? ? _('Yes'): _('No')
+ %td{ data: { label: _('Actions') } }
+ = render Pajamas::ButtonComponent.new(href: edit_admin_application_path(application), size: :small, button_options: { class: 'gl-mr-3' }) do
+ = _('Edit')
+ = render 'delete_form', application: application
= paginate @applications, theme: 'gitlab'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index e41c85d1550..89bb1d11d1d 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -2712,6 +2712,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: environments_stop_job_success
+ :worker_name: Environments::StopJobSuccessWorker
+ :feature_category: :continuous_delivery
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: error_tracking_issue_link
:worker_name: ErrorTrackingIssueLinkWorker
:feature_category: :error_tracking
diff --git a/app/workers/build_success_worker.rb b/app/workers/build_success_worker.rb
index 247105d2a1a..f5baa220715 100644
--- a/app/workers/build_success_worker.rb
+++ b/app/workers/build_success_worker.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
+# Deprecated and will be removed in 17.0.
+# Use `Environments::StopJobSuccessWorker` instead.
class BuildSuccessWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
diff --git a/app/workers/environments/stop_job_success_worker.rb b/app/workers/environments/stop_job_success_worker.rb
new file mode 100644
index 00000000000..cc7d83512f3
--- /dev/null
+++ b/app/workers/environments/stop_job_success_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Environments
+ class StopJobSuccessWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+ idempotent!
+ feature_category :continuous_delivery
+
+ def perform(job_id, _params = {})
+ Ci::Build.find_by_id(job_id).try do |build|
+ stop_environment(build) if build.stops_environment? && build.stop_action_successful?
+ end
+ end
+
+ private
+
+ def stop_environment(build)
+ build.persisted_environment.fire_state_event(:stop_complete)
+ end
+ end
+end