Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
7212129029
commit
8a9790b0db
|
@ -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>
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}.',
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
fragment BaseCiVariable on CiVariable {
|
||||
__typename
|
||||
id
|
||||
key
|
||||
value
|
||||
variableType
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
|
||||
|
||||
query getVariables {
|
||||
ciVariables {
|
||||
nodes {
|
||||
...BaseCiVariable
|
||||
... on CiInstanceVariable {
|
||||
masked
|
||||
protected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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') }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
= 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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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}",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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**:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>`.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 |
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
@ -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 = {} } = {}) => {
|
||||
|
|
|
@ -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 }),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue