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() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
return this.environments.filter((resultString) =>
resultString.toLowerCase().includes(lowerCasedSearchTerm),
);
return this.environments.filter((environment) => {
return environment.toLowerCase().includes(lowerCasedSearchTerm);
});
},
shouldRenderCreateButton() {
return this.searchTerm && !this.environments.includes(this.searchTerm);

View File

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

View File

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

View File

@ -47,6 +47,13 @@ export const defaultVariableState = {
variableType: types.variableType,
};
// eslint-disable-next-line @gitlab/require-i18n-strings
export const groupString = 'Group';
// eslint-disable-next-line @gitlab/require-i18n-strings
export const instanceString = 'Instance';
// eslint-disable-next-line @gitlab/require-i18n-strings
export const projectString = 'Instance';
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
export const AWS_TIP_MESSAGE = __(
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',

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

View File

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

View File

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

View File

@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :disable_query_limiting, only: [:usage_data]
before_action do
push_frontend_feature_flag(:ci_variable_settings_graphql)
end
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token

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
field :closed_at, Types::TimeType, null: true,
description: 'Timestamp of when the work item was closed.'
field :confidential, GraphQL::Types::Boolean, null: false,
description: 'Indicates the work item is confidential.'
field :created_at, Types::TimeType, null: false,
description: 'Timestamp of when the work item was created.'
field :description, GraphQL::Types::String, null: true,
description: 'Description of the work item.'
field :id, Types::GlobalIDType[::WorkItem], null: false,
@ -22,6 +26,8 @@ module Types
description: 'State of the work item.'
field :title, GraphQL::Types::String, null: false,
description: 'Title of the work item.'
field :updated_at, Types::TimeType, null: false,
description: 'Timestamp of when the work item was last updated.'
field :widgets,
[Types::WorkItems::WidgetInterface],
null: true,

View File

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

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

View File

@ -3,7 +3,7 @@
- if current_user.can_create_group?
.page-title-controls
= link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { testid: "new-group-button" }
= link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { qa_selector: "new_group_button", testid: "new-group-button" }
.top-area
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do

View File

@ -4,7 +4,7 @@
.devise-errors
= render "devise/shared/error_messages", resource: resource
.form-group.gl-px-5.gl-pt-5
= f.label :email, class: "gl-mb-1" if Feature.enabled?(:restyle_login_page, @project)
= f.label :email, class: ("gl-mb-1" if Feature.enabled?(:restyle_login_page))
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
= _('Requires your primary GitLab email address.')

View File

@ -32,7 +32,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'groups/runners/settings'

View File

@ -41,7 +41,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
= link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'projects/runners/settings'

View File

@ -1,5 +1,5 @@
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
= render Pajamas::AlertComponent.new(alert_options: { class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner' },
= render Pajamas::AlertComponent.new(alert_options: { class: 'auto-devops-implicitly-enabled-banner', data: { qa_selector: 'auto_devops_banner_content' } },
close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
data: { project_id: project.id }}) do |c|
= c.body do

View File

@ -5,7 +5,7 @@
%span.js-clone-dropdown-label
= enabled_protocol_button(container, enabled_protocol)
- else
%a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
%a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
%span.js-clone-dropdown-label
= default_clone_protocol.upcase
= sprite_icon('chevron-down', css_class: 'gl-icon')

View File

@ -1,6 +1,6 @@
- if any_projects?(@projects)
.dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
%a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
%a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= gl_loading_icon(inline: true, color: 'light')
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
%button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button{ 'aria-label': _('Toggle project select') }
%button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button{ 'aria-label': _('Toggle project select') }

View File

@ -13,8 +13,8 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]), data: { qa_selector: "branches_dropdown_content" } }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
= dropdown_filter _("Search branches and tags")

View File

@ -3,5 +3,5 @@
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
icon_classes: 'spin')
- elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body', qa_selector: 'update_now_button' }, title: _('Update now') do
= sprite_icon("retry")

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')
%span.collapse-text.gl-ml-3= _("Collapse sidebar")

View File

@ -45,7 +45,7 @@
%span.token-never-expires-label= _('Never')
- if resource
%td= resource.member(token.user).human_access
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right qa-revoke-button #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger' }
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger', qa_selector: 'revoke_button' }
- else
.settings-message.text-center
= no_active_tokens_message

View File

@ -1,6 +1,6 @@
.row.empty-state.labels
.col-12
.svg-content.qa-label-svg
.svg-content{ data: { qa_selector: 'label_svg_content' } }
= image_tag 'illustrations/labels.svg'
.col-12
.text-content
@ -8,7 +8,7 @@
%p= _("You can also star a label to make it a priority label.")
.text-center
- if can?(current_user, :admin_label, @project)
= link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm qa-label-create-new', title: _('New label'), id: 'new_label_link'
= link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn gl-button btn-confirm-secondary', title: _('Generate a default set of labels'), id: 'generate_labels_link'
- if can?(current_user, :admin_label, @group)
= link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'

View File

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

View File

@ -1,7 +1,7 @@
.row.empty-state
.col-12
.svg-content
= image_tag 'illustrations/labels.svg', data: { qa_selector: 'svg_content' }
= image_tag 'illustrations/labels.svg'
.text-content.gl-text-center.gl-pt-0!
%h4= _('There are no topics to show.')
%p= _('Add topics to projects to help users find them.')

View File

@ -3,7 +3,7 @@
- if can?(current_user, :create_wiki, @wiki.container)
- create_path = wiki_page_path(@wiki, params[:id], view: 'create')
- create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
- create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm', title: s_('WikiEmpty|Create your first page'), data: { qa_selector: 'create_first_page_link' }
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
%h4.text-left

View File

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

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|
= 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_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
- dropdown_data.merge!(data_options)
- dropdown_data.merge!(data_options, qa_selector: "issuable_label_dropdown")
- label_name = local_assigns.fetch(:label_name, _('Labels'))
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
- classes << 'js-extra-options' if extra_options
@ -22,7 +22,7 @@
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect.qa-issuable-label{ class: classes.join(' '), type: "button", data: dropdown_data }
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)

View File

@ -7,8 +7,8 @@
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone'))
- if selected.present? || params[:milestone_title].present?
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone') } }) do
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", dropdown_qa_selector: "issuable_milestone_dropdown_content",
placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do
- if project
%ul.dropdown-footer-list
- if can? current_user, :admin_milestone, project

View File

@ -26,14 +26,14 @@
= _('To-Do')
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
.block.assignee.qa-assignee-block{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}" }
.block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity]
#js-severity
- if reviewers
.block.reviewer.qa-reviewer-block
.block.reviewer
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
- if issuable_sidebar[:supports_escalation]

View File

@ -35,7 +35,7 @@
= form.label :milestone_id, _('Milestone'), class: "col-12"
.col-12
.issuable-form-select-holder
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
.form-group.row
= form.label :label_ids, _('Labels'), class: "col-12"

View File

@ -8,4 +8,4 @@
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
= link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
= link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 #{'hide' if issuable.assignees.include?(current_user)}", data: { qa_selector: 'assign_to_me_link' }

View File

@ -9,7 +9,7 @@
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', dir: 'auto'
autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { qa_selector: 'issuable_form_title_field' }
- if issuable.respond_to?(:draft?)
.form-text.text-muted

View File

@ -4,20 +4,20 @@
.form-group.row
.col-12
= f.label :title
= f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
= f.text_field :title, class: "gl-form-input form-control js-label-title", required: true, autofocus: true, data: { qa_selector: 'label_title_field' }
= render_if_exists 'shared/labels/create_label_help_text'
.form-group.row
.col-12
= f.label :description
= f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
= f.text_field :description, class: "gl-form-input form-control js-quick-submit", data: { qa_selector: 'label_description_field' }
.form-group.row
.col-12
= f.label :color, _("Background color")
.input-group
.input-group-prepend
.input-group-text.label-color-preview &nbsp;
= f.text_field :color, class: "gl-form-input form-control qa-label-color"
= f.text_field :color, class: "gl-form-input form-control", data: { qa_selector: 'label_color_field' }
.form-text.text-muted
= _('Choose any color.')
%br
@ -28,7 +28,7 @@
- if @label.persisted?
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
- else
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button gl-mr-2'
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
- if @label.persisted?
- presented_label = @label.present

View File

@ -14,8 +14,8 @@
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
= render 'shared/labels/sort_dropdown'
- if labels_or_filters && can_admin_label && @project
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { class: 'qa-label-create-new' }) do
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
= _('New label')
- if labels_or_filters && can_admin_label && @group
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { class: 'qa-label-create-new' }) do
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
= _('New label')

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' : ''
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...'
= form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f|
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
class: "project-filter-form-field form-control #{form_field_classes}",

View File

@ -10,7 +10,7 @@
%td.merge_access_levels-container
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
= dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
- if user_merge_access_levels.any?
%p.small

View File

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

View File

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

View File

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

View File

@ -110,6 +110,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success'
- 'i_code_review_merge_request_widget_metrics_expand_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed'
- 'i_code_review_merge_request_widget_status_checks_view'
- 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
- 'i_code_review_merge_request_widget_status_checks_expand'
- 'i_code_review_merge_request_widget_status_checks_expand_success'
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
- name: code_review_category_monthly_active_users
operator: OR
source: redis
@ -208,6 +214,12 @@
- 'i_code_review_merge_request_widget_metrics_expand_success'
- 'i_code_review_merge_request_widget_metrics_expand_warning'
- 'i_code_review_merge_request_widget_metrics_expand_failed'
- 'i_code_review_merge_request_widget_status_checks_view'
- 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
- 'i_code_review_merge_request_widget_status_checks_expand'
- 'i_code_review_merge_request_widget_status_checks_expand_success'
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
- name: code_review_extension_category_monthly_active_users
operator: OR
source: redis

View File

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

