Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-16 18:12:52 +00:00
parent 7212129029
commit 8a9790b0db
106 changed files with 1018 additions and 238 deletions

View File

@ -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>

View File

@ -33,9 +33,9 @@ export default {
}, },
filteredEnvironments() { filteredEnvironments() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.environments.filter((resultString) => return this.environments.filter((environment) => {
resultString.toLowerCase().includes(lowerCasedSearchTerm), return environment.toLowerCase().includes(lowerCasedSearchTerm);
); });
}, },
shouldRenderCreateButton() { shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm); return this.searchTerm && !this.environments.includes(this.searchTerm);

View File

@ -33,7 +33,7 @@ import {
VARIABLE_ACTIONS, VARIABLE_ACTIONS,
variableOptions, variableOptions,
} from '../constants'; } from '../constants';
import { createJoinedEnvironments } from '../utils';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue'; import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens'; import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
@ -98,9 +98,15 @@ export default {
required: false, required: false,
default: () => {}, default: () => {},
}, },
variables: {
type: Array,
required: false,
default: () => [],
},
}, },
data() { data() {
return { return {
newEnvironments: [],
isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true', isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
typeOptions: variableOptions, typeOptions: variableOptions,
validationErrorEventProperty: '', validationErrorEventProperty: '',
@ -128,6 +134,9 @@ export default {
isTipVisible() { isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key); return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
}, },
joinedEnvironments() {
return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
},
maskedFeedback() { maskedFeedback() {
return this.displayMaskedError ? __('This variable can not be masked.') : ''; return this.displayMaskedError ? __('This variable can not be masked.') : '';
}, },
@ -176,7 +185,7 @@ export default {
this.$emit('add-variable', this.variable); this.$emit('add-variable', this.variable);
}, },
createEnvironmentScope(env) { createEnvironmentScope(env) {
this.$emit('create-environment-scope', env); this.newEnvironments.push(env);
}, },
deleteVariable() { deleteVariable() {
this.$emit('delete-variable', this.variable); this.$emit('delete-variable', this.variable);
@ -314,7 +323,7 @@ export default {
v-if="areScopedVariablesAvailable" v-if="areScopedVariablesAvailable"
class="gl-w-full" class="gl-w-full"
:selected-environment-scope="variable.environmentScope" :selected-environment-scope="variable.environmentScope"
:environments="environments" :environments="joinedEnvironments"
@select-environment="setEnvironmentScope" @select-environment="setEnvironmentScope"
@create-environment-scope="createEnvironmentScope" @create-environment-scope="createEnvironmentScope"
/> />

View File

@ -1,6 +1,5 @@
<script> <script>
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants'; import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import { createJoinedEnvironments } from '../utils';
import CiVariableTable from './ci_variable_table.vue'; import CiVariableTable from './ci_variable_table.vue';
import CiVariableModal from './ci_variable_modal.vue'; import CiVariableModal from './ci_variable_modal.vue';
@ -17,7 +16,8 @@ export default {
}, },
environments: { environments: {
type: Array, type: Array,
required: true, required: false,
default: () => [],
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
@ -36,9 +36,6 @@ export default {
}; };
}, },
computed: { computed: {
joinedEnvironments() {
return createJoinedEnvironments(this.variables, this.environments);
},
showModal() { showModal() {
return VARIABLE_ACTIONS.includes(this.mode); return VARIABLE_ACTIONS.includes(this.mode);
}, },
@ -80,7 +77,8 @@ export default {
<ci-variable-modal <ci-variable-modal
v-if="showModal" v-if="showModal"
:are-scoped-variables-available="areScopedVariablesAvailable" :are-scoped-variables-available="areScopedVariablesAvailable"
:environments="joinedEnvironments" :environments="environments"
:variables="variables"
:mode="mode" :mode="mode"
:selected-variable="selectedVariable" :selected-variable="selectedVariable"
@add-variable="addVariable" @add-variable="addVariable"

View File

@ -47,6 +47,13 @@ export const defaultVariableState = {
variableType: types.variableType, 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_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
export const AWS_TIP_MESSAGE = __( 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}.', '%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',

View File

@ -0,0 +1,7 @@
fragment BaseCiVariable on CiVariable {
__typename
id
key
value
variableType
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -0,0 +1,13 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
query getVariables {
ciVariables {
nodes {
...BaseCiVariable
... on CiInstanceVariable {
masked
protected
}
}
}
}

View File

@ -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 });
},
},
};

View File

@ -2,8 +2,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; 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 LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import { resolvers } from './graphql/resolvers';
import createStore from './store'; import createStore from './store';
const mountCiVariableListApp = (containerEl) => { const mountCiVariableListApp = (containerEl) => {
@ -13,8 +14,12 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink, awsTipDeployLink,
awsTipLearnLink, awsTipLearnLink,
containsVariableReferenceLink, containsVariableReferenceLink,
endpoint,
environmentScopeLink, environmentScopeLink,
group, groupId,
groupPath,
isGroup,
isProject,
maskedEnvironmentVariablesLink, maskedEnvironmentVariablesLink,
maskableRegex, maskableRegex,
projectFullPath, projectFullPath,
@ -23,13 +28,16 @@ const mountCiVariableListApp = (containerEl) => {
protectedEnvironmentVariablesLink, protectedEnvironmentVariablesLink,
} = containerEl.dataset; } = containerEl.dataset;
const isGroup = parseBoolean(group); const parsedIsProject = parseBoolean(isProject);
const parsedIsGroup = parseBoolean(isGroup);
const isProtectedByDefault = parseBoolean(protectedByDefault); const isProtectedByDefault = parseBoolean(protectedByDefault);
const component = CiAdminVariables;
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(), defaultClient: createDefaultClient(resolvers),
}); });
return new Vue({ return new Vue({
@ -41,8 +49,12 @@ const mountCiVariableListApp = (containerEl) => {
awsTipDeployLink, awsTipDeployLink,
awsTipLearnLink, awsTipLearnLink,
containsVariableReferenceLink, containsVariableReferenceLink,
endpoint,
environmentScopeLink, environmentScopeLink,
isGroup, groupId,
groupPath,
isGroup: parsedIsGroup,
isProject: parsedIsProject,
isProtectedByDefault, isProtectedByDefault,
maskedEnvironmentVariablesLink, maskedEnvironmentVariablesLink,
maskableRegex, maskableRegex,
@ -51,7 +63,7 @@ const mountCiVariableListApp = (containerEl) => {
protectedEnvironmentVariablesLink, protectedEnvironmentVariablesLink,
}, },
render(createElement) { render(createElement) {
return createElement(CiVariableSettings); return createElement(component);
}, },
}); });
}; };

