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
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml4
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--.rubocop_todo/rspec/feature_category.yml1
-rw-r--r--.rubocop_todo/style/class_and_module_children.yml2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue87
-rw-r--r--app/assets/javascripts/environments/components/environment_namespace_selector.vue136
-rw-r--r--app/assets/javascripts/environments/constants.js2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_app.vue116
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js37
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js13
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js25
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js12
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/index.js2
-rw-r--r--app/assets/javascripts/ide/mount_oauth_callback.js12
-rw-r--r--app/assets/javascripts/pages/ide/index/index.js (renamed from app/assets/javascripts/pages/ide/index.js)0
-rw-r--r--app/assets/javascripts/pages/ide/oauth_redirect/index.js3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue4
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue1
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue1
-rw-r--r--app/controllers/ide_controller.rb19
-rw-r--r--app/controllers/user_settings/active_sessions_controller.rb (renamed from app/controllers/profiles/active_sessions_controller.rb)4
-rw-r--r--app/graphql/mutations/branch_rules/update.rb50
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/active_sessions_helper.rb2
-rw-r--r--app/helpers/ide_helper.rb15
-rw-r--r--app/helpers/nav/new_dropdown_helper.rb6
-rw-r--r--app/helpers/ssh_keys_helper.rb1
-rw-r--r--app/models/application_setting.rb1
-rw-r--r--app/models/ci/instance_variable.rb4
-rw-r--r--app/views/ide/oauth_redirect.html.haml3
-rw-r--r--app/views/shared/_ide_root.html.haml2
-rw-r--r--app/views/user_settings/active_sessions/_active_session.html.haml (renamed from app/views/profiles/active_sessions/_active_session.html.haml)0
-rw-r--r--app/views/user_settings/active_sessions/index.html.haml (renamed from app/views/profiles/active_sessions/index.html.haml)2
-rw-r--r--app/workers/all_queues.yml18
-rw-r--r--app/workers/ci/catalog/resources/process_sync_events_worker.rb12
-rw-r--r--config/feature_flags/development/cache_autocomplete_sources_members.yml2
-rw-r--r--config/feature_flags/development/web_ide_oauth.yml8
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/routes.rb1
-rw-r--r--config/routes/profile.rb2
-rw-r--r--config/routes/user_settings.rb14
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--data/deprecations/16-7-dependency-proxy-group-deploy-token.yml13
-rw-r--r--doc/administration/audit_event_streaming/audit_event_types.md3
-rw-r--r--doc/administration/pages/index.md143
-rw-r--r--doc/api/graphql/reference/index.md31
-rw-r--r--doc/ci/jobs/ci_job_token.md4
-rw-r--r--doc/update/deprecations.md18
-rw-r--r--doc/user/application_security/dependency_list/index.md4
-rw-r--r--doc/user/application_security/dependency_scanning/index.md2
-rw-r--r--doc/user/compliance/compliance_center/index.md28
-rw-r--r--doc/user/project/repository/signed_commits/index.md17
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml2
-rw-r--r--lib/gitlab/git/blob.rb2
-rw-r--r--lib/gitlab/nav/top_nav_menu_item.rb2
-rw-r--r--lib/gitlab/web_ide/default_oauth_application.rb57
-rw-r--r--lib/sidebars/user_settings/menus/active_sessions_menu.rb2
-rw-r--r--locale/gitlab.pot47
-rw-r--r--qa/qa/page/component/project_selector.rb10
-rw-r--r--qa/qa/page/main/menu.rb20
-rw-r--r--qa/qa/page/sub_menus/common.rb22
-rw-r--r--spec/controllers/user_settings/active_sessions_controller_spec.rb (renamed from spec/controllers/profiles/active_sessions_controller_spec.rb)2
-rw-r--r--spec/features/environments/environments_folder_spec.rb63
-rw-r--r--spec/features/user_settings/active_sessions_spec.rb (renamed from spec/features/profiles/active_sessions_spec.rb)6
-rw-r--r--spec/frontend/environments/environment_flux_resource_selector_spec.js2
-rw-r--r--spec/frontend/environments/environment_form_spec.js168
-rw-r--r--spec/frontend/environments/environment_namespace_selector_spec.js217
-rw-r--r--spec/frontend/environments/folder/environments_folder_app_spec.js47
-rw-r--r--spec/frontend/environments/graphql/mock_data.js150
-rw-r--r--spec/frontend/environments/graphql/resolvers/base_spec.js29
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js28
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js16
-rw-r--r--spec/frontend/ide/mount_oauth_callback_spec.js53
-rw-r--r--spec/helpers/nav/new_dropdown_helper_spec.rb6
-rw-r--r--spec/lib/gitlab/web_ide/default_oauth_application_spec.rb87
-rw-r--r--spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb2
-rw-r--r--spec/requests/api/graphql/mutations/branch_rules/update_spec.rb95
-rw-r--r--spec/requests/ide_controller_spec.rb86
-rw-r--r--spec/routing/routing_spec.rb10
-rw-r--r--spec/services/projects/hashed_storage/base_attachment_service_spec.rb2
-rw-r--r--spec/support/rspec_order_todo.yml4
-rw-r--r--spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb2
-rw-r--r--spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb20
-rw-r--r--workhorse/go.mod5
-rw-r--r--workhorse/go.sum9
94 files changed, 1803 insertions, 390 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index dca7ff24008..128b89773a0 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -837,7 +837,9 @@ Rails/TimeZone:
Rails/SaveBang:
Enabled: true
AllowImplicitReturn: false
- AllowedReceivers: ['ActionDispatch::TestRequest']
+ AllowedReceivers:
+ - ActionDispatch::TestRequest
+ - Tempfile
Include:
- 'spec/**/*.rb'
- 'ee/spec/**/*.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 471aed8912f..c568dce5138 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -3201,7 +3201,7 @@ Layout/LineLength:
- 'spec/features/milestones/user_views_milestone_spec.rb'
- 'spec/features/milestones/user_views_milestones_spec.rb'
- 'spec/features/oauth_login_spec.rb'
- - 'spec/features/profiles/active_sessions_spec.rb'
+ - 'spec/features/user_settings/active_sessions_spec.rb'
- 'spec/features/profiles/password_spec.rb'
- 'spec/features/profiles/personal_access_tokens_spec.rb'
- 'spec/features/profiles/two_factor_auths_spec.rb'
diff --git a/.rubocop_todo/rspec/feature_category.yml b/.rubocop_todo/rspec/feature_category.yml
index c956d05f6f0..4ea3134accf 100644
--- a/.rubocop_todo/rspec/feature_category.yml
+++ b/.rubocop_todo/rspec/feature_category.yml
@@ -1605,7 +1605,6 @@ RSpec/FeatureCategory:
- 'spec/controllers/oauth/tokens_controller_spec.rb'
- 'spec/controllers/passwords_controller_spec.rb'
- 'spec/controllers/profiles/accounts_controller_spec.rb'
- - 'spec/controllers/profiles/active_sessions_controller_spec.rb'
- 'spec/controllers/profiles/avatars_controller_spec.rb'
- 'spec/controllers/profiles/emails_controller_spec.rb'
- 'spec/controllers/profiles/gpg_keys_controller_spec.rb'
diff --git a/.rubocop_todo/style/class_and_module_children.yml b/.rubocop_todo/style/class_and_module_children.yml
index 89b20f231f9..fb5031f513c 100644
--- a/.rubocop_todo/style/class_and_module_children.yml
+++ b/.rubocop_todo/style/class_and_module_children.yml
@@ -102,7 +102,7 @@ Style/ClassAndModuleChildren:
- 'app/controllers/oauth/token_info_controller.rb'
- 'app/controllers/oauth/tokens_controller.rb'
- 'app/controllers/profiles/accounts_controller.rb'
- - 'app/controllers/profiles/active_sessions_controller.rb'
+ - 'app/controllers/user_settings/active_sessions_controller.rb'
- 'app/controllers/profiles/application_controller.rb'
- 'app/controllers/profiles/avatars_controller.rb'
- 'app/controllers/profiles/chat_names_controller.rb'
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 15058411922..e6c4740ae00 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-9171724863b73d3e98c1b2021a6095a58ac128e1
+b8fda37aff7f658c1b0195fd36cefc93fa3bb718
diff --git a/Gemfile b/Gemfile
index 0ebc784193f..a691eb85ae6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -390,7 +390,7 @@ group :development do
gem 'solargraph', '~> 0.47.2', require: false # rubocop:todo Gemfile/MissingFeatureCategory
gem 'letter_opener_web', '~> 2.0.0' # rubocop:todo Gemfile/MissingFeatureCategory
- gem 'lookbook', '~> 2.0', '>= 2.0.1' # rubocop:todo Gemfile/MissingFeatureCategory
+ gem 'lookbook', '~> 2.2' # rubocop:todo Gemfile/MissingFeatureCategory
# Better errors handler
gem 'better_errors', '~> 2.10.1' # rubocop:todo Gemfile/MissingFeatureCategory
diff --git a/Gemfile.checksum b/Gemfile.checksum
index bb6e4f41724..0e6b7025851 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -347,7 +347,7 @@
{"name":"lockbox","version":"1.3.0","platform":"ruby","checksum":"ca8e5806e4e0c56d1d762ac5cf401940ff53fc37554ef623d3313c7a6331a3ea"},
{"name":"lograge","version":"0.11.2","platform":"ruby","checksum":"4cbd1554b86f545d795eff15a0c24fd25057d2ac4e1caa5fc186168b3da932ef"},
{"name":"loofah","version":"2.22.0","platform":"ruby","checksum":"10d76e070c86b12fec74b6a9515fd1940f4459198b991342d0a7897d86c372fe"},
-{"name":"lookbook","version":"2.0.1","platform":"ruby","checksum":"0f14729c8c992810de0792a0be865a5792e5765fbaea5950cce74c6e5c73fc4a"},
+{"name":"lookbook","version":"2.2.0","platform":"ruby","checksum":"27c869364cf8bb8e9c61e43f909cbf5aec91dc35f4db791c3d0b7287a637a2d9"},
{"name":"lru_redux","version":"1.1.0","platform":"ruby","checksum":"ee71d0ccab164c51de146c27b480a68b3631d5b4297b8ffe8eda1c72de87affb"},
{"name":"lumberjack","version":"1.2.7","platform":"ruby","checksum":"a5c6aae6b4234f1420dbcd80b23e3bca0817bd239440dde097ebe3fa63c63b1f"},
{"name":"mail","version":"2.8.1","platform":"ruby","checksum":"ec3b9fadcf2b3755c78785cb17bc9a0ca9ee9857108a64b6f5cfc9c0b5bfc9ad"},
diff --git a/Gemfile.lock b/Gemfile.lock
index bce114b23e8..5be2590f8fa 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1019,7 +1019,7 @@ GEM
loofah (2.22.0)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
- lookbook (2.0.1)
+ lookbook (2.2.0)
activemodel
css_parser
htmlbeautifier (~> 1.3)
@@ -1950,7 +1950,7 @@ DEPENDENCIES
lockbox (~> 1.3.0)
lograge (~> 0.5)
loofah (~> 2.22.0)
- lookbook (~> 2.0, >= 2.0.1)
+ lookbook (~> 2.2)
lru_redux
mail (= 2.8.1)
mail-smtp_pool (~> 0.1.0)!
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index c6cf6b7e24b..a4c2d4fcc51 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -7,7 +7,6 @@ import {
GlCollapsibleListbox,
GlLink,
GlSprintf,
- GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isAbsolute } from '~/lib/utils/url_utility';
@@ -19,9 +18,9 @@ import {
import csrf from '~/lib/utils/csrf';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql';
import getUserAuthorizedAgents from '../graphql/queries/user_authorized_agents.query.graphql';
import EnvironmentFluxResourceSelector from './environment_flux_resource_selector.vue';
+import EnvironmentNamespaceSelector from './environment_namespace_selector.vue';
export default {
components: {
@@ -32,8 +31,8 @@ export default {
GlCollapsibleListbox,
GlLink,
GlSprintf,
- GlAlert,
EnvironmentFluxResourceSelector,
+ EnvironmentNamespaceSelector,
},
mixins: [glFeatureFlagsMixin()],
inject: {
@@ -72,8 +71,6 @@ export default {
urlFeedback: __('The URL should start with http:// or https://'),
agentLabel: s__('Environments|GitLab agent'),
agentHelpText: s__('Environments|Select agent'),
- namespaceLabel: s__('Environments|Kubernetes namespace (optional)'),
- namespaceHelpText: s__('Environments|Select namespace'),
save: __('Save'),
cancel: __('Cancel'),
reset: __('Reset'),
@@ -93,35 +90,9 @@ export default {
selectedAgentId: this.environment.clusterAgentId,
agentSearchTerm: '',
selectedNamespace: this.environment.kubernetesNamespace,
- k8sNamespaces: [],
- namespaceSearchTerm: '',
kubernetesError: '',
};
},
- apollo: {
- k8sNamespaces: {
- query: getNamespacesQuery,
- skip() {
- return !this.showNamespaceSelector;
- },
- variables() {
- return {
- configuration: this.k8sAccessConfiguration,
- };
- },
- update(data) {
- return data?.k8sNamespaces || [];
- },
- error(error) {
- this.kubernetesError = error.message;
- },
- result(result) {
- if (!result?.error && !result.errors?.length) {
- this.kubernetesError = null;
- }
- },
- },
- },
computed: {
loadingNamespacesList() {
return this.$apollo.queries.k8sNamespaces.loading;
@@ -161,26 +132,9 @@ export default {
item.text.toLowerCase().includes(lowerCasedSearchTerm),
);
},
- namespacesList() {
- return this.k8sNamespaces.map((item) => {
- return {
- value: item.metadata.name,
- text: item.metadata.name,
- };
- });
- },
- filteredNamespacesList() {
- const lowerCasedSearchTerm = this.namespaceSearchTerm.toLowerCase();
- return this.namespacesList.filter((item) =>
- item.text.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
showNamespaceSelector() {
return Boolean(this.selectedAgentId);
},
- namespaceDropdownToggleText() {
- return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
- },
showFluxResourceSelector() {
return Boolean(this.selectedNamespace && this.selectedAgentId);
},
@@ -239,9 +193,6 @@ export default {
fluxResourcePath: null,
});
},
- onNamespaceSearch(search) {
- this.namespaceSearchTerm = search;
- },
},
};
</script>
@@ -334,34 +285,14 @@ export default {
/>
</gl-form-group>
- <gl-form-group
+ <environment-namespace-selector
v-if="showNamespaceSelector"
- :label="$options.i18n.namespaceLabel"
- label-for="environment_namespace"
- >
- <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5">
- {{ kubernetesError }}
- </gl-alert>
- <gl-collapsible-listbox
- v-else
- id="environment_namespace"
- v-model="selectedNamespace"
- class="gl-w-full"
- data-testid="namespace-selector"
- block
- :items="filteredNamespacesList"
- :loading="loadingNamespacesList"
- :toggle-text="namespaceDropdownToggleText"
- :header-text="$options.i18n.namespaceHelpText"
- :reset-button-label="$options.i18n.reset"
- :searchable="true"
- @search="onNamespaceSearch"
- @select="
- onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null })
- "
- @reset="onChange({ ...environment, kubernetesNamespace: null })"
- />
- </gl-form-group>
+ :namespace="selectedNamespace"
+ :configuration="k8sAccessConfiguration"
+ @change="
+ onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null })
+ "
+ />
<environment-flux-resource-selector
v-if="showFluxResourceSelector"
diff --git a/app/assets/javascripts/environments/components/environment_namespace_selector.vue b/app/assets/javascripts/environments/components/environment_namespace_selector.vue
new file mode 100644
index 00000000000..101d70d36f3
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_namespace_selector.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlFormGroup, GlCollapsibleListbox, GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlCollapsibleListbox,
+ GlAlert,
+ GlButton,
+ GlSprintf,
+ },
+ props: {
+ configuration: {
+ required: true,
+ type: Object,
+ },
+ namespace: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ i18n: {
+ namespaceLabel: s__('Environments|Kubernetes namespace (optional)'),
+ namespaceHelpText: s__('Environments|Select namespace'),
+ selectButton: s__('Environments|Or select namespace: %{searchTerm}'),
+ reset: __('Reset'),
+ },
+ data() {
+ return {
+ k8sNamespaces: [],
+ searchTerm: '',
+ kubernetesError: '',
+ };
+ },
+ apollo: {
+ k8sNamespaces: {
+ query: getNamespacesQuery,
+
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sNamespaces?.map((item) => {
+ return {
+ value: item.metadata.name,
+ text: item.metadata.name,
+ };
+ }) || []
+ );
+ },
+ error(error) {
+ this.kubernetesError = error.message;
+ },
+ result(result) {
+ if (!result?.error && !result.errors?.length) {
+ this.kubernetesError = null;
+ }
+ },
+ },
+ },
+ computed: {
+ loadingNamespacesList() {
+ return this.$apollo.queries.k8sNamespaces.loading;
+ },
+ filteredNamespacesList() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.k8sNamespaces.filter((item) =>
+ item.text.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ namespaceDropdownToggleText() {
+ return this.namespace || this.$options.i18n.namespaceHelpText;
+ },
+ shouldRenderSelectButton() {
+ const hasSearchedItem = this.k8sNamespaces.some(
+ (item) => item.text === this.searchTerm.toLowerCase(),
+ );
+ return this.searchTerm && !hasSearchedItem;
+ },
+ },
+ methods: {
+ onChange(namespace) {
+ this.$emit('change', namespace);
+ },
+ onNamespaceSearch(search) {
+ this.searchTerm = search;
+ },
+ onSelect(namespace) {
+ this.onChange(namespace);
+ this.$refs.namespaceSelector.close();
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="$options.i18n.namespaceLabel" label-for="environment_namespace">
+ <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5">
+ {{ kubernetesError }}
+ </gl-alert>
+ <gl-collapsible-listbox
+ id="environment_namespace"
+ ref="namespaceSelector"
+ :selected="namespace"
+ class="gl-w-full"
+ block
+ :items="filteredNamespacesList"
+ :loading="loadingNamespacesList"
+ :toggle-text="namespaceDropdownToggleText"
+ :header-text="$options.i18n.namespaceHelpText"
+ :reset-button-label="$options.i18n.reset"
+ :searchable="true"
+ @search="onNamespaceSearch"
+ @select="onChange"
+ @reset="onChange"
+ >
+ <template v-if="shouldRenderSelectButton" #footer>
+ <gl-button
+ category="tertiary"
+ class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!"
+ :class="{ 'gl-mt-3': !filteredNamespacesList.length }"
+ @click="onSelect(searchTerm)"
+ >
+ <gl-sprintf :message="$options.i18n.selectButton">
+ <template #searchTerm>{{ searchTerm }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </template>
+ </gl-collapsible-listbox>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 4f0eabb26a4..2fe9008c042 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -162,7 +162,7 @@ const ERROR_OTHER = 'other';
export const CLUSTER_AGENT_ERROR_MESSAGES = {
[ERROR_UNAUTHORIZED]: s__(
- 'Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again.',
+ "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it.",
),
[ERROR_FORBIDDEN]: s__(
'Environment|Forbidden to access the cluster agent from this environment.',
diff --git a/app/assets/javascripts/environments/folder/environments_folder_app.vue b/app/assets/javascripts/environments/folder/environments_folder_app.vue
index f2c1b2f5cdf..6804e5cc0c7 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_app.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_app.vue
@@ -1,13 +1,29 @@
<script>
-import { GlSkeletonLoader } from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlSkeletonLoader, GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import folderQuery from '../graphql/queries/folder.query.graphql';
+import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
+import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
+import environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql';
import EnvironmentItem from '../components/new_environment_item.vue';
+import StopEnvironmentModal from '../components/stop_environment_modal.vue';
+import ConfirmRollbackModal from '../components/confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
+import CanaryUpdateModal from '../components/canary_update_modal.vue';
+import { ENVIRONMENTS_SCOPE } from '../constants';
export default {
components: {
+ GlBadge,
+ GlTabs,
+ GlTab,
GlSkeletonLoader,
EnvironmentItem,
+ StopEnvironmentModal,
+ ConfirmRollbackModal,
+ DeleteEnvironmentModal,
+ CanaryUpdateModal,
},
props: {
folderName: {
@@ -18,6 +34,20 @@ export default {
type: String,
required: true,
},
+ scope: {
+ type: String,
+ required: true,
+ default: ENVIRONMENTS_SCOPE.ACTIVE,
+ },
+ },
+ data() {
+ return {
+ environmentToDelete: {},
+ environmentToRollback: {},
+ environmentToStop: {},
+ environmentToChangeCanary: {},
+ weight: 0,
+ };
},
apollo: {
folder: {
@@ -25,11 +55,27 @@ export default {
variables() {
return {
environment: this.environmentQueryData,
- scope: '',
+ scope: this.scope,
search: '',
perPage: 10,
};
},
+ pollInterval: 3000,
+ },
+ environmentToDelete: {
+ query: environmentToDeleteQuery,
+ },
+ environmentToRollback: {
+ query: environmentToRollbackQuery,
+ },
+ environmentToStop: {
+ query: environmentToStopQuery,
+ },
+ environmentToChangeCanary: {
+ query: environmentToChangeCanaryQuery,
+ },
+ weight: {
+ query: environmentToChangeCanaryQuery,
},
},
computed: {
@@ -42,18 +88,65 @@ export default {
isLoading() {
return this.$apollo.queries.folder.loading;
},
+ activeCount() {
+ return this.folder?.activeCount ?? '-';
+ },
+ stoppedCount() {
+ return this.folder?.stoppedCount ?? '-';
+ },
+ activeTab() {
+ return this.scope === ENVIRONMENTS_SCOPE.ACTIVE ? 0 : 1;
+ },
+ },
+ methods: {
+ setScope(scope) {
+ if (scope !== this.scope) {
+ this.$router.push({ query: { scope } });
+ }
+ },
},
i18n: {
pageTitle: s__('Environments|Environments'),
+ active: __('Active'),
+ stopped: __('Stopped'),
},
+ ENVIRONMENTS_SCOPE,
};
</script>
<template>
<div>
+ <delete-environment-modal :environment="environmentToDelete" graphql />
+ <stop-environment-modal :environment="environmentToStop" graphql />
+ <confirm-rollback-modal :environment="environmentToRollback" graphql />
+ <canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
<h4 class="gl-font-weight-normal" data-testid="folder-name">
{{ $options.i18n.pageTitle }} /
<b>{{ folderName }}</b>
</h4>
+ <gl-tabs :value="activeTab" query-param-name="scope">
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
+ >
+ <template #title>
+ <span>{{ $options.i18n.active }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ activeCount }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.STOPPED"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.STOPPED)"
+ >
+ <template #title>
+ <span>{{ $options.i18n.stopped }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ stoppedCount }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
<div v-if="isLoading">
<div
v-for="n in 3"
@@ -63,12 +156,15 @@ export default {
<gl-skeleton-loader :lines="2" />
</div>
</div>
- <environment-item
- v-for="environment in environments"
- :key="environment.name"
- :environment="environment"
- class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3"
- in-folder
- />
+ <div v-else>
+ <environment-item
+ v-for="environment in environments"
+ :id="environment.name"
+ :key="environment.name"
+ :environment="environment"
+ class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3"
+ in-folder
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 2f06f003bb2..44a5d1e3662 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
import { apolloProvider } from '../graphql/client';
@@ -17,28 +18,46 @@ export default () => {
const el = document.getElementById('environments-folder-list-view');
const environmentsData = el.dataset;
if (gon.features.environmentsFolderNewLook) {
+ Vue.use(VueRouter);
+
const folderName = environmentsData.environmentsDataFolderName;
const folderPath = environmentsData.environmentsDataEndpoint.replace('.json', '');
const projectPath = environmentsData.environmentsDataProjectPath;
const helpPagePath = environmentsData.environmentsDataHelpPagePath;
+ const router = new VueRouter({
+ mode: 'history',
+ base: window.location.pathname,
+ routes: [
+ {
+ path: '/',
+ name: 'environments_folder',
+ component: EnvironmentsFolderApp,
+ props: (route) => ({
+ scope: route.query.scope,
+ folderName,
+ folderPath,
+ }),
+ },
+ ],
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+ });
+
return new Vue({
el,
- components: {
- EnvironmentsFolderApp,
- },
provide: {
projectPath,
helpPagePath,
},
apolloProvider,
+ router,
render(createElement) {
- return createElement('environments-folder-app', {
- props: {
- folderName,
- folderPath,
- },
- });
+ return createElement('router-view');
},
});
}
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
index 404b7024cde..8b0ac039290 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/base.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -75,7 +75,7 @@ export const baseQueries = (endpoint) => ({
});
export const baseMutations = {
- stopEnvironmentREST(_, { environment }, { client }) {
+ stopEnvironmentREST(_, { environment }, { client, cache }) {
client.writeQuery({
query: isEnvironmentStoppingQuery,
variables: { environment },
@@ -84,6 +84,9 @@ export const baseMutations = {
return axios
.post(environment.stopPath)
.then(() => buildErrors())
+ .then(() => {
+ cache.evict({ fieldName: 'folder' });
+ })
.catch(() => {
client.writeQuery({
query: isEnvironmentStoppingQuery,
@@ -95,10 +98,11 @@ export const baseMutations = {
]);
});
},
- deleteEnvironment(_, { environment: { deletePath } }) {
+ deleteEnvironment(_, { environment: { deletePath } }, { cache }) {
return axios
.delete(deletePath)
.then(() => buildErrors())
+ .then(() => cache.evict({ fieldName: 'folder' }))
.catch(() =>
buildErrors([
s__(
@@ -107,10 +111,13 @@ export const baseMutations = {
]),
);
},
- rollbackEnvironment(_, { environment, isLastDeployment }) {
+ rollbackEnvironment(_, { environment, isLastDeployment }, { cache }) {
return axios
.post(environment?.retryUrl)
.then(() => buildErrors())
+ .then(() => {
+ cache.evict({ fieldName: 'folder' });
+ })
.catch(() => {
buildErrors([
isLastDeployment
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 868830c953a..f5fb4c8be2f 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -6,10 +6,13 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import csrf from '~/lib/utils/csrf';
import Tracking from '~/tracking';
-import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
-import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
+import {
+ getBaseConfig,
+ getOAuthConfig,
+ setupRootElement,
+ handleTracking,
+} from './lib/gitlab_web_ide';
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
-import { handleTracking } from './lib/gitlab_web_ide/handle_tracking_event';
const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
const remotePath = cleanLeadingSeparator(remotePathArg);
@@ -51,15 +54,21 @@ export const initGitlabWebIDE = async (el) => {
: null;
const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
+ const oauthConfig = getOAuthConfig(el.dataset);
+ const httpHeaders = oauthConfig
+ ? undefined
+ : // Use same headers as defined in axios_utils (not needed in oauth)
+ {
+ [csrf.headerKey]: csrf.token,
+ 'X-Requested-With': 'XMLHttpRequest',
+ };
+
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
start(rootEl, {
...getBaseConfig(),
nonce,
- // Use same headers as defined in axios_utils
- httpHeaders: {
- [csrf.headerKey]: csrf.token,
- 'X-Requested-With': 'XMLHttpRequest',
- },
+ httpHeaders,
+ auth: oauthConfig,
projectPath,
ref,
filePath,
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js
new file mode 100644
index 00000000000..5493a9ba7c7
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js
@@ -0,0 +1,12 @@
+export const getOAuthConfig = ({ clientId, callbackUrl }) => {
+ if (!clientId) {
+ return undefined;
+ }
+
+ return {
+ type: 'oauth',
+ clientId,
+ callbackUrl,
+ protectRefreshToken: true,
+ };
+};
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
index 8311e11672e..87e0002c8c8 100644
--- a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
@@ -1,2 +1,4 @@
export * from './get_base_config';
+export * from './get_oauth_config';
+export * from './handle_tracking_event';
export * from './setup_root_element';
diff --git a/app/assets/javascripts/ide/mount_oauth_callback.js b/app/assets/javascripts/ide/mount_oauth_callback.js
new file mode 100644
index 00000000000..79fffb24f8e
--- /dev/null
+++ b/app/assets/javascripts/ide/mount_oauth_callback.js
@@ -0,0 +1,12 @@
+import { oauthCallback } from '@gitlab/web-ide';
+import { getBaseConfig, getOAuthConfig } from './lib/gitlab_web_ide';
+
+export const mountOAuthCallback = () => {
+ const el = document.getElementById('ide');
+
+ return oauthCallback({
+ ...getBaseConfig(),
+ username: gon.current_username,
+ auth: getOAuthConfig(el.dataset),
+ });
+};
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index/index.js
index 15933256e75..15933256e75 100644
--- a/app/assets/javascripts/pages/ide/index.js
+++ b/app/assets/javascripts/pages/ide/index/index.js
diff --git a/app/assets/javascripts/pages/ide/oauth_redirect/index.js b/app/assets/javascripts/pages/ide/oauth_redirect/index.js
new file mode 100644
index 00000000000..ee9233fab38
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/oauth_redirect/index.js
@@ -0,0 +1,3 @@
+import { mountOAuthCallback } from '~/ide/mount_oauth_callback';
+
+mountOAuthCallback();
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
index 50fcd3c9350..478d261d06c 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
@@ -37,7 +37,6 @@ export default {
category="tertiary"
size="small"
class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
- data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>
{{ __('Edit') }}
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index a672e254004..f93a0256bd4 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -111,7 +111,7 @@ export default {
:id="`menu-section-button-${itemId}`"
class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-2 gl-py-2 gl-px-3 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus"
:class="computedLinkClasses"
- data-qa-selector="menu_section_button"
+ data-testid="menu-section-button"
:data-qa-section-name="item.title"
v-bind="buttonProps"
@click="isExpanded = !isExpanded"
@@ -153,7 +153,7 @@ export default {
:id="itemId"
v-model="isExpanded"
class="gl-list-style-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease"
- data-qa-selector="menu_section"
+ data-testid="menu-section"
:data-qa-section-name="item.title"
>
<slot>
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 52015484cb5..3beec4ccc14 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -188,7 +188,6 @@ export default {
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
- data-qa-selector="navbar"
:inert="sidebarState.isCollapsed"
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 5dab74374df..1e8f0a81088 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -150,7 +150,7 @@ export default {
href: this.data.sign_out_link,
extraAttrs: {
'data-method': 'post',
- 'data-testid': 'sign_out_link',
+ 'data-testid': 'sign-out-link',
class: 'sign-out-link',
},
},
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index 2bdc8a174d0..e12e06a2454 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -36,11 +36,6 @@ export default {
required: false,
default: 'confirm-danger-button',
},
- buttonQaSelector: {
- type: String,
- required: false,
- default: null,
- },
buttonVariant: {
type: String,
required: false,
@@ -58,7 +53,6 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
- :data-qa-selector="buttonQaSelector"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 67ad7769c7c..f3b483c5f53 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -100,7 +100,7 @@ export default {
type="search"
class="mb-3"
autofocus
- data-qa-selector="project_search_field"
+ data-testid="project-search-field"
@input="onInput"
/>
<div class="d-flex flex-column">
@@ -120,7 +120,7 @@ export default {
:project="project"
:matcher="searchQuery"
class="js-project-list-item"
- data-qa-selector="project_list_item"
+ data-testid="project-list-item"
@click="projectClicked(project)"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index c1ec39e1545..b81d288d932 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -110,7 +110,6 @@ export default {
:loading="isLoading"
:variant="variant"
:category="category"
- :data-qa-selector="`${feature.type}_mr_button`"
@click="mutate"
>{{ $options.i18n.buttonLabel }}</gl-button
>
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index d9566121dcd..4a4d41f3e6f 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -5,7 +5,8 @@ class IdeController < ApplicationController
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
- before_action :authorize_read_project!
+ before_action :authorize_read_project!, only: [:index]
+ before_action :ensure_web_ide_oauth_application!, only: [:index]
before_action do
push_frontend_feature_flag(:build_service_proxy)
@@ -27,12 +28,28 @@ class IdeController < ApplicationController
render layout: helpers.use_new_web_ide? ? 'fullscreen' : 'application'
end
+ def oauth_redirect
+ return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user)
+ # TODO - It's **possible** we end up here and no oauth application has been set up.
+ # We need to have better handling of these edge cases. Here's a follow-up issue:
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/433322
+ return render_404 unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application
+
+ render layout: 'fullscreen', locals: { minimal: true }
+ end
+
private
def authorize_read_project!
render_404 unless can?(current_user, :read_project, project)
end
+ def ensure_web_ide_oauth_application!
+ return unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user)
+
+ ::Gitlab::WebIde::DefaultOauthApplication.ensure_oauth_application!
+ end
+
def fork_info(project, branch)
return if can?(current_user, :push_code, project)
diff --git a/app/controllers/profiles/active_sessions_controller.rb b/app/controllers/user_settings/active_sessions_controller.rb
index 5a86179b89f..da5664a8c1b 100644
--- a/app/controllers/profiles/active_sessions_controller.rb
+++ b/app/controllers/user_settings/active_sessions_controller.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-class Profiles::ActiveSessionsController < Profiles::ApplicationController
+class UserSettings::ActiveSessionsController < Profiles::ApplicationController
feature_category :system_access
def index
@@ -13,7 +13,7 @@ class Profiles::ActiveSessionsController < Profiles::ApplicationController
current_user.forget_me!
respond_to do |format|
- format.html { redirect_to profile_active_sessions_url, status: :found }
+ format.html { redirect_to user_settings_active_sessions_url, status: :found }
format.js { head :ok }
end
end
diff --git a/app/graphql/mutations/branch_rules/update.rb b/app/graphql/mutations/branch_rules/update.rb
new file mode 100644
index 00000000000..c10e11970eb
--- /dev/null
+++ b/app/graphql/mutations/branch_rules/update.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Mutations
+ module BranchRules
+ class Update < BaseMutation
+ graphql_name 'BranchRuleUpdate'
+
+ include FindsProject
+
+ authorize :admin_project
+
+ argument :id, ::Types::GlobalIDType[::ProtectedBranch],
+ required: true,
+ description: 'Global ID of the protected branch.'
+
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Branch name, with wildcards, for the branch rules.'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path to the project that the branch is associated with.'
+
+ field :branch_rule,
+ Types::Projects::BranchRuleType,
+ null: true,
+ description: 'Branch rule after mutation.'
+
+ def resolve(id:, project_path:, name:)
+ protected_branch = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(id,
+ expected_type: ::ProtectedBranch))
+ raise_resource_not_available_error! unless protected_branch
+
+ project = authorized_find!(project_path)
+
+ protected_branch = ::ProtectedBranches::UpdateService.new(project, current_user,
+ { name: name }).execute(protected_branch)
+
+ if protected_branch.errors.empty?
+ {
+ branch_rule: ::Projects::BranchRule.new(project, protected_branch),
+ errors: []
+ }
+ else
+ { errors: errors_on_object(protected_branch) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 9b3c443200d..5593911d1fb 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -110,6 +110,7 @@ module Types
mount_mutation Mutations::Organizations::Update, alpha: { milestone: '16.7' }
mount_mutation Mutations::Projects::SyncFork, calls_gitaly: true, alpha: { milestone: '15.9' }
mount_mutation Mutations::Projects::Star, alpha: { milestone: '16.7' }
+ mount_mutation Mutations::BranchRules::Update, alpha: { milestone: '16.7' }
mount_mutation Mutations::Releases::Create
mount_mutation Mutations::Releases::Update
mount_mutation Mutations::Releases::Delete
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
index cfe0b747e78..48639526c31 100644
--- a/app/helpers/active_sessions_helper.rb
+++ b/app/helpers/active_sessions_helper.rb
@@ -24,6 +24,6 @@ module ActiveSessionsHelper
end
def revoke_session_path(active_session)
- profile_active_session_path(active_session.session_private_id)
+ user_settings_active_session_path(active_session.session_private_id)
end
end
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index f2d393f1f77..2ec11b8a9ed 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -52,6 +52,19 @@ module IdeHelper
{}
end
+ def new_ide_oauth_data
+ return {} unless ::Gitlab::WebIde::DefaultOauthApplication.feature_enabled?(current_user)
+ return {} unless ::Gitlab::WebIde::DefaultOauthApplication.oauth_application
+
+ client_id = ::Gitlab::WebIde::DefaultOauthApplication.oauth_application.uid
+ callback_url = ::Gitlab::WebIde::DefaultOauthApplication.oauth_callback_url
+
+ {
+ 'client-id' => client_id,
+ 'callback-url' => callback_url
+ }
+ end
+
def new_ide_data(project:)
{
'project-path' => project&.path_with_namespace,
@@ -59,7 +72,7 @@ module IdeHelper
# We will replace these placeholders in the FE
'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path'),
'editor-font' => new_ide_fonts.to_json
- }.merge(new_ide_code_suggestions_data)
+ }.merge(new_ide_code_suggestions_data).merge(new_ide_oauth_data)
end
def legacy_ide_data(project:)
diff --git a/app/helpers/nav/new_dropdown_helper.rb b/app/helpers/nav/new_dropdown_helper.rb
index 88e834b537a..af81e7832c8 100644
--- a/app/helpers/nav/new_dropdown_helper.rb
+++ b/app/helpers/nav/new_dropdown_helper.rb
@@ -116,7 +116,7 @@ module Nav
id: 'general_new_project',
title: _('New project/repository'),
href: new_project_path,
- data: { track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_project_link' }
+ data: { track_action: 'click_link_new_project', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-project-link' }
)
)
end
@@ -127,7 +127,7 @@ module Nav
id: 'general_new_group',
title: _('New group'),
href: new_group_path,
- data: { track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_group_link' }
+ data: { track_action: 'click_link_new_group', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-group-link' }
)
)
end
@@ -149,7 +149,7 @@ module Nav
id: 'general_new_snippet',
title: _('New snippet'),
href: new_snippet_path,
- data: { track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global_new_snippet_link' }
+ data: { track_action: 'click_link_new_snippet_parent', track_label: 'plus_menu_dropdown', track_property: 'navigation_top', testid: 'global-new-snippet-link' }
)
)
end
diff --git a/app/helpers/ssh_keys_helper.rb b/app/helpers/ssh_keys_helper.rb
index 974a6869528..d640e7ffba9 100644
--- a/app/helpers/ssh_keys_helper.rb
+++ b/app/helpers/ssh_keys_helper.rb
@@ -33,7 +33,6 @@ module SshKeysHelper
title: title,
aria_label: title,
modal_attributes: {
- 'data-qa-selector': 'ssh_key_revoke_modal',
title: _('Are you sure you want to revoke this SSH key?'),
message: _('This action cannot be undone, and will permanently delete the %{key} SSH key. All commits signed using this SSH key will be marked as unverified.') % { key: key.title },
okVariant: 'danger',
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 7942b2b9236..51cdd993c97 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -42,6 +42,7 @@ class ApplicationSetting < MainClusterwide::ApplicationRecord
add_authentication_token_field :error_tracking_access_token, encrypted: :required
belongs_to :push_rule
+ belongs_to :web_ide_oauth_application, class_name: 'Doorkeeper::Application'
alias_attribute :housekeeping_optimize_repository_period, :housekeeping_incremental_repack_period
diff --git a/app/models/ci/instance_variable.rb b/app/models/ci/instance_variable.rb
index 3e572dbe18f..179befb8469 100644
--- a/app/models/ci/instance_variable.rb
+++ b/app/models/ci/instance_variable.rb
@@ -47,5 +47,9 @@ module Ci
end
end
end
+
+ def audit_details
+ key
+ end
end
end
diff --git a/app/views/ide/oauth_redirect.html.haml b/app/views/ide/oauth_redirect.html.haml
new file mode 100644
index 00000000000..e0ce9f9768c
--- /dev/null
+++ b/app/views/ide/oauth_redirect.html.haml
@@ -0,0 +1,3 @@
+- page_title _("IDE")
+
+= render partial: 'shared/ide_root', locals: { data: new_ide_oauth_data, loading_text: _('Authenticating...') }
diff --git a/app/views/shared/_ide_root.html.haml b/app/views/shared/_ide_root.html.haml
index db3e76e188c..173b081d693 100644
--- a/app/views/shared/_ide_root.html.haml
+++ b/app/views/shared/_ide_root.html.haml
@@ -5,6 +5,6 @@
-# 100vh because of the presence of the bottom bar
#ide.gl-h-full{ data: data }
- .web-ide-loader.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-h-full.gl-mr-auto.gl-ml-auto
+ .web-ide-loader.gl-display-flex.gl-justify-content-center.gl-align-items-center.gl-flex-direction-column.gl-h-full.gl-mx-auto
= brand_header_logo
%h3.clblack.gl-mt-6= loading_text
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/user_settings/active_sessions/_active_session.html.haml
index e91c28e6e84..e91c28e6e84 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/user_settings/active_sessions/_active_session.html.haml
diff --git a/app/views/profiles/active_sessions/index.html.haml b/app/views/user_settings/active_sessions/index.html.haml
index baca9559e08..fb1ca1a431c 100644
--- a/app/views/profiles/active_sessions/index.html.haml
+++ b/app/views/user_settings/active_sessions/index.html.haml
@@ -12,4 +12,4 @@
= render Pajamas::CardComponent.new(card_options: { class: 'gl-border-0' }, body_options: { class: 'gl-p-0' }) do |c|
- c.with_body do
%ul.list-group.list-group-flush
- = render partial: 'profiles/active_sessions/active_session', collection: @sessions
+ = render partial: 'user_settings/active_sessions/active_session', collection: @sessions
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 89047bda445..3d8c349910e 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -246,6 +246,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:ci_catalog_resources_process_sync_events
+ :worker_name: Ci::Catalog::Resources::ProcessSyncEventsWorker
+ :feature_category: :pipeline_composition
+ :has_external_dependencies: false
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:ci_delete_unit_tests
:worker_name: Ci::DeleteUnitTestsWorker
:feature_category: :code_testing
@@ -2694,15 +2703,6 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: ci_catalog_resources_process_sync_events
- :worker_name: Ci::Catalog::Resources::ProcessSyncEventsWorker
- :feature_category: :pipeline_composition
- :has_external_dependencies: false
- :urgency: :high
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: ci_delete_objects
:worker_name: Ci::DeleteObjectsWorker
:feature_category: :continuous_integration
diff --git a/app/workers/ci/catalog/resources/process_sync_events_worker.rb b/app/workers/ci/catalog/resources/process_sync_events_worker.rb
index a577f36858d..10a3e4f0a35 100644
--- a/app/workers/ci/catalog/resources/process_sync_events_worker.rb
+++ b/app/workers/ci/catalog/resources/process_sync_events_worker.rb
@@ -5,8 +5,18 @@ module Ci
module Resources
# This worker can be called multiple times simultaneously but only one can process events
# at a time. This is ensured by `try_obtain_lease` in `Ci::ProcessSyncEventsService`.
+ #
+ # This worker is enqueued in 3 ways:
+ # 1. By Project model callback after updating one of the columns referenced in
+ # `Ci::Catalog::Resource#sync_with_project`.
+ # 2. Every minute by cron job. This ensures we process SyncEvents from direct/bulk
+ # database updates that do not use the Project AR model.
+ # 3. By `Ci::ProcessSyncEventsService` if there are any remaining pending
+ # SyncEvents after processing.
+ #
class ProcessSyncEventsWorker
include ApplicationWorker
+ include CronjobQueue # rubocop: disable Scalability/CronWorkerContext -- Periodic processing is required
feature_category :pipeline_composition
@@ -17,6 +27,8 @@ module Ci
deduplicate :until_executed, if_deduplicated: :reschedule_once, ttl: 1.minute
def perform
+ return if Feature.disabled?(:ci_process_catalog_resource_sync_events)
+
results = ::Ci::ProcessSyncEventsService.new(
::Ci::Catalog::Resources::SyncEvent, ::Ci::Catalog::Resource
).execute
diff --git a/config/feature_flags/development/cache_autocomplete_sources_members.yml b/config/feature_flags/development/cache_autocomplete_sources_members.yml
index c80a7490031..f55c604153a 100644
--- a/config/feature_flags/development/cache_autocomplete_sources_members.yml
+++ b/config/feature_flags/development/cache_autocomplete_sources_members.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/427452
milestone: '16.5'
type: development
group: group::global search
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/web_ide_oauth.yml b/config/feature_flags/development/web_ide_oauth.yml
new file mode 100644
index 00000000000..fc3132d01b4
--- /dev/null
+++ b/config/feature_flags/development/web_ide_oauth.yml
@@ -0,0 +1,8 @@
+---
+name: web_ide_oauth
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138015
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433324
+milestone: '16.7'
+type: development
+group: group::ide
+default_enabled: false
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index ef1199fefef..24182e6b7db 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -700,6 +700,9 @@ Settings.cron_jobs['deactivated_pages_deployments_delete_cron_worker']['job_clas
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker'] ||= {}
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['ci_schedule_unlock_pipelines_in_queue_worker']['job_class'] = 'Ci::ScheduleUnlockPipelinesInQueueCronWorker'
+Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker'] ||= {}
+Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker']['cron'] ||= '*/1 * * * *'
+Settings.cron_jobs['ci_catalog_resources_process_sync_events_worker']['job_class'] = 'Ci::Catalog::Resources::ProcessSyncEventsWorker'
Gitlab.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= {}
diff --git a/config/routes.rb b/config/routes.rb
index 18c3e257f8e..b433a88f1c5 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -131,6 +131,7 @@ InitializerConnections.raise_if_new_database_connection do
scope :ide, as: :ide, format: false do
get '/', to: 'ide#index'
get '/project', to: 'ide#index'
+ get '/oauth_redirect', to: 'ide#oauth_redirect'
scope path: 'project/:project_id', as: :project, constraints: { project_id: Gitlab::PathRegex.full_namespace_route_regex } do
%w[edit tree blob].each do |action|
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index b4f00fa4ad8..36feb075f4b 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -61,7 +61,7 @@ resource :profile, only: [:show, :update] do
put :revoke
end
end
- resources :active_sessions, only: [:index, :destroy]
+
resources :emails, only: [:index, :create, :destroy] do
member do
put :resend_confirmation_instructions
diff --git a/config/routes/user_settings.rb b/config/routes/user_settings.rb
index 478d807c8b5..f7891118af3 100644
--- a/config/routes/user_settings.rb
+++ b/config/routes/user_settings.rb
@@ -1,7 +1,17 @@
# frozen_string_literal: true
-scope module: 'user_settings' do
- namespace :user_settings do
+namespace :user_settings do
+ scope module: 'user_settings' do
get :authentication_log
end
+ resources :active_sessions, only: [:index, :destroy]
+end
+
+# Redirect routes till GitLab 17.0 release
+
+resource :profile, only: [] do
+ resources :active_sessions, only: [:destroy], controller: 'user_settings/active_sessions'
+ member do
+ get :active_sessions, to: redirect('-/user_settings/active_sessions')
+ end
end
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 25a4299ab70..77503814158 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -157,8 +157,6 @@
- 1
- - ci_cancel_redundant_pipelines
- 1
-- - ci_catalog_resources_process_sync_events
- - 1
- - ci_delete_objects
- 1
- - ci_initialize_pipelines_iid_sequence
diff --git a/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml b/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml
new file mode 100644
index 00000000000..d19159f36b8
--- /dev/null
+++ b/data/deprecations/16-7-dependency-proxy-group-deploy-token.yml
@@ -0,0 +1,13 @@
+- title: "Dependency Proxy: group access tokens to have additional scope checks for service accounts"
+ announcement_milestone: "16.7"
+ removal_milestone: "17.0"
+ breaking_change: true
+ reporter: trizzi
+ stage: Package
+ issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/431386
+ body: |
+ When using the Dependency Proxy for containers with a group access token, `docker login` and `docker pull` requests with insufficient scopes for Dependency Proxy are not rejected.
+
+ GitLab 16.7 adds checks for group access tokens authenticating for the dependency proxy for containers. This is a breaking change, because tokens without the required scopes will fail.
+
+ To help avoid being impacted by this breaking change, create new group access tokens with the [required scopes](https://docs.gitlab.com/ee/user/packages/dependency_proxy/#authenticate-with-the-dependency-proxy), and update your workflow variables and scripts with those new tokens.
diff --git a/doc/administration/audit_event_streaming/audit_event_types.md b/doc/administration/audit_event_streaming/audit_event_types.md
index b1ff9640d77..28cab04553a 100644
--- a/doc/administration/audit_event_streaming/audit_event_types.md
+++ b/doc/administration/audit_event_streaming/audit_event_types.md
@@ -180,6 +180,9 @@ Audit event types belong to the following product categories.
| [`ci_group_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a CI variable is created at a group level| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_group_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a group's CI variable is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_group_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a group's CI variable is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
+| [`ci_instance_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI variable is created| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) |
+| [`ci_instance_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI varialbe is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) |
+| [`ci_instance_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/131882) | When an instance level CI variable is changed| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [16.5](https://gitlab.com/gitlab-org/gitlab/-/issues/8070) |
| [`ci_variable_created`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a CI variable is created at a project level| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_variable_deleted`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is deleted| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
| [`ci_variable_updated`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91983) | Triggered when a project's CI variable is updated| **{check-circle}** Yes | **{check-circle}** Yes | GitLab [15.2](https://gitlab.com/gitlab-org/gitlab/-/issues/363090) |
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index d450ee1ec6d..e88d60aebe7 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -93,6 +93,42 @@ Where `example.io` is the domain GitLab Pages is served from,
`192.0.2.1` is the IPv4 address of your GitLab instance, and `2001:db8::1` is the
IPv6 address. If you don't have IPv6, you can omit the `AAAA` record.
+#### For namespace in URL path, without wildcard DNS **(EXPERIMENT)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md).
+
+FLAG:
+On self-managed GitLab, by default this feature is available.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
+Prerequisites:
+
+- Your instance must use the Linux package installation method.
+
+If you need support for namespace in the URL path to remove the requirement for wildcard DNS:
+
+1. Enable the GitLab Pages flag for this feature by adding
+ `gitlab_pages["namespace_in_path"] = true` to `gitlab.rb`. For more information,
+ see [Use environment variables](#use-environment-variables).
+1. In your DNS provider, add entries for `example.com` and `projects.example.com`.
+ In both lines, replace `example.com` with your domain name, and `192.0.0.0` with
+ the IPv4 version of your IP address. The entries look like this:
+
+ ```plaintext
+ example.com 1800 IN A 192.0.0.0
+ projects.example.com 1800 IN A 192.0.0.0
+ ```
+
+1. Optional. If your GitLab instance has an IPv6 address, add entries for it.
+ In both lines, replace `example.com` with your domain name, and `2001:db8::1` with
+ the IPv6 version of your IP address. The entries look like this:
+
+ ```plaintext
+ example.com 1800 IN AAAA 2001:db8::1
+ projects.example.com 1800 IN AAAA 2001:db8::1
+ ```
+
#### DNS configuration for custom domains
If support for custom domains is needed, all subdomains of the Pages root domain should point to the
@@ -149,6 +185,47 @@ The Pages daemon doesn't listen to the outside world.
Watch the [video tutorial](https://youtu.be/dD8c7WNcc6s) for this configuration.
+### Pages domain without wildcard DNS **(EXPERIMENT)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md).
+
+FLAG:
+On self-managed GitLab, by default this feature is available.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
+This configuration is the minimum setup for GitLab Pages. It is the base for all
+other configurations. In this configuration, NGINX proxies all requests to the daemon,
+because the GitLab Pages daemon doesn't listen to the outside world.
+
+Prerequisites:
+
+- Your instance must use the Linux package installation method.
+- You have configured DNS setup
+ [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns).
+
+1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable
+ the feature flag:
+
+ ```ruby
+ # External_url here is only for reference
+ external_url "http://example.com"
+ pages_external_url 'http://example.io'
+
+ pages_nginx['enable'] = true
+
+ # Set this feature flag to enable this feature
+ # For more information, see https://docs.gitlab.com/ee/administration/pages/index.html#use-environment-variables
+ gitlab_pages["namespace_in_path"] = true
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
+
+NGINX uses the custom proxy header `X-Gitlab-Namespace-In-Path`
+to send the namespace to the GitLab Pages daemon.
+
+The resulting URL scheme is `http://example.io/<namespace>/<project_slug>`.
+
### Wildcard domains with TLS support
**Requirements:**
@@ -163,7 +240,7 @@ URL scheme: `https://<namespace>.example.io/<project_slug>`
NGINX proxies all requests to the daemon. Pages daemon doesn't listen to the
outside world.
-1. Place the wildcard LTS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`.
+1. Place the wildcard TLS certificate for `*.example.io` and the key inside `/etc/gitlab/ssl`.
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby
@@ -195,6 +272,69 @@ Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitl
then run `gitlab-ctl reconfigure`. For more information, read
[GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947).
+### Pages domain with TLS support, without wildcard DNS **(EXPERIMENT)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17584) in GitLab 16.7. This feature is an [Experiment](../../policy/experiment-beta-support.md).
+
+FLAG:
+On self-managed GitLab, by default this feature is available.
+On GitLab.com, this feature is not available.
+This feature is not ready for production use.
+
+Prerequisites:
+
+- Your instance must use the Linux package installation method.
+- You have configured DNS setup
+ [without a wildcard](#for-namespace-in-url-path-without-wildcard-dns).
+- You have a single TLS certificate that covers your domain (like `example.com`)
+ and the `projects.*` version of your domain, like `projects.example.com`.
+
+In this configuration, NGINX proxies all requests to the daemon. The GitLab Pages
+daemon doesn't listen to the outside world:
+
+1. Add your TLS certificate and key as mentioned in the prerequisites into `/etc/gitlab/ssl`.
+1. In `/etc/gitlab/gitlab.rb`, set the external URL for GitLab Pages, and enable
+ the feature flag:
+
+ ```ruby
+ # The external_url field is here only for reference.
+ external_url "https://example.com"
+ pages_external_url 'https://example.io'
+
+ pages_nginx['enable'] = true
+ pages_nginx['redirect_http_to_https'] = true
+
+ # Set this feature flag to enable this feature
+ # For more information, see https://docs.gitlab.com/ee/administration/pages/index.html#use-environment-variables
+ gitlab_pages["namespace_in_path"] = true
+ ```
+
+1. If your TLS certificate and key don't match the name of your domain, like
+ `example.io.crt` and `example.io.key`,
+ add the full paths for the certificate and key files to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
+ pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
+ ```
+
+1. [Reconfigure GitLab](../restart_gitlab.md#reconfigure-a-linux-package-installation).
+
+ WARNING:
+ GitLab Pages does not update the OAuth application if changes are made to the redirect URI.
+ Before you reconfigure, remove the `gitlab_pages` section from `/etc/gitlab/gitlab-secrets.json`,
+ then run `gitlab-ctl reconfigure`. For more information, see
+ [GitLab Pages does not regenerate OAuth](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/3947).
+
+1. If you're using [Pages Access Control](#access-control), update the redirect URI in the GitLab Pages
+ [System OAuth application](../../integration/oauth_provider.md#create-an-instance-wide-application)
+ to use the HTTPS protocol.
+
+NGINX uses the custom proxy header `X-Gitlab-Namespace-In-Path`
+to send the namespace to the GitLab Pages daemon.
+
+The resulting URL scheme is `https://example.io/<namespace>/<project_slug>`.
+
### Wildcard domains with TLS-terminating Load Balancer
**Requirements:**
@@ -269,6 +409,7 @@ control over how the Pages daemon runs and serves content in your environment.
| `log_directory` | Absolute path to a log directory. |
| `log_format` | The log output format: `text` or `json`. |
| `log_verbose` | Verbose logging, true/false. |
+| `namespace_in_path` | (Experimental) Enable or disable namespace in the URL path. This requires `pages_nginx[enable] = true`. Sets `rewrite` configuration in NGINX to support [without wildcard DNS setup](#for-namespace-in-url-path-without-wildcard-dns). Default: `false` |
| `propagate_correlation_id` | Set to true (false by default) to re-use existing Correlation ID from the incoming request header `X-Request-ID` if present. If a reverse proxy sets this header, the value is propagated in the request chain. |
| `max_connections` | Limit on the number of concurrent connections to the HTTP, HTTPS or proxy listeners. |
| `max_uri_length` | The maximum length of URIs accepted by GitLab Pages. Set to 0 for unlimited length. [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5729) in GitLab 14.5.
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b0a6e52e838..dfa933fb006 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -1945,6 +1945,31 @@ Input type: `BoardListUpdateLimitMetricsInput`
| <a id="mutationboardlistupdatelimitmetricserrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationboardlistupdatelimitmetricslist"></a>`list` | [`BoardList`](#boardlist) | Updated list. |
+### `Mutation.branchRuleUpdate`
+
+WARNING:
+**Introduced** in 16.7.
+This feature is an Experiment. It can be changed or removed at any time.
+
+Input type: `BranchRuleUpdateInput`
+
+#### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationbranchruleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationbranchruleupdateid"></a>`id` | [`ProtectedBranchID!`](#protectedbranchid) | Global ID of the protected branch. |
+| <a id="mutationbranchruleupdatename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. |
+| <a id="mutationbranchruleupdateprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path to the project that the branch is associated with. |
+
+#### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="mutationbranchruleupdatebranchrule"></a>`branchRule` | [`BranchRule`](#branchrule) | Branch rule after mutation. |
+| <a id="mutationbranchruleupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
+| <a id="mutationbranchruleupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
+
### `Mutation.buildForecast`
WARNING:
@@ -32484,6 +32509,12 @@ A `ProjectImportStateID` is a global ID. It is encoded as a string.
An example `ProjectImportStateID` is: `"gid://gitlab/ProjectImportState/1"`.
+### `ProtectedBranchID`
+
+A `ProtectedBranchID` is a global ID. It is encoded as a string.
+
+An example `ProtectedBranchID` is: `"gid://gitlab/ProtectedBranch/1"`.
+
### `ReleaseID`
A `ReleaseID` is a global ID. It is encoded as a string.
diff --git a/doc/ci/jobs/ci_job_token.md b/doc/ci/jobs/ci_job_token.md
index 77bf10c5dc2..ba2d63c12e4 100644
--- a/doc/ci/jobs/ci_job_token.md
+++ b/doc/ci/jobs/ci_job_token.md
@@ -124,8 +124,8 @@ Prerequisites:
- You must have the Maintainer role for the project.
-1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
-1. On the left sidebar, select **Settings > General**.
+1. On the left sidebar, select **Search or go to** and find your project.
+1. Select **Settings > General**.
1. Expand **Visibility, project features, permissions**.
1. Set the visibility to **Only project members** for the features you want to restrict access to.
- The ability to fetch artifacts is controlled by the CI/CD visibility setting.
diff --git a/doc/update/deprecations.md b/doc/update/deprecations.md
index aa6c89da5b9..66b5d5be59a 100644
--- a/doc/update/deprecations.md
+++ b/doc/update/deprecations.md
@@ -315,6 +315,24 @@ In 16.3, the names of these settings were changed to clarify their meanings: the
</div>
+<div class="deprecation breaking-change" data-milestone="17.0">
+
+### Dependency Proxy: group access tokens to have additional scope checks for service accounts
+
+<div class="deprecation-notes">
+- Announced in GitLab <span class="milestone">16.7</span>
+- Removal in GitLab <span class="milestone">17.0</span> ([breaking change](https://docs.gitlab.com/ee/update/terminology.html#breaking-change))
+- To discuss this change or learn more, see the [deprecation issue](https://gitlab.com/gitlab-org/gitlab/-/issues/431386).
+</div>
+
+When using the Dependency Proxy for containers with a group access token, `docker login` and `docker pull` requests with insufficient scopes for Dependency Proxy are not rejected.
+
+GitLab 16.7 adds checks for group access tokens authenticating for the dependency proxy for containers. This is a breaking change, because tokens without the required scopes will fail.
+
+To help avoid being impacted by this breaking change, create new group access tokens with the [required scopes](https://docs.gitlab.com/ee/user/packages/dependency_proxy/#authenticate-with-the-dependency-proxy), and update your workflow variables and scripts with those new tokens.
+
+</div>
+
<div class="deprecation " data-milestone="17.0">
### Deprecate GraphQL fields related to the temporary storage increase
diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md
index 34f7bd9ca63..d263fcde77f 100644
--- a/doc/user/application_security/dependency_list/index.md
+++ b/doc/user/application_security/dependency_list/index.md
@@ -37,7 +37,7 @@ To view your project's dependencies, ensure you meet the following requirements:
To view the dependencies of a project or all projects in a group:
-1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group.
+1. On the left sidebar, select **Search or go to** and find your project or group.
1. Select **Secure > Dependency list**.
Details of each dependency are listed, sorted by decreasing severity of vulnerabilities (if any). You can sort the list instead by component name or packager.
@@ -98,6 +98,6 @@ list shows only the results of the last successful pipeline that ran on the defa
To download the dependency list:
-1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project or group.
+1. On the left sidebar, select **Search or go to** and find your project or group.
1. Select **Secure > Dependency list**.
1. Select **Export**.
diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md
index e2d2f090524..896a00af3b4 100644
--- a/doc/user/application_security/dependency_scanning/index.md
+++ b/doc/user/application_security/dependency_scanning/index.md
@@ -710,7 +710,7 @@ your GitLab CI/CD configuration file is complex.
To enable dependency scanning:
-1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
+1. On the left sidebar, select **Search or go to** and find your project.
1. Select **Build > Pipeline editor**.
1. Copy and paste the following to the bottom of the `.gitlab-ci.yml` file:
diff --git a/doc/user/compliance/compliance_center/index.md b/doc/user/compliance/compliance_center/index.md
index 42e9296b11a..f745259c763 100644
--- a/doc/user/compliance/compliance_center/index.md
+++ b/doc/user/compliance/compliance_center/index.md
@@ -31,7 +31,7 @@ Prerequisites:
To view the standards adherence dashboard for a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
### GitLab standard
@@ -99,7 +99,7 @@ Prerequisites:
To view the compliance violations report:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
You can sort the compliance report on:
@@ -186,7 +186,7 @@ Prerequisites:
To export a report of merge request compliance violations for projects in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. In the top-right corner, select **Export**.
1. Select **Export violations report**.
@@ -233,7 +233,7 @@ If the commit has a related merge commit, then the following are also included:
To generate the Chain of Custody report:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. In the top-right corner, select **Export**.
1. Select **Export chain of custody report**.
@@ -250,7 +250,7 @@ details for the provided commit SHA.
To generate a commit-specific Chain of Custody report:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. In the top-right corner, select **Export**.
1. Select **Export custody report of a specific commit**.
1. Enter the commit SHA, and then select **Export custody report**.
@@ -282,7 +282,7 @@ Prerequisites:
To view the compliance projects report:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
### Apply a compliance framework to projects in a group
@@ -299,7 +299,7 @@ Prerequisites:
To apply a compliance framework to one project in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
1. Next to the project you want to add the compliance framework to, select **{plus}** **Add framework**.
1. Select an existing compliance framework or create a new one.
@@ -307,7 +307,7 @@ To apply a compliance framework to one project in a group:
To apply a compliance framework to multiple projects in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
1. Select multiple projects.
1. From the **Choose one bulk action** dropdown list, select **Apply framework to selected projects**.
@@ -328,14 +328,14 @@ Prerequisites:
To remove a compliance framework from one project in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
1. Next to the compliance framework to remove from the project, select **{close}** on the framework label.
To remove a compliance framework from multiple projects in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
1. Select multiple projects.
1. From the **Choose one bulk action** dropdown list, select **Remove framework from selected projects**.
@@ -357,7 +357,7 @@ Prerequisites:
To export a report of compliance frameworks on projects in a group:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. In the top-right corner, select **Export**.
1. Select **Export list of project frameworks**.
@@ -370,13 +370,13 @@ A report is compiled and delivered to your email inbox as an attachment.
To filter the list of compliance frameworks:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Projects** tab.
1. In the search field:
1. Select the attribute you want to filter by.
1. Select an operator.
1. Select from the list of options or enter text for the search.
-1. Select **Search** (**{search}**).
+1. Select **Search**.
Repeat this process to filter by multiple attributes.
@@ -405,5 +405,5 @@ Prerequisites:
To view the compliance projects report:
1. On the left sidebar, select **Search or go to** and find your group.
-1. On the left sidebar, select **Secure > Compliance center**.
+1. Select **Secure > Compliance center**.
1. On the page, select the **Frameworks** tab.
diff --git a/doc/user/project/repository/signed_commits/index.md b/doc/user/project/repository/signed_commits/index.md
index c4abcf34490..297ae31048e 100644
--- a/doc/user/project/repository/signed_commits/index.md
+++ b/doc/user/project/repository/signed_commits/index.md
@@ -22,15 +22,14 @@ Sign commits with your:
## Verify commits
You can review commits for a merge request, or for an entire project, to confirm
-they are signed:
-
-1. To review commits for a project:
- 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
- 1. Select **Code > Commits**.
-1. To review commits for a merge request:
- 1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
- 1. On the left sidebar, select **Merge requests**, then select your merge request.
- 1. Select **Commits**.
+they are signed.
+
+1. On the left sidebar, select **Search or go to** and find your project.
+1. To review commits:
+ - For a project, select **Code > Commits**.
+ - For a merge request:
+ 1. Select **Merge requests**, then select your merge request.
+ 1. Select **Commits**.
1. Identify the commit you want to review. Signed commits show either a **Verified**
or **Unverified** badge, depending on the verification status of the signature.
Unsigned commits do not display a badge:
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 3f8bcbbc635..111df0af67a 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.50.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.51.0'
build:
stage: build
diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
index 3f8bcbbc635..111df0af67a 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml
@@ -1,5 +1,5 @@
variables:
- AUTO_BUILD_IMAGE_VERSION: 'v1.50.0'
+ AUTO_BUILD_IMAGE_VERSION: 'v1.51.0'
build:
stage: build
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index 3744c81f51d..aa59caa4268 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -149,7 +149,7 @@ module Gitlab
return if @data == '' # don't mess with submodule blobs
# Even if we return early, recalculate whether this blob is binary in
- # case a blob was initialized as text but the full data isn't
+ # case a blob was initialized as text but the full data isn'tspec/requests/api/graphql/mutations/branch_rules/update_spec.rb:
@binary = nil
return if @loaded_all_data
diff --git a/lib/gitlab/nav/top_nav_menu_item.rb b/lib/gitlab/nav/top_nav_menu_item.rb
index e7790fd77d0..f6fea97dae9 100644
--- a/lib/gitlab/nav/top_nav_menu_item.rb
+++ b/lib/gitlab/nav/top_nav_menu_item.rb
@@ -21,7 +21,7 @@ module Gitlab
href: href,
view: view.to_s,
css_class: css_class,
- data: data || { testid: 'menu_item_link', qa_title: title },
+ data: data || { testid: 'menu-item-link', qa_title: title },
partial: partial,
component: component
}
diff --git a/lib/gitlab/web_ide/default_oauth_application.rb b/lib/gitlab/web_ide/default_oauth_application.rb
new file mode 100644
index 00000000000..01b7637c1c0
--- /dev/null
+++ b/lib/gitlab/web_ide/default_oauth_application.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module WebIde
+ module DefaultOauthApplication
+ class << self
+ def feature_enabled?(current_user)
+ Feature.enabled?(:vscode_web_ide, current_user) && Feature.enabled?(:web_ide_oauth, current_user)
+ end
+
+ def oauth_application
+ application_settings.web_ide_oauth_application
+ end
+
+ def oauth_callback_url
+ Gitlab::Routing.url_helpers.ide_oauth_redirect_url
+ end
+
+ def ensure_oauth_application!
+ return if oauth_application
+
+ should_expire_cache = false
+
+ application_settings.transaction do
+ # note: This should run very rarely and should be safe for us to do a lock
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/132496#note_1587293087
+ application_settings.lock!
+
+ # note: `lock!`` breaks applicaiton_settings cache and will trigger another query.
+ # We need to double check here so that requests previously waiting on the lock can
+ # now just skip.
+ next if oauth_application
+
+ application = Doorkeeper::Application.new(
+ name: 'GitLab Web IDE',
+ redirect_uri: oauth_callback_url,
+ scopes: ['api'],
+ trusted: true,
+ confidential: false)
+ application.save!
+ application_settings.update!(web_ide_oauth_application: application)
+ should_expire_cache = true
+ end
+
+ # note: This needs to happen outside the transaction, but only if we actually changed something
+ ::Gitlab::CurrentSettings.expire_current_application_settings if should_expire_cache
+ end
+
+ private
+
+ def application_settings
+ ::Gitlab::CurrentSettings.current_application_settings
+ end
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/user_settings/menus/active_sessions_menu.rb b/lib/sidebars/user_settings/menus/active_sessions_menu.rb
index f806c04e77c..3ddf8700556 100644
--- a/lib/sidebars/user_settings/menus/active_sessions_menu.rb
+++ b/lib/sidebars/user_settings/menus/active_sessions_menu.rb
@@ -8,7 +8,7 @@ module Sidebars
override :link
def link
- profile_active_sessions_path
+ user_settings_active_sessions_path
end
override :title
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c16aaa3e9b8..172b3642dd7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4427,6 +4427,9 @@ msgstr ""
msgid "After the export is complete, download the data file from a notification email or from this page. You can then import the data file from the %{strong_text_start}Create new group%{strong_text_end} page of another GitLab instance."
msgstr ""
+msgid "After the report is generated, an email will be sent with the report attached."
+msgstr ""
+
msgid "After you enable the integration, the following protected variable is created for CI/CD use:"
msgstr ""
@@ -5030,9 +5033,6 @@ msgstr ""
msgid "An email notification was recently sent from the admin panel. Please wait %{wait_time_in_words} before attempting to send another message."
msgstr ""
-msgid "An email will be sent with the report attached after it is generated."
-msgstr ""
-
msgid "An empty GitLab User field will add the FogBugz user's full name (e.g. \"By John Smith\") in the description of all issues and comments. It will also associate and/or assign these issues and comments with the project creator."
msgstr ""
@@ -7130,6 +7130,9 @@ msgstr ""
msgid "Authenticated web requests"
msgstr ""
+msgid "Authenticating..."
+msgstr ""
+
msgid "Authentication"
msgstr ""
@@ -11990,6 +11993,9 @@ msgstr ""
msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions uses third-party AI services."
msgstr ""
+msgid "CodeSuggestions|Subject to the %{terms_link_start}Testing Terms of Use%{link_end}. Code Suggestions uses third-party AI services.|To start receiving Code Suggestions after enabling this feature, you must install and configure a %{ide_extension_link_start}supported IDE editor extension%{link_end}."
+msgstr ""
+
msgid "CodeownersValidation|An error occurred while loading the validation errors. Please try again later."
msgstr ""
@@ -12438,16 +12444,16 @@ msgstr ""
msgid "Completed in %{duration_seconds} seconds (%{relative_time})"
msgstr ""
-msgid "Compliance Center Export|(limited to 15 MB)"
+msgid "Compliance Center Export|Example: 2dc6aa3"
msgstr ""
-msgid "Compliance Center Export|Example: 2dc6aa3"
+msgid "Compliance Center Export|Export chain of custody report"
msgstr ""
-msgid "Compliance Center Export|Export as CSV"
+msgid "Compliance Center Export|Export chain of custody report as a CSV file (limited to 15MB)."
msgstr ""
-msgid "Compliance Center Export|Export chain of custody report"
+msgid "Compliance Center Export|Export chain of custody report of a specific commit as a CSV file (limited to 15MB)."
msgstr ""
msgid "Compliance Center Export|Export custody report"
@@ -12459,6 +12465,12 @@ msgstr ""
msgid "Compliance Center Export|Export list of project frameworks"
msgstr ""
+msgid "Compliance Center Export|Export list of project frameworks as a CSV file."
+msgstr ""
+
+msgid "Compliance Center Export|Export merge request violations as a CSV file."
+msgstr ""
+
msgid "Compliance Center Export|Export violations report"
msgstr ""
@@ -12468,6 +12480,9 @@ msgstr ""
msgid "Compliance Center Export|Send email of the chosen report as CSV"
msgstr ""
+msgid "Compliance Center Export|You will be emailed after the export is processed."
+msgstr ""
+
msgid "Compliance Center|Frameworks"
msgstr ""
@@ -12483,6 +12498,9 @@ msgstr ""
msgid "Compliance framework"
msgstr ""
+msgid "ComplianceChainOfCustody| Chain of custody export"
+msgstr ""
+
msgid "ComplianceFrameworksReport|Associated Projects"
msgstr ""
@@ -12495,6 +12513,9 @@ msgstr ""
msgid "ComplianceFrameworksReport|Edit framework"
msgstr ""
+msgid "ComplianceFrameworks| Frameworks export"
+msgstr ""
+
msgid "ComplianceFrameworks|Active compliance frameworks"
msgstr ""
@@ -12507,9 +12528,6 @@ msgstr ""
msgid "ComplianceFrameworks|Cancel"
msgstr ""
-msgid "ComplianceFrameworks|Compliance Frameworks Export"
-msgstr ""
-
msgid "ComplianceFrameworks|Compliance framework created"
msgstr ""
@@ -12798,7 +12816,7 @@ msgstr ""
msgid "ComplianceStandardsAdherence|View details (fix available)"
msgstr ""
-msgid "ComplianceViolations|Compliance Violations Export"
+msgid "ComplianceViolations| Violations export"
msgstr ""
msgid "ComplianceViolations|Your Compliance Violations CSV export for the group \"%{group_name}\" has been attached to this email."
@@ -18942,6 +18960,9 @@ msgstr ""
msgid "Environments|Open live environment"
msgstr ""
+msgid "Environments|Or select namespace: %{searchTerm}"
+msgstr ""
+
msgid "Environments|Re-deploy environment"
msgstr ""
@@ -19101,10 +19122,10 @@ msgstr ""
msgid "Environment|Unauthorized to access %{resourceType} from this environment."
msgstr ""
-msgid "Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again."
+msgid "Environment|Unhealthy"
msgstr ""
-msgid "Environment|Unhealthy"
+msgid "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it."
msgstr ""
msgid "Epic"
diff --git a/qa/qa/page/component/project_selector.rb b/qa/qa/page/component/project_selector.rb
index 54bd95c5422..6a6c42a1afa 100644
--- a/qa/qa/page/component/project_selector.rb
+++ b/qa/qa/page/component/project_selector.rb
@@ -10,20 +10,20 @@ module QA
super
base.view 'app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue' do
- element :project_search_field
- element :project_list_item
+ element 'project-search-field'
+ element 'project-list-item'
end
end
def fill_project_search_input(project_name)
- fill_element :project_search_field, project_name
+ fill_element 'project-search-field', project_name
end
def select_project
wait_until(sleep_interval: 2, reload: false) do
- has_element? :project_list_item
+ has_element? 'project-list-item'
end
- click_element :project_list_item
+ click_element 'project-list-item'
end
end
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 6890f490617..a1bb1475a0c 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -10,7 +10,7 @@ module QA
include SubMenus::SuperSidebar::GlobalSearchModal
view 'app/assets/javascripts/super_sidebar/components/super_sidebar.vue' do
- element :navbar, required: true # TODO: rename to sidebar once it's default implementation
+ element 'super-sidebar', required: true
end
view 'app/assets/javascripts/super_sidebar/components/user_bar.vue' do
@@ -20,7 +20,7 @@ module QA
view 'app/assets/javascripts/super_sidebar/components/user_menu.vue' do
element 'user-dropdown', required: !Runtime::Env.phone_layout?
element 'user-avatar-content', required: !Runtime::Env.phone_layout?
- element :sign_out_link
+ element 'sign-out-link'
element 'edit-profile-link'
end
@@ -35,12 +35,8 @@ module QA
element 'todos-shortcut-button', required: !Runtime::Env.phone_layout?
end
- view 'app/assets/javascripts/super_sidebar/components/global_search/components/global_search.vue' do
- element 'global-search-input'
- end
-
view 'lib/gitlab/nav/top_nav_menu_item.rb' do
- element :menu_item_link
+ element 'menu-item-link'
end
view 'app/views/layouts/header/_new_dropdown.html.haml' do
@@ -48,9 +44,9 @@ module QA
end
view 'app/helpers/nav/new_dropdown_helper.rb' do
- element :global_new_group_link
- element :global_new_project_link
- element :global_new_snippet_link
+ element 'global-new-group-link'
+ element 'global-new-project-link'
+ element 'global-new-snippet-link'
end
def go_to_projects
@@ -112,7 +108,7 @@ module QA
break true unless signed_in?
within_user_menu do
- click_element :sign_out_link
+ click_element 'sign-out-link'
end
not_signed_in?
@@ -164,7 +160,7 @@ module QA
private
def within_user_menu(&block)
- within_element(:navbar) do
+ within_element('super-sidebar') do
click_element 'user-avatar-content' unless has_element?('user-profile-link', wait: 1)
within_element('user-dropdown', &block)
diff --git a/qa/qa/page/sub_menus/common.rb b/qa/qa/page/sub_menus/common.rb
index bfe6b30eff6..5196957c0b3 100644
--- a/qa/qa/page/sub_menus/common.rb
+++ b/qa/qa/page/sub_menus/common.rb
@@ -9,7 +9,20 @@ module QA
base.class_eval do
view 'app/assets/javascripts/super_sidebar/components/super_sidebar.vue' do
- element :navbar
+ element 'super-sidebar'
+ end
+
+ view 'app/assets/javascripts/super_sidebar/components/menu_section.vue' do
+ element 'menu-section-button'
+ element 'menu-section'
+ end
+
+ view 'app/assets/javascripts/super_sidebar/components/nav_item.vue' do
+ element 'nav-item-link'
+ end
+
+ view 'app/views/layouts/header/_new_dropdown.html.haml' do
+ element 'new-menu-toggle'
end
end
end
@@ -28,16 +41,15 @@ module QA
# Open sidebar navigation submenu
#
# @param [String] parent_menu_name
- # @param [String] parent_section_id
# @param [String] sub_menu
# @return [void]
def open_submenu(parent_menu_name, sub_menu)
# prevent closing sub-menu if it was already open
- unless has_element?(:menu_section, section_name: parent_menu_name, wait: 0)
- click_element(:menu_section_button, section_name: parent_menu_name)
+ unless has_element?('menu-section', section_name: parent_menu_name, wait: 0)
+ click_element('menu-section-button', section_name: parent_menu_name)
end
- within_element(:menu_section, section_name: parent_menu_name) do
+ within_element('menu-section', section_name: parent_menu_name) do
click_element('nav-item-link', submenu_item: sub_menu)
end
end
diff --git a/spec/controllers/profiles/active_sessions_controller_spec.rb b/spec/controllers/user_settings/active_sessions_controller_spec.rb
index 12cf4f982e9..01c1095fef5 100644
--- a/spec/controllers/profiles/active_sessions_controller_spec.rb
+++ b/spec/controllers/user_settings/active_sessions_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Profiles::ActiveSessionsController do
+RSpec.describe UserSettings::ActiveSessionsController, feature_category: :system_access do
describe 'DELETE destroy' do
let_it_be(:user) { create(:user) }
diff --git a/spec/features/environments/environments_folder_spec.rb b/spec/features/environments/environments_folder_spec.rb
index 6b0306a70a8..9e2932e315f 100644
--- a/spec/features/environments/environments_folder_spec.rb
+++ b/spec/features/environments/environments_folder_spec.rb
@@ -8,11 +8,43 @@ RSpec.describe 'Environments Folder page', :js, feature_category: :environment_m
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let!(:envs) { create_list(:environment, 4, :with_folders, project: project, folder: folder_name) }
+ let!(:stopped_env) { create(:environment, :stopped, :with_folders, project: project, folder: folder_name) }
def get_env_name(environment)
environment.name.split('/').last
end
+ def find_env_element(environment)
+ find_by_id(environment.name)
+ end
+
+ def stop_environment(environment)
+ environment_item = find_env_element(environment)
+ within(environment_item) do
+ click_button 'Stop'
+ end
+
+ within('.modal') do
+ click_button 'Stop environment'
+ end
+
+ wait_for_requests
+ end
+
+ def redeploy_environment(environment)
+ environment_item = find_env_element(environment)
+ within(environment_item) do
+ click_button 'More actions'
+ click_button 'Delete environment'
+ end
+
+ within('.modal') do
+ click_button 'Delete environment'
+ end
+
+ wait_for_requests
+ end
+
before_all do
project.add_role(user, :developer)
end
@@ -36,6 +68,37 @@ RSpec.describe 'Environments Folder page', :js, feature_category: :environment_m
expect(page).not_to have_content('production')
envs.each { |env| expect(page).to have_content(get_env_name(env)) }
end
+
+ it 'shows scope tabs' do
+ expect(page).to have_content("Active")
+ expect(page).to have_content("Stopped")
+ end
+
+ it 'can stop the environment' do
+ environment_to_stop = envs.first
+
+ stop_environment(environment_to_stop)
+
+ expect(page).not_to have_content(get_env_name(environment_to_stop))
+ end
+
+ describe 'stopped environments tab' do
+ before do
+ element = find('a', text: 'Stopped')
+ element.click
+ wait_for_requests
+ end
+
+ it 'shows stopped environments on stopped tab' do
+ expect(page).to have_content(get_env_name(stopped_env))
+ end
+
+ it 'can re-start the environment' do
+ redeploy_environment(stopped_env)
+
+ expect(page).not_to have_content(get_env_name(stopped_env))
+ end
+ end
end
describe 'legacy folders page' do
diff --git a/spec/features/profiles/active_sessions_spec.rb b/spec/features/user_settings/active_sessions_spec.rb
index 2e800ae88b6..5d1d4bc6490 100644
--- a/spec/features/profiles/active_sessions_spec.rb
+++ b/spec/features/user_settings/active_sessions_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
end
using_session :session1 do
- visit profile_active_sessions_path
+ visit user_settings_active_sessions_path
expect(page).to(have_selector('ul.list-group li.list-group-item', text: 'Signed in on', count: 2))
@@ -93,7 +93,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
using_session :session1 do
gitlab_sign_in(user)
- visit profile_active_sessions_path
+ visit user_settings_active_sessions_path
expect(page).to have_link('Revoke', count: 1)
@@ -105,7 +105,7 @@ RSpec.describe 'Profile > Active Sessions', :clean_gitlab_redis_shared_state, fe
end
using_session :session2 do
- visit profile_active_sessions_path
+ visit user_settings_active_sessions_path
expect(page).to have_content('You need to sign in or sign up before continuing.')
end
diff --git a/spec/frontend/environments/environment_flux_resource_selector_spec.js b/spec/frontend/environments/environment_flux_resource_selector_spec.js
index ba3375c731f..8dab8fdd96a 100644
--- a/spec/frontend/environments/environment_flux_resource_selector_spec.js
+++ b/spec/frontend/environments/environment_flux_resource_selector_spec.js
@@ -25,7 +25,7 @@ const DEFAULT_PROPS = {
fluxResourcePath: '',
};
-describe('~/environments/components/form.vue', () => {
+describe('~/environments/components/flux_resource_selector.vue', () => {
let wrapper;
const kustomizationItem = {
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 478ac8d6e0e..f3dfc7a72f2 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -1,11 +1,12 @@
-import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
import getUserAuthorizedAgents from '~/environments/graphql/queries/user_authorized_agents.query.graphql';
import EnvironmentFluxResourceSelector from '~/environments/components/environment_flux_resource_selector.vue';
+import EnvironmentNamespaceSelector from '~/environments/components/environment_namespace_selector.vue';
import createMockApollo from '../__helpers__/mock_apollo_helper';
import { mockKasTunnelUrl } from './mock_data';
@@ -36,13 +37,16 @@ const configuration = {
credentials: 'include',
};
+const environmentWithAgentAndNamespace = {
+ ...DEFAULT_PROPS.environment,
+ clusterAgent: { id: '12', name: 'agent-2' },
+ clusterAgentId: '2',
+ kubernetesNamespace: 'agent',
+};
+
describe('~/environments/components/form.vue', () => {
let wrapper;
- const getNamespacesQueryResult = jest
- .fn()
- .mockReturnValue([{ metadata: { name: 'default' } }, { metadata: { name: 'agent' } }]);
-
const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
@@ -53,7 +57,7 @@ describe('~/environments/components/form.vue', () => {
},
});
- const createWrapperWithApollo = ({ propsData = {}, queryResult = null } = {}) => {
+ const createWrapperWithApollo = (propsData = {}) => {
Vue.use(VueApollo);
const requestHandlers = [
@@ -70,12 +74,6 @@ describe('~/environments/components/form.vue', () => {
],
];
- const mockResolvers = {
- Query: {
- k8sNamespaces: queryResult || getNamespacesQueryResult,
- },
- };
-
return mountExtended(EnvironmentForm, {
provide: {
...PROVIDE,
@@ -84,13 +82,12 @@ describe('~/environments/components/form.vue', () => {
...DEFAULT_PROPS,
...propsData,
},
- apolloProvider: createMockApollo(requestHandlers, mockResolvers),
+ apolloProvider: createMockApollo(requestHandlers, []),
});
};
const findAgentSelector = () => wrapper.findByTestId('agent-selector');
- const findNamespaceSelector = () => wrapper.findByTestId('namespace-selector');
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const findNamespaceSelector = () => wrapper.findComponent(EnvironmentNamespaceSelector);
const findFluxResourceSelector = () => wrapper.findComponent(EnvironmentFluxResourceSelector);
const selectAgent = async () => {
@@ -326,91 +323,15 @@ describe('~/environments/components/form.vue', () => {
expect(findNamespaceSelector().exists()).toBe(true);
});
- it('requests the kubernetes namespaces with the correct configuration', async () => {
- await waitForPromises();
-
- expect(getNamespacesQueryResult).toHaveBeenCalledWith(
- {},
- { configuration },
- expect.anything(),
- expect.anything(),
- );
- });
-
- it('sets the loading prop while fetching the list', async () => {
- expect(findNamespaceSelector().props('loading')).toBe(true);
-
- await waitForPromises();
-
- expect(findNamespaceSelector().props('loading')).toBe(false);
- });
-
- it('renders a list of available namespaces', async () => {
- await waitForPromises();
-
- expect(findNamespaceSelector().props('items')).toEqual([
- { text: 'default', value: 'default' },
- { text: 'agent', value: 'agent' },
- ]);
- });
-
- it('filters the namespaces list on user search', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('search', 'default');
-
- expect(findNamespaceSelector().props('items')).toEqual([
- { value: 'default', text: 'default' },
- ]);
- });
-
- it('updates namespace selector field with the name of selected namespace', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
-
- expect(findNamespaceSelector().props('toggleText')).toBe('agent');
- });
-
it('emits changes to the kubernetesNamespace', async () => {
await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
+ findNamespaceSelector().vm.$emit('change', 'agent');
+ await nextTick();
expect(wrapper.emitted('change')[1]).toEqual([
{ name: '', externalUrl: '', kubernetesNamespace: 'agent', fluxResourcePath: null },
]);
});
-
- it('clears namespace selector when another agent was selected', async () => {
- await waitForPromises();
- await findNamespaceSelector().vm.$emit('select', 'agent');
-
- expect(findNamespaceSelector().props('toggleText')).toBe('agent');
-
- await findAgentSelector().vm.$emit('select', '1');
- expect(findNamespaceSelector().props('toggleText')).toBe(
- EnvironmentForm.i18n.namespaceHelpText,
- );
- });
- });
-
- describe('when cannot connect to the cluster', () => {
- const error = new Error('Error from the cluster_client API');
-
- beforeEach(async () => {
- wrapper = createWrapperWithApollo({
- queryResult: jest.fn().mockRejectedValueOnce(error),
- });
-
- await selectAgent();
- await waitForPromises();
- });
-
- it("doesn't render the namespace selector", () => {
- expect(findNamespaceSelector().exists()).toBe(false);
- });
-
- it('renders an alert', () => {
- expect(findAlert().text()).toBe('Error from the cluster_client API');
- });
});
});
@@ -431,16 +352,6 @@ describe('~/environments/components/form.vue', () => {
it("doesn't render flux resource selector", () => {
expect(findFluxResourceSelector().exists()).toBe(false);
});
-
- it('renders the flux resource selector when the namespace is selected', async () => {
- await findNamespaceSelector().vm.$emit('select', 'agent');
-
- expect(findFluxResourceSelector().props()).toEqual({
- namespace: 'agent',
- fluxResourcePath: '',
- configuration,
- });
- });
});
});
@@ -451,9 +362,7 @@ describe('~/environments/components/form.vue', () => {
clusterAgentId: '1',
};
beforeEach(() => {
- wrapper = createWrapperWithApollo({
- propsData: { environment: environmentWithAgent },
- });
+ wrapper = createWrapperWithApollo({ environment: environmentWithAgent });
});
it('updates agent selector field with the name of the associated agent', () => {
@@ -468,45 +377,46 @@ describe('~/environments/components/form.vue', () => {
it('renders a list of available namespaces', async () => {
await waitForPromises();
- expect(findNamespaceSelector().props('items')).toEqual([
- { text: 'default', value: 'default' },
- { text: 'agent', value: 'agent' },
- ]);
+ expect(findNamespaceSelector().exists()).toBe(true);
});
});
describe('when environment has an associated kubernetes namespace', () => {
- const environmentWithAgentAndNamespace = {
- ...DEFAULT_PROPS.environment,
- clusterAgent: { id: '1', name: 'agent-1' },
- clusterAgentId: '1',
- kubernetesNamespace: 'default',
- };
beforeEach(() => {
- wrapper = createWrapperWithApollo({
- propsData: { environment: environmentWithAgentAndNamespace },
- });
+ wrapper = createWrapperWithApollo({ environment: environmentWithAgentAndNamespace });
});
it('updates namespace selector with the name of the associated namespace', async () => {
await waitForPromises();
- expect(findNamespaceSelector().props('toggleText')).toBe('default');
+ expect(findNamespaceSelector().props('namespace')).toBe('agent');
+ });
+
+ it('clears namespace selector when another agent was selected', async () => {
+ expect(findNamespaceSelector().props('namespace')).toBe('agent');
+
+ findAgentSelector().vm.$emit('select', '1');
+ await nextTick();
+
+ expect(findNamespaceSelector().props('namespace')).toBe(null);
+ });
+
+ it('renders the flux resource selector when the namespace is selected', () => {
+ expect(findFluxResourceSelector().props()).toEqual({
+ namespace: 'agent',
+ fluxResourcePath: '',
+ configuration,
+ });
});
});
describe('when environment has an associated flux resource', () => {
const fluxResourcePath = 'path/to/flux/resource';
- const environmentWithAgentAndNamespace = {
- ...DEFAULT_PROPS.environment,
- clusterAgent: { id: '1', name: 'agent-1' },
- clusterAgentId: '1',
- kubernetesNamespace: 'default',
+ const environmentWithFluxResource = {
+ ...environmentWithAgentAndNamespace,
fluxResourcePath,
};
beforeEach(() => {
- wrapper = createWrapperWithApollo({
- propsData: { environment: environmentWithAgentAndNamespace },
- });
+ wrapper = createWrapperWithApollo({ environment: environmentWithFluxResource });
});
it('provides flux resource path to the flux resource selector component', () => {
diff --git a/spec/frontend/environments/environment_namespace_selector_spec.js b/spec/frontend/environments/environment_namespace_selector_spec.js
new file mode 100644
index 00000000000..53e4f807751
--- /dev/null
+++ b/spec/frontend/environments/environment_namespace_selector_spec.js
@@ -0,0 +1,217 @@
+import { GlAlert, GlCollapsibleListbox, GlButton } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import EnvironmentNamespaceSelector from '~/environments/components/environment_namespace_selector.vue';
+import { stubComponent } from 'helpers/stub_component';
+import createMockApollo from '../__helpers__/mock_apollo_helper';
+import { mockKasTunnelUrl } from './mock_data';
+
+const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ headers: {
+ 'GitLab-Agent-Id': 2,
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ },
+ credentials: 'include',
+};
+
+const DEFAULT_PROPS = {
+ namespace: '',
+ configuration,
+};
+
+describe('~/environments/components/namespace_selector.vue', () => {
+ let wrapper;
+
+ const getNamespacesQueryResult = jest
+ .fn()
+ .mockReturnValue([
+ { metadata: { name: 'default' } },
+ { metadata: { name: 'agent' } },
+ { metadata: { name: 'test-agent' } },
+ ]);
+
+ const closeMock = jest.fn();
+
+ const createWrapper = ({ propsData = {}, queryResult = null } = {}) => {
+ Vue.use(VueApollo);
+
+ const mockResolvers = {
+ Query: {
+ k8sNamespaces: queryResult || getNamespacesQueryResult,
+ },
+ };
+
+ return shallowMount(EnvironmentNamespaceSelector, {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...propsData,
+ },
+ stubs: {
+ GlCollapsibleListbox: stubComponent(GlCollapsibleListbox, {
+ template: `<div><slot name="footer"></slot></div>`,
+ methods: {
+ close: closeMock,
+ },
+ }),
+ },
+ apolloProvider: createMockApollo([], mockResolvers),
+ });
+ };
+
+ const findNamespaceSelector = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findSelectButton = () => wrapper.findComponent(GlButton);
+
+ const searchNamespace = async (searchTerm = 'test') => {
+ findNamespaceSelector().vm.$emit('search', searchTerm);
+ await nextTick();
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('renders namespace selector', () => {
+ expect(findNamespaceSelector().exists()).toBe(true);
+ });
+
+ it('requests the namespaces', async () => {
+ await waitForPromises();
+
+ expect(getNamespacesQueryResult).toHaveBeenCalled();
+ });
+
+ it('sets the loading prop while fetching the list', async () => {
+ expect(findNamespaceSelector().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('loading')).toBe(false);
+ });
+
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('items')).toMatchObject([
+ {
+ text: 'default',
+ value: 'default',
+ },
+ {
+ text: 'agent',
+ value: 'agent',
+ },
+ {
+ text: 'test-agent',
+ value: 'test-agent',
+ },
+ ]);
+ });
+
+ it('filters the namespaces list on user search', async () => {
+ await waitForPromises();
+ await searchNamespace('agent');
+
+ expect(findNamespaceSelector().props('items')).toMatchObject([
+ {
+ text: 'agent',
+ value: 'agent',
+ },
+ {
+ text: 'test-agent',
+ value: 'test-agent',
+ },
+ ]);
+ });
+
+ it('emits changes to the namespace', () => {
+ findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(wrapper.emitted('change')).toEqual([['agent']]);
+ });
+ });
+
+ describe('custom select button', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper();
+ await waitForPromises();
+ });
+
+ it("doesn't render custom select button before searching", () => {
+ expect(findSelectButton().exists()).toBe(false);
+ });
+
+ it("doesn't render custom select button when the search is found in the namespaces list", async () => {
+ await searchNamespace('test-agent');
+ expect(findSelectButton().exists()).toBe(false);
+ });
+
+ it('renders custom select button when the namespace searched for is not found in the namespaces list', async () => {
+ await searchNamespace();
+ expect(findSelectButton().exists()).toBe(true);
+ });
+
+ it('emits custom filled namespace name to the `change` event', async () => {
+ await searchNamespace();
+ findSelectButton().vm.$emit('click');
+
+ expect(wrapper.emitted('change')).toEqual([['test']]);
+ });
+
+ it('closes the listbox after the custom value for the namespace was selected', async () => {
+ await searchNamespace();
+ findSelectButton().vm.$emit('click');
+
+ expect(closeMock).toHaveBeenCalled();
+ });
+ });
+
+ describe('when environment has an associated namespace', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ propsData: { namespace: 'existing-namespace' },
+ });
+ });
+
+ it('updates namespace selector with the name of the associated namespace', () => {
+ expect(findNamespaceSelector().props('toggleText')).toBe('existing-namespace');
+ });
+ });
+
+ describe('on error', () => {
+ const error = new Error('Error from the cluster_client API');
+
+ beforeEach(async () => {
+ wrapper = createWrapper({
+ queryResult: jest.fn().mockRejectedValueOnce(error),
+ });
+ await waitForPromises();
+ });
+
+ it('renders an alert with the error text', () => {
+ expect(findAlert().text()).toContain(error.message);
+ });
+
+ it('renders an empty namespace selector', () => {
+ expect(findNamespaceSelector().props('items')).toMatchObject([]);
+ });
+
+ it('renders custom select button when the user performs search', async () => {
+ await searchNamespace();
+
+ expect(findSelectButton().exists()).toBe(true);
+ });
+
+ it('emits custom filled namespace name to the `change` event', async () => {
+ await searchNamespace();
+ findSelectButton().vm.$emit('click');
+
+ expect(wrapper.emitted('change')).toEqual([['test']]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/folder/environments_folder_app_spec.js b/spec/frontend/environments/folder/environments_folder_app_spec.js
index 262e742ba5c..fbb252fb152 100644
--- a/spec/frontend/environments/folder/environments_folder_app_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_app_spec.js
@@ -1,12 +1,21 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlTab } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentsFolderAppComponent from '~/environments/folder/environments_folder_app.vue';
import EnvironmentItem from '~/environments/components/new_environment_item.vue';
+import StopEnvironmentModal from '~/environments/components/stop_environment_modal.vue';
+import ConfirmRollbackModal from '~/environments/components/confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from '~/environments/components/delete_environment_modal.vue';
+import CanaryUpdateModal from '~/environments/components/canary_update_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { resolvedFolder } from '../graphql/mock_data';
+import {
+ resolvedFolder,
+ resolvedEnvironment,
+ resolvedEnvironmentToDelete,
+ resolvedEnvironmentToRollback,
+} from '../graphql/mock_data';
Vue.use(VueApollo);
@@ -20,6 +29,11 @@ describe('EnvironmentsFolderAppComponent', () => {
const mockResolvers = {
Query: {
folder: environmentFolderMock,
+ environmentToDelete: jest.fn().mockReturnValue(resolvedEnvironmentToDelete),
+ environmentToRollback: jest.fn().mockReturnValue(resolvedEnvironment),
+ environmentToChangeCanary: jest.fn().mockReturnValue(resolvedEnvironment),
+ environmentToStop: jest.fn().mockReturnValue(resolvedEnvironment),
+ weight: jest.fn().mockReturnValue(1),
},
};
@@ -47,6 +61,7 @@ describe('EnvironmentsFolderAppComponent', () => {
propsData: {
folderName: mockFolderName,
folderPath: '/gitlab-org/test-project/-/environments/folder/dev',
+ scope: 'active',
},
});
};
@@ -54,6 +69,7 @@ describe('EnvironmentsFolderAppComponent', () => {
const findHeader = () => wrapper.findByTestId('folder-name');
const findEnvironmentItems = () => wrapper.findAllComponents(EnvironmentItem);
const findSkeletonLoaders = () => wrapper.findAllComponents(GlSkeletonLoader);
+ const findTabs = () => wrapper.findAllComponents(GlTab);
it('should render a header with the folder name', () => {
createWrapper();
@@ -76,5 +92,32 @@ describe('EnvironmentsFolderAppComponent', () => {
const items = findEnvironmentItems();
expect(items.length).toBe(resolvedFolder.environments.length);
});
+
+ it('should render active and stopped tabs', () => {
+ const tabs = findTabs();
+ expect(tabs.length).toBe(2);
+ });
+
+ [
+ [StopEnvironmentModal, resolvedEnvironment],
+ [DeleteEnvironmentModal, resolvedEnvironmentToDelete],
+ [ConfirmRollbackModal, resolvedEnvironmentToRollback],
+ ].forEach(([Component, expectedEnvironment]) =>
+ it(`should render ${Component.name} component`, () => {
+ const modal = wrapper.findComponent(Component);
+
+ expect(modal.exists()).toBe(true);
+ expect(modal.props().environment).toEqual(expectedEnvironment);
+ expect(modal.props().graphql).toBe(true);
+ }),
+ );
+
+ it(`should render CanaryUpdateModal component`, () => {
+ const modal = wrapper.findComponent(CanaryUpdateModal);
+
+ expect(modal.exists()).toBe(true);
+ expect(modal.props().environment).toEqual(resolvedEnvironment);
+ expect(modal.props().weight).toBe(1);
+ });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 7f5e46f51ad..efc63a80e89 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -930,3 +930,153 @@ export const fluxKustomizationsMock = [
];
export const fluxResourcePathMock = 'path/to/flux/resource';
+
+export const resolvedEnvironmentToDelete = {
+ __typename: 'LocalEnvironment',
+ id: 41,
+ name: 'review/hello',
+ deletePath: '/api/v4/projects/8/environments/41',
+};
+
+export const resolvedEnvironmentToRollback = {
+ __typename: 'LocalEnvironment',
+ id: 41,
+ name: 'review/hello',
+ lastDeployment: {
+ id: 78,
+ iid: 24,
+ sha: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ ref: { name: 'main', refPath: '/h5bp/html5-boilerplate/-/tree/main' },
+ status: 'success',
+ createdAt: '2022-01-07T15:47:27.415Z',
+ deployedAt: '2022-01-07T15:47:32.450Z',
+ tierInYaml: 'staging',
+ tag: false,
+ isLast: true,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ webUrl: 'http://gck.test:3000/root',
+ showStatus: false,
+ path: '/root',
+ },
+ deployable: {
+ id: 1014,
+ name: 'deploy-prod',
+ started: '2022-01-07T15:47:31.037Z',
+ complete: true,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ retryPath: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ playable: false,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.404Z',
+ updatedAt: '2022-01-07T15:47:32.341Z',
+ status: {
+ action: {
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ method: 'post',
+ path: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+ title: 'Retry',
+ },
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1014',
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ group: 'success',
+ hasDetails: true,
+ icon: 'status_success',
+ illustration: {
+ image:
+ '/assets/illustrations/skipped-job_empty-29a8a37d8a61d1b6f68cf3484f9024e53cd6eb95e28eae3554f8011a1146bf27.svg',
+ size: 'svg-430',
+ title: 'This job does not have a trace.',
+ },
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ },
+ commit: {
+ id: 'f3ba6dd84f8f891373e9b869135622b954852db1',
+ shortId: 'f3ba6dd8',
+ createdAt: '2022-01-07T15:47:26.000+00:00',
+ parentIds: ['3213b6ac17afab99be37d5d38f38c6c8407387cc'],
+ title: 'Update .gitlab-ci.yml file',
+ message: 'Update .gitlab-ci.yml file',
+ authorName: 'Administrator',
+ authorEmail: 'admin@example.com',
+ authoredDate: '2022-01-07T15:47:26.000+00:00',
+ committerName: 'Administrator',
+ committerEmail: 'admin@example.com',
+ committedDate: '2022-01-07T15:47:26.000+00:00',
+ trailers: {},
+ webUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 1,
+ name: 'Administrator',
+ path: '/root',
+ showStatus: false,
+ state: 'active',
+ username: 'root',
+ webUrl: 'http://gck.test:3000/root',
+ },
+ authorGravatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ commitUrl:
+ 'http://gck.test:3000/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ commitPath: '/h5bp/html5-boilerplate/-/commit/f3ba6dd84f8f891373e9b869135622b954852db1',
+ },
+ manualActions: [
+ {
+ id: 1015,
+ name: 'deploy-staging',
+ started: null,
+ complete: false,
+ archived: false,
+ buildPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ playPath: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ playable: true,
+ scheduled: false,
+ createdAt: '2022-01-07T15:47:27.422Z',
+ updatedAt: '2022-01-07T15:47:28.557Z',
+ status: {
+ icon: 'status_manual',
+ text: 'manual',
+ label: 'manual play action',
+ group: 'manual',
+ tooltip: 'manual action',
+ hasDetails: true,
+ detailsPath: '/h5bp/html5-boilerplate/-/jobs/1015',
+ illustration: {
+ image:
+ '/assets/illustrations/manual_action-c55aee2c5f9ebe9f72751480af8bb307be1a6f35552f344cc6d1bf979d3422f6.svg',
+ size: 'svg-394',
+ title: 'This job requires a manual action',
+ content:
+ 'This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.',
+ },
+ favicon:
+ '/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png',
+ action: {
+ icon: 'play',
+ title: 'Play',
+ path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
+ method: 'post',
+ buttonTitle: 'Run job',
+ },
+ },
+ },
+ ],
+ scheduledActions: [],
+ cluster: null,
+ },
+ retryUrl: '/h5bp/html5-boilerplate/-/jobs/1014/retry',
+};
diff --git a/spec/frontend/environments/graphql/resolvers/base_spec.js b/spec/frontend/environments/graphql/resolvers/base_spec.js
index e01cf18c40d..244c86fa679 100644
--- a/spec/frontend/environments/graphql/resolvers/base_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/base_spec.js
@@ -147,10 +147,10 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
-
+ const cache = { evict: jest.fn() };
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
- await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client });
+ await mockResolvers.Mutation.stopEnvironmentREST(null, { environment }, { client, cache });
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
@@ -161,6 +161,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
variables: { environment },
data: { isEnvironmentStopping: true },
});
+ expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' });
});
it('should set is stopping to false if stop fails', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
@@ -183,27 +184,39 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('rollbackEnvironment', () => {
it('should post to the retry environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
+ const cache = { evict: jest.fn() };
- await mockResolvers.Mutation.rollbackEnvironment(null, {
- environment: { retryUrl: ENDPOINT },
- });
+ await mockResolvers.Mutation.rollbackEnvironment(
+ null,
+ {
+ environment: { retryUrl: ENDPOINT },
+ },
+ { cache },
+ );
expect(mock.history.post).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'post' }),
);
+ expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' });
});
});
describe('deleteEnvironment', () => {
it('should DELETE to the delete environment path', async () => {
mock.onDelete(ENDPOINT).reply(HTTP_STATUS_OK);
+ const cache = { evict: jest.fn() };
- await mockResolvers.Mutation.deleteEnvironment(null, {
- environment: { deletePath: ENDPOINT },
- });
+ await mockResolvers.Mutation.deleteEnvironment(
+ null,
+ {
+ environment: { deletePath: ENDPOINT },
+ },
+ { cache },
+ );
expect(mock.history.delete).toContainEqual(
expect.objectContaining({ url: ENDPOINT, method: 'delete' }),
);
+ expect(cache.evict).toHaveBeenCalledWith({ fieldName: 'folder' });
});
});
describe('cancelAutoStop', () => {
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 6a5bedb0bbb..d7a16bec1c3 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -40,6 +40,9 @@ const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/gitlab-mono/GitLabMo
const TEST_EDITOR_FONT_FORMAT = 'woff2';
const TEST_EDITOR_FONT_FAMILY = 'GitLab Mono';
+const TEST_OAUTH_CLIENT_ID = 'oauth-client-id-123abc';
+const TEST_OAUTH_CALLBACK_URL = 'https://example.com/oauth_callback';
+
describe('ide/init_gitlab_web_ide', () => {
let resolveConfirm;
@@ -231,4 +234,29 @@ describe('ide/init_gitlab_web_ide', () => {
);
});
});
+
+ describe('when oauth info is in dataset', () => {
+ beforeEach(() => {
+ findRootElement().dataset.clientId = TEST_OAUTH_CLIENT_ID;
+ findRootElement().dataset.callbackUrl = TEST_OAUTH_CALLBACK_URL;
+
+ createSubject();
+ });
+
+ it('calls start with element', () => {
+ expect(start).toHaveBeenCalledTimes(1);
+ expect(start).toHaveBeenCalledWith(
+ findRootElement(),
+ expect.objectContaining({
+ auth: {
+ type: 'oauth',
+ clientId: TEST_OAUTH_CLIENT_ID,
+ callbackUrl: TEST_OAUTH_CALLBACK_URL,
+ protectRefreshToken: true,
+ },
+ httpHeaders: undefined,
+ }),
+ );
+ });
+ });
});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js
new file mode 100644
index 00000000000..3431068937f
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_oauth_config_spec.js
@@ -0,0 +1,16 @@
+import { getOAuthConfig } from '~/ide/lib/gitlab_web_ide/get_oauth_config';
+
+describe('~/ide/lib/gitlab_web_ide/get_oauth_config', () => {
+ it('returns undefined if no clientId found', () => {
+ expect(getOAuthConfig({})).toBeUndefined();
+ });
+
+ it('returns auth config from dataset', () => {
+ expect(getOAuthConfig({ clientId: 'test-clientId', callbackUrl: 'test-callbackUrl' })).toEqual({
+ type: 'oauth',
+ clientId: 'test-clientId',
+ callbackUrl: 'test-callbackUrl',
+ protectRefreshToken: true,
+ });
+ });
+});
diff --git a/spec/frontend/ide/mount_oauth_callback_spec.js b/spec/frontend/ide/mount_oauth_callback_spec.js
new file mode 100644
index 00000000000..6ac0b4e4615
--- /dev/null
+++ b/spec/frontend/ide/mount_oauth_callback_spec.js
@@ -0,0 +1,53 @@
+import { oauthCallback } from '@gitlab/web-ide';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mountOAuthCallback } from '~/ide/mount_oauth_callback';
+
+jest.mock('@gitlab/web-ide');
+
+const TEST_USERNAME = 'gandalf.the.grey';
+const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/webpack/assets/gitlab-web-ide/public/path';
+
+const TEST_OAUTH_CLIENT_ID = 'oauth-client-id-123abc';
+const TEST_OAUTH_CALLBACK_URL = 'https://example.com/oauth_callback';
+
+describe('~/ide/mount_oauth_callback', () => {
+ const createRootElement = () => {
+ const el = document.createElement('div');
+
+ el.id = 'ide';
+ el.dataset.clientId = TEST_OAUTH_CLIENT_ID;
+ el.dataset.callbackUrl = TEST_OAUTH_CALLBACK_URL;
+
+ document.body.append(el);
+ };
+
+ beforeEach(() => {
+ gon.current_username = TEST_USERNAME;
+ process.env.GITLAB_WEB_IDE_PUBLIC_PATH = TEST_GITLAB_WEB_IDE_PUBLIC_PATH;
+
+ createRootElement();
+ });
+
+ afterEach(() => {
+ document.body.innerHTML = '';
+ });
+
+ it('calls oauthCallback', () => {
+ expect(oauthCallback).not.toHaveBeenCalled();
+
+ mountOAuthCallback();
+
+ expect(oauthCallback).toHaveBeenCalledTimes(1);
+ expect(oauthCallback).toHaveBeenCalledWith({
+ auth: {
+ type: 'oauth',
+ callbackUrl: TEST_OAUTH_CALLBACK_URL,
+ clientId: TEST_OAUTH_CLIENT_ID,
+ protectRefreshToken: true,
+ },
+ gitlabUrl: TEST_HOST,
+ baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ username: TEST_USERNAME,
+ });
+ });
+});
diff --git a/spec/helpers/nav/new_dropdown_helper_spec.rb b/spec/helpers/nav/new_dropdown_helper_spec.rb
index 4252e10c922..a69ac8b3c19 100644
--- a/spec/helpers/nav/new_dropdown_helper_spec.rb
+++ b/spec/helpers/nav/new_dropdown_helper_spec.rb
@@ -84,7 +84,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_project',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- testid: 'global_new_project_link'
+ testid: 'global-new-project-link'
}
)
)
@@ -107,7 +107,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_group',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- testid: 'global_new_group_link'
+ testid: 'global-new-group-link'
}
)
)
@@ -130,7 +130,7 @@ RSpec.describe Nav::NewDropdownHelper, feature_category: :navigation do
track_action: 'click_link_new_snippet_parent',
track_label: 'plus_menu_dropdown',
track_property: 'navigation_top',
- testid: 'global_new_snippet_link'
+ testid: 'global-new-snippet-link'
}
)
)
diff --git a/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb b/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb
new file mode 100644
index 00000000000..9bfdc799aec
--- /dev/null
+++ b/spec/lib/gitlab/web_ide/default_oauth_application_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::WebIde::DefaultOauthApplication, feature_category: :web_ide do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:oauth_application) { create(:oauth_application, owner: nil) }
+
+ describe '#feature_enabled?' do
+ where(:vscode_web_ide, :web_ide_oauth, :expectation) do
+ [
+ [ref(:current_user), false, false],
+ [false, ref(:current_user), false],
+ [ref(:current_user), ref(:current_user), true]
+ ]
+ end
+
+ with_them do
+ it 'returns the expected value' do
+ stub_feature_flags(vscode_web_ide: vscode_web_ide, web_ide_oauth: web_ide_oauth)
+
+ expect(described_class.feature_enabled?(current_user)).to be(expectation)
+ end
+ end
+ end
+
+ describe '#oauth_application' do
+ it 'returns web_ide_oauth_application from application_settings' do
+ expect(described_class.oauth_application).to be_nil
+
+ stub_application_setting({ web_ide_oauth_application: oauth_application })
+
+ expect(described_class.oauth_application).to be(oauth_application)
+ end
+ end
+
+ describe '#oauth_callback_url' do
+ it 'returns route URL for oauth callback' do
+ expect(described_class.oauth_callback_url).to eq(Gitlab::Routing.url_helpers.ide_oauth_redirect_url)
+ end
+ end
+
+ describe '#ensure_oauth_application!' do
+ it 'if web_ide_oauth_application already exists, does nothing' do
+ expect(application_settings).not_to receive(:lock!)
+ expect(::Doorkeeper::Application).not_to receive(:new)
+
+ stub_application_setting({ web_ide_oauth_application: oauth_application })
+
+ described_class.ensure_oauth_application!
+ end
+
+ it 'if web_ide_oauth_application created while locked, does nothing' do
+ expect(application_settings).to receive(:lock!) do
+ stub_application_setting({ web_ide_oauth_application: oauth_application })
+ end
+ expect(::Doorkeeper::Application).not_to receive(:new)
+ expect(::Gitlab::CurrentSettings).not_to receive(:expire_current_application_settings)
+
+ described_class.ensure_oauth_application!
+ end
+
+ it 'creates web_ide_oauth_application' do
+ expect(application_settings).to receive(:transaction).and_call_original
+ expect(::Doorkeeper::Application).to receive(:new).and_call_original
+ expect(::Gitlab::CurrentSettings).to receive(:expire_current_application_settings).and_call_original
+
+ expect(application_settings.web_ide_oauth_application).to be_nil
+
+ described_class.ensure_oauth_application!
+
+ result = application_settings.web_ide_oauth_application
+ expect(result).not_to be_nil
+ expect(result).to have_attributes(
+ name: 'GitLab Web IDE',
+ redirect_uri: described_class.oauth_callback_url,
+ scopes: ['api'],
+ trusted: true,
+ confidential: false
+ )
+ end
+ end
+
+ def application_settings
+ ::Gitlab::CurrentSettings.current_application_settings
+ end
+end
diff --git a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb
index be5f826ee58..d4b9c359a98 100644
--- a/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb
+++ b/spec/lib/sidebars/user_settings/menus/active_sessions_menu_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Sidebars::UserSettings::Menus::ActiveSessionsMenu, feature_category: :navigation do
it_behaves_like 'User settings menu',
- link: '/-/profile/active_sessions',
+ link: '/-/user_settings/active_sessions',
title: _('Active Sessions'),
icon: 'monitor-lines',
active_routes: { controller: :active_sessions }
diff --git a/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb
new file mode 100644
index 00000000000..14874bdfaa8
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/branch_rules/update_spec.rb
@@ -0,0 +1,95 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'BranchRuleUpdate', feature_category: :source_code_management do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:user) { create(:user) }
+ let!(:branch_rule_1) { create(:protected_branch, project: project, name: name_1) }
+ let!(:branch_rule_2) { create(:protected_branch, project: project, name: name_2) }
+ let(:name_1) { "name_1" }
+ let(:name_2) { "name_2" }
+ let(:new_name) { "new name" }
+ let(:id) { branch_rule_1.to_global_id }
+ let(:project_path) { project.full_path }
+ let(:name) { new_name }
+ let(:params) do
+ {
+ id: id,
+ project_path: project_path,
+ name: name
+ }
+ end
+
+ let(:mutation) { graphql_mutation(:branch_rule_update, params) }
+
+ subject(:post_mutation) { post_graphql_mutation(mutation, current_user: user) }
+
+ def mutation_response
+ graphql_mutation_response(:branch_rule_update)
+ end
+
+ context 'when the user does not have permission' do
+ before_all do
+ project.add_developer(user)
+ end
+
+ it 'does not update the branch rule' do
+ expect { post_mutation }.not_to change { branch_rule_1 }
+ end
+ end
+
+ context 'when the user can update a branch rules' do
+ let(:current_user) { user }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it 'updates the protected branch' do
+ post_mutation
+
+ expect(branch_rule_1.reload.name).to eq(new_name)
+ end
+
+ it 'returns the updated branch rule' do
+ post_mutation
+
+ expect(mutation_response).to have_key('branchRule')
+ expect(mutation_response['branchRule']['name']).to eq(new_name)
+ expect(mutation_response['errors']).to be_empty
+ end
+
+ context 'when name already exists for the project' do
+ let(:params) do
+ {
+ id: id,
+ project_path: project_path,
+ name: name_2
+ }
+ end
+
+ it 'returns an error' do
+ post_mutation
+
+ expect(mutation_response['errors'].first).to eq('Name has already been taken')
+ end
+ end
+
+ context 'when the protected branch cannot be found' do
+ let(:id) { "gid://gitlab/ProtectedBranch/#{non_existing_record_id}" }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+
+ context 'when the project cannot be found' do
+ let(:project_path) { 'not a project path' }
+
+ it_behaves_like 'a mutation that returns top-level errors',
+ errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
+ end
+ end
+end
diff --git a/spec/requests/ide_controller_spec.rb b/spec/requests/ide_controller_spec.rb
index fae3d9533c5..20d890fadbf 100644
--- a/spec/requests/ide_controller_spec.rb
+++ b/spec/requests/ide_controller_spec.rb
@@ -164,6 +164,14 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to render_template('layouts/application')
end
+
+ it 'does not create oauth application' do
+ expect(Doorkeeper::Application).not_to receive(:new)
+
+ subject
+
+ expect(web_ide_oauth_application).to be_nil
+ end
end
describe 'vscode IDE' do
@@ -177,6 +185,40 @@ RSpec.describe IdeController, feature_category: :web_ide do
expect(response).to render_template('layouts/fullscreen')
end
end
+
+ describe 'with web_ide_oauth flag off' do
+ before do
+ stub_feature_flags(web_ide_oauth: false)
+ end
+
+ it 'does not create oauth application' do
+ expect(Doorkeeper::Application).not_to receive(:new)
+
+ subject
+
+ expect(web_ide_oauth_application).to be_nil
+ end
+ end
+
+ it 'ensures web_ide_oauth_application' do
+ expect(Doorkeeper::Application).to receive(:new).and_call_original
+
+ subject
+
+ expect(web_ide_oauth_application).not_to be_nil
+ expect(web_ide_oauth_application[:name]).to eq('GitLab Web IDE')
+ end
+
+ it 'when web_ide_oauth_application already exists, does not create new one' do
+ existing_app = create(:oauth_application, owner_id: nil, owner_type: nil)
+
+ stub_application_setting({ web_ide_oauth_application: existing_app })
+ expect(Doorkeeper::Application).not_to receive(:new)
+
+ subject
+
+ expect(web_ide_oauth_application).to eq(existing_app)
+ end
end
describe 'content security policy' do
@@ -199,4 +241,48 @@ RSpec.describe IdeController, feature_category: :web_ide do
end
end
end
+
+ describe '#oauth_redirect', :aggregate_failures do
+ subject(:oauth_redirect) { get '/-/ide/oauth_redirect' }
+
+ it 'with no web_ide_oauth_application, returns not_found' do
+ oauth_redirect
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'with web_ide_oauth_application set' do
+ before do
+ stub_application_setting({
+ web_ide_oauth_application: create(:oauth_application, owner_id: nil, owner_type: nil)
+ })
+ end
+
+ it 'returns ok and renders view' do
+ oauth_redirect
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'with vscode_web_ide flag off, returns not_found' do
+ stub_feature_flags(vscode_web_ide: false)
+
+ oauth_redirect
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'with web_ide_oauth flag off, returns not_found' do
+ stub_feature_flags(web_ide_oauth: false)
+
+ oauth_redirect
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ def web_ide_oauth_application
+ ::Gitlab::CurrentSettings.current_application_settings.web_ide_oauth_application
+ end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1bd138ea148..9edb9c842ab 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -394,10 +394,18 @@ RSpec.describe JwksController, "routing" do
end
end
-# user_settings_authentication_log GET /-/user_settings/authentication_log(.:format) system_access/user_settings#authentication_log
+# user_settings_authentication_log GET /-/user_settings/authentication_log(.:format) user_settings/user_settings#authentication_log
RSpec.describe UserSettings::UserSettingsController, 'routing', feature_category: :system_access do
it 'to #authentication_log' do
expect(get('/-/user_settings/authentication_log')).to route_to('user_settings/user_settings#authentication_log')
end
end
+
+# user_settings_active_sessions_log GET /-/user_settings_active_sessions_log(.:format) user_settings/active_sessions#index#
+
+RSpec.describe UserSettings::ActiveSessionsController, 'routing', feature_category: :system_access do
+ it 'to #index' do
+ expect(get('/-/user_settings/active_sessions')).to route_to('user_settings/active_sessions#index')
+ end
+end
diff --git a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
index e32747ad907..4834af79225 100644
--- a/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
+++ b/spec/services/projects/hashed_storage/base_attachment_service_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Projects::HashedStorage::BaseAttachmentService, feature_category:
describe '#move_folder!' do
context 'when old_path is not a directory' do
it 'adds information to the logger and returns true' do
- Tempfile.create do |old_path| # rubocop:disable Rails/SaveBang
+ Tempfile.create do |old_path|
new_path = "#{old_path}-new"
expect(subject.send(:move_folder!, old_path, new_path)).to be_truthy
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index fe84a80dae6..ccb3dfc7ffc 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -3218,7 +3218,7 @@
- './spec/controllers/omniauth_callbacks_controller_spec.rb'
- './spec/controllers/passwords_controller_spec.rb'
- './spec/controllers/profiles/accounts_controller_spec.rb'
-- './spec/controllers/profiles/active_sessions_controller_spec.rb'
+- './spec/controllers/user_settings/active_sessions_controller_spec.rb'
- './spec/controllers/profiles/avatars_controller_spec.rb'
- './spec/controllers/profiles_controller_spec.rb'
- './spec/controllers/profiles/emails_controller_spec.rb'
@@ -3727,7 +3727,7 @@
- './spec/features/password_reset_spec.rb'
- './spec/features/populate_new_pipeline_vars_with_params_spec.rb'
- './spec/features/profiles/account_spec.rb'
-- './spec/features/profiles/active_sessions_spec.rb'
+- './spec/features/user_settings/active_sessions_spec.rb'
- './spec/features/profiles/chat_names_spec.rb'
- './spec/features/profiles/emails_spec.rb'
- './spec/features/profiles/gpg_keys_spec.rb'
diff --git a/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb b/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
index 27869974b39..ed9fdc3825b 100644
--- a/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
+++ b/spec/tooling/lib/tooling/parallel_rspec_runner_spec.rb
@@ -55,7 +55,7 @@ RSpec.describe Tooling::ParallelRSpecRunner, feature_category: :tooling do # rub
context 'given filter tests file' do
let(:filter_tests_file) do
- Tempfile.create.tap do |f| # rubocop:disable Rails/SaveBang -- Tempfile has no create! method
+ Tempfile.create.tap do |f|
f.write(filter_tests.join(' '))
f.rewind
end
diff --git a/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb b/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb
index 3c5f7bc0bf9..fba8bb50a32 100644
--- a/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb
+++ b/spec/workers/ci/catalog/resources/process_sync_events_worker_spec.rb
@@ -16,14 +16,14 @@ RSpec.describe Ci::Catalog::Resources::ProcessSyncEventsWorker, feature_category
end
describe '#perform' do
- let_it_be(:project) { create(:project) }
+ let_it_be(:project) { create(:project, name: 'Old Name') }
let_it_be(:resource) { create(:ci_catalog_resource, project: project) }
before_all do
create(:ci_catalog_resource_sync_event, catalog_resource: resource, status: :processed)
create_list(:ci_catalog_resource_sync_event, 2, catalog_resource: resource)
# PG trigger adds an event for this update
- project.update!(name: 'Name', description: 'Test', visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ project.update!(name: 'New Name', description: 'Test', visibility_level: Gitlab::VisibilityLevel::INTERNAL)
end
subject(:perform) { worker.perform }
@@ -48,5 +48,21 @@ RSpec.describe Ci::Catalog::Resources::ProcessSyncEventsWorker, feature_category
perform
end
+
+ context 'when FF `ci_process_catalog_resource_sync_events` is disabled' do
+ before do
+ stub_feature_flags(ci_process_catalog_resource_sync_events: false)
+ end
+
+ it 'does not process the sync events', :aggregate_failures do
+ expect(worker).not_to receive(:log_extra_metadata_on_done)
+
+ expect { perform }.not_to change { Ci::Catalog::Resources::SyncEvent.status_pending.count }
+
+ expect(resource.reload.name).to eq('Old Name')
+ expect(resource.reload.description).to be_nil
+ expect(resource.reload.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
end
end
diff --git a/workhorse/go.mod b/workhorse/go.mod
index fc29ef136ba..f14a1e86d42 100644
--- a/workhorse/go.mod
+++ b/workhorse/go.mod
@@ -28,9 +28,9 @@ require (
golang.org/x/image v0.7.0
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616
golang.org/x/net v0.17.0
- golang.org/x/oauth2 v0.10.0
+ golang.org/x/oauth2 v0.11.0
golang.org/x/tools v0.14.0
- google.golang.org/grpc v1.58.3
+ google.golang.org/grpc v1.59.0
google.golang.org/protobuf v1.31.0
honnef.co/go/tools v0.4.6
)
@@ -68,7 +68,6 @@ require (
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
- github.com/google/go-cmp v0.5.9 // indirect
github.com/google/pprof v0.0.0-20230705174524-200ffdc848b8 // indirect
github.com/google/s2a-go v0.1.4 // indirect
github.com/google/uuid v1.3.1 // indirect
diff --git a/workhorse/go.sum b/workhorse/go.sum
index cc065837dd5..56bfe4e54ab 100644
--- a/workhorse/go.sum
+++ b/workhorse/go.sum
@@ -245,7 +245,6 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-replayers/grpcreplay v1.1.0 h1:S5+I3zYyZ+GQz68OfbURDdt/+cSMqCK1wrvNx7WBzTE=
github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
@@ -593,8 +592,8 @@ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
-golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
+golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
+golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
golang.org/x/sync v0.0.0-20170517211232-f52d1811a629/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -894,8 +893,8 @@ google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
-google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
-google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
+google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
+google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=