View File

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

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 UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default.
> - Custom HTTP headers UI [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) in GitLab 15.3. [Feature flag `custom_headers_streaming_audit_events_ui`](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) removed.
> - [Improved user experience](https://gitlab.com/gitlab-org/gitlab/-/issues/367963) in GitLab 15.4.
Users can set a streaming destination for a top-level group to receive all audit events about the group, its subgroups, and
projects as structured JSON.
@ -40,13 +41,12 @@ Users with the Owner role for a group can add streaming destinations for it:
1. On the top bar, select **Menu > Groups** and find your group.
1. On the left sidebar, select **Security & Compliance > Audit events**.
1. On the main area, select **Streams** tab.
- When the destination list is empty, select **Add stream** to show the section for adding destinations.
- When the destination list is not empty, select **Add stream** (**{plus}**) to show the section for adding destinations.
1. Select **Add streaming destination** to show the section for adding destinations.
1. Enter the destination URL to add.
1. Optional. Locate the **Custom HTTP headers** table.
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
20 headers per streaming destination.
1. After all headers have been filled out, select **Add** to add the new streaming destination.
@ -149,7 +149,7 @@ To update a streaming destinations custom HTTP headers:
1. Locate the header that you wish to update.
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
20 headers per streaming destination.
1. Select **Save** to update the streaming destination.

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,
assume we have direct access.
Direct Git access is enable by default in Omnibus GitLab because it fills in the correct repository
paths in the GitLab configuration file `config/gitlab.yml`. This satisfies the UUID check.
Versions of GitLab 15.3 and later disable direct Git access by default.
For versions of GitLab prior to 15.3, direct Git access is enabled by
default in Omnibus GitLab because it fills in the correct repository
paths in the GitLab configuration file `config/gitlab.yml`. This
satisfies the UUID check.
### Transition to Gitaly Cluster

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
[Rugged](https://github.com/libgit2/rugged).
From GitLab 12.1, GitLab automatically detects if Rugged can and should be used per storage.
If you previously enabled Rugged using the feature flag and you want to use automatic detection instead,
you must unset the feature flag:
Versions of GitLab after 12.2 and prior to 15.3 automatically detect if
Rugged can and should be used per storage.
NOTE:
GitLab 15.3 and later disables this automatic detection. Auto-detection can be enabled via the
`skip_rugged_auto_detect` feature flag:
```ruby
Feature.disable(:skip_rugged_auto_detect)
```
In addition, if you previously enabled Rugged using the feature flag and
you want to use automatic detection instead, you must unset the feature
flag:
```shell
sudo gitlab-rake gitlab:features:unset_rugged

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>>]
```
## Issue boards
### In case of issue boards not loading properly and it's getting time out. Call the Issue Rebalancing service to fix this
```ruby
p = Project.find_by_full_path('<username-or-group>/<project-name>')
Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
```
## Imports and exports
### Import a project

View File

@ -18856,7 +18856,9 @@ Represents vulnerability letter grades with associated projects.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="workitemclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the work item was closed. |
| <a id="workitemconfidential"></a>`confidential` | [`Boolean!`](#boolean) | Indicates the work item is confidential. |
| <a id="workitemcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the work item was created. |
| <a id="workitemdescription"></a>`description` | [`String`](#string) | Description of the work item. |
| <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
| <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
@ -18865,6 +18867,7 @@ Represents vulnerability letter grades with associated projects.
| <a id="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. |
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
| <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
| <a id="workitemupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the work item was last updated. |
| <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. |
| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. |
| <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |

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,
the `staging` job is marked as _failed_.
#### Trigger job configuration keywords
#### Trigger job configuration limitations
Trigger jobs can use only a limited set of the GitLab CI/CD [configuration keywords](../yaml/index.md).
The keywords available for use in trigger jobs are:
@ -90,6 +90,8 @@ The keywords available for use in trigger jobs are:
- [`extends`](../yaml/index.md#extends)
- [`needs`](../yaml/index.md#needs), but not [`needs:project`](../yaml/index.md#needsproject)
Trigger jobs cannot use [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables).
#### Specify a downstream pipeline branch
You can specify a branch name for the downstream pipeline to use.
@ -182,9 +184,12 @@ downstream-job:
trigger: my/project
```
In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the
upstream pipeline is passed to the `downstream-job` job. It is available
in the context of all downstream builds.
In this scenario, the `UPSTREAM_BRANCH` variable with the value of the upstream pipeline's
`$CI_COMMIT_REF_NAME` is passed to `downstream-job`. It is available in the
context of all downstream builds.
You cannot use this method to forward [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
to a downstream pipeline, as they are not available in trigger jobs.
Upstream pipelines take precedence over downstream ones. If there are two
variables with the same name defined in both upstream and downstream projects,

View File

@ -722,6 +722,9 @@ variables:
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28940) in GitLab Runner 15.1.
NOTE:
Zip archives are the only supported artifact type. Follow [the issue for details](https://gitlab.com/gitlab-org/gitlab/-/issues/367203).
GitLab Runner can generate and produce attestation metadata for all build artifacts. To enable this feature, you must set the `RUNNER_GENERATE_ARTIFACTS_METADATA` environment variable to `true`. This variable can either be set globally or it can be set for individual jobs. The metadata is in rendered in a plain text `.json` file that's stored with the artifact. The file name is as follows: `{JOB_ID}-artifacts-metadata.json`.
### Attestation format

View File

@ -132,10 +132,17 @@ These restrictions exist because `after_script` scripts are executed in a
## Persisted variables
The following variables are known as "persisted":
Some predefined variables are called "persisted".
Pipeline-level persisted variables:
- `CI_PIPELINE_ID`
- `CI_PIPELINE_URL`
Job-level persisted variables:
- `CI_JOB_ID`
- `CI_JOB_URL`
- `CI_JOB_TOKEN`
- `CI_JOB_STARTED_AT`
- `CI_REGISTRY_USER`
@ -144,7 +151,7 @@ The following variables are known as "persisted":
- `CI_DEPLOY_USER`
- `CI_DEPLOY_PASSWORD`
They are:
Persisted variables are:
- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
- Runner.
@ -153,6 +160,9 @@ They are:
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
- In the `only`, `except`, and `rules` [variables expressions](../jobs/job_control.md#cicd-variable-expressions).
[Pipeline trigger jobs](../yaml/index.md#trigger) cannot use job-level persisted variables,
but can use pipeline-level persisted variables.
Some of the persisted variables contain tokens and cannot be used by some definitions
due to security reasons.

View File

@ -3933,6 +3933,8 @@ trigger_job:
and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule)
are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward)
to forward these variables to downstream pipelines.
- [Job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
are not available in trigger jobs.
**Related topics**:

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

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

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).
### Backticks in Markdown
Use backticks for:
- [Code blocks](#code-blocks).
- Error messages.
### Markdown Rules
GitLab ensures that the Markdown used across all documentation is consistent, as

View File

@ -280,7 +280,7 @@ You can use Vale:
Vale returns three types of results:
- **Error** - For branding and trademark issues, words or phrases with ambiguous meanings, and anything that causes content on
- **Error** - For branding guidelines, trademark guidelines, and anything that causes content on
the docs site to render incorrectly.
- **Warning** - For Technical Writing team style preferences.
- **Suggestion** - For basic technical writing tenets and best practices.
@ -337,6 +337,29 @@ general complexity level of the page.
The readability score is calculated based on the number of words per sentence, and the number
of syllables per word. For more information, see [the Vale documentation](https://vale.sh/docs/topics/styles/#metric).
#### When to add a new Vale rule
It's tempting to add a Vale rule for every style guide rule. However, we should be
mindful of the effort to create and enforce a Vale rule, and the noise it creates.
In general, follow these guidelines:
- If you add an [error-level Vale rule](#vale-result-types), you must fix
the existing occurrences of the issue in the documentation before you can add the rule.
If there are too many issues to fix in a single merge request, add the rule at a
`warning` level. Then, fix the existing issues in follow-up merge requests.
When the issues are fixed, promote the rule to an `error`.
- If you add a warning-level or suggestion-level rule, consider:
- How many more warnings or suggestions it creates in the Vale output. If the
number of additional warnings is significant, the rule might be too broad.
- How often an author might ignore it because it's acceptable in the context.
If the rule is too subjective, it cannot be adequately enforced and creates
unnecessary additional warnings.
### Install linters
At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in

View File

@ -25,7 +25,7 @@ To ensure access to your cluster is safe:
- Each agent has a separate context (`kubecontext`).
- Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
You do not need to have a runner in the cluster with the agent.
The CI/CD workflow requires runners to be registered with GitLab, but these runners do not have to be in the cluster where the agent is.
## GitLab CI/CD workflow steps

View File

@ -642,3 +642,20 @@ A few things to remember:
- For performance and visibility reasons, each list shows the first 20 issues
by default. If you have more than 20 issues, start scrolling down and the next
20 appear.
## Troubleshooting issue boards
### Use Rails console to fix issue boards not loading and timing out
If you see issue board not loading and timing out in UI, use Rails console to call the Issue Rebalancing service to fix it:
1. [Start a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session).
1. Run these commands:
```ruby
p = Project.find_by_full_path('<username-or-group>/<project-name>')
Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
```
1. To exit the Rails console, type `quit`.

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)**
1. Input the issue number or paste in the full URL of the issue.
![Adding a related issue](img/related_issues_add_v12_8.png)
![Adding a related issue](img/related_issues_add_v15_3.png)
Issues of the same project can be specified just by the reference number.
Issues from a different project require additional information like the
@ -54,7 +54,7 @@ To link one issue to another:
When you have finished adding all linked issues, you can see
them categorized so their relationships can be better understood visually.
![Related issue block](img/related_issue_block_v12_8.png)
![Related issue block](img/related_issue_block_v15_3.png)
You can also add a linked issue from a commit message or the description in another issue or MR.
[Learn more about crosslinking issues](crosslinking_issues.md).
@ -66,7 +66,7 @@ right-side of each issue token to remove.
Due to the bi-directional relationship, the relationship no longer appears in either issue.
![Removing a related issue](img/related_issues_remove_v12_8.png)
![Removing a related issue](img/related_issues_remove_v15_3.png)
Access our [permissions](../../permissions.md) page for more information.

View File

@ -12,8 +12,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Namespace storage limit
Namespaces on a GitLab SaaS Free tier have a 5 GB storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
This limit is not visible on the storage quota page, but we plan to make it visible and enforced starting October 19, 2022.
Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
This limit is not visible on the Usage quotas page, but will be prior to [enforcement](#namespace-storage-limit-enforcement-schedule). Self-managed deployments are not affected.
Storage types that add to the total namespace storage are:
@ -22,7 +22,7 @@ Storage types that add to the total namespace storage are:
- Artifacts
- Container registry
- Package registry
- Dependecy proxy
- Dependency proxy
- Wiki
- Snippets
@ -30,30 +30,18 @@ If your total namespace storage exceeds the available namespace storage quota, a
To prevent exceeding the namespace storage quota, you can:
1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier).
1. [Reduce storage usage](#manage-your-storage-usage).
1. Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page.
1. Apply for [GitLab for Education](https://about.gitlab.com/solutions/education/join/), [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/), or [GitLab for Startups](https://about.gitlab.com/solutions/startups/) if you meet the eligibility requirements.
1. Consider using a [self-managed instance](../subscriptions/self_managed/) of GitLab which does not have these limits on the free tier.
1. [Purchase additional storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) units at $60/year for 10GB of storage.
1. [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality.
1. [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) to learn more about your options and ask questions.
### Namespace storage limit enforcement schedule
Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces.
We will start with a large limit enforcement and eventually reduce it to 5 GB.
Storage limits for GitLab SaaS Free tier namespaces will not be enforced prior to 2022-10-19. Storage limits for GitLab SaaS Paid tier namespaces will not be enforced for prior to 2023-02-15.
Impacted users are notified via email and in-app notifications will begin 2022-08-22.
Only GitLab SaaS users are impacted - the limits are not applicable to self-managed users.
The following table describes the enforcement schedule, which is subject to change.
| Planned enforcement date | Limit | Status |
| ----------------------- | ----- | ------ |
| October 19, 2022 | 45,000 GB | Not enforced |
| October 20, 2022 | 7,500 GB | Not enforced |
| October 24, 2022 | 500 GB | Not enforced |
| October 27, 2022 | 75 GB | Not enforced |
| November 2, 2022 | 10 GB | Not enforced |
| November 9, 2022 | 5 GB | Not enforced |
Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage).
Impacted users are notified via email and in-app notifications at least 60 days prior to enforcement.
### Project storage limit

View File

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

View File

@ -425,3 +425,28 @@
redis_slot: code_review
category: code_review
aggregation: weekly
## Status Checks
- name: i_code_review_merge_request_widget_status_checks_view
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_full_report_clicked
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_success
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_warning
redis_slot: code_review
category: code_review
aggregation: weekly
- name: i_code_review_merge_request_widget_status_checks_expand_failed
redis_slot: code_review
category: code_review
aggregation: weekly

View File

@ -5,7 +5,7 @@ module Gitlab
class MergeRequestWidgetExtensionCounter < BaseCounter
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
PREFIX = 'i_code_review_merge_request_widget'
WIDGETS = %w[accessibility code_quality terraform test_summary metrics].freeze
WIDGETS = %w[accessibility code_quality status_checks terraform test_summary metrics].freeze
class << self
private

View File

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

View File

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

View File

@ -23,6 +23,10 @@ module QA
element :create_token_button
end
base.view 'app/views/shared/access_tokens/_table.html.haml' do
element :revoke_button
end
base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
end

View File

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

View File

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

View File

@ -6,13 +6,8 @@ module QA
class Groups < Page::Base
include Page::Component::GroupsFilter
view 'app/views/shared/groups/_search_form.html.haml' do
element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern
element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern
end
view 'app/views/dashboard/_groups_head.html.haml' do
element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern
element :new_group_button
end
def has_group?(name)
@ -26,7 +21,7 @@ module QA
end
def click_new_group
click_on 'New group'
click_element(:new_group_button)
end
end
end

View File

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

View File

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

View File

@ -7,26 +7,26 @@ module QA
include Component::LazyLoader
view 'app/views/shared/labels/_nav.html.haml' do
element :label_create_new
element :create_new_label_button
end
view 'app/views/shared/empty_states/_labels.html.haml' do
element :label_svg
element :label_svg_content
end
view 'app/views/shared/empty_states/_priority_labels.html.haml' do
element :label_svg
element :label_svg_content
end
def click_new_label_button
# The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
# before clicking the button.
within_element(:label_svg) do
within_element(:label_svg_content) do
has_element?(:js_lazy_loaded)
end
click_element :label_create_new
click_element :create_new_label_button
end
end
end

View File

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

View File

@ -17,10 +17,6 @@ module QA
element :allowed_to_merge_dropdown
end
view 'app/views/shared/projects/protected_branches/_update_protected_branch.html.haml' do
element :allowed_to_merge
end
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
element :protected_branches_list
end

View File

@ -67,8 +67,8 @@ module QA
end
view 'app/views/shared/_ref_switcher.html.haml' do
element :branches_select
element :branches_dropdown
element :branches_dropdown_content
end
view 'app/views/projects/blob/viewers/_loading.html.haml' do
@ -176,9 +176,9 @@ module QA
end
def switch_to_branch(branch_name)
find_element(:branches_select).click
find_element(:branches_dropdown).click
within_element(:branches_dropdown) do
within_element(:branches_dropdown_content) do
click_on branch_name
end
end

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
it 'does not hide the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
end
@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
it 'hides the incident help text' do
@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do
end
it 'shows the milestone select' do
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
end
it 'hides the weight input' do

View File

@ -137,7 +137,7 @@ RSpec.describe 'File blob', :js do
context 'when ref switch' do
def switch_ref_to(ref_name)
first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
first('[data-testid="branches-select"]').click
page.within '.project-refs-form' do
click_link ref_name

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

View File

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

View File

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

View File

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

View File

@ -7,12 +7,13 @@ import { allEnvironments } from '~/ci_variable_list/constants';
describe('utils', () => {
const environments = ['dev', 'prod'];
const newEnvironments = ['staging'];
describe('createJoinedEnvironments', () => {
it('returns only `environments` if `variables` argument is undefined', () => {
const variables = undefined;
expect(createJoinedEnvironments(variables, environments)).toEqual(environments);
expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
});
it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
@ -21,7 +22,7 @@ describe('utils', () => {
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
expect(createJoinedEnvironments(variables, environments)).toEqual([
expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0],
envScope1,
envScope2,
@ -29,13 +30,22 @@ describe('utils', () => {
]);
});
it('returns combined list with new environments included', () => {
const variables = undefined;
expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
...environments,
...newEnvironments,
]);
});
it('removes duplicate environments', () => {
const envScope1 = environments[0];
const envScope2 = 'new2';
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
expect(createJoinedEnvironments(variables, environments)).toEqual([
expect(createJoinedEnvironments(variables, environments, [])).toEqual([
environments[0],
envScope2,
environments[1],

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