View File

@ -2,20 +2,25 @@ import { uniq } from 'lodash';
import { allEnvironments } from './constants'; 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 * and create a new Array that concatenate the environment list
* with the environment scopes find in the variable list. This is * with the environment scopes find in the variable list. This is
* useful for variable settings so that we can render a list of all * 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 * environment scopes available based on the list of envs, the ones the user
* is found under each variable. * added explictly and what is found under each variable.
* @param {Array} variables * @param {Array} variables
* @param {Array} environments * @param {Array} environments
* @returns {Array} - Array of environments * @returns {Array} - Array of environments
*/ */
export const createJoinedEnvironments = (variables = [], environments = []) => { export const createJoinedEnvironments = (
variables = [],
environments = [],
newEnvironments = [],
) => {
const scopesFromVariables = variables.map((variable) => variable.environmentScope); const scopesFromVariables = variables.map((variable) => variable.environmentScope);
return uniq(environments.concat(scopesFromVariables)).sort(); return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
}; };
/** /**

View File

@ -7,11 +7,12 @@ const DEFERRED_LINK_CLASS = 'deferred-link';
export default class PersistentUserCallout { export default class PersistentUserCallout {
constructor(container, options = container.dataset) { constructor(container, options = container.dataset) {
const { dismissEndpoint, featureId, groupId, deferLinks } = options; const { dismissEndpoint, featureId, groupId, namespaceId, deferLinks } = options;
this.container = container; this.container = container;
this.dismissEndpoint = dismissEndpoint; this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId; this.featureId = featureId;
this.groupId = groupId; this.groupId = groupId;
this.namespaceId = namespaceId;
this.deferLinks = parseBoolean(deferLinks); this.deferLinks = parseBoolean(deferLinks);
this.closeButtons = this.container.querySelectorAll('.js-close'); this.closeButtons = this.container.querySelectorAll('.js-close');
@ -56,6 +57,7 @@ export default class PersistentUserCallout {
.post(this.dismissEndpoint, { .post(this.dismissEndpoint, {
feature_name: this.featureId, feature_name: this.featureId,
group_id: this.groupId, group_id: this.groupId,
namespace_id: this.namespaceId,
}) })
.then(() => { .then(() => {
this.container.remove(); this.container.remove();

View File

@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :disable_query_limiting, only: [:usage_data] 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 feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
:general, :reporting, :metrics_and_profiling, :network, :general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token :preferences, :update, :reset_health_check_token

View File

@ -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

View File

@ -6,8 +6,12 @@ module Types
authorize :read_work_item 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, field :confidential, GraphQL::Types::Boolean, null: false,
description: 'Indicates the work item is confidential.' 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, field :description, GraphQL::Types::String, null: true,
description: 'Description of the work item.' description: 'Description of the work item.'
field :id, Types::GlobalIDType[::WorkItem], null: false, field :id, Types::GlobalIDType[::WorkItem], null: false,
@ -22,6 +26,8 @@ module Types
description: 'State of the work item.' description: 'State of the work item.'
field :title, GraphQL::Types::String, null: false, field :title, GraphQL::Types::String, null: false,
description: 'Title of the work item.' 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, field :widgets,
[Types::WorkItems::WidgetInterface], [Types::WorkItems::WidgetInterface],
null: true, null: true,

View File

@ -216,6 +216,10 @@ class Event < ApplicationRecord
target_type == 'DesignManagement::Design' target_type == 'DesignManagement::Design'
end end
def work_item?
target_type == 'WorkItem'
end
def milestone def milestone
target if milestone? target if milestone?
end end
@ -399,7 +403,8 @@ class Event < ApplicationRecord
read_milestone: %i[milestone?], read_milestone: %i[milestone?],
read_wiki: %i[wiki_page?], read_wiki: %i[wiki_page?],
read_design: %i[design_note? design?], read_design: %i[design_note? design?],
read_note: %i[note?] read_note: %i[note?],
read_work_item: %i[work_item?]
} }
end end

View File

@ -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

View File

@ -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 } = 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_group = !@group.nil?
- is_project = !@project.nil?
#js-ci-project-variables{ data: { endpoint: save_endpoint, #js-ci-project-variables{ data: { endpoint: save_endpoint,
is_project: is_project.to_s,
project_id: @project&.id || '', 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, maskable_regex: ci_variable_maskable_regex,
protected_by_default: ci_variable_protected_by_default?.to_s, protected_by_default: ci_variable_protected_by_default?.to_s,
aws_logo_svg_path: image_path('aws_logo.svg'), aws_logo_svg_path: image_path('aws_logo.svg'),

View File

@ -3,7 +3,7 @@
- if current_user.can_create_group? - if current_user.can_create_group?
.page-title-controls .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 .top-area
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do

View File

@ -4,7 +4,7 @@
.devise-errors .devise-errors
= render "devise/shared/error_messages", resource: resource = render "devise/shared/error_messages", resource: resource
.form-group.gl-px-5.gl-pt-5 .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.') = 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 .form-text.text-muted
= _('Requires your primary GitLab email address.') = _('Requires your primary GitLab email address.')

View File

@ -32,7 +32,7 @@
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") = _("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 .settings-content
= render 'groups/runners/settings' = render 'groups/runners/settings'

View File

@ -41,7 +41,7 @@
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
%p %p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") = _("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 .settings-content
= render 'projects/runners/settings' = render 'projects/runners/settings'

View File

@ -1,5 +1,5 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user) - 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', close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
data: { project_id: project.id }}) do |c| data: { project_id: project.id }}) do |c|
= c.body do = c.body do

View File

@ -5,7 +5,7 @@
%span.js-clone-dropdown-label %span.js-clone-dropdown-label
= enabled_protocol_button(container, enabled_protocol) = enabled_protocol_button(container, enabled_protocol)
- else - 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 %span.js-clone-dropdown-label
= default_clone_protocol.upcase = default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon') = sprite_icon('chevron-down', css_class: 'gl-icon')

View File

@ -1,6 +1,6 @@
- if any_projects?(@projects) - if any_projects?(@projects)
.dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' } .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') = 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] = 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') }

View File

@ -13,8 +13,8 @@
- @options && @options.each do |key, value| - @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil = hidden_field_tag key, value, id: nil
.dropdown .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_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.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) } .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-page-one
= dropdown_title _("Switch branch/tag") = dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags") = dropdown_filter _("Search branches and tags")

View File

@ -3,5 +3,5 @@
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } }, button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
icon_classes: 'spin') icon_classes: 'spin')
- elsif remote_mirror.enabled? - 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") = sprite_icon("retry")

View File

@ -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') = sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
%span.collapse-text.gl-ml-3= _("Collapse sidebar") %span.collapse-text.gl-ml-3= _("Collapse sidebar")

View File

@ -45,7 +45,7 @@
%span.token-never-expires-label= _('Never') %span.token-never-expires-label= _('Never')
- if resource - if resource
%td= resource.member(token.user).human_access %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 - else
.settings-message.text-center .settings-message.text-center
= no_active_tokens_message = no_active_tokens_message

View File

@ -1,6 +1,6 @@
.row.empty-state.labels .row.empty-state.labels
.col-12 .col-12
.svg-content.qa-label-svg .svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/labels.svg' = image_tag 'illustrations/labels.svg'
.col-12 .col-12
.text-content .text-content
@ -8,7 +8,7 @@
%p= _("You can also star a label to make it a priority label.") %p= _("You can also star a label to make it a priority label.")
.text-center .text-center
- if can?(current_user, :admin_label, @project) - 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' = 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) - 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' = link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'

View File

@ -1,5 +1,5 @@
.text-center .text-center
.svg-content.qa-label-svg .svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/priority_labels.svg' = image_tag 'illustrations/priority_labels.svg'
- if can?(current_user, :admin_label, @project) - if can?(current_user, :admin_label, @project)
%p %p

View File

@ -1,7 +1,7 @@
.row.empty-state .row.empty-state
.col-12 .col-12
.svg-content .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! .text-content.gl-text-center.gl-pt-0!
%h4= _('There are no topics to show.') %h4= _('There are no topics to show.')
%p= _('Add topics to projects to help users find them.') %p= _('Add topics to projects to help users find them.')

View File

@ -3,7 +3,7 @@
- if can?(current_user, :create_wiki, @wiki.container) - if can?(current_user, :create_wiki, @wiki.container)
- create_path = wiki_page_path(@wiki, params[:id], view: 'create') - 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 = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left %h4.text-left

View File

@ -1,6 +1,6 @@
.row.empty-state.empty-state-wiki .row.empty-state.empty-state-wiki
.col-12 .col-12
.svg-content.qa-svg-content .svg-content{ data: { qa_selector: 'svg_content' } }
= image_tag image_path = image_tag image_path
.col-12 .col-12
.text-content.text-center .text-content.text-center

View File

@ -1,2 +1,2 @@
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f| = 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'

View File

@ -11,7 +11,7 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label')) - 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 = 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')) - label_name = local_assigns.fetch(:label_name, _('Labels'))
- no_default_styles = local_assigns.fetch(:no_default_styles, false) - no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options - 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 = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown .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 - apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) } %span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name) = multi_label_name(selected, label_name)

View File

@ -7,8 +7,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone')) - dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone'))
- if selected.present? || params[:milestone_title].present? - if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id) = 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", = 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') } }) do 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 - if project
%ul.dropdown-footer-list %ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project - if can? current_user, :admin_milestone, project

View File

@ -26,14 +26,14 @@
= _('To-Do') = _('To-Do')
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } } .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 = render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity] - if issuable_sidebar[:supports_severity]
#js-severity #js-severity
- if reviewers - if reviewers
.block.reviewer.qa-reviewer-block .block.reviewer
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in = render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation] - if issuable_sidebar[:supports_escalation]

View File

@ -35,7 +35,7 @@
= form.label :milestone_id, _('Milestone'), class: "col-12" = form.label :milestone_id, _('Milestone'), class: "col-12"
.col-12 .col-12
.issuable-form-select-holder .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-group.row
= form.label :label_ids, _('Labels'), class: "col-12" = form.label :label_ids, _('Labels'), class: "col-12"

View File

@ -8,4 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' } = 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)) = 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' }

View File

@ -9,7 +9,7 @@
%div{ data: { testid: 'issue-title-input-field' } } %div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true, = 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?) - if issuable.respond_to?(:draft?)
.form-text.text-muted .form-text.text-muted

View File

@ -4,20 +4,20 @@
.form-group.row .form-group.row
.col-12 .col-12
= f.label :title = 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' = render_if_exists 'shared/labels/create_label_help_text'
.form-group.row .form-group.row
.col-12 .col-12
= f.label :description = 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 .form-group.row
.col-12 .col-12
= f.label :color, _("Background color") = f.label :color, _("Background color")
.input-group .input-group
.input-group-prepend .input-group-prepend
.input-group-text.label-color-preview &nbsp; .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 .form-text.text-muted
= _('Choose any color.') = _('Choose any color.')
%br %br
@ -28,7 +28,7 @@
- if @label.persisted? - if @label.persisted?
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2' = f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
- else - 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' = link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
- if @label.persisted? - if @label.persisted?
- presented_label = @label.present - presented_label = @label.present

View File

@ -14,8 +14,8 @@
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') }) = render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
= render 'shared/labels/sort_dropdown' = render 'shared/labels/sort_dropdown'
- if labels_or_filters && can_admin_label && @project - 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') = _('New label')
- if labels_or_filters && can_admin_label && @group - 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') = _('New label')

View File

@ -1,7 +1,7 @@
- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : '' - 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...' - 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], = search_field_tag :name, params[:name],
placeholder: placeholder, placeholder: placeholder,
class: "project-filter-form-field form-control #{form_field_classes}", class: "project-filter-form-field form-control #{form_field_classes}",

View File

@ -10,7 +10,7 @@
%td.merge_access_levels-container %td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level = hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
= dropdown_tag( (merge_access_levels.first&.humanize || 'Select') , = 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) }}) data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
- if user_merge_access_levels.any? - if user_merge_access_levels.any?
%p.small %p.small

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370795
milestone: '15.3' milestone: '15.3'
type: development type: development
group: group::source code group: group::source code
default_enabled: false default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367845
milestone: '15.2' milestone: '15.2'
type: ops type: ops
group: group::memory group: group::memory
default_enabled: false default_enabled: true

View File

@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370708
milestone: '15.3' milestone: '15.3'
type: ops type: ops
group: group::gitaly group: group::gitaly
default_enabled: false default_enabled: true

View File

@ -110,6 +110,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success' - '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_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed' - '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 - name: code_review_category_monthly_active_users
operator: OR operator: OR
source: redis source: redis
@ -208,6 +214,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success' - '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_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed' - '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 - name: code_review_extension_category_monthly_active_users
operator: OR operator: OR
source: redis source: redis

View File

@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
get 'alert_management/:id', to: 'alert_management#details', as: 'alert_management_alert' 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_items
get 'work_items/*work_items_path' => 'work_items#index', as: :work_item
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create' post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'

View File

@ -64,6 +64,7 @@ scope '-/users', module: :users do
end end
resources :callouts, only: [:create] resources :callouts, only: [:create]
resources :namespace_callouts, only: [:create]
resources :group_callouts, only: [:create] resources :group_callouts, only: [:create]
resources :project_callouts, only: [:create] resources :project_callouts, only: [:create]
end end

View File

@ -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 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 [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. > - 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 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. 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 top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Security & Compliance > Audit events**. 1. On the left sidebar, select **Security & Compliance > Audit events**.
1. On the main area, select **Streams** tab. 1. On the main area, select **Streams** tab.
- When the destination list is empty, select **Add stream** to show the section for adding destinations. 1. Select **Add streaming destination** 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. Enter the destination URL to add. 1. Enter the destination URL to add.
1. Optional. Locate the **Custom HTTP headers** table. 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 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). [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. 20 headers per streaming destination.
1. After all headers have been filled out, select **Add** to add the new 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. 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 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). [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. 20 headers per streaming destination.
1. Select **Save** to update the streaming destination. 1. Select **Save** to update the streaming destination.

View File

@ -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, - GitLab Rails tries to read the metadata file directly. If it exists, and if the UUID's match,
assume we have direct access. assume we have direct access.
Direct Git access is enable by default in Omnibus GitLab because it fills in the correct repository Versions of GitLab 15.3 and later disable direct Git access by default.
paths in the GitLab configuration file `config/gitlab.yml`. This satisfies the UUID check.
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 ### Transition to Gitaly Cluster

View File

@ -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 [direct Git access](gitaly/index.md#direct-access-to-git-in-gitlab) using
[Rugged](https://github.com/libgit2/rugged). [Rugged](https://github.com/libgit2/rugged).
From GitLab 12.1, GitLab automatically detects if Rugged can and should be used per storage. Versions of GitLab after 12.2 and prior to 15.3 automatically detect if
If you previously enabled Rugged using the feature flag and you want to use automatic detection instead, Rugged can and should be used per storage.
you must unset the feature flag:
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 ```shell
sudo gitlab-rake gitlab:features:unset_rugged sudo gitlab-rake gitlab:features:unset_rugged

View File

@ -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>>] => [#<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 ## Imports and exports
### Import a project ### Import a project

View File

@ -18856,7 +18856,9 @@ Represents vulnerability letter grades with associated projects.
| Name | Type | Description | | 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="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="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="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. | | <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="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. |
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title 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="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="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="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. | | <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |

View File

@ -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, or the user does not have [permission](../../user/permissions.md) to create a pipeline there,
the `staging` job is marked as _failed_. 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). 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: 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) - [`extends`](../yaml/index.md#extends)
- [`needs`](../yaml/index.md#needs), but not [`needs:project`](../yaml/index.md#needsproject) - [`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 #### Specify a downstream pipeline branch
You can specify a branch name for the downstream pipeline to use. You can specify a branch name for the downstream pipeline to use.
@ -182,9 +184,12 @@ downstream-job:
trigger: my/project trigger: my/project
``` ```
In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the In this scenario, the `UPSTREAM_BRANCH` variable with the value of the upstream pipeline's
upstream pipeline is passed to the `downstream-job` job. It is available `$CI_COMMIT_REF_NAME` is passed to `downstream-job`. It is available in the
in the context of all downstream builds. 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 Upstream pipelines take precedence over downstream ones. If there are two
variables with the same name defined in both upstream and downstream projects, variables with the same name defined in both upstream and downstream projects,

View File

@ -722,6 +722,9 @@ variables:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28940) in GitLab Runner 15.1. > [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`. 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 ### Attestation format

View File

@ -132,10 +132,17 @@ These restrictions exist because `after_script` scripts are executed in a
## Persisted variables ## Persisted variables
The following variables are known as "persisted": Some predefined variables are called "persisted".
Pipeline-level persisted variables:
- `CI_PIPELINE_ID` - `CI_PIPELINE_ID`
- `CI_PIPELINE_URL`
Job-level persisted variables:
- `CI_JOB_ID` - `CI_JOB_ID`
- `CI_JOB_URL`
- `CI_JOB_TOKEN` - `CI_JOB_TOKEN`
- `CI_JOB_STARTED_AT` - `CI_JOB_STARTED_AT`
- `CI_REGISTRY_USER` - `CI_REGISTRY_USER`
@ -144,7 +151,7 @@ The following variables are known as "persisted":
- `CI_DEPLOY_USER` - `CI_DEPLOY_USER`
- `CI_DEPLOY_PASSWORD` - `CI_DEPLOY_PASSWORD`
They are: Persisted variables are:
- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is: - Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
- Runner. - Runner.
@ -153,6 +160,9 @@ They are:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab. - 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). - 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 Some of the persisted variables contain tokens and cannot be used by some definitions
due to security reasons. due to security reasons.

View File

@ -3933,6 +3933,8 @@ trigger_job:
and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule) and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule)
are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward) are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward)
to forward these variables to downstream pipelines. 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**: **Related topics**:

View File

@ -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: be done within a transactional migration, then the migration would look like this:
```ruby ```ruby
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[1.0] class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.0]
disable_ddl_transaction! disable_ddl_transaction!
def up def up
@ -158,7 +158,7 @@ renaming. For example
```ruby ```ruby
# A regular migration in db/migrate # A regular migration in db/migrate
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[1.0] class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.0]
disable_ddl_transaction! disable_ddl_transaction!
def up def up
@ -186,7 +186,7 @@ We can perform this cleanup using
```ruby ```ruby
# A post-deployment migration in db/post_migrate # 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! disable_ddl_transaction!
def up def up
@ -233,7 +233,7 @@ as follows:
```ruby ```ruby
# A regular migration in db/migrate # A regular migration in db/migrate
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[1.0] class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.0]
disable_ddl_transaction! disable_ddl_transaction!
def up def up
@ -252,7 +252,7 @@ Next we need to clean up our changes using a post-deployment migration:
```ruby ```ruby
# A post-deployment migration in db/post_migrate # 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! disable_ddl_transaction!
def up def up

View File

@ -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 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>`. 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>`.

View File

@ -164,6 +164,13 @@ Also, do not use links as part of heading text.
See also [heading guidelines for specific topic types](../structure.md). See also [heading guidelines for specific topic types](../structure.md).
### Backticks in Markdown
Use backticks for:
- [Code blocks](#code-blocks).
- Error messages.
### Markdown Rules ### Markdown Rules
GitLab ensures that the Markdown used across all documentation is consistent, as GitLab ensures that the Markdown used across all documentation is consistent, as

View File

@ -280,7 +280,7 @@ You can use Vale:
Vale returns three types of results: 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. the docs site to render incorrectly.
- **Warning** - For Technical Writing team style preferences. - **Warning** - For Technical Writing team style preferences.
- **Suggestion** - For basic technical writing tenets and best practices. - **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 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). 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 ### Install linters
At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in

View File

@ -25,7 +25,7 @@ To ensure access to your cluster is safe:
- Each agent has a separate context (`kubecontext`). - 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. - 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 ## GitLab CI/CD workflow steps

View File

@ -642,3 +642,20 @@ A few things to remember:
- For performance and visibility reasons, each list shows the first 20 issues - 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 by default. If you have more than 20 issues, start scrolling down and the next
20 appear. 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`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -37,7 +37,7 @@ To link one issue to another:
- **[is blocked by](#blocking-issues)** - **[is blocked by](#blocking-issues)**
1. Input the issue number or paste in the full URL of the issue. 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 of the same project can be specified just by the reference number.
Issues from a different project require additional information like the 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 When you have finished adding all linked issues, you can see
them categorized so their relationships can be better understood visually. 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. 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). [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. 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. Access our [permissions](../../permissions.md) page for more information.

View File

@ -12,8 +12,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Namespace storage limit ## 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/). 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 storage quota page, but we plan to make it visible and enforced starting October 19, 2022. 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: 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 - Artifacts
- Container registry - Container registry
- Package registry - Package registry
- Dependecy proxy - Dependency proxy
- Wiki - Wiki
- Snippets - 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: To prevent exceeding the namespace storage quota, you can:
1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer). 1. Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page.
1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier). 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. [Reduce storage usage](#manage-your-storage-usage). 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 ### Namespace storage limit enforcement schedule
Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces. 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.
We will start with a large limit enforcement and eventually reduce it to 5 GB.
Impacted users are notified via email and in-app notifications will begin 2022-08-22. Impacted users are notified via email and in-app notifications at least 60 days prior to enforcement.
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).
### Project storage limit ### Project storage limit

View File

@ -45,6 +45,7 @@ module Gitlab
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupDeletedEvent
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent

View File

@ -425,3 +425,28 @@
redis_slot: code_review redis_slot: code_review
category: code_review category: code_review
aggregation: weekly 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

View File

@ -5,7 +5,7 @@ module Gitlab
class MergeRequestWidgetExtensionCounter < BaseCounter class MergeRequestWidgetExtensionCounter < BaseCounter
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
PREFIX = 'i_code_review_merge_request_widget' 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 class << self
private private

View File

@ -5329,6 +5329,11 @@ msgstr ""
msgid "AuditLogs|User Events" msgid "AuditLogs|User Events"
msgstr "" msgstr ""
msgid "AuditStreams|%d destination"
msgid_plural "AuditStreams|%d destinations"
msgstr[0] ""
msgstr[1] ""
msgid "AuditStreams|A header with this name already exists." msgid "AuditStreams|A header with this name already exists."
msgstr "" msgstr ""
@ -5344,10 +5349,16 @@ msgstr ""
msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems." msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems."
msgstr "" msgstr ""
msgid "AuditStreams|Add another custom header"
msgstr ""
msgid "AuditStreams|Add external stream destination" msgid "AuditStreams|Add external stream destination"
msgstr "" msgstr ""
msgid "AuditStreams|Add stream" msgid "AuditStreams|Add header"
msgstr ""
msgid "AuditStreams|Add streaming destination"
msgstr "" msgstr ""
msgid "AuditStreams|An error occurred when creating external audit event stream destination. Please try it again." 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" msgid "AuditStreams|Cancel editing"
msgstr "" msgstr ""
msgid "AuditStreams|Custom HTTP headers" msgid "AuditStreams|Custom HTTP headers (optional)"
msgstr "" msgstr ""
msgid "AuditStreams|Delete %{link}" msgid "AuditStreams|Delete %{link}"
@ -5386,6 +5397,9 @@ msgstr ""
msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached." msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached."
msgstr "" msgstr ""
msgid "AuditStreams|Remove custom header"
msgstr ""
msgid "AuditStreams|Save external stream destination" msgid "AuditStreams|Save external stream destination"
msgstr "" msgstr ""
@ -5395,9 +5409,6 @@ msgstr ""
msgid "AuditStreams|Stream added successfully" msgid "AuditStreams|Stream added successfully"
msgstr "" msgstr ""
msgid "AuditStreams|Stream count icon"
msgstr ""
msgid "AuditStreams|Stream deleted successfully" msgid "AuditStreams|Stream deleted successfully"
msgstr "" msgstr ""
@ -19443,9 +19454,6 @@ msgstr ""
msgid "How do I configure Akismet?" msgid "How do I configure Akismet?"
msgstr "" msgstr ""
msgid "How do I configure runners?"
msgstr ""
msgid "How do I configure this integration?" msgid "How do I configure this integration?"
msgstr "" msgstr ""
@ -43987,6 +43995,9 @@ msgstr ""
msgid "What does this command do?" msgid "What does this command do?"
msgstr "" msgstr ""
msgid "What is GitLab Runner?"
msgstr ""
msgid "What is Markdown?" msgid "What is Markdown?"
msgstr "" msgstr ""

View File

@ -5,7 +5,7 @@ module QA
module Alert module Alert
class AutoDevopsAlert < Page::Base class AutoDevopsAlert < Page::Base
view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do
element :auto_devops_banner element :auto_devops_banner_content
end end
end end
end end

View File

@ -23,6 +23,10 @@ module QA
element :create_token_button element :create_token_button
end 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 base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
end end

View File

@ -10,7 +10,7 @@ module QA
super super
base.view 'app/views/shared/groups/_search_form.html.haml' do base.view 'app/views/shared/groups/_search_form.html.haml' do
element :groups_filter element :groups_filter_field
end end
base.view 'app/assets/javascripts/groups/components/groups.vue' do base.view 'app/assets/javascripts/groups/components/groups.vue' do
@ -22,7 +22,7 @@ module QA
def has_filtered_group?(name) def has_filtered_group?(name)
# Filter and submit to reload the page and only retrieve the filtered results # 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 # Since we submitted after filtering, the presence of
# groups_list_tree_container means we have the complete filtered list # groups_list_tree_container means we have the complete filtered list

View File

@ -35,7 +35,7 @@ module QA
end end
base.view 'app/views/shared/issuable/_sidebar.html.haml' do base.view 'app/views/shared/issuable/_sidebar.html.haml' do
element :assignee_block element :assignee_block_container
element :milestone_block element :milestone_block
end end
@ -127,7 +127,7 @@ module QA
private private
def wait_assignees_block_finish_loading 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 wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
finished_loading_block? finished_loading_block?
yield yield

View File

@ -6,13 +6,8 @@ module QA
class Groups < Page::Base class Groups < Page::Base
include Page::Component::GroupsFilter 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 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 end
def has_group?(name) def has_group?(name)
@ -26,7 +21,7 @@ module QA
end end
def click_new_group def click_new_group
click_on 'New group' click_element(:new_group_button)
end end
end end
end end

View File

@ -5,7 +5,7 @@ module QA
module Dashboard module Dashboard
class Projects < Page::Base class Projects < Page::Base
view 'app/views/shared/projects/_search_form.html.haml' do view 'app/views/shared/projects/_search_form.html.haml' do
element :project_filter_form, required: true element :project_filter_form_container, required: true
end end
view 'app/views/shared/projects/_project.html.haml' do view 'app/views/shared/projects/_project.html.haml' do
@ -24,7 +24,7 @@ module QA
end end
def filter_by_name(name) def filter_by_name(name)
within_element(:project_filter_form) do within_element(:project_filter_form_container) do
fill_in :name, with: name fill_in :name, with: name
end end
end end
@ -44,7 +44,7 @@ module QA
end end
def clear_project_filter def clear_project_filter
fill_element(:project_filter_form, "") fill_element(:project_filter_form_container, "")
end end
end end
end end

View File

@ -5,11 +5,7 @@ module QA
module Issuable module Issuable
class New < Page::Base class New < Page::Base
view 'app/views/shared/issuable/form/_title.html.haml' do view 'app/views/shared/issuable/form/_title.html.haml' do
element :issuable_form_title element :issuable_form_title_field
end
view 'app/views/shared/issuable/form/_metadata.html.haml' do
element :issuable_milestone_dropdown
end end
view 'app/views/shared/form_elements/_description.html.haml' do view 'app/views/shared/form_elements/_description.html.haml' do
@ -17,11 +13,12 @@ module QA
end end
view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do 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 end
view 'app/views/shared/issuable/_label_dropdown.html.haml' do view 'app/views/shared/issuable/_label_dropdown.html.haml' do
element :issuable_label element :issuable_label_dropdown
end end
view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
@ -33,7 +30,7 @@ module QA
end end
def fill_title(title) def fill_title(title)
fill_element :issuable_form_title, title fill_element :issuable_form_title_field, title
end end
def fill_description(description) def fill_description(description)
@ -42,7 +39,7 @@ module QA
def choose_milestone(milestone) def choose_milestone(milestone)
click_element :issuable_milestone_dropdown click_element :issuable_milestone_dropdown
within_element(:issuable_dropdown_menu_milestone) do within_element(:issuable_milestone_dropdown_content) do
click_on milestone.title click_on milestone.title
end end
end end
@ -55,11 +52,11 @@ module QA
end end
def select_label(label) def select_label(label)
click_element :issuable_label click_element :issuable_label_dropdown
click_link label.title 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 end
def assign_to_me def assign_to_me

View File

@ -7,26 +7,26 @@ module QA
include Component::LazyLoader include Component::LazyLoader
view 'app/views/shared/labels/_nav.html.haml' do view 'app/views/shared/labels/_nav.html.haml' do
element :label_create_new element :create_new_label_button
end end
view 'app/views/shared/empty_states/_labels.html.haml' do view 'app/views/shared/empty_states/_labels.html.haml' do
element :label_svg element :label_svg_content
end end
view 'app/views/shared/empty_states/_priority_labels.html.haml' do view 'app/views/shared/empty_states/_priority_labels.html.haml' do
element :label_svg element :label_svg_content
end end
def click_new_label_button 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 # 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?) # This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
# before clicking the button. # before clicking the button.
within_element(:label_svg) do within_element(:label_svg_content) do
has_element?(:js_lazy_loaded) has_element?(:js_lazy_loaded)
end end
click_element :label_create_new click_element :create_new_label_button
end end
end end
end end

View File

@ -5,9 +5,9 @@ module QA
module Label module Label
class New < Page::Base class New < Page::Base
view 'app/views/shared/labels/_form.html.haml' do view 'app/views/shared/labels/_form.html.haml' do
element :label_title element :label_title_field
element :label_description element :label_description_field
element :label_color element :label_color_field
element :label_create_button element :label_create_button
end end
@ -16,15 +16,15 @@ module QA
end end
def fill_title(title) def fill_title(title)
fill_element :label_title, title fill_element :label_title_field, title
end end
def fill_description(description) def fill_description(description)
fill_element :label_description, description fill_element :label_description_field, description
end end
def fill_color(color) def fill_color(color)
fill_element :label_color, color fill_element :label_color_field, color
end end
end end
end end

View File

@ -17,10 +17,6 @@ module QA
element :allowed_to_merge_dropdown element :allowed_to_merge_dropdown
end 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 view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
element :protected_branches_list element :protected_branches_list
end end

View File

@ -67,8 +67,8 @@ module QA
end end
view 'app/views/shared/_ref_switcher.html.haml' do view 'app/views/shared/_ref_switcher.html.haml' do
element :branches_select
element :branches_dropdown element :branches_dropdown
element :branches_dropdown_content
end end
view 'app/views/projects/blob/viewers/_loading.html.haml' do view 'app/views/projects/blob/viewers/_loading.html.haml' do
@ -176,9 +176,9 @@ module QA
end end
def switch_to_branch(branch_name) 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 click_on branch_name
end end
end end

View File

@ -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

View File

@ -188,7 +188,7 @@ RSpec.describe "User creates issue" do
end end
it 'does not hide the milestone select' do 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
end end
@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do
end end
it 'shows the milestone select' do 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 end
it 'hides the incident help text' do it 'hides the incident help text' do
@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do
end end
it 'shows the milestone select' do 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 end
it 'hides the weight input' do it 'hides the weight input' do

View File

@ -137,7 +137,7 @@ RSpec.describe 'File blob', :js do
context 'when ref switch' do context 'when ref switch' do
def switch_ref_to(ref_name) 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 page.within '.project-refs-form' do
click_link ref_name click_link ref_name

View File

@ -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 });
},
);
});
});

View File

@ -10,6 +10,7 @@ import {
EVENT_LABEL, EVENT_LABEL,
EVENT_ACTION, EVENT_ACTION,
ENVIRONMENT_SCOPE_LINK_TITLE, ENVIRONMENT_SCOPE_LINK_TITLE,
instanceString,
} from '~/ci_variable_list/constants'; } from '~/ci_variable_list/constants';
import { mockVariablesWithScopes } from '../mocks'; import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs'; import ModalStub from '../stubs';
@ -19,6 +20,7 @@ describe('Ci variable modal', () => {
let trackingSpy; let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$'; const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
const mockVariables = mockVariablesWithScopes(instanceString);
const defaultProvide = { const defaultProvide = {
awsLogoSvgPath: '/logo', awsLogoSvgPath: '/logo',
@ -38,6 +40,7 @@ describe('Ci variable modal', () => {
environments: [], environments: [],
mode: ADD_VARIABLE_ACTION, mode: ADD_VARIABLE_ACTION,
selectedVariable: {}, selectedVariable: {},
variable: [],
}; };
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => { const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
@ -81,22 +84,22 @@ describe('Ci variable modal', () => {
}); });
it('shows the submit button as disabled ', () => { 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', () => { describe('when a key/value pair is present', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } }); createComponent({ props: { selectedVariable: mockVariables[0] } });
}); });
it('shows the submit button as enabled ', () => { it('shows the submit button as enabled ', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
}); });
}); });
describe('events', () => { describe('events', () => {
const [currentVariable] = mockVariablesWithScopes; const [currentVariable] = mockVariables;
beforeEach(() => { beforeEach(() => {
createComponent({ props: { selectedVariable: currentVariable } }); createComponent({ props: { selectedVariable: currentVariable } });
@ -123,9 +126,9 @@ describe('Ci variable modal', () => {
}); });
it('updates the protected value to true', () => { it('updates the protected value to true', () => {
expect( expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
findProtectedVariableCheckbox().attributes('data-is-protected-checked'), 'true',
).toBeTruthy(); );
}); });
}); });
@ -151,7 +154,7 @@ describe('Ci variable modal', () => {
describe('Adding a new non-AWS variable', () => { describe('Adding a new non-AWS variable', () => {
beforeEach(() => { beforeEach(() => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } }); createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
}); });
@ -164,7 +167,7 @@ describe('Ci variable modal', () => {
describe('Adding a new AWS variable', () => { describe('Adding a new AWS variable', () => {
beforeEach(() => { beforeEach(() => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
const AWSKeyVariable = { const AWSKeyVariable = {
...variable, ...variable,
key: AWS_ACCESS_KEY_ID, key: AWS_ACCESS_KEY_ID,
@ -183,7 +186,7 @@ describe('Ci variable modal', () => {
describe('Reference warning when adding a variable', () => { describe('Reference warning when adding a variable', () => {
describe('with a $ character', () => { describe('with a $ character', () => {
beforeEach(() => { beforeEach(() => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
const variableWithDollarSign = { const variableWithDollarSign = {
...variable, ...variable,
value: 'valueWith$', value: 'valueWith$',
@ -201,7 +204,7 @@ describe('Ci variable modal', () => {
describe('without a $ character', () => { describe('without a $ character', () => {
beforeEach(() => { beforeEach(() => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
createComponent({ createComponent({
mountFn: mountExtended, mountFn: mountExtended,
props: { selectedVariable: variable }, props: { selectedVariable: variable },
@ -215,7 +218,7 @@ describe('Ci variable modal', () => {
}); });
describe('Editing a variable', () => { describe('Editing a variable', () => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
beforeEach(() => { beforeEach(() => {
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } }); createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
@ -286,7 +289,7 @@ describe('Ci variable modal', () => {
describe('when the mask state is invalid', () => { describe('when the mask state is invalid', () => {
beforeEach(async () => { beforeEach(async () => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
const invalidMaskVariable = { const invalidMaskVariable = {
...variable, ...variable,
value: 'd:;', value: 'd:;',
@ -301,7 +304,7 @@ describe('Ci variable modal', () => {
}); });
it('disables the submit button', () => { it('disables the submit button', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy(); expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
}); });
it('shows the correct error text', () => { it('shows the correct error text', () => {
@ -326,7 +329,7 @@ describe('Ci variable modal', () => {
${'unsupported|char'} | ${false} | ${0} | ${null} ${'unsupported|char'} | ${false} | ${0} | ${null}
`('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => { `('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
beforeEach(async () => { beforeEach(async () => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
const invalidKeyVariable = { const invalidKeyVariable = {
...variable, ...variable,
value: '', value: '',
@ -359,7 +362,7 @@ describe('Ci variable modal', () => {
describe('when masked variable has acceptable value', () => { describe('when masked variable has acceptable value', () => {
beforeEach(() => { beforeEach(() => {
const [variable] = mockVariablesWithScopes; const [variable] = mockVariables;
const validMaskandKeyVariable = { const validMaskandKeyVariable = {
...variable, ...variable,
key: AWS_ACCESS_KEY_ID, key: AWS_ACCESS_KEY_ID,
@ -373,7 +376,7 @@ describe('Ci variable modal', () => {
}); });
it('does not disable the submit button', () => { it('does not disable the submit button', () => {
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy(); expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
}); });
}); });
}); });

View File

@ -3,8 +3,12 @@ import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue'; import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.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 {
import { createJoinedEnvironments, mapEnvironmentNames } from '~/ci_variable_list/utils'; 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'; import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
@ -15,7 +19,7 @@ describe('Ci variable table', () => {
areScopedVariablesAvailable: true, areScopedVariablesAvailable: true,
environments: mapEnvironmentNames(mockEnvs), environments: mapEnvironmentNames(mockEnvs),
isLoading: false, isLoading: false,
variables: mockVariablesWithScopes, variables: mockVariablesWithScopes(projectString),
}; };
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable); const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
@ -51,7 +55,8 @@ describe('Ci variable table', () => {
expect(findCiVariableModal().props()).toEqual({ expect(findCiVariableModal().props()).toEqual({
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable, areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: createJoinedEnvironments(defaultProps.variables, defaultProps.environments), environments: defaultProps.environments,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION, mode: ADD_VARIABLE_ACTION,
selectedVariable: {}, selectedVariable: {},
}); });

View File

@ -1,5 +1,6 @@
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue'; import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
import { projectString } from '~/ci_variable_list/constants';
import { mockVariables } from '../mocks'; import { mockVariables } from '../mocks';
describe('Ci variable table', () => { describe('Ci variable table', () => {
@ -7,7 +8,7 @@ describe('Ci variable table', () => {
const defaultProps = { const defaultProps = {
isLoading: false, isLoading: false,
variables: mockVariables, variables: mockVariables(projectString),
}; };
const createComponent = ({ props = {} } = {}) => { const createComponent = ({ props = {} } = {}) => {

View File

@ -1,11 +1,12 @@
import { variableTypes } from '~/ci_variable_list/constants'; import { variableTypes, instanceString } from '~/ci_variable_list/constants';
export const devName = 'dev'; export const devName = 'dev';
export const prodName = 'prod'; export const prodName = 'prod';
export const mockVariables = [ export const mockVariables = (kind) => {
return [
{ {
__typename: 'CiVariable', __typename: `Ci${kind}Variable`,
id: 1, id: 1,
key: 'my-var', key: 'my-var',
masked: false, masked: false,
@ -14,7 +15,7 @@ export const mockVariables = [
variableType: variableTypes.variableType, variableType: variableTypes.variableType,
}, },
{ {
__typename: 'CiVariable', __typename: `Ci${kind}Variable`,
id: 2, id: 2,
key: 'secret', key: 'secret',
masked: true, masked: true,
@ -23,20 +24,22 @@ export const mockVariables = [
variableType: variableTypes.fileType, variableType: variableTypes.fileType,
}, },
]; ];
};
export const mockVariablesWithScopes = mockVariables.map((variable) => { export const mockVariablesWithScopes = (kind) =>
mockVariables(kind).map((variable) => {
return { ...variable, environmentScope: '*' }; return { ...variable, environmentScope: '*' };
}); });
const createDefaultVars = ({ withScope = true } = {}) => { const createDefaultVars = ({ withScope = true, kind } = {}) => {
let base = mockVariables; let base = mockVariables(kind);
if (withScope) { if (withScope) {
base = mockVariablesWithScopes; base = mockVariablesWithScopes(kind);
} }
return { return {
__typename: 'CiVariableConnection', __typename: `Ci${kind}VariableConnection`,
nodes: base, nodes: base,
}; };
}; };
@ -101,7 +104,7 @@ export const mockGroupVariables = {
export const mockAdminVariables = { export const mockAdminVariables = {
data: { data: {
ciVariables: createDefaultVars({ withScope: false }), ciVariables: createDefaultVars({ withScope: false, kind: instanceString }),
}, },
}; };

View File

@ -7,12 +7,13 @@ import { allEnvironments } from '~/ci_variable_list/constants';
describe('utils', () => { describe('utils', () => {
const environments = ['dev', 'prod']; const environments = ['dev', 'prod'];
const newEnvironments = ['staging'];
describe('createJoinedEnvironments', () => { describe('createJoinedEnvironments', () => {
it('returns only `environments` if `variables` argument is undefined', () => { it('returns only `environments` if `variables` argument is undefined', () => {
const variables = 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', () => { 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 }]; const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
expect(createJoinedEnvironments(variables, environments)).toEqual([ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0], environments[0],
envScope1, envScope1,
envScope2, 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', () => { it('removes duplicate environments', () => {
const envScope1 = environments[0]; const envScope1 = environments[0];
const envScope2 = 'new2'; const envScope2 = 'new2';
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }]; const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
expect(createJoinedEnvironments(variables, environments)).toEqual([ expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0], environments[0],
envScope2, envScope2,
environments[1], environments[1],

Some files were not shown because too many files have changed in this diff Show More