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:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-08-16 21:12:52 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-08-16 21:12:52 +0300
commit8a9790b0db723db32f8dff511ee032e5e8e3b583 (patch)
tree8173501b91ea0ada6a68d656786867b2abcc97f9
parent7212129029f4e7e68614066cc43802faba42c554 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue101
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue6
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue15
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue10
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js7
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql7
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql16
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql16
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql16
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql13
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/resolvers.js67
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js24
-rw-r--r--app/assets/javascripts/ci_variable_list/utils.js15
-rw-r--r--app/assets/javascripts/persistent_user_callout.js4
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/users/namespace_callouts_controller.rb17
-rw-r--r--app/graphql/types/work_item_type.rb6
-rw-r--r--app/models/event.rb7
-rw-r--r--app/services/users/dismiss_namespace_callout_service.rb11
-rw-r--r--app/views/ci/variables/_index.html.haml7
-rw-r--r--app/views/dashboard/_groups_head.html.haml2
-rw-r--r--app/views/devise/passwords/new.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml4
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--app/views/shared/_sidebar_toggle_button.html.haml2
-rw-r--r--app/views/shared/access_tokens/_table.html.haml2
-rw-r--r--app/views/shared/empty_states/_labels.html.haml4
-rw-r--r--app/views/shared/empty_states/_priority_labels.html.haml2
-rw-r--r--app/views/shared/empty_states/_topics.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml2
-rw-r--r--app/views/shared/groups/_search_form.html.haml2
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/form/_metadata.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml2
-rw-r--r--app/views/shared/issuable/form/_title.html.haml2
-rw-r--r--app/views/shared/labels/_form.html.haml8
-rw-r--r--app/views/shared/labels/_nav.html.haml4
-rw-r--r--app/views/shared/projects/_search_form.html.haml2
-rw-r--r--app/views/shared/projects/protected_branches/_update_protected_branch.html.haml2
-rw-r--r--config/feature_flags/development/searchable_fork_targets.yml2
-rw-r--r--config/feature_flags/ops/report_jemalloc_stats.yml2
-rw-r--r--config/feature_flags/ops/skip_rugged_auto_detect.yml2
-rw-r--r--config/metrics/aggregates/code_review.yml12
-rw-r--r--config/routes/project.rb1
-rw-r--r--config/routes/user.rb1
-rw-r--r--doc/administration/audit_event_streaming.md8
-rw-r--r--doc/administration/gitaly/index.md8
-rw-r--r--doc/administration/nfs.md17
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md10
-rw-r--r--doc/api/graphql/reference/index.md3
-rw-r--r--doc/ci/pipelines/multi_project_pipelines.md13
-rw-r--r--doc/ci/runners/configure_runners.md3
-rw-r--r--doc/ci/variables/where_variables_can_be_used.md14
-rw-r--r--doc/ci/yaml/index.md2
-rw-r--r--doc/development/database/avoiding_downtime_in_migrations.md10
-rw-r--r--doc/development/documentation/structure.md1
-rw-r--r--doc/development/documentation/styleguide/index.md7
-rw-r--r--doc/development/documentation/testing.md25
-rw-r--r--doc/user/clusters/agent/ci_cd_workflow.md2
-rw-r--r--doc/user/project/issue_board.md17
-rw-r--r--doc/user/project/issues/img/related_issue_block_v12_8.pngbin35817 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/related_issue_block_v15_3.pngbin0 -> 28910 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_add_v12_8.pngbin32939 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_add_v15_3.pngbin0 -> 24947 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_remove_v12_8.pngbin10708 -> 0 bytes
-rw-r--r--doc/user/project/issues/img/related_issues_remove_v15_3.pngbin0 -> 12113 bytes
-rw-r--r--doc/user/project/issues/related_issues.md6
-rw-r--r--doc/user/usage_quotas.md34
-rw-r--r--lib/gitlab/event_store.rb1
-rw-r--r--lib/gitlab/usage_data_counters/known_events/code_review_events.yml25
-rw-r--r--lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb2
-rw-r--r--locale/gitlab.pot27
-rw-r--r--qa/qa/page/alert/auto_devops_alert.rb2
-rw-r--r--qa/qa/page/component/access_tokens.rb4
-rw-r--r--qa/qa/page/component/groups_filter.rb4
-rw-r--r--qa/qa/page/component/issuable/sidebar.rb4
-rw-r--r--qa/qa/page/dashboard/groups.rb9
-rw-r--r--qa/qa/page/dashboard/projects.rb6
-rw-r--r--qa/qa/page/issuable/new.rb19
-rw-r--r--qa/qa/page/label/index.rb10
-rw-r--r--qa/qa/page/label/new.rb12
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb4
-rw-r--r--qa/qa/page/project/show.rb6
-rw-r--r--spec/features/admin_variables_spec.rb34
-rw-r--r--spec/features/issues/user_creates_issue_spec.rb6
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js178
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js37
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js13
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_table_spec.js3
-rw-r--r--spec/frontend/ci_variable_list/mocks.js61
-rw-r--r--spec/frontend/ci_variable_list/utils_spec.js16
-rw-r--r--spec/graphql/types/work_item_type_spec.rb3
-rw-r--r--spec/models/event_spec.rb24
-rw-r--r--spec/requests/users/namespace_callouts_spec.rb57
-rw-r--r--spec/services/users/dismiss_namespace_callout_service_spec.rb24
-rw-r--r--spec/support/shared_examples/features/variable_list_shared_examples.rb10
-rw-r--r--spec/workers/pages/invalidate_domain_cache_worker_spec.rb10
106 files changed, 1018 insertions, 238 deletions
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
new file mode 100644
index 00000000000..83bad9eb518
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/components/ci_admin_variables.vue
@@ -0,0 +1,101 @@
+<script>
+import createFlash from '~/flash';
+import getAdminVariables from '../graphql/queries/variables.query.graphql';
+import {
+ ADD_MUTATION_ACTION,
+ DELETE_MUTATION_ACTION,
+ UPDATE_MUTATION_ACTION,
+ genericMutationErrorText,
+ variableFetchErrorText,
+} from '../constants';
+import addAdminVariable from '../graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '../graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '../graphql/mutations/admin_update_variable.mutation.graphql';
+import ciVariableSettings from './ci_variable_settings.vue';
+
+export default {
+ components: {
+ ciVariableSettings,
+ },
+ inject: ['endpoint'],
+ data() {
+ return {
+ adminVariables: [],
+ isInitialLoading: true,
+ };
+ },
+ apollo: {
+ adminVariables: {
+ query: getAdminVariables,
+ update(data) {
+ return data?.ciVariables?.nodes || [];
+ },
+ error() {
+ createFlash({ message: variableFetchErrorText });
+ },
+ watchLoading(flag) {
+ if (!flag) {
+ this.isInitialLoading = false;
+ }
+ },
+ },
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.adminVariables.loading && this.isInitialLoading;
+ },
+ },
+ methods: {
+ addVariable(variable) {
+ this.variableMutation(ADD_MUTATION_ACTION, variable);
+ },
+ deleteVariable(variable) {
+ this.variableMutation(DELETE_MUTATION_ACTION, variable);
+ },
+ updateVariable(variable) {
+ this.variableMutation(UPDATE_MUTATION_ACTION, variable);
+ },
+ async variableMutation(mutationAction, variable) {
+ try {
+ const currentMutation = this.$options.mutationData[mutationAction];
+ const { data } = await this.$apollo.mutate({
+ mutation: currentMutation.action,
+ variables: {
+ endpoint: this.endpoint,
+ variable,
+ },
+ });
+
+ const { errors } = data[currentMutation.name];
+
+ if (errors.length > 0) {
+ createFlash({ message: errors[0] });
+ } else {
+ // The writing to cache for admin variable is not working
+ // because there is no ID in the cache at the top level.
+ // We therefore need to manually refetch.
+ this.$apollo.queries.adminVariables.refetch();
+ }
+ } catch {
+ createFlash({ message: genericMutationErrorText });
+ }
+ },
+ },
+ mutationData: {
+ [ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
+ [UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
+ [DELETE_MUTATION_ACTION]: { action: deleteAdminVariable, name: 'deleteAdminVariable' },
+ },
+};
+</script>
+
+<template>
+ <ci-variable-settings
+ :are-scoped-variables-available="false"
+ :is-loading="isLoading"
+ :variables="adminVariables"
+ @add-variable="addVariable"
+ @delete-variable="deleteVariable"
+ @update-variable="updateVariable"
+ />
+</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
index 8ee7132bb25..c9002edc1ab 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_environments_dropdown.vue
@@ -33,9 +33,9 @@ export default {
},
filteredEnvironments() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.environments.filter((resultString) =>
- resultString.toLowerCase().includes(lowerCasedSearchTerm),
- );
+ return this.environments.filter((environment) => {
+ return environment.toLowerCase().includes(lowerCasedSearchTerm);
+ });
},
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index dc57f3fe4ce..5ba63de8c96 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -33,7 +33,7 @@ import {
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
-
+import { createJoinedEnvironments } from '../utils';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@@ -98,9 +98,15 @@ export default {
required: false,
default: () => {},
},
+ variables: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
+ newEnvironments: [],
isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
typeOptions: variableOptions,
validationErrorEventProperty: '',
@@ -128,6 +134,9 @@ export default {
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
+ joinedEnvironments() {
+ return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
+ },
maskedFeedback() {
return this.displayMaskedError ? __('This variable can not be masked.') : '';
},
@@ -176,7 +185,7 @@ export default {
this.$emit('add-variable', this.variable);
},
createEnvironmentScope(env) {
- this.$emit('create-environment-scope', env);
+ this.newEnvironments.push(env);
},
deleteVariable() {
this.$emit('delete-variable', this.variable);
@@ -314,7 +323,7 @@ export default {
v-if="areScopedVariablesAvailable"
class="gl-w-full"
:selected-environment-scope="variable.environmentScope"
- :environments="environments"
+ :environments="joinedEnvironments"
@select-environment="setEnvironmentScope"
@create-environment-scope="createEnvironmentScope"
/>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 29578c6f710..81e3a983ea3 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -1,6 +1,5 @@
<script>
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
-import { createJoinedEnvironments } from '../utils';
import CiVariableTable from './ci_variable_table.vue';
import CiVariableModal from './ci_variable_modal.vue';
@@ -17,7 +16,8 @@ export default {
},
environments: {
type: Array,
- required: true,
+ required: false,
+ default: () => [],
},
isLoading: {
type: Boolean,
@@ -36,9 +36,6 @@ export default {
};
},
computed: {
- joinedEnvironments() {
- return createJoinedEnvironments(this.variables, this.environments);
- },
showModal() {
return VARIABLE_ACTIONS.includes(this.mode);
},
@@ -80,7 +77,8 @@ export default {
<ci-variable-modal
v-if="showModal"
:are-scoped-variables-available="areScopedVariablesAvailable"
- :environments="joinedEnvironments"
+ :environments="environments"
+ :variables="variables"
:mode="mode"
:selected-variable="selectedVariable"
@add-variable="addVariable"
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index e42a728a44e..5d22974ffbb 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -47,6 +47,13 @@ export const defaultVariableState = {
variableType: types.variableType,
};
+// eslint-disable-next-line @gitlab/require-i18n-strings
+export const groupString = 'Group';
+// eslint-disable-next-line @gitlab/require-i18n-strings
+export const instanceString = 'Instance';
+// eslint-disable-next-line @gitlab/require-i18n-strings
+export const projectString = 'Instance';
+
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
export const AWS_TIP_MESSAGE = __(
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',
diff --git a/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
new file mode 100644
index 00000000000..a28ca4eebc9
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql
@@ -0,0 +1,7 @@
+fragment BaseCiVariable on CiVariable {
+ __typename
+ id
+ key
+ value
+ variableType
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
new file mode 100644
index 00000000000..eba4b0c32f8
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql
@@ -0,0 +1,16 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation addAdminVariable($variable: CiVariable!, $endpoint: String!) {
+ addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiInstanceVariable {
+ protected
+ masked
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
new file mode 100644
index 00000000000..96eb8c794bc
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql
@@ -0,0 +1,16 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation deleteAdminVariable($variable: CiVariable!, $endpoint: String!) {
+ deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiInstanceVariable {
+ protected
+ masked
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
new file mode 100644
index 00000000000..c0388507bb8
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql
@@ -0,0 +1,16 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+mutation updateAdminVariable($variable: CiVariable!, $endpoint: String!) {
+ updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiInstanceVariable {
+ protected
+ masked
+ }
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
new file mode 100644
index 00000000000..95056842b49
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
@@ -0,0 +1,13 @@
+#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
+
+query getVariables {
+ ciVariables {
+ nodes {
+ ...BaseCiVariable
+ ... on CiInstanceVariable {
+ masked
+ protected
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/resolvers.js b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
new file mode 100644
index 00000000000..7b57e97a4b8
--- /dev/null
+++ b/app/assets/javascripts/ci_variable_list/graphql/resolvers.js
@@ -0,0 +1,67 @@
+import axios from 'axios';
+import {
+ convertObjectPropsToCamelCase,
+ convertObjectPropsToSnakeCase,
+} from '../../lib/utils/common_utils';
+import { getIdFromGraphQLId } from '../../graphql_shared/utils';
+import { instanceString } from '../constants';
+import getAdminVariables from './queries/variables.query.graphql';
+
+const prepareVariableForApi = ({ variable, destroy = false }) => {
+ return {
+ ...convertObjectPropsToSnakeCase(variable),
+ id: getIdFromGraphQLId(variable?.id),
+ variable_type: variable.variableType.toLowerCase(),
+ secret_value: variable.value,
+ _destroy: destroy,
+ };
+};
+
+const mapVariableTypes = (variables = [], kind) => {
+ return variables.map((ciVar) => {
+ return {
+ __typename: `Ci${kind}Variable`,
+ ...convertObjectPropsToCamelCase(ciVar),
+ variableType: ciVar.variable_type ? ciVar.variable_type.toUpperCase() : ciVar.variableType,
+ };
+ });
+};
+
+const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
+ return {
+ errors,
+ ciVariables: {
+ __typename: `Ci${instanceString}VariableConnection`,
+ nodes: mapVariableTypes(data.variables, instanceString),
+ },
+ };
+};
+
+const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false }) => {
+ try {
+ const { data } = await axios.patch(endpoint, {
+ variables_attributes: [prepareVariableForApi({ variable, destroy })],
+ });
+
+ return prepareAdminGraphQLResponse({ data });
+ } catch (e) {
+ return prepareAdminGraphQLResponse({
+ data: cache.readQuery({ query: getAdminVariables }),
+ errors: [...e.response.data],
+ });
+ }
+};
+
+export const resolvers = {
+ Mutation: {
+ addAdminVariable: async (_, { endpoint, variable }, { cache }) => {
+ return callAdminEndpoint({ endpoint, variable, cache });
+ },
+ updateAdminVariable: async (_, { endpoint, variable }, { cache }) => {
+ return callAdminEndpoint({ endpoint, variable, cache });
+ },
+ deleteAdminVariable: async (_, { endpoint, variable }, { cache }) => {
+ return callAdminEndpoint({ endpoint, variable, cache, destroy: true });
+ },
+ },
+};
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 2b54af6a2a4..713a453561e 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -2,8 +2,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import CiVariableSettings from './components/ci_variable_settings.vue';
+import CiAdminVariables from './components/ci_admin_variables.vue';
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
+import { resolvers } from './graphql/resolvers';
import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
@@ -13,8 +14,12 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink,
awsTipLearnLink,
containsVariableReferenceLink,
+ endpoint,
environmentScopeLink,
- group,
+ groupId,
+ groupPath,
+ isGroup,
+ isProject,
maskedEnvironmentVariablesLink,
maskableRegex,
projectFullPath,
@@ -23,13 +28,16 @@ const mountCiVariableListApp = (containerEl) => {
protectedEnvironmentVariablesLink,
} = containerEl.dataset;
- const isGroup = parseBoolean(group);
+ const parsedIsProject = parseBoolean(isProject);
+ const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault);
+ const component = CiAdminVariables;
+
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
+ defaultClient: createDefaultClient(resolvers),
});
return new Vue({
@@ -41,8 +49,12 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink,
awsTipLearnLink,
containsVariableReferenceLink,
+ endpoint,
environmentScopeLink,
- isGroup,
+ groupId,
+ groupPath,
+ isGroup: parsedIsGroup,
+ isProject: parsedIsProject,
isProtectedByDefault,
maskedEnvironmentVariablesLink,
maskableRegex,
@@ -51,7 +63,7 @@ const mountCiVariableListApp = (containerEl) => {
protectedEnvironmentVariablesLink,
},
render(createElement) {
- return createElement(CiVariableSettings);
+ return createElement(component);
},
});
};
diff --git a/app/assets/javascripts/ci_variable_list/utils.js b/app/assets/javascripts/ci_variable_list/utils.js
index 009e56469d1..1faa97a5f73 100644
--- a/app/assets/javascripts/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci_variable_list/utils.js
@@ -2,20 +2,25 @@ import { uniq } from 'lodash';
import { allEnvironments } from './constants';
/**
- * This function takes aa list of variable and environments
+ * This function takes a list of variable, environments and
+ * new environments added through the scope dropdown
* and create a new Array that concatenate the environment list
* with the environment scopes find in the variable list. This is
* useful for variable settings so that we can render a list of all
- * environment scopes available based on both the list of envs and what
- * is found under each variable.
+ * environment scopes available based on the list of envs, the ones the user
+ * added explictly and what is found under each variable.
* @param {Array} variables
* @param {Array} environments
* @returns {Array} - Array of environments
*/
-export const createJoinedEnvironments = (variables = [], environments = []) => {
+export const createJoinedEnvironments = (
+ variables = [],
+ environments = [],
+ newEnvironments = [],
+) => {
const scopesFromVariables = variables.map((variable) => variable.environmentScope);
- return uniq(environments.concat(scopesFromVariables)).sort();
+ return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
};
/**
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 7c424088c8b..9cea89f4990 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -7,11 +7,12 @@ const DEFERRED_LINK_CLASS = 'deferred-link';
export default class PersistentUserCallout {
constructor(container, options = container.dataset) {
- const { dismissEndpoint, featureId, groupId, deferLinks } = options;
+ const { dismissEndpoint, featureId, groupId, namespaceId, deferLinks } = options;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
this.groupId = groupId;
+ this.namespaceId = namespaceId;
this.deferLinks = parseBoolean(deferLinks);
this.closeButtons = this.container.querySelectorAll('.js-close');
@@ -56,6 +57,7 @@ export default class PersistentUserCallout {
.post(this.dismissEndpoint, {
feature_name: this.featureId,
group_id: this.groupId,
+ namespace_id: this.namespaceId,
})
.then(() => {
this.container.remove();
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index e05e87ffd89..6f21b123eb0 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :disable_query_limiting, only: [:usage_data]
+ before_action do
+ push_frontend_feature_flag(:ci_variable_settings_graphql)
+ end
+
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token
diff --git a/app/controllers/users/namespace_callouts_controller.rb b/app/controllers/users/namespace_callouts_controller.rb
new file mode 100644
index 00000000000..d4876382dfe
--- /dev/null
+++ b/app/controllers/users/namespace_callouts_controller.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Users
+ class NamespaceCalloutsController < Users::CalloutsController
+ private
+
+ def callout
+ Users::DismissNamespaceCalloutService.new(
+ container: nil, current_user: current_user, params: callout_params
+ ).execute
+ end
+
+ def callout_params
+ params.permit(:namespace_id).merge(feature_name: feature_name)
+ end
+ end
+end
diff --git a/app/graphql/types/work_item_type.rb b/app/graphql/types/work_item_type.rb
index c1c28c569d1..68b55386daa 100644
--- a/app/graphql/types/work_item_type.rb
+++ b/app/graphql/types/work_item_type.rb
@@ -6,8 +6,12 @@ module Types
authorize :read_work_item
+ field :closed_at, Types::TimeType, null: true,
+ description: 'Timestamp of when the work item was closed.'
field :confidential, GraphQL::Types::Boolean, null: false,
description: 'Indicates the work item is confidential.'
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the work item was created.'
field :description, GraphQL::Types::String, null: true,
description: 'Description of the work item.'
field :id, Types::GlobalIDType[::WorkItem], null: false,
@@ -22,6 +26,8 @@ module Types
description: 'State of the work item.'
field :title, GraphQL::Types::String, null: false,
description: 'Title of the work item.'
+ field :updated_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the work item was last updated.'
field :widgets,
[Types::WorkItems::WidgetInterface],
null: true,
diff --git a/app/models/event.rb b/app/models/event.rb
index 5349ad029ec..a20ca0dc423 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -216,6 +216,10 @@ class Event < ApplicationRecord
target_type == 'DesignManagement::Design'
end
+ def work_item?
+ target_type == 'WorkItem'
+ end
+
def milestone
target if milestone?
end
@@ -399,7 +403,8 @@ class Event < ApplicationRecord
read_milestone: %i[milestone?],
read_wiki: %i[wiki_page?],
read_design: %i[design_note? design?],
- read_note: %i[note?]
+ read_note: %i[note?],
+ read_work_item: %i[work_item?]
}
end
diff --git a/app/services/users/dismiss_namespace_callout_service.rb b/app/services/users/dismiss_namespace_callout_service.rb
new file mode 100644
index 00000000000..51261a93e20
--- /dev/null
+++ b/app/services/users/dismiss_namespace_callout_service.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Users
+ class DismissNamespaceCalloutService < DismissCalloutService
+ private
+
+ def callout
+ current_user.find_or_initialize_namespace_callout(params[:feature_name], params[:namespace_id])
+ end
+ end
+end
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 9ef2599a2a6..02c468cebd7 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -6,10 +6,15 @@
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
- is_group = !@group.nil?
+- is_project = !@project.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint,
+ is_project: is_project.to_s,
project_id: @project&.id || '',
- group: is_group.to_s,
+ project_full_path: @project&.full_path || '',
+ is_group: is_group.to_s,
+ group_id: @group&.id || '',
+ group_path: @group&.full_path,
maskable_regex: ci_variable_maskable_regex,
protected_by_default: ci_variable_protected_by_default?.to_s,
aws_logo_svg_path: image_path('aws_logo.svg'),
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index f0f1413831a..813c1cdbfe4 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -3,7 +3,7 @@
- if current_user.can_create_group?
.page-title-controls
- = link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { testid: "new-group-button" }
+ = link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { qa_selector: "new_group_button", testid: "new-group-button" }
.top-area
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 820d5ccb4f9..1400ac9ca72 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -4,7 +4,7 @@
.devise-errors
= render "devise/shared/error_messages", resource: resource
.form-group.gl-px-5.gl-pt-5
- = f.label :email, class: "gl-mb-1" if Feature.enabled?(:restyle_login_page, @project)
+ = f.label :email, class: ("gl-mb-1" if Feature.enabled?(:restyle_login_page))
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
= _('Requires your primary GitLab email address.')
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 3b117022d1e..88352ea351c 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -32,7 +32,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
- = link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'groups/runners/settings'
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 09f9ca60b3e..dd9cc296d52 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -41,7 +41,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
- = link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'projects/runners/settings'
diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
index 1f37e33a037..ac7d56520f7 100644
--- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
+++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml
@@ -1,5 +1,5 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
- = render Pajamas::AlertComponent.new(alert_options: { class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner' },
+ = render Pajamas::AlertComponent.new(alert_options: { class: 'auto-devops-implicitly-enabled-banner', data: { qa_selector: 'auto_devops_banner_content' } },
close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
data: { project_id: project.id }}) do |c|
= c.body do
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 80b50f7a3de..6b502ee928e 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -5,7 +5,7 @@
%span.js-clone-dropdown-label
= enabled_protocol_button(container, enabled_protocol)
- else
- %a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
+ %a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 821f1ede422..0bd5d1795d0 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if any_projects?(@projects)
.dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
- %a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
+ %a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= gl_loading_icon(inline: true, color: 'light')
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button{ 'aria-label': _('Toggle project select') }
+ %button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button{ 'aria-label': _('Toggle project select') }
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 74e0a088656..20bf2141cc3 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -13,8 +13,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
- .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
+ .dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]), data: { qa_selector: "branches_dropdown_content" } }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index f3942aa5dc2..770d335a88b 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -3,5 +3,5 @@
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
icon_classes: 'spin')
- elsif remote_mirror.enabled?
- = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
+ = link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body', qa_selector: 'update_now_button' }, title: _('Update now') do
= sprite_icon("retry")
diff --git a/app/views/shared/_sidebar_toggle_button.html.haml b/app/views/shared/_sidebar_toggle_button.html.haml
index 0a74e47fa4c..4cdf1340d64 100644
--- a/app/views/shared/_sidebar_toggle_button.html.haml
+++ b/app/views/shared/_sidebar_toggle_button.html.haml
@@ -1,4 +1,4 @@
-%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
+%a.toggle-sidebar-button.js-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
= sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
diff --git a/app/views/shared/access_tokens/_table.html.haml b/app/views/shared/access_tokens/_table.html.haml
index 5ca9cf8d9a4..53c6800f93d 100644
--- a/app/views/shared/access_tokens/_table.html.haml
+++ b/app/views/shared/access_tokens/_table.html.haml
@@ -45,7 +45,7 @@
%span.token-never-expires-label= _('Never')
- if resource
%td= resource.member(token.user).human_access
- %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right qa-revoke-button #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger' }
+ %td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger', qa_selector: 'revoke_button' }
- else
.settings-message.text-center
= no_active_tokens_message
diff --git a/app/views/shared/empty_states/_labels.html.haml b/app/views/shared/empty_states/_labels.html.haml
index 4e5e04ba4d4..e96fcd11cef 100644
--- a/app/views/shared/empty_states/_labels.html.haml
+++ b/app/views/shared/empty_states/_labels.html.haml
@@ -1,6 +1,6 @@
.row.empty-state.labels
.col-12
- .svg-content.qa-label-svg
+ .svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/labels.svg'
.col-12
.text-content
@@ -8,7 +8,7 @@
%p= _("You can also star a label to make it a priority label.")
.text-center
- if can?(current_user, :admin_label, @project)
- = link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm qa-label-create-new', title: _('New label'), id: 'new_label_link'
+ = link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn gl-button btn-confirm-secondary', title: _('Generate a default set of labels'), id: 'generate_labels_link'
- if can?(current_user, :admin_label, @group)
= link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
diff --git a/app/views/shared/empty_states/_priority_labels.html.haml b/app/views/shared/empty_states/_priority_labels.html.haml
index a93f6e4c795..3381c5f0c67 100644
--- a/app/views/shared/empty_states/_priority_labels.html.haml
+++ b/app/views/shared/empty_states/_priority_labels.html.haml
@@ -1,5 +1,5 @@
.text-center
- .svg-content.qa-label-svg
+ .svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/priority_labels.svg'
- if can?(current_user, :admin_label, @project)
%p
diff --git a/app/views/shared/empty_states/_topics.html.haml b/app/views/shared/empty_states/_topics.html.haml
index fd82a853037..0283e852c7d 100644
--- a/app/views/shared/empty_states/_topics.html.haml
+++ b/app/views/shared/empty_states/_topics.html.haml
@@ -1,7 +1,7 @@
.row.empty-state
.col-12
.svg-content
- = image_tag 'illustrations/labels.svg', data: { qa_selector: 'svg_content' }
+ = image_tag 'illustrations/labels.svg'
.text-content.gl-text-center.gl-pt-0!
%h4= _('There are no topics to show.')
%p= _('Add topics to projects to help users find them.')
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
index 552b100d5dd..8304a2f18a0 100644
--- a/app/views/shared/empty_states/_wikis.html.haml
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -3,7 +3,7 @@
- if can?(current_user, :create_wiki, @wiki.container)
- create_path = wiki_page_path(@wiki, params[:id], view: 'create')
- - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm', title: s_('WikiEmpty|Create your first page'), data: { qa_selector: 'create_first_page_link' }
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
index 3b100f832b2..0b7034838ed 100644
--- a/app/views/shared/empty_states/_wikis_layout.html.haml
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -1,6 +1,6 @@
.row.empty-state.empty-state-wiki
.col-12
- .svg-content.qa-svg-content
+ .svg-content{ data: { qa_selector: 'svg_content' } }
= image_tag image_path
.col-12
.text-content.text-center
diff --git a/app/views/shared/groups/_search_form.html.haml b/app/views/shared/groups/_search_form.html.haml
index a574394694d..2afac0ad733 100644
--- a/app/views/shared/groups/_search_form.html.haml
+++ b/app/views/shared/groups/_search_form.html.haml
@@ -1,2 +1,2 @@
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
- = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field'
+ = search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', data: { qa_selector: 'groups_filter_field' }, spellcheck: false, id: 'group-filter-form-field'
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index 08883bb3372..af63839d7c1 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -11,7 +11,7 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
-- dropdown_data.merge!(data_options)
+- dropdown_data.merge!(data_options, qa_selector: "issuable_label_dropdown")
- label_name = local_assigns.fetch(:label_name, _('Labels'))
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
@@ -22,7 +22,7 @@
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect.qa-issuable-label{ class: classes.join(' '), type: "button", data: dropdown_data }
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index dc713337747..ef539029272 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -7,8 +7,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone'))
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
-= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
- placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone') } }) do
+= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", dropdown_qa_selector: "issuable_milestone_dropdown_content",
+ placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 321311e1d96..6da094924a0 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -26,14 +26,14 @@
= _('To-Do')
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
- .block.assignee.qa-assignee-block{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}" }
+ .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity]
#js-severity
- if reviewers
- .block.reviewer.qa-reviewer-block
+ .block.reviewer
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation]
diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml
index ca84bb6d987..76469b34832 100644
--- a/app/views/shared/issuable/form/_metadata.html.haml
+++ b/app/views/shared/issuable/form/_metadata.html.haml
@@ -35,7 +35,7 @@
= form.label :milestone_id, _('Milestone'), class: "col-12"
.col-12
.issuable-form-select-holder
- = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
.form-group.row
= form.label :label_ids, _('Labels'), class: "col-12"
diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index f9c3c11eed8..efecffbcc2e 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -8,4 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
- = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
+ = link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 #{'hide' if issuable.assignees.include?(current_user)}", data: { qa_selector: 'assign_to_me_link' }
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index e7c0833de0f..51f49c7ca8e 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -9,7 +9,7 @@
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
- autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', dir: 'auto'
+ autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { qa_selector: 'issuable_form_title_field' }
- if issuable.respond_to?(:draft?)
.form-text.text-muted
diff --git a/app/views/shared/labels/_form.html.haml b/app/views/shared/labels/_form.html.haml
index 1df02d57030..cf8bd23b153 100644
--- a/app/views/shared/labels/_form.html.haml
+++ b/app/views/shared/labels/_form.html.haml
@@ -4,20 +4,20 @@
.form-group.row
.col-12
= f.label :title
- = f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
+ = f.text_field :title, class: "gl-form-input form-control js-label-title", required: true, autofocus: true, data: { qa_selector: 'label_title_field' }
= render_if_exists 'shared/labels/create_label_help_text'
.form-group.row
.col-12
= f.label :description
- = f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
+ = f.text_field :description, class: "gl-form-input form-control js-quick-submit", data: { qa_selector: 'label_description_field' }
.form-group.row
.col-12
= f.label :color, _("Background color")
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
- = f.text_field :color, class: "gl-form-input form-control qa-label-color"
+ = f.text_field :color, class: "gl-form-input form-control", data: { qa_selector: 'label_color_field' }
.form-text.text-muted
= _('Choose any color.')
%br
@@ -28,7 +28,7 @@
- if @label.persisted?
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
- else
- = f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button gl-mr-2'
+ = f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
- if @label.persisted?
- presented_label = @label.present
diff --git a/app/views/shared/labels/_nav.html.haml b/app/views/shared/labels/_nav.html.haml
index 622ad9db425..c82a22c73b8 100644
--- a/app/views/shared/labels/_nav.html.haml
+++ b/app/views/shared/labels/_nav.html.haml
@@ -14,8 +14,8 @@
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
= render 'shared/labels/sort_dropdown'
- if labels_or_filters && can_admin_label && @project
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { class: 'qa-label-create-new' }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
= _('New label')
- if labels_or_filters && can_admin_label && @group
- = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { class: 'qa-label-create-new' }) do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
= _('New label')
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index e96a9152c80..51a5c9dd38f 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -1,7 +1,7 @@
- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : ''
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...'
-= form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f|
+= form_tag filter_projects_path, method: :get, class: 'project-filter-form', data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
class: "project-filter-form-field form-control #{form_field_classes}",
diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
index 32b9044c551..d10196a83cc 100644
--- a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
@@ -10,7 +10,7 @@
%td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
= dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
- options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
+ options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
- if user_merge_access_levels.any?
%p.small
diff --git a/config/feature_flags/development/searchable_fork_targets.yml b/config/feature_flags/development/searchable_fork_targets.yml
index aeeeb66d2f8..3baea34623a 100644
--- a/config/feature_flags/development/searchable_fork_targets.yml
+++ b/config/feature_flags/development/searchable_fork_targets.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370795
milestone: '15.3'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/ops/report_jemalloc_stats.yml b/config/feature_flags/ops/report_jemalloc_stats.yml
index 2bbf63d2d78..9cf5fd9d14b 100644
--- a/config/feature_flags/ops/report_jemalloc_stats.yml
+++ b/config/feature_flags/ops/report_jemalloc_stats.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367845
milestone: '15.2'
type: ops
group: group::memory
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/ops/skip_rugged_auto_detect.yml b/config/feature_flags/ops/skip_rugged_auto_detect.yml
index c454fad54ec..57da3bba4ea 100644
--- a/config/feature_flags/ops/skip_rugged_auto_detect.yml
+++ b/config/feature_flags/ops/skip_rugged_auto_detect.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370708
milestone: '15.3'
type: ops
group: group::gitaly
-default_enabled: false
+default_enabled: true
diff --git a/config/metrics/aggregates/code_review.yml b/config/metrics/aggregates/code_review.yml
index a9be0680bfb..2e1eab78fa3 100644
--- a/config/metrics/aggregates/code_review.yml
+++ b/config/metrics/aggregates/code_review.yml
@@ -110,6 +110,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success'
- 'i_code_review_merge_request_widget_metrics_expand_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed'
+ - 'i_code_review_merge_request_widget_status_checks_view'
+ - 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
+ - 'i_code_review_merge_request_widget_status_checks_expand'
+ - 'i_code_review_merge_request_widget_status_checks_expand_success'
+ - 'i_code_review_merge_request_widget_status_checks_expand_warning'
+ - 'i_code_review_merge_request_widget_status_checks_expand_failed'
- name: code_review_category_monthly_active_users
operator: OR
source: redis
@@ -208,6 +214,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success'
- 'i_code_review_merge_request_widget_metrics_expand_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed'
+ - 'i_code_review_merge_request_widget_status_checks_view'
+ - 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
+ - 'i_code_review_merge_request_widget_status_checks_expand'
+ - 'i_code_review_merge_request_widget_status_checks_expand_success'
+ - 'i_code_review_merge_request_widget_status_checks_expand_warning'
+ - 'i_code_review_merge_request_widget_status_checks_expand_failed'
- name: code_review_extension_category_monthly_active_users
operator: OR
source: redis
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 1ee962fb2a1..53d9be13611 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'alert_management/:id', to: 'alert_management#details', as: 'alert_management_alert'
get 'work_items/*work_items_path' => 'work_items#index', as: :work_items
+ get 'work_items/*work_items_path' => 'work_items#index', as: :work_item
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'
diff --git a/config/routes/user.rb b/config/routes/user.rb
index 0c1bc1956a9..a9e0d7df88f 100644
--- a/config/routes/user.rb
+++ b/config/routes/user.rb
@@ -64,6 +64,7 @@ scope '-/users', module: :users do
end
resources :callouts, only: [:create]
+ resources :namespace_callouts, only: [:create]
resources :group_callouts, only: [:create]
resources :project_callouts, only: [:create]
end
diff --git a/doc/administration/audit_event_streaming.md b/doc/administration/audit_event_streaming.md
index 8095ca8753a..3bb0ce41861 100644
--- a/doc/administration/audit_event_streaming.md
+++ b/doc/administration/audit_event_streaming.md
@@ -16,6 +16,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - Custom HTTP headers API [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366524) in GitLab 15.3. [Feature flag `streaming_audit_event_headers`](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) removed.
> - Custom HTTP headers UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default.
> - Custom HTTP headers UI [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) in GitLab 15.3. [Feature flag `custom_headers_streaming_audit_events_ui`](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) removed.
+> - [Improved user experience](https://gitlab.com/gitlab-org/gitlab/-/issues/367963) in GitLab 15.4.
Users can set a streaming destination for a top-level group to receive all audit events about the group, its subgroups, and
projects as structured JSON.
@@ -40,13 +41,12 @@ Users with the Owner role for a group can add streaming destinations for it:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Security & Compliance > Audit events**.
1. On the main area, select **Streams** tab.
- - When the destination list is empty, select **Add stream** to show the section for adding destinations.
- - When the destination list is not empty, select **Add stream** (**{plus}**) to show the section for adding destinations.
+1. Select **Add streaming destination** to show the section for adding destinations.
1. Enter the destination URL to add.
1. Optional. Locate the **Custom HTTP headers** table.
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
-1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
+1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
20 headers per streaming destination.
1. After all headers have been filled out, select **Add** to add the new streaming destination.
@@ -149,7 +149,7 @@ To update a streaming destinations custom HTTP headers:
1. Locate the header that you wish to update.
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
-1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
+1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
20 headers per streaming destination.
1. Select **Save** to update the streaming destination.
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 82c9492ebcc..e433c588cdd 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -685,8 +685,12 @@ To see if GitLab can access the repository file system directly, we use the foll
- GitLab Rails tries to read the metadata file directly. If it exists, and if the UUID's match,
assume we have direct access.
-Direct Git access is enable by default in Omnibus GitLab because it fills in the correct repository
-paths in the GitLab configuration file `config/gitlab.yml`. This satisfies the UUID check.
+Versions of GitLab 15.3 and later disable direct Git access by default.
+
+For versions of GitLab prior to 15.3, direct Git access is enabled by
+default in Omnibus GitLab because it fills in the correct repository
+paths in the GitLab configuration file `config/gitlab.yml`. This
+satisfies the UUID check.
### Transition to Gitaly Cluster
diff --git a/doc/administration/nfs.md b/doc/administration/nfs.md
index 81138a31bc9..29f00fec3f7 100644
--- a/doc/administration/nfs.md
+++ b/doc/administration/nfs.md
@@ -98,9 +98,20 @@ NFS performance with GitLab can in some cases be improved with
[direct Git access](gitaly/index.md#direct-access-to-git-in-gitlab) using
[Rugged](https://github.com/libgit2/rugged).
-From GitLab 12.1, GitLab automatically detects if Rugged can and should be used per storage.
-If you previously enabled Rugged using the feature flag and you want to use automatic detection instead,
-you must unset the feature flag:
+Versions of GitLab after 12.2 and prior to 15.3 automatically detect if
+Rugged can and should be used per storage.
+
+NOTE:
+GitLab 15.3 and later disables this automatic detection. Auto-detection can be enabled via the
+`skip_rugged_auto_detect` feature flag:
+
+```ruby
+Feature.disable(:skip_rugged_auto_detect)
+```
+
+In addition, if you previously enabled Rugged using the feature flag and
+you want to use automatic detection instead, you must unset the feature
+flag:
```shell
sudo gitlab-rake gitlab:features:unset_rugged
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index b452d7c0564..aa4dbec4f95 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -423,16 +423,6 @@ projects = Project.find_by_sql("SELECT * FROM projects WHERE name LIKE '%ject'")
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
```
-## Issue boards
-
-### In case of issue boards not loading properly and it's getting time out. Call the Issue Rebalancing service to fix this
-
-```ruby
-p = Project.find_by_full_path('<username-or-group>/<project-name>')
-
-Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
-```
-
## Imports and exports
### Import a project
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 11d179ffcfa..c7af8e7c129 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -18856,7 +18856,9 @@ Represents vulnerability letter grades with associated projects.
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="workitemclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the work item was closed. |
| <a id="workitemconfidential"></a>`confidential` | [`Boolean!`](#boolean) | Indicates the work item is confidential. |
+| <a id="workitemcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the work item was created. |
| <a id="workitemdescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
@@ -18865,6 +18867,7 @@ Represents vulnerability letter grades with associated projects.
| <a id="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. |
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
| <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
+| <a id="workitemupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the work item was last updated. |
| <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. |
| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. |
| <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |
diff --git a/doc/ci/pipelines/multi_project_pipelines.md b/doc/ci/pipelines/multi_project_pipelines.md
index 93034aa233f..81c0639817f 100644
--- a/doc/ci/pipelines/multi_project_pipelines.md
+++ b/doc/ci/pipelines/multi_project_pipelines.md
@@ -76,7 +76,7 @@ downstream project (`my/deployment`) too. If the downstream project is not found
or the user does not have [permission](../../user/permissions.md) to create a pipeline there,
the `staging` job is marked as _failed_.
-#### Trigger job configuration keywords
+#### Trigger job configuration limitations
Trigger jobs can use only a limited set of the GitLab CI/CD [configuration keywords](../yaml/index.md).
The keywords available for use in trigger jobs are:
@@ -90,6 +90,8 @@ The keywords available for use in trigger jobs are:
- [`extends`](../yaml/index.md#extends)
- [`needs`](../yaml/index.md#needs), but not [`needs:project`](../yaml/index.md#needsproject)
+Trigger jobs cannot use [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables).
+
#### Specify a downstream pipeline branch
You can specify a branch name for the downstream pipeline to use.
@@ -182,9 +184,12 @@ downstream-job:
trigger: my/project
```
-In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the
-upstream pipeline is passed to the `downstream-job` job. It is available
-in the context of all downstream builds.
+In this scenario, the `UPSTREAM_BRANCH` variable with the value of the upstream pipeline's
+`$CI_COMMIT_REF_NAME` is passed to `downstream-job`. It is available in the
+context of all downstream builds.
+
+You cannot use this method to forward [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
+to a downstream pipeline, as they are not available in trigger jobs.
Upstream pipelines take precedence over downstream ones. If there are two
variables with the same name defined in both upstream and downstream projects,
diff --git a/doc/ci/runners/configure_runners.md b/doc/ci/runners/configure_runners.md
index bd8cb6627e0..3efa697bf2f 100644
--- a/doc/ci/runners/configure_runners.md
+++ b/doc/ci/runners/configure_runners.md
@@ -722,6 +722,9 @@ variables:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28940) in GitLab Runner 15.1.
+NOTE:
+Zip archives are the only supported artifact type. Follow [the issue for details](https://gitlab.com/gitlab-org/gitlab/-/issues/367203).
+
GitLab Runner can generate and produce attestation metadata for all build artifacts. To enable this feature, you must set the `RUNNER_GENERATE_ARTIFACTS_METADATA` environment variable to `true`. This variable can either be set globally or it can be set for individual jobs. The metadata is in rendered in a plain text `.json` file that's stored with the artifact. The file name is as follows: `{JOB_ID}-artifacts-metadata.json`.
### Attestation format
diff --git a/doc/ci/variables/where_variables_can_be_used.md b/doc/ci/variables/where_variables_can_be_used.md
index a93d0814391..db83f2a14f3 100644
--- a/doc/ci/variables/where_variables_can_be_used.md
+++ b/doc/ci/variables/where_variables_can_be_used.md
@@ -132,10 +132,17 @@ These restrictions exist because `after_script` scripts are executed in a
## Persisted variables
-The following variables are known as "persisted":
+Some predefined variables are called "persisted".
+
+Pipeline-level persisted variables:
- `CI_PIPELINE_ID`
+- `CI_PIPELINE_URL`
+
+Job-level persisted variables:
+
- `CI_JOB_ID`
+- `CI_JOB_URL`
- `CI_JOB_TOKEN`
- `CI_JOB_STARTED_AT`
- `CI_REGISTRY_USER`
@@ -144,7 +151,7 @@ The following variables are known as "persisted":
- `CI_DEPLOY_USER`
- `CI_DEPLOY_PASSWORD`
-They are:
+Persisted variables are:
- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
- Runner.
@@ -153,6 +160,9 @@ They are:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- In the `only`, `except`, and `rules` [variables expressions](../jobs/job_control.md#cicd-variable-expressions).
+[Pipeline trigger jobs](../yaml/index.md#trigger) cannot use job-level persisted variables,
+but can use pipeline-level persisted variables.
+
Some of the persisted variables contain tokens and cannot be used by some definitions
due to security reasons.
diff --git a/doc/ci/yaml/index.md b/doc/ci/yaml/index.md
index d6fb118f4af..466cfc5e6ba 100644
--- a/doc/ci/yaml/index.md
+++ b/doc/ci/yaml/index.md
@@ -3933,6 +3933,8 @@ trigger_job:
and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule)
are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward)
to forward these variables to downstream pipelines.
+- [Job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
+ are not available in trigger jobs.
**Related topics**:
diff --git a/doc/development/database/avoiding_downtime_in_migrations.md b/doc/development/database/avoiding_downtime_in_migrations.md
index 6dd47220a56..79c76b351c8 100644
--- a/doc/development/database/avoiding_downtime_in_migrations.md
+++ b/doc/development/database/avoiding_downtime_in_migrations.md
@@ -103,7 +103,7 @@ If the `down` method requires adding back any dropped indexes or constraints, th
be done within a transactional migration, then the migration would look like this:
```ruby
-class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[1.0]
+class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
@@ -158,7 +158,7 @@ renaming. For example
```ruby
# A regular migration in db/migrate
-class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[1.0]
+class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
@@ -186,7 +186,7 @@ We can perform this cleanup using
```ruby
# A post-deployment migration in db/post_migrate
-class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[1.0]
+class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
@@ -233,7 +233,7 @@ as follows:
```ruby
# A regular migration in db/migrate
-class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[1.0]
+class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
@@ -252,7 +252,7 @@ Next we need to clean up our changes using a post-deployment migration:
```ruby
# A post-deployment migration in db/post_migrate
-class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[1.0]
+class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[2.0]
disable_ddl_transaction!
def up
diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md
index 6c356b1906f..a5d1290a17a 100644
--- a/doc/development/documentation/structure.md
+++ b/doc/development/documentation/structure.md
@@ -177,6 +177,7 @@ Troubleshooting can be one of three categories:
```
If multiple causes or workarounds exist, consider putting them into a table format.
+ If you use the exact error message, surround it in backticks so it's styled as code.
If a page has more than five troubleshooting topics, put the content on a separate page that has troubleshooting information exclusively. Name the page `Troubleshooting <featurename>`.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index a434e3d9fb0..709e6b2d0d9 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -164,6 +164,13 @@ Also, do not use links as part of heading text.
See also [heading guidelines for specific topic types](../structure.md).
+### Backticks in Markdown
+
+Use backticks for:
+
+- [Code blocks](#code-blocks).
+- Error messages.
+
### Markdown Rules
GitLab ensures that the Markdown used across all documentation is consistent, as
diff --git a/doc/development/documentation/testing.md b/doc/development/documentation/testing.md
index 85219ec0364..428a57a11fb 100644
--- a/doc/development/documentation/testing.md
+++ b/doc/development/documentation/testing.md
@@ -280,7 +280,7 @@ You can use Vale:
Vale returns three types of results:
-- **Error** - For branding and trademark issues, words or phrases with ambiguous meanings, and anything that causes content on
+- **Error** - For branding guidelines, trademark guidelines, and anything that causes content on
the docs site to render incorrectly.
- **Warning** - For Technical Writing team style preferences.
- **Suggestion** - For basic technical writing tenets and best practices.
@@ -337,6 +337,29 @@ general complexity level of the page.
The readability score is calculated based on the number of words per sentence, and the number
of syllables per word. For more information, see [the Vale documentation](https://vale.sh/docs/topics/styles/#metric).
+#### When to add a new Vale rule
+
+It's tempting to add a Vale rule for every style guide rule. However, we should be
+mindful of the effort to create and enforce a Vale rule, and the noise it creates.
+
+In general, follow these guidelines:
+
+- If you add an [error-level Vale rule](#vale-result-types), you must fix
+ the existing occurrences of the issue in the documentation before you can add the rule.
+
+ If there are too many issues to fix in a single merge request, add the rule at a
+ `warning` level. Then, fix the existing issues in follow-up merge requests.
+ When the issues are fixed, promote the rule to an `error`.
+
+- If you add a warning-level or suggestion-level rule, consider:
+
+ - How many more warnings or suggestions it creates in the Vale output. If the
+ number of additional warnings is significant, the rule might be too broad.
+
+ - How often an author might ignore it because it's acceptable in the context.
+ If the rule is too subjective, it cannot be adequately enforced and creates
+ unnecessary additional warnings.
+
### Install linters
At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in
diff --git a/doc/user/clusters/agent/ci_cd_workflow.md b/doc/user/clusters/agent/ci_cd_workflow.md
index d383cf3d7cb..16b92eb92a3 100644
--- a/doc/user/clusters/agent/ci_cd_workflow.md
+++ b/doc/user/clusters/agent/ci_cd_workflow.md
@@ -25,7 +25,7 @@ To ensure access to your cluster is safe:
- Each agent has a separate context (`kubecontext`).
- Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
-You do not need to have a runner in the cluster with the agent.
+The CI/CD workflow requires runners to be registered with GitLab, but these runners do not have to be in the cluster where the agent is.
## GitLab CI/CD workflow steps
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index c8ecb2fd2e6..787e990526d 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -642,3 +642,20 @@ A few things to remember:
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues, start scrolling down and the next
20 appear.
+
+## Troubleshooting issue boards
+
+### Use Rails console to fix issue boards not loading and timing out
+
+If you see issue board not loading and timing out in UI, use Rails console to call the Issue Rebalancing service to fix it:
+
+1. [Start a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session).
+1. Run these commands:
+
+ ```ruby
+ p = Project.find_by_full_path('<username-or-group>/<project-name>')
+
+ Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
+ ```
+
+1. To exit the Rails console, type `quit`.
diff --git a/doc/user/project/issues/img/related_issue_block_v12_8.png b/doc/user/project/issues/img/related_issue_block_v12_8.png
deleted file mode 100644
index ce261f26ce6..00000000000
--- a/doc/user/project/issues/img/related_issue_block_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/related_issue_block_v15_3.png b/doc/user/project/issues/img/related_issue_block_v15_3.png
new file mode 100644
index 00000000000..827ddeabf10
--- /dev/null
+++ b/doc/user/project/issues/img/related_issue_block_v15_3.png
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_add_v12_8.png b/doc/user/project/issues/img/related_issues_add_v12_8.png
deleted file mode 100644
index 8a06d005a5f..00000000000
--- a/doc/user/project/issues/img/related_issues_add_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_add_v15_3.png b/doc/user/project/issues/img/related_issues_add_v15_3.png
new file mode 100644
index 00000000000..7c6edf61427
--- /dev/null
+++ b/doc/user/project/issues/img/related_issues_add_v15_3.png
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_remove_v12_8.png b/doc/user/project/issues/img/related_issues_remove_v12_8.png
deleted file mode 100644
index a8dff4c7052..00000000000
--- a/doc/user/project/issues/img/related_issues_remove_v12_8.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/issues/img/related_issues_remove_v15_3.png b/doc/user/project/issues/img/related_issues_remove_v15_3.png
new file mode 100644
index 00000000000..577bbed9df3
--- /dev/null
+++ b/doc/user/project/issues/img/related_issues_remove_v15_3.png
Binary files differ
diff --git a/doc/user/project/issues/related_issues.md b/doc/user/project/issues/related_issues.md
index 028dd2ea473..9dc361b403f 100644
--- a/doc/user/project/issues/related_issues.md
+++ b/doc/user/project/issues/related_issues.md
@@ -37,7 +37,7 @@ To link one issue to another:
- **[is blocked by](#blocking-issues)**
1. Input the issue number or paste in the full URL of the issue.
- ![Adding a related issue](img/related_issues_add_v12_8.png)
+ ![Adding a related issue](img/related_issues_add_v15_3.png)
Issues of the same project can be specified just by the reference number.
Issues from a different project require additional information like the
@@ -54,7 +54,7 @@ To link one issue to another:
When you have finished adding all linked issues, you can see
them categorized so their relationships can be better understood visually.
-![Related issue block](img/related_issue_block_v12_8.png)
+![Related issue block](img/related_issue_block_v15_3.png)
You can also add a linked issue from a commit message or the description in another issue or MR.
[Learn more about crosslinking issues](crosslinking_issues.md).
@@ -66,7 +66,7 @@ right-side of each issue token to remove.
Due to the bi-directional relationship, the relationship no longer appears in either issue.
-![Removing a related issue](img/related_issues_remove_v12_8.png)
+![Removing a related issue](img/related_issues_remove_v15_3.png)
Access our [permissions](../../permissions.md) page for more information.
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index 9713d3adb3b..5d78b4bb795 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -12,8 +12,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Namespace storage limit
-Namespaces on a GitLab SaaS Free tier have a 5 GB storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
-This limit is not visible on the storage quota page, but we plan to make it visible and enforced starting October 19, 2022.
+Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
+This limit is not visible on the Usage quotas page, but will be prior to [enforcement](#namespace-storage-limit-enforcement-schedule). Self-managed deployments are not affected.
Storage types that add to the total namespace storage are:
@@ -22,7 +22,7 @@ Storage types that add to the total namespace storage are:
- Artifacts
- Container registry
- Package registry
-- Dependecy proxy
+- Dependency proxy
- Wiki
- Snippets
@@ -30,30 +30,18 @@ If your total namespace storage exceeds the available namespace storage quota, a
To prevent exceeding the namespace storage quota, you can:
-1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
-1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier).
-1. [Reduce storage usage](#manage-your-storage-usage).
+1. Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page.
+1. Apply for [GitLab for Education](https://about.gitlab.com/solutions/education/join/), [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/), or [GitLab for Startups](https://about.gitlab.com/solutions/startups/) if you meet the eligibility requirements.
+1. Consider using a [self-managed instance](../subscriptions/self_managed/) of GitLab which does not have these limits on the free tier.
+1. [Purchase additional storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) units at $60/year for 10GB of storage.
+1. [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality.
+1. [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) to learn more about your options and ask questions.
### Namespace storage limit enforcement schedule
-Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces.
-We will start with a large limit enforcement and eventually reduce it to 5 GB.
+Storage limits for GitLab SaaS Free tier namespaces will not be enforced prior to 2022-10-19. Storage limits for GitLab SaaS Paid tier namespaces will not be enforced for prior to 2023-02-15.
-Impacted users are notified via email and in-app notifications will begin 2022-08-22.
-Only GitLab SaaS users are impacted - the limits are not applicable to self-managed users.
-
-The following table describes the enforcement schedule, which is subject to change.
-
-| Planned enforcement date | Limit | Status |
-| ----------------------- | ----- | ------ |
-| October 19, 2022 | 45,000 GB | Not enforced |
-| October 20, 2022 | 7,500 GB | Not enforced |
-| October 24, 2022 | 500 GB | Not enforced |
-| October 27, 2022 | 75 GB | Not enforced |
-| November 2, 2022 | 10 GB | Not enforced |
-| November 9, 2022 | 5 GB | Not enforced |
-
-Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage).
+Impacted users are notified via email and in-app notifications at least 60 days prior to enforcement.
### Project storage limit
diff --git a/lib/gitlab/event_store.rb b/lib/gitlab/event_store.rb
index 1e7f0012eaf..b45970cb45a 100644
--- a/lib/gitlab/event_store.rb
+++ b/lib/gitlab/event_store.rb
@@ -45,6 +45,7 @@ module Gitlab
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
+ store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupDeletedEvent
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent
diff --git a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
index d4e4e3d2482..c21b99ba834 100644
--- a/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/code_review_events.yml
@@ -425,3 +425,28 @@
redis_slot: code_review
category: code_review
aggregation: weekly
+## Status Checks
+- name: i_code_review_merge_request_widget_status_checks_view
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_merge_request_widget_status_checks_full_report_clicked
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_merge_request_widget_status_checks_expand
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_merge_request_widget_status_checks_expand_success
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_merge_request_widget_status_checks_expand_warning
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
+- name: i_code_review_merge_request_widget_status_checks_expand_failed
+ redis_slot: code_review
+ category: code_review
+ aggregation: weekly
diff --git a/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb b/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
index 5eb48864cfe..dafc36ab7ce 100644
--- a/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
+++ b/lib/gitlab/usage_data_counters/merge_request_widget_extension_counter.rb
@@ -5,7 +5,7 @@ module Gitlab
class MergeRequestWidgetExtensionCounter < BaseCounter
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
PREFIX = 'i_code_review_merge_request_widget'
- WIDGETS = %w[accessibility code_quality terraform test_summary metrics].freeze
+ WIDGETS = %w[accessibility code_quality status_checks terraform test_summary metrics].freeze
class << self
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 261dca88d54..a802ae8bed5 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5329,6 +5329,11 @@ msgstr ""
msgid "AuditLogs|User Events"
msgstr ""
+msgid "AuditStreams|%d destination"
+msgid_plural "AuditStreams|%d destinations"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "AuditStreams|A header with this name already exists."
msgstr ""
@@ -5344,10 +5349,16 @@ msgstr ""
msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems."
msgstr ""
+msgid "AuditStreams|Add another custom header"
+msgstr ""
+
msgid "AuditStreams|Add external stream destination"
msgstr ""
-msgid "AuditStreams|Add stream"
+msgid "AuditStreams|Add header"
+msgstr ""
+
+msgid "AuditStreams|Add streaming destination"
msgstr ""
msgid "AuditStreams|An error occurred when creating external audit event stream destination. Please try it again."
@@ -5365,7 +5376,7 @@ msgstr ""
msgid "AuditStreams|Cancel editing"
msgstr ""
-msgid "AuditStreams|Custom HTTP headers"
+msgid "AuditStreams|Custom HTTP headers (optional)"
msgstr ""
msgid "AuditStreams|Delete %{link}"
@@ -5386,6 +5397,9 @@ msgstr ""
msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached."
msgstr ""
+msgid "AuditStreams|Remove custom header"
+msgstr ""
+
msgid "AuditStreams|Save external stream destination"
msgstr ""
@@ -5395,9 +5409,6 @@ msgstr ""
msgid "AuditStreams|Stream added successfully"
msgstr ""
-msgid "AuditStreams|Stream count icon"
-msgstr ""
-
msgid "AuditStreams|Stream deleted successfully"
msgstr ""
@@ -19443,9 +19454,6 @@ msgstr ""
msgid "How do I configure Akismet?"
msgstr ""
-msgid "How do I configure runners?"
-msgstr ""
-
msgid "How do I configure this integration?"
msgstr ""
@@ -43987,6 +43995,9 @@ msgstr ""
msgid "What does this command do?"
msgstr ""
+msgid "What is GitLab Runner?"
+msgstr ""
+
msgid "What is Markdown?"
msgstr ""
diff --git a/qa/qa/page/alert/auto_devops_alert.rb b/qa/qa/page/alert/auto_devops_alert.rb
index 8f66c805b77..26801c4996c 100644
--- a/qa/qa/page/alert/auto_devops_alert.rb
+++ b/qa/qa/page/alert/auto_devops_alert.rb
@@ -5,7 +5,7 @@ module QA
module Alert
class AutoDevopsAlert < Page::Base
view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do
- element :auto_devops_banner
+ element :auto_devops_banner_content
end
end
end
diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb
index 6f0ff436439..36c0f8c2f00 100644
--- a/qa/qa/page/component/access_tokens.rb
+++ b/qa/qa/page/component/access_tokens.rb
@@ -23,6 +23,10 @@ module QA
element :create_token_button
end
+ base.view 'app/views/shared/access_tokens/_table.html.haml' do
+ element :revoke_button
+ end
+
base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
end
diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb
index f82bb81a3fc..ff61c91f0f6 100644
--- a/qa/qa/page/component/groups_filter.rb
+++ b/qa/qa/page/component/groups_filter.rb
@@ -10,7 +10,7 @@ module QA
super
base.view 'app/views/shared/groups/_search_form.html.haml' do
- element :groups_filter
+ element :groups_filter_field
end
base.view 'app/assets/javascripts/groups/components/groups.vue' do
@@ -22,7 +22,7 @@ module QA
def has_filtered_group?(name)
# Filter and submit to reload the page and only retrieve the filtered results
- find_element(:groups_filter).set(name).send_keys(:return)
+ find_element(:groups_filter_field).set(name).send_keys(:return)
# Since we submitted after filtering, the presence of
# groups_list_tree_container means we have the complete filtered list
diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb
index 4131731111f..68da89dc81d 100644
--- a/qa/qa/page/component/issuable/sidebar.rb
+++ b/qa/qa/page/component/issuable/sidebar.rb
@@ -35,7 +35,7 @@ module QA
end
base.view 'app/views/shared/issuable/_sidebar.html.haml' do
- element :assignee_block
+ element :assignee_block_container
element :milestone_block
end
@@ -127,7 +127,7 @@ module QA
private
def wait_assignees_block_finish_loading
- within_element(:assignee_block) do
+ within_element(:assignee_block_container) do
wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
finished_loading_block?
yield
diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb
index 52853376f17..644d19d6bcb 100644
--- a/qa/qa/page/dashboard/groups.rb
+++ b/qa/qa/page/dashboard/groups.rb
@@ -6,13 +6,8 @@ module QA
class Groups < Page::Base
include Page::Component::GroupsFilter
- view 'app/views/shared/groups/_search_form.html.haml' do
- element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern
- element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern
- end
-
view 'app/views/dashboard/_groups_head.html.haml' do
- element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern
+ element :new_group_button
end
def has_group?(name)
@@ -26,7 +21,7 @@ module QA
end
def click_new_group
- click_on 'New group'
+ click_element(:new_group_button)
end
end
end
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index a0b42598962..10529ed69e1 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -5,7 +5,7 @@ module QA
module Dashboard
class Projects < Page::Base
view 'app/views/shared/projects/_search_form.html.haml' do
- element :project_filter_form, required: true
+ element :project_filter_form_container, required: true
end
view 'app/views/shared/projects/_project.html.haml' do
@@ -24,7 +24,7 @@ module QA
end
def filter_by_name(name)
- within_element(:project_filter_form) do
+ within_element(:project_filter_form_container) do
fill_in :name, with: name
end
end
@@ -44,7 +44,7 @@ module QA
end
def clear_project_filter
- fill_element(:project_filter_form, "")
+ fill_element(:project_filter_form_container, "")
end
end
end
diff --git a/qa/qa/page/issuable/new.rb b/qa/qa/page/issuable/new.rb
index 0c95f722080..f3e6a84ef54 100644
--- a/qa/qa/page/issuable/new.rb
+++ b/qa/qa/page/issuable/new.rb
@@ -5,11 +5,7 @@ module QA
module Issuable
class New < Page::Base
view 'app/views/shared/issuable/form/_title.html.haml' do
- element :issuable_form_title
- end
-
- view 'app/views/shared/issuable/form/_metadata.html.haml' do
- element :issuable_milestone_dropdown
+ element :issuable_form_title_field
end
view 'app/views/shared/form_elements/_description.html.haml' do
@@ -17,11 +13,12 @@ module QA
end
view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do
- element :issuable_dropdown_menu_milestone
+ element :issuable_milestone_dropdown
+ element :issuable_milestone_dropdown_content
end
view 'app/views/shared/issuable/_label_dropdown.html.haml' do
- element :issuable_label
+ element :issuable_label_dropdown
end
view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
@@ -33,7 +30,7 @@ module QA
end
def fill_title(title)
- fill_element :issuable_form_title, title
+ fill_element :issuable_form_title_field, title
end
def fill_description(description)
@@ -42,7 +39,7 @@ module QA
def choose_milestone(milestone)
click_element :issuable_milestone_dropdown
- within_element(:issuable_dropdown_menu_milestone) do
+ within_element(:issuable_milestone_dropdown_content) do
click_on milestone.title
end
end
@@ -55,11 +52,11 @@ module QA
end
def select_label(label)
- click_element :issuable_label
+ click_element :issuable_label_dropdown
click_link label.title
- click_element :issuable_label # So that the dropdown goes away(click away action)
+ click_element :issuable_label_dropdown # So that the dropdown goes away(click away action)
end
def assign_to_me
diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb
index e73d40b37ac..e19bc0838c9 100644
--- a/qa/qa/page/label/index.rb
+++ b/qa/qa/page/label/index.rb
@@ -7,26 +7,26 @@ module QA
include Component::LazyLoader
view 'app/views/shared/labels/_nav.html.haml' do
- element :label_create_new
+ element :create_new_label_button
end
view 'app/views/shared/empty_states/_labels.html.haml' do
- element :label_svg
+ element :label_svg_content
end
view 'app/views/shared/empty_states/_priority_labels.html.haml' do
- element :label_svg
+ element :label_svg_content
end
def click_new_label_button
# The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
# before clicking the button.
- within_element(:label_svg) do
+ within_element(:label_svg_content) do
has_element?(:js_lazy_loaded)
end
- click_element :label_create_new
+ click_element :create_new_label_button
end
end
end
diff --git a/qa/qa/page/label/new.rb b/qa/qa/page/label/new.rb
index a40179489c1..12427443280 100644
--- a/qa/qa/page/label/new.rb
+++ b/qa/qa/page/label/new.rb
@@ -5,9 +5,9 @@ module QA
module Label
class New < Page::Base
view 'app/views/shared/labels/_form.html.haml' do
- element :label_title
- element :label_description
- element :label_color
+ element :label_title_field
+ element :label_description_field
+ element :label_color_field
element :label_create_button
end
@@ -16,15 +16,15 @@ module QA
end
def fill_title(title)
- fill_element :label_title, title
+ fill_element :label_title_field, title
end
def fill_description(description)
- fill_element :label_description, description
+ fill_element :label_description_field, description
end
def fill_color(color)
- fill_element :label_color, color
+ fill_element :label_color_field, color
end
end
end
diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb
index 308cf6366a7..35fc87f717c 100644
--- a/qa/qa/page/project/settings/protected_branches.rb
+++ b/qa/qa/page/project/settings/protected_branches.rb
@@ -17,10 +17,6 @@ module QA
element :allowed_to_merge_dropdown
end
- view 'app/views/shared/projects/protected_branches/_update_protected_branch.html.haml' do
- element :allowed_to_merge
- end
-
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
element :protected_branches_list
end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 022e08215be..e048afee8b3 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -67,8 +67,8 @@ module QA
end
view 'app/views/shared/_ref_switcher.html.haml' do
- element :branches_select
element :branches_dropdown
+ element :branches_dropdown_content
end
view 'app/views/projects/blob/viewers/_loading.html.haml' do
@@ -176,9 +176,9 @@ module QA
end
def switch_to_branch(branch_name)
- find_element(:branches_select).click
+ find_element(:branches_dropdown).click
- within_element(:branches_dropdown) do
+ within_element(:branches_dropdown_content) do
click_on branch_name
end
end
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
new file mode 100644
index 00000000000..174d4567520
--- /dev/null
+++ b/spec/features/admin_variables_spec.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Instance variables', :js do
+ let(:admin) { create(:admin) }
+ let(:page_path) { ci_cd_admin_application_settings_path }
+
+ let_it_be(:variable) { create(:ci_instance_variable, key: 'test_key', value: 'test_value', masked: true) }
+
+ before do
+ stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ wait_for_requests
+ end
+
+ context 'with disabled ff `ci_variable_settings_graphql' do
+ before do
+ stub_feature_flags(ci_variable_settings_graphql: false)
+ visit page_path
+ end
+
+ it_behaves_like 'variable list', isAdmin: true
+ end
+
+ context 'with enabled ff `ci_variable_settings_graphql' do
+ before do
+ visit page_path
+ end
+
+ it_behaves_like 'variable list', isAdmin: true
+ end
+end
diff --git a/spec/features/issues/user_creates_issue_spec.rb b/spec/features/issues/user_creates_issue_spec.rb
index f2be85a4d0e..e29911e3263 100644
--- a/spec/features/issues/user_creates_issue_spec.rb
+++ b/spec/features/issues/user_creates_issue_spec.rb
@@ -188,7 +188,7 @@ RSpec.describe "User creates issue" do
end
it 'does not hide the milestone select' do
- expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
end
@@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
- expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
it 'hides the incident help text' do
@@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
- expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
+ expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
it 'hides the weight input' do
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index f5cafa2b2ec..13a4c1b5912 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -137,7 +137,7 @@ RSpec.describe 'File blob', :js do
context 'when ref switch' do
def switch_ref_to(ref_name)
- first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
+ first('[data-testid="branches-select"]').click
page.within '.project-refs-form' do
click_link ref_name
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
new file mode 100644
index 00000000000..920ceaefb70
--- /dev/null
+++ b/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
@@ -0,0 +1,178 @@
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
+import { resolvers } from '~/ci_variable_list/graphql/resolvers';
+
+import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
+import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
+
+import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+
+import { genericMutationErrorText, variableFetchErrorText } from '~/ci_variable_list/constants';
+
+import { mockAdminVariables, newVariable } from '../mocks';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const mockProvide = {
+ endpoint: '/variables',
+};
+
+describe('Ci Admin Variable list', () => {
+ let wrapper;
+
+ let mockApollo;
+ let mockVariables;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findCiTable = () => wrapper.findComponent(GlTable);
+ const findCiSettings = () => wrapper.findComponent(ciVariableSettings);
+
+ // eslint-disable-next-line consistent-return
+ const createComponentWithApollo = async ({ isLoading = false } = {}) => {
+ const handlers = [[getAdminVariables, mockVariables]];
+
+ mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = shallowMount(ciAdminVariables, {
+ provide: mockProvide,
+ apolloProvider: mockApollo,
+ stubs: { ciVariableSettings, ciVariableTable },
+ });
+
+ if (!isLoading) {
+ return waitForPromises();
+ }
+ };
+
+ beforeEach(() => {
+ mockVariables = jest.fn();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('while queries are being fetch', () => {
+ beforeEach(() => {
+ createComponentWithApollo({ isLoading: true });
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCiTable().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries are resolved', () => {
+ describe('successfuly', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo();
+ });
+
+ it('passes down the expected environments as props', () => {
+ expect(findCiSettings().props('environments')).toEqual([]);
+ });
+
+ it('passes down the expected variables as props', () => {
+ expect(findCiSettings().props('variables')).toEqual(
+ mockAdminVariables.data.ciVariables.nodes,
+ );
+ });
+
+ it('createFlash was not called', () => {
+ expect(createFlash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with an error for variables', () => {
+ beforeEach(async () => {
+ mockVariables.mockRejectedValue();
+
+ await createComponentWithApollo();
+ });
+
+ it('calls createFlash with the expected error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({ message: variableFetchErrorText });
+ });
+ });
+ });
+
+ describe('mutations', () => {
+ beforeEach(async () => {
+ mockVariables.mockResolvedValue(mockAdminVariables);
+
+ await createComponentWithApollo();
+ });
+ it.each`
+ actionName | mutation | event
+ ${'add'} | ${addAdminVariable} | ${'add-variable'}
+ ${'update'} | ${updateAdminVariable} | ${'update-variable'}
+ ${'delete'} | ${deleteAdminVariable} | ${'delete-variable'}
+ `(
+ 'calls the right mutation when user performs $actionName variable',
+ async ({ event, mutation }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue();
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ endpoint: mockProvide.endpoint,
+ variable: newVariable,
+ },
+ });
+ },
+ );
+
+ it.each`
+ actionName | event | mutationName
+ ${'add'} | ${'add-variable'} | ${'addAdminVariable'}
+ ${'update'} | ${'update-variable'} | ${'updateAdminVariable'}
+ ${'delete'} | ${'delete-variable'} | ${'deleteAdminVariable'}
+ `(
+ 'throws with the specific graphql error if present when user performs $actionName variable',
+ async ({ event, mutationName }) => {
+ const graphQLErrorMessage = 'There is a problem with this graphQL action';
+ jest
+ .spyOn(wrapper.vm.$apollo, 'mutate')
+ .mockResolvedValue({ data: { [mutationName]: { errors: [graphQLErrorMessage] } } });
+ await findCiSettings().vm.$emit(event, newVariable);
+ await nextTick();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: graphQLErrorMessage });
+ },
+ );
+
+ it.each`
+ actionName | event
+ ${'add'} | ${'add-variable'}
+ ${'update'} | ${'update-variable'}
+ ${'delete'} | ${'delete-variable'}
+ `(
+ 'throws generic error when the mutation fails with no graphql errors and user performs $actionName variable',
+ async ({ event }) => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockImplementationOnce(() => {
+ throw new Error();
+ });
+ await findCiSettings().vm.$emit(event, newVariable);
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({ message: genericMutationErrorText });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index 51b902d97dc..e5019e3261e 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -10,6 +10,7 @@ import {
EVENT_LABEL,
EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE,
+ instanceString,
} from '~/ci_variable_list/constants';
import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs';
@@ -19,6 +20,7 @@ describe('Ci variable modal', () => {
let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+ const mockVariables = mockVariablesWithScopes(instanceString);
const defaultProvide = {
awsLogoSvgPath: '/logo',
@@ -38,6 +40,7 @@ describe('Ci variable modal', () => {
environments: [],
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
+ variable: [],
};
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@@ -81,22 +84,22 @@ describe('Ci variable modal', () => {
});
it('shows the submit button as disabled ', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
});
});
describe('when a key/value pair is present', () => {
beforeEach(() => {
- createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } });
+ createComponent({ props: { selectedVariable: mockVariables[0] } });
});
it('shows the submit button as enabled ', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
describe('events', () => {
- const [currentVariable] = mockVariablesWithScopes;
+ const [currentVariable] = mockVariables;
beforeEach(() => {
createComponent({ props: { selectedVariable: currentVariable } });
@@ -123,9 +126,9 @@ describe('Ci variable modal', () => {
});
it('updates the protected value to true', () => {
- expect(
- findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
- ).toBeTruthy();
+ expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
+ 'true',
+ );
});
});
@@ -151,7 +154,7 @@ describe('Ci variable modal', () => {
describe('Adding a new non-AWS variable', () => {
beforeEach(() => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
});
@@ -164,7 +167,7 @@ describe('Ci variable modal', () => {
describe('Adding a new AWS variable', () => {
beforeEach(() => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
const AWSKeyVariable = {
...variable,
key: AWS_ACCESS_KEY_ID,
@@ -183,7 +186,7 @@ describe('Ci variable modal', () => {
describe('Reference warning when adding a variable', () => {
describe('with a $ character', () => {
beforeEach(() => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
const variableWithDollarSign = {
...variable,
value: 'valueWith$',
@@ -201,7 +204,7 @@ describe('Ci variable modal', () => {
describe('without a $ character', () => {
beforeEach(() => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
createComponent({
mountFn: mountExtended,
props: { selectedVariable: variable },
@@ -215,7 +218,7 @@ describe('Ci variable modal', () => {
});
describe('Editing a variable', () => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
beforeEach(() => {
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
@@ -286,7 +289,7 @@ describe('Ci variable modal', () => {
describe('when the mask state is invalid', () => {
beforeEach(async () => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
const invalidMaskVariable = {
...variable,
value: 'd:;',
@@ -301,7 +304,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
});
it('shows the correct error text', () => {
@@ -326,7 +329,7 @@ describe('Ci variable modal', () => {
${'unsupported|char'} | ${false} | ${0} | ${null}
`('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
beforeEach(async () => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
const invalidKeyVariable = {
...variable,
value: '',
@@ -359,7 +362,7 @@ describe('Ci variable modal', () => {
describe('when masked variable has acceptable value', () => {
beforeEach(() => {
- const [variable] = mockVariablesWithScopes;
+ const [variable] = mockVariables;
const validMaskandKeyVariable = {
...variable,
key: AWS_ACCESS_KEY_ID,
@@ -373,7 +376,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
});
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
index fb79611229c..5c77ce71b41 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
@@ -3,8 +3,12 @@ import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION } from '~/ci_variable_list/constants';
-import { createJoinedEnvironments, mapEnvironmentNames } from '~/ci_variable_list/utils';
+import {
+ ADD_VARIABLE_ACTION,
+ EDIT_VARIABLE_ACTION,
+ projectString,
+} from '~/ci_variable_list/constants';
+import { mapEnvironmentNames } from '~/ci_variable_list/utils';
import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
@@ -15,7 +19,7 @@ describe('Ci variable table', () => {
areScopedVariablesAvailable: true,
environments: mapEnvironmentNames(mockEnvs),
isLoading: false,
- variables: mockVariablesWithScopes,
+ variables: mockVariablesWithScopes(projectString),
};
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
@@ -51,7 +55,8 @@ describe('Ci variable table', () => {
expect(findCiVariableModal().props()).toEqual({
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
- environments: createJoinedEnvironments(defaultProps.variables, defaultProps.environments),
+ environments: defaultProps.environments,
+ variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
index b5b4881aa44..8a4c35173ec 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,5 +1,6 @@
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import { projectString } from '~/ci_variable_list/constants';
import { mockVariables } from '../mocks';
describe('Ci variable table', () => {
@@ -7,7 +8,7 @@ describe('Ci variable table', () => {
const defaultProps = {
isLoading: false,
- variables: mockVariables,
+ variables: mockVariables(projectString),
};
const createComponent = ({ props = {} } = {}) => {
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci_variable_list/mocks.js
index 1ba50c74152..07dc7a8c91f 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci_variable_list/mocks.js
@@ -1,42 +1,45 @@
-import { variableTypes } from '~/ci_variable_list/constants';
+import { variableTypes, instanceString } from '~/ci_variable_list/constants';
export const devName = 'dev';
export const prodName = 'prod';
-export const mockVariables = [
- {
- __typename: 'CiVariable',
- id: 1,
- key: 'my-var',
- masked: false,
- protected: true,
- value: 'env_val',
- variableType: variableTypes.variableType,
- },
- {
- __typename: 'CiVariable',
- id: 2,
- key: 'secret',
- masked: true,
- protected: false,
- value: 'the_secret_value',
- variableType: variableTypes.fileType,
- },
-];
+export const mockVariables = (kind) => {
+ return [
+ {
+ __typename: `Ci${kind}Variable`,
+ id: 1,
+ key: 'my-var',
+ masked: false,
+ protected: true,
+ value: 'env_val',
+ variableType: variableTypes.variableType,
+ },
+ {
+ __typename: `Ci${kind}Variable`,
+ id: 2,
+ key: 'secret',
+ masked: true,
+ protected: false,
+ value: 'the_secret_value',
+ variableType: variableTypes.fileType,
+ },
+ ];
+};
-export const mockVariablesWithScopes = mockVariables.map((variable) => {
- return { ...variable, environmentScope: '*' };
-});
+export const mockVariablesWithScopes = (kind) =>
+ mockVariables(kind).map((variable) => {
+ return { ...variable, environmentScope: '*' };
+ });
-const createDefaultVars = ({ withScope = true } = {}) => {
- let base = mockVariables;
+const createDefaultVars = ({ withScope = true, kind } = {}) => {
+ let base = mockVariables(kind);
if (withScope) {
- base = mockVariablesWithScopes;
+ base = mockVariablesWithScopes(kind);
}
return {
- __typename: 'CiVariableConnection',
+ __typename: `Ci${kind}VariableConnection`,
nodes: base,
};
};
@@ -101,7 +104,7 @@ export const mockGroupVariables = {
export const mockAdminVariables = {
data: {
- ciVariables: createDefaultVars({ withScope: false }),
+ ciVariables: createDefaultVars({ withScope: false, kind: instanceString }),
},
};
diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci_variable_list/utils_spec.js
index 1676e786515..081c399792f 100644
--- a/spec/frontend/ci_variable_list/utils_spec.js
+++ b/spec/frontend/ci_variable_list/utils_spec.js
@@ -7,12 +7,13 @@ import { allEnvironments } from '~/ci_variable_list/constants';
describe('utils', () => {
const environments = ['dev', 'prod'];
+ const newEnvironments = ['staging'];
describe('createJoinedEnvironments', () => {
it('returns only `environments` if `variables` argument is undefined', () => {
const variables = undefined;
- expect(createJoinedEnvironments(variables, environments)).toEqual(environments);
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
});
it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
@@ -21,7 +22,7 @@ describe('utils', () => {
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
- expect(createJoinedEnvironments(variables, environments)).toEqual([
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0],
envScope1,
envScope2,
@@ -29,13 +30,22 @@ describe('utils', () => {
]);
});
+ it('returns combined list with new environments included', () => {
+ const variables = undefined;
+
+ expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
+ ...environments,
+ ...newEnvironments,
+ ]);
+ });
+
it('removes duplicate environments', () => {
const envScope1 = environments[0];
const envScope2 = 'new2';
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
- expect(createJoinedEnvironments(variables, environments)).toEqual([
+ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0],
envScope2,
environments[1],
diff --git a/spec/graphql/types/work_item_type_spec.rb b/spec/graphql/types/work_item_type_spec.rb
index 153934c374c..228f9b6ec07 100644
--- a/spec/graphql/types/work_item_type_spec.rb
+++ b/spec/graphql/types/work_item_type_spec.rb
@@ -22,6 +22,9 @@ RSpec.describe GitlabSchema.types['WorkItem'] do
userPermissions
widgets
work_item_type
+ created_at
+ updated_at
+ closed_at
]
fields.each do |field_name|
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index e94e3ef84a4..9700852e567 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -264,6 +264,8 @@ RSpec.describe Event do
let(:project) { public_project }
let(:issue) { create(:issue, project: project, author: author, assignees: [assignee]) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) }
+ let(:work_item) { create(:work_item, project: project, author: author) }
+ let(:confidential_work_item) { create(:work_item, :confidential, project: project, author: author) }
let(:project_snippet) { create(:project_snippet, :public, project: project, author: author) }
let(:personal_snippet) { create(:personal_snippet, :public, author: author) }
let(:design) { create(:design, issue: issue, project: project) }
@@ -380,6 +382,28 @@ RSpec.describe Event do
end
end
+ context 'work item event' do
+ context 'for non confidential work item' do
+ let(:target) { work_item }
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_all }
+ end
+
+ include_examples 'visible to assignee and author', true
+ end
+
+ context 'for confidential work item' do
+ let(:target) { confidential_work_item }
+
+ include_examples 'visibility examples' do
+ let(:visibility) { visible_to_none_except(:member, :admin) }
+ end
+
+ include_examples 'visible to author', true
+ end
+ end
+
context 'issue note event' do
context 'on non confidential issues' do
let(:target) { note_on_issue }
diff --git a/spec/requests/users/namespace_callouts_spec.rb b/spec/requests/users/namespace_callouts_spec.rb
new file mode 100644
index 00000000000..5a4e269eefb
--- /dev/null
+++ b/spec/requests/users/namespace_callouts_spec.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Namespace callouts' do
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'POST /-/users/namespace_callouts' do
+ let(:params) { { feature_name: feature_name, namespace_id: user.namespace.id } }
+
+ subject { post namespace_callouts_path, params: params, headers: { 'ACCEPT' => 'application/json' } }
+
+ context 'with valid feature name and group' do
+ let(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
+
+ context 'when callout entry does not exist' do
+ it 'creates a callout entry with dismissed state' do
+ expect { subject }.to change { Users::NamespaceCallout.count }.by(1)
+ end
+
+ it 'returns success' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when callout entry already exists' do
+ let!(:callout) do
+ create(:namespace_callout,
+ feature_name: Users::GroupCallout.feature_names.each_key.first,
+ user: user,
+ namespace: user.namespace)
+ end
+
+ it 'returns success', :aggregate_failures do
+ expect { subject }.not_to change { Users::NamespaceCallout.count }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+ end
+
+ context 'with invalid feature name' do
+ let(:feature_name) { 'bogus_feature_name' }
+
+ it 'returns bad request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/users/dismiss_namespace_callout_service_spec.rb b/spec/services/users/dismiss_namespace_callout_service_spec.rb
new file mode 100644
index 00000000000..fbcdb66c9e8
--- /dev/null
+++ b/spec/services/users/dismiss_namespace_callout_service_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Users::DismissNamespaceCalloutService do
+ describe '#execute' do
+ let_it_be(:user) { create(:user) }
+
+ let(:params) { { feature_name: feature_name, namespace_id: user.namespace.id } }
+ let(:feature_name) { Users::NamespaceCallout.feature_names.each_key.first }
+
+ subject(:execute) do
+ described_class.new(
+ container: nil, current_user: user, params: params
+ ).execute
+ end
+
+ it_behaves_like 'dismissing user callout', Users::NamespaceCallout
+
+ it 'sets the namespace_id' do
+ expect(execute.namespace_id).to eq(user.namespace.id)
+ end
+ end
+end
diff --git a/spec/support/shared_examples/features/variable_list_shared_examples.rb b/spec/support/shared_examples/features/variable_list_shared_examples.rb
index c63faace6b2..9d81c0e9a3e 100644
--- a/spec/support/shared_examples/features/variable_list_shared_examples.rb
+++ b/spec/support/shared_examples/features/variable_list_shared_examples.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-RSpec.shared_examples 'variable list' do
+RSpec.shared_examples 'variable list' do |is_admin|
it 'shows a list of variables' do
page.within('[data-testid="ci-variable-table"]') do
expect(find('.js-ci-variable-row:nth-child(1) td[data-label="Key"]').text).to eq(variable.key)
@@ -166,7 +166,7 @@ RSpec.shared_examples 'variable list' do
wait_for_requests
expect(find('.flash-container')).to be_present
- expect(find('[data-testid="alert-danger"]').text).to have_content('Variables key (key) has already been taken')
+ expect(find('[data-testid="alert-danger"]').text).to have_content('(key) has already been taken')
end
it 'prevents a variable to be added if no values are provided when a variable is set to masked' do
@@ -257,7 +257,11 @@ RSpec.shared_examples 'variable list' do
end
it 'shows a message regarding the changed default' do
- expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default'
+ if is_admin
+ expect(page).to have_content 'Environment variables on this GitLab instance are configured to be protected by default'
+ else
+ expect(page).to have_content 'Environment variables are configured by your administrator to be protected by default'
+ end
end
end
diff --git a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb b/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
index 75d7c9f82b4..9272e26a34f 100644
--- a/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
+++ b/spec/workers/pages/invalidate_domain_cache_worker_spec.rb
@@ -116,6 +116,16 @@ RSpec.describe Pages::InvalidateDomainCacheWorker do
{ type: :namespace, id: 2 }
]
+ it_behaves_like 'clears caches with',
+ event_class: Groups::GroupDeletedEvent,
+ event_data: {
+ group_id: 1,
+ root_namespace_id: 3
+ },
+ caches: [
+ { type: :namespace, id: 3 }
+ ]
+
context 'when namespace based cache keys are duplicated' do
# de-dups namespace cache keys
it_behaves_like 'clears caches with',