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() {
|
filteredEnvironments() {
|
||||||
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
|
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
|
||||||
return this.environments.filter((resultString) =>
|
return this.environments.filter((environment) => {
|
||||||
resultString.toLowerCase().includes(lowerCasedSearchTerm),
|
return environment.toLowerCase().includes(lowerCasedSearchTerm);
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
shouldRenderCreateButton() {
|
shouldRenderCreateButton() {
|
||||||
return this.searchTerm && !this.environments.includes(this.searchTerm);
|
return this.searchTerm && !this.environments.includes(this.searchTerm);
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
VARIABLE_ACTIONS,
|
VARIABLE_ACTIONS,
|
||||||
variableOptions,
|
variableOptions,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
import { createJoinedEnvironments } from '../utils';
|
||||||
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
|
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
|
||||||
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
|
import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
|
||||||
|
|
||||||
|
@ -98,9 +98,15 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: () => {},
|
default: () => {},
|
||||||
},
|
},
|
||||||
|
variables: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
newEnvironments: [],
|
||||||
isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
|
isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
|
||||||
typeOptions: variableOptions,
|
typeOptions: variableOptions,
|
||||||
validationErrorEventProperty: '',
|
validationErrorEventProperty: '',
|
||||||
|
@ -128,6 +134,9 @@ export default {
|
||||||
isTipVisible() {
|
isTipVisible() {
|
||||||
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
|
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
|
||||||
},
|
},
|
||||||
|
joinedEnvironments() {
|
||||||
|
return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
|
||||||
|
},
|
||||||
maskedFeedback() {
|
maskedFeedback() {
|
||||||
return this.displayMaskedError ? __('This variable can not be masked.') : '';
|
return this.displayMaskedError ? __('This variable can not be masked.') : '';
|
||||||
},
|
},
|
||||||
|
@ -176,7 +185,7 @@ export default {
|
||||||
this.$emit('add-variable', this.variable);
|
this.$emit('add-variable', this.variable);
|
||||||
},
|
},
|
||||||
createEnvironmentScope(env) {
|
createEnvironmentScope(env) {
|
||||||
this.$emit('create-environment-scope', env);
|
this.newEnvironments.push(env);
|
||||||
},
|
},
|
||||||
deleteVariable() {
|
deleteVariable() {
|
||||||
this.$emit('delete-variable', this.variable);
|
this.$emit('delete-variable', this.variable);
|
||||||
|
@ -314,7 +323,7 @@ export default {
|
||||||
v-if="areScopedVariablesAvailable"
|
v-if="areScopedVariablesAvailable"
|
||||||
class="gl-w-full"
|
class="gl-w-full"
|
||||||
:selected-environment-scope="variable.environmentScope"
|
:selected-environment-scope="variable.environmentScope"
|
||||||
:environments="environments"
|
:environments="joinedEnvironments"
|
||||||
@select-environment="setEnvironmentScope"
|
@select-environment="setEnvironmentScope"
|
||||||
@create-environment-scope="createEnvironmentScope"
|
@create-environment-scope="createEnvironmentScope"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
|
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
|
||||||
import { createJoinedEnvironments } from '../utils';
|
|
||||||
import CiVariableTable from './ci_variable_table.vue';
|
import CiVariableTable from './ci_variable_table.vue';
|
||||||
import CiVariableModal from './ci_variable_modal.vue';
|
import CiVariableModal from './ci_variable_modal.vue';
|
||||||
|
|
||||||
|
@ -17,7 +16,8 @@ export default {
|
||||||
},
|
},
|
||||||
environments: {
|
environments: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: false,
|
||||||
|
default: () => [],
|
||||||
},
|
},
|
||||||
isLoading: {
|
isLoading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -36,9 +36,6 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
joinedEnvironments() {
|
|
||||||
return createJoinedEnvironments(this.variables, this.environments);
|
|
||||||
},
|
|
||||||
showModal() {
|
showModal() {
|
||||||
return VARIABLE_ACTIONS.includes(this.mode);
|
return VARIABLE_ACTIONS.includes(this.mode);
|
||||||
},
|
},
|
||||||
|
@ -80,7 +77,8 @@ export default {
|
||||||
<ci-variable-modal
|
<ci-variable-modal
|
||||||
v-if="showModal"
|
v-if="showModal"
|
||||||
:are-scoped-variables-available="areScopedVariablesAvailable"
|
:are-scoped-variables-available="areScopedVariablesAvailable"
|
||||||
:environments="joinedEnvironments"
|
:environments="environments"
|
||||||
|
:variables="variables"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
:selected-variable="selectedVariable"
|
:selected-variable="selectedVariable"
|
||||||
@add-variable="addVariable"
|
@add-variable="addVariable"
|
||||||
|
|
|
@ -47,6 +47,13 @@ export const defaultVariableState = {
|
||||||
variableType: types.variableType,
|
variableType: types.variableType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
export const groupString = 'Group';
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
export const instanceString = 'Instance';
|
||||||
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
export const projectString = 'Instance';
|
||||||
|
|
||||||
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
|
export const AWS_TIP_DISMISSED_COOKIE_NAME = 'ci_variable_list_constants_aws_tip_dismissed';
|
||||||
export const AWS_TIP_MESSAGE = __(
|
export const AWS_TIP_MESSAGE = __(
|
||||||
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',
|
'%{deployLinkStart}Use a template to deploy to ECS%{deployLinkEnd}, or use a docker image to %{commandsLinkStart}run AWS commands in GitLab CI/CD%{commandsLinkEnd}.',
|
||||||
|
|
|
@ -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 VueApollo from 'vue-apollo';
|
||||||
import createDefaultClient from '~/lib/graphql';
|
import createDefaultClient from '~/lib/graphql';
|
||||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||||
import CiVariableSettings from './components/ci_variable_settings.vue';
|
import CiAdminVariables from './components/ci_admin_variables.vue';
|
||||||
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
|
import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
|
||||||
|
import { resolvers } from './graphql/resolvers';
|
||||||
import createStore from './store';
|
import createStore from './store';
|
||||||
|
|
||||||
const mountCiVariableListApp = (containerEl) => {
|
const mountCiVariableListApp = (containerEl) => {
|
||||||
|
@ -13,8 +14,12 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
awsTipDeployLink,
|
awsTipDeployLink,
|
||||||
awsTipLearnLink,
|
awsTipLearnLink,
|
||||||
containsVariableReferenceLink,
|
containsVariableReferenceLink,
|
||||||
|
endpoint,
|
||||||
environmentScopeLink,
|
environmentScopeLink,
|
||||||
group,
|
groupId,
|
||||||
|
groupPath,
|
||||||
|
isGroup,
|
||||||
|
isProject,
|
||||||
maskedEnvironmentVariablesLink,
|
maskedEnvironmentVariablesLink,
|
||||||
maskableRegex,
|
maskableRegex,
|
||||||
projectFullPath,
|
projectFullPath,
|
||||||
|
@ -23,13 +28,16 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
protectedEnvironmentVariablesLink,
|
protectedEnvironmentVariablesLink,
|
||||||
} = containerEl.dataset;
|
} = containerEl.dataset;
|
||||||
|
|
||||||
const isGroup = parseBoolean(group);
|
const parsedIsProject = parseBoolean(isProject);
|
||||||
|
const parsedIsGroup = parseBoolean(isGroup);
|
||||||
const isProtectedByDefault = parseBoolean(protectedByDefault);
|
const isProtectedByDefault = parseBoolean(protectedByDefault);
|
||||||
|
|
||||||
|
const component = CiAdminVariables;
|
||||||
|
|
||||||
Vue.use(VueApollo);
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
const apolloProvider = new VueApollo({
|
const apolloProvider = new VueApollo({
|
||||||
defaultClient: createDefaultClient(),
|
defaultClient: createDefaultClient(resolvers),
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Vue({
|
return new Vue({
|
||||||
|
@ -41,8 +49,12 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
awsTipDeployLink,
|
awsTipDeployLink,
|
||||||
awsTipLearnLink,
|
awsTipLearnLink,
|
||||||
containsVariableReferenceLink,
|
containsVariableReferenceLink,
|
||||||
|
endpoint,
|
||||||
environmentScopeLink,
|
environmentScopeLink,
|
||||||
isGroup,
|
groupId,
|
||||||
|
groupPath,
|
||||||
|
isGroup: parsedIsGroup,
|
||||||
|
isProject: parsedIsProject,
|
||||||
isProtectedByDefault,
|
isProtectedByDefault,
|
||||||
maskedEnvironmentVariablesLink,
|
maskedEnvironmentVariablesLink,
|
||||||
maskableRegex,
|
maskableRegex,
|
||||||
|
@ -51,7 +63,7 @@ const mountCiVariableListApp = (containerEl) => {
|
||||||
protectedEnvironmentVariablesLink,
|
protectedEnvironmentVariablesLink,
|
||||||
},
|
},
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement(CiVariableSettings);
|
return createElement(component);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,20 +2,25 @@ import { uniq } from 'lodash';
|
||||||
import { allEnvironments } from './constants';
|
import { allEnvironments } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function takes aa list of variable and environments
|
* This function takes a list of variable, environments and
|
||||||
|
* new environments added through the scope dropdown
|
||||||
* and create a new Array that concatenate the environment list
|
* and create a new Array that concatenate the environment list
|
||||||
* with the environment scopes find in the variable list. This is
|
* with the environment scopes find in the variable list. This is
|
||||||
* useful for variable settings so that we can render a list of all
|
* useful for variable settings so that we can render a list of all
|
||||||
* environment scopes available based on both the list of envs and what
|
* environment scopes available based on the list of envs, the ones the user
|
||||||
* is found under each variable.
|
* added explictly and what is found under each variable.
|
||||||
* @param {Array} variables
|
* @param {Array} variables
|
||||||
* @param {Array} environments
|
* @param {Array} environments
|
||||||
* @returns {Array} - Array of environments
|
* @returns {Array} - Array of environments
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const createJoinedEnvironments = (variables = [], environments = []) => {
|
export const createJoinedEnvironments = (
|
||||||
|
variables = [],
|
||||||
|
environments = [],
|
||||||
|
newEnvironments = [],
|
||||||
|
) => {
|
||||||
const scopesFromVariables = variables.map((variable) => variable.environmentScope);
|
const scopesFromVariables = variables.map((variable) => variable.environmentScope);
|
||||||
return uniq(environments.concat(scopesFromVariables)).sort();
|
return uniq([...environments, ...newEnvironments, ...scopesFromVariables]).sort();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,11 +7,12 @@ const DEFERRED_LINK_CLASS = 'deferred-link';
|
||||||
|
|
||||||
export default class PersistentUserCallout {
|
export default class PersistentUserCallout {
|
||||||
constructor(container, options = container.dataset) {
|
constructor(container, options = container.dataset) {
|
||||||
const { dismissEndpoint, featureId, groupId, deferLinks } = options;
|
const { dismissEndpoint, featureId, groupId, namespaceId, deferLinks } = options;
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.dismissEndpoint = dismissEndpoint;
|
this.dismissEndpoint = dismissEndpoint;
|
||||||
this.featureId = featureId;
|
this.featureId = featureId;
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
|
this.namespaceId = namespaceId;
|
||||||
this.deferLinks = parseBoolean(deferLinks);
|
this.deferLinks = parseBoolean(deferLinks);
|
||||||
this.closeButtons = this.container.querySelectorAll('.js-close');
|
this.closeButtons = this.container.querySelectorAll('.js-close');
|
||||||
|
|
||||||
|
@ -56,6 +57,7 @@ export default class PersistentUserCallout {
|
||||||
.post(this.dismissEndpoint, {
|
.post(this.dismissEndpoint, {
|
||||||
feature_name: this.featureId,
|
feature_name: this.featureId,
|
||||||
group_id: this.groupId,
|
group_id: this.groupId,
|
||||||
|
namespace_id: this.namespaceId,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
|
|
|
@ -13,6 +13,10 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
||||||
|
|
||||||
before_action :disable_query_limiting, only: [:usage_data]
|
before_action :disable_query_limiting, only: [:usage_data]
|
||||||
|
|
||||||
|
before_action do
|
||||||
|
push_frontend_feature_flag(:ci_variable_settings_graphql)
|
||||||
|
end
|
||||||
|
|
||||||
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
|
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
|
||||||
:general, :reporting, :metrics_and_profiling, :network,
|
:general, :reporting, :metrics_and_profiling, :network,
|
||||||
:preferences, :update, :reset_health_check_token
|
:preferences, :update, :reset_health_check_token
|
||||||
|
|
|
@ -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
|
authorize :read_work_item
|
||||||
|
|
||||||
|
field :closed_at, Types::TimeType, null: true,
|
||||||
|
description: 'Timestamp of when the work item was closed.'
|
||||||
field :confidential, GraphQL::Types::Boolean, null: false,
|
field :confidential, GraphQL::Types::Boolean, null: false,
|
||||||
description: 'Indicates the work item is confidential.'
|
description: 'Indicates the work item is confidential.'
|
||||||
|
field :created_at, Types::TimeType, null: false,
|
||||||
|
description: 'Timestamp of when the work item was created.'
|
||||||
field :description, GraphQL::Types::String, null: true,
|
field :description, GraphQL::Types::String, null: true,
|
||||||
description: 'Description of the work item.'
|
description: 'Description of the work item.'
|
||||||
field :id, Types::GlobalIDType[::WorkItem], null: false,
|
field :id, Types::GlobalIDType[::WorkItem], null: false,
|
||||||
|
@ -22,6 +26,8 @@ module Types
|
||||||
description: 'State of the work item.'
|
description: 'State of the work item.'
|
||||||
field :title, GraphQL::Types::String, null: false,
|
field :title, GraphQL::Types::String, null: false,
|
||||||
description: 'Title of the work item.'
|
description: 'Title of the work item.'
|
||||||
|
field :updated_at, Types::TimeType, null: false,
|
||||||
|
description: 'Timestamp of when the work item was last updated.'
|
||||||
field :widgets,
|
field :widgets,
|
||||||
[Types::WorkItems::WidgetInterface],
|
[Types::WorkItems::WidgetInterface],
|
||||||
null: true,
|
null: true,
|
||||||
|
|
|
@ -216,6 +216,10 @@ class Event < ApplicationRecord
|
||||||
target_type == 'DesignManagement::Design'
|
target_type == 'DesignManagement::Design'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def work_item?
|
||||||
|
target_type == 'WorkItem'
|
||||||
|
end
|
||||||
|
|
||||||
def milestone
|
def milestone
|
||||||
target if milestone?
|
target if milestone?
|
||||||
end
|
end
|
||||||
|
@ -399,7 +403,8 @@ class Event < ApplicationRecord
|
||||||
read_milestone: %i[milestone?],
|
read_milestone: %i[milestone?],
|
||||||
read_wiki: %i[wiki_page?],
|
read_wiki: %i[wiki_page?],
|
||||||
read_design: %i[design_note? design?],
|
read_design: %i[design_note? design?],
|
||||||
read_note: %i[note?]
|
read_note: %i[note?],
|
||||||
|
read_work_item: %i[work_item?]
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -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 }
|
= s_('Environment variables are configured by your administrator to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
|
||||||
|
|
||||||
- is_group = !@group.nil?
|
- is_group = !@group.nil?
|
||||||
|
- is_project = !@project.nil?
|
||||||
|
|
||||||
#js-ci-project-variables{ data: { endpoint: save_endpoint,
|
#js-ci-project-variables{ data: { endpoint: save_endpoint,
|
||||||
|
is_project: is_project.to_s,
|
||||||
project_id: @project&.id || '',
|
project_id: @project&.id || '',
|
||||||
group: is_group.to_s,
|
project_full_path: @project&.full_path || '',
|
||||||
|
is_group: is_group.to_s,
|
||||||
|
group_id: @group&.id || '',
|
||||||
|
group_path: @group&.full_path,
|
||||||
maskable_regex: ci_variable_maskable_regex,
|
maskable_regex: ci_variable_maskable_regex,
|
||||||
protected_by_default: ci_variable_protected_by_default?.to_s,
|
protected_by_default: ci_variable_protected_by_default?.to_s,
|
||||||
aws_logo_svg_path: image_path('aws_logo.svg'),
|
aws_logo_svg_path: image_path('aws_logo.svg'),
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
- if current_user.can_create_group?
|
- if current_user.can_create_group?
|
||||||
.page-title-controls
|
.page-title-controls
|
||||||
= link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { testid: "new-group-button" }
|
= link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { qa_selector: "new_group_button", testid: "new-group-button" }
|
||||||
|
|
||||||
.top-area
|
.top-area
|
||||||
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
|
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
.devise-errors
|
.devise-errors
|
||||||
= render "devise/shared/error_messages", resource: resource
|
= render "devise/shared/error_messages", resource: resource
|
||||||
.form-group.gl-px-5.gl-pt-5
|
.form-group.gl-px-5.gl-pt-5
|
||||||
= f.label :email, class: "gl-mb-1" if Feature.enabled?(:restyle_login_page, @project)
|
= f.label :email, class: ("gl-mb-1" if Feature.enabled?(:restyle_login_page))
|
||||||
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
|
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
|
||||||
.form-text.text-muted
|
.form-text.text-muted
|
||||||
= _('Requires your primary GitLab email address.')
|
= _('Requires your primary GitLab email address.')
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
%p
|
%p
|
||||||
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
|
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
|
||||||
= link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
|
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
|
||||||
.settings-content
|
.settings-content
|
||||||
= render 'groups/runners/settings'
|
= render 'groups/runners/settings'
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
= expanded ? _('Collapse') : _('Expand')
|
= expanded ? _('Collapse') : _('Expand')
|
||||||
%p
|
%p
|
||||||
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
|
= _("Runners are processes that pick up and execute CI/CD jobs for GitLab.")
|
||||||
= link_to s_('How do I configure runners?'), help_page_path('ci/runners/index'), target: '_blank', rel: 'noopener noreferrer'
|
= link_to s_('What is GitLab Runner?'), 'https://docs.gitlab.com/runner/', target: '_blank', rel: 'noopener noreferrer'
|
||||||
.settings-content
|
.settings-content
|
||||||
= render 'projects/runners/settings'
|
= render 'projects/runners/settings'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
|
- if show_auto_devops_implicitly_enabled_banner?(project, current_user)
|
||||||
= render Pajamas::AlertComponent.new(alert_options: { class: 'qa-auto-devops-banner auto-devops-implicitly-enabled-banner' },
|
= render Pajamas::AlertComponent.new(alert_options: { class: 'auto-devops-implicitly-enabled-banner', data: { qa_selector: 'auto_devops_banner_content' } },
|
||||||
close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
|
close_button_options: { class: 'hide-auto-devops-implicitly-enabled-banner',
|
||||||
data: { project_id: project.id }}) do |c|
|
data: { project_id: project.id }}) do |c|
|
||||||
= c.body do
|
= c.body do
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
%span.js-clone-dropdown-label
|
%span.js-clone-dropdown-label
|
||||||
= enabled_protocol_button(container, enabled_protocol)
|
= enabled_protocol_button(container, enabled_protocol)
|
||||||
- else
|
- else
|
||||||
%a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn.qa-clone-dropdown{ href: '#', data: { toggle: 'dropdown' } }
|
%a#clone-dropdown.input-group-text.gl-button.btn.btn-default.btn-icon.clone-dropdown-btn{ href: '#', data: { toggle: 'dropdown', qa_selector: 'clone_dropdown' } }
|
||||||
%span.js-clone-dropdown-label
|
%span.js-clone-dropdown-label
|
||||||
= default_clone_protocol.upcase
|
= default_clone_protocol.upcase
|
||||||
= sprite_icon('chevron-down', css_class: 'gl-icon')
|
= sprite_icon('chevron-down', css_class: 'gl-icon')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- if any_projects?(@projects)
|
- if any_projects?(@projects)
|
||||||
.dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
|
.dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
|
||||||
%a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated.qa-new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
|
%a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
|
||||||
= gl_loading_icon(inline: true, color: 'light')
|
= gl_loading_icon(inline: true, color: 'light')
|
||||||
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
|
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
|
||||||
%button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button.qa-new-project-item-select-button{ 'aria-label': _('Toggle project select') }
|
%button.btn.dropdown-toggle.btn-confirm.btn-md.gl-button.gl-dropdown-toggle.dropdown-toggle-split.new-project-item-select-button{ 'aria-label': _('Toggle project select') }
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
- @options && @options.each do |key, value|
|
- @options && @options.each do |key, value|
|
||||||
= hidden_field_tag key, value, id: nil
|
= hidden_field_tag key, value, id: nil
|
||||||
.dropdown
|
.dropdown
|
||||||
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true }, { toggle_class: "js-project-refs-dropdown qa-branches-select" }
|
= dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
|
||||||
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging.qa-branches-dropdown{ class: ("dropdown-menu-right" if local_assigns[:align_right]) }
|
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]), data: { qa_selector: "branches_dropdown_content" } }
|
||||||
.dropdown-page-one
|
.dropdown-page-one
|
||||||
= dropdown_title _("Switch branch/tag")
|
= dropdown_title _("Switch branch/tag")
|
||||||
= dropdown_filter _("Search branches and tags")
|
= dropdown_filter _("Search branches and tags")
|
||||||
|
|
|
@ -3,5 +3,5 @@
|
||||||
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
|
button_options: { class: 'disabled', title: _('Updating'), data: { toggle: 'tooltip', container: 'body', qa_selector: 'updating_button' } },
|
||||||
icon_classes: 'spin')
|
icon_classes: 'spin')
|
||||||
- elsif remote_mirror.enabled?
|
- elsif remote_mirror.enabled?
|
||||||
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button qa-update-now-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
|
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn btn-icon gl-button rspec-update-now-button", data: { toggle: 'tooltip', container: 'body', qa_selector: 'update_now_button' }, title: _('Update now') do
|
||||||
= sprite_icon("retry")
|
= sprite_icon("retry")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
%a.toggle-sidebar-button.js-toggle-sidebar.qa-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
|
%a.toggle-sidebar-button.js-toggle-sidebar.rspec-toggle-sidebar{ role: "button", type: "button", title: "Toggle sidebar" }
|
||||||
= sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
|
= sprite_icon('chevron-double-lg-left', size: 12, css_class: 'icon-chevron-double-lg-left')
|
||||||
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
|
%span.collapse-text.gl-ml-3= _("Collapse sidebar")
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@
|
||||||
%span.token-never-expires-label= _('Never')
|
%span.token-never-expires-label= _('Never')
|
||||||
- if resource
|
- if resource
|
||||||
%td= resource.member(token.user).human_access
|
%td= resource.member(token.user).human_access
|
||||||
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right qa-revoke-button #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger' }
|
%td= link_to _('Revoke'), revoke_route_helper.call(token), method: :put, class: "gl-button btn btn-danger btn-sm float-right #{'btn-danger-secondary' unless token.expires?}", aria: { label: _('Revoke') }, data: { confirm: _('Are you sure you want to revoke this %{type}? This action cannot be undone.') % { type: type }, 'confirm-btn-variant': 'danger', qa_selector: 'revoke_button' }
|
||||||
- else
|
- else
|
||||||
.settings-message.text-center
|
.settings-message.text-center
|
||||||
= no_active_tokens_message
|
= no_active_tokens_message
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.row.empty-state.labels
|
.row.empty-state.labels
|
||||||
.col-12
|
.col-12
|
||||||
.svg-content.qa-label-svg
|
.svg-content{ data: { qa_selector: 'label_svg_content' } }
|
||||||
= image_tag 'illustrations/labels.svg'
|
= image_tag 'illustrations/labels.svg'
|
||||||
.col-12
|
.col-12
|
||||||
.text-content
|
.text-content
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
%p= _("You can also star a label to make it a priority label.")
|
%p= _("You can also star a label to make it a priority label.")
|
||||||
.text-center
|
.text-center
|
||||||
- if can?(current_user, :admin_label, @project)
|
- if can?(current_user, :admin_label, @project)
|
||||||
= link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm qa-label-create-new', title: _('New label'), id: 'new_label_link'
|
= link_to _('New label'), new_project_label_path(@project), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
|
||||||
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn gl-button btn-confirm-secondary', title: _('Generate a default set of labels'), id: 'generate_labels_link'
|
= link_to _('Generate a default set of labels'), generate_project_labels_path(@project), method: :post, class: 'btn gl-button btn-confirm-secondary', title: _('Generate a default set of labels'), id: 'generate_labels_link'
|
||||||
- if can?(current_user, :admin_label, @group)
|
- if can?(current_user, :admin_label, @group)
|
||||||
= link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
|
= link_to _('New label'), new_group_label_path(@group), class: 'btn gl-button btn-confirm', title: _('New label'), id: 'new_label_link'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
.text-center
|
.text-center
|
||||||
.svg-content.qa-label-svg
|
.svg-content{ data: { qa_selector: 'label_svg_content' } }
|
||||||
= image_tag 'illustrations/priority_labels.svg'
|
= image_tag 'illustrations/priority_labels.svg'
|
||||||
- if can?(current_user, :admin_label, @project)
|
- if can?(current_user, :admin_label, @project)
|
||||||
%p
|
%p
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
.row.empty-state
|
.row.empty-state
|
||||||
.col-12
|
.col-12
|
||||||
.svg-content
|
.svg-content
|
||||||
= image_tag 'illustrations/labels.svg', data: { qa_selector: 'svg_content' }
|
= image_tag 'illustrations/labels.svg'
|
||||||
.text-content.gl-text-center.gl-pt-0!
|
.text-content.gl-text-center.gl-pt-0!
|
||||||
%h4= _('There are no topics to show.')
|
%h4= _('There are no topics to show.')
|
||||||
%p= _('Add topics to projects to help users find them.')
|
%p= _('Add topics to projects to help users find them.')
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
- if can?(current_user, :create_wiki, @wiki.container)
|
- if can?(current_user, :create_wiki, @wiki.container)
|
||||||
- create_path = wiki_page_path(@wiki, params[:id], view: 'create')
|
- create_path = wiki_page_path(@wiki, params[:id], view: 'create')
|
||||||
- create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm qa-create-first-page-link', title: s_('WikiEmpty|Create your first page')
|
- create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn gl-button btn-confirm', title: s_('WikiEmpty|Create your first page'), data: { qa_selector: 'create_first_page_link' }
|
||||||
|
|
||||||
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
|
= render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
|
||||||
%h4.text-left
|
%h4.text-left
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.row.empty-state.empty-state-wiki
|
.row.empty-state.empty-state-wiki
|
||||||
.col-12
|
.col-12
|
||||||
.svg-content.qa-svg-content
|
.svg-content{ data: { qa_selector: 'svg_content' } }
|
||||||
= image_tag image_path
|
= image_tag image_path
|
||||||
.col-12
|
.col-12
|
||||||
.text-content.text-center
|
.text-content.text-center
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
|
= form_tag request.path, method: :get, class: "group-filter-form js-group-filter-form", id: 'group-filter-form' do |f|
|
||||||
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter qa-groups-filter', spellcheck: false, id: 'group-filter-form-field'
|
= search_field_tag :filter, params[:filter], placeholder: s_('GroupsTree|Search by name'), class: 'group-filter-form-field form-control js-groups-list-filter', data: { qa_selector: 'groups_filter_field' }, spellcheck: false, id: 'group-filter-form-field'
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
|
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by label'))
|
||||||
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
|
- dropdown_data = label_dropdown_data(edit_context, labels: labels_filter_path_with_defaults(only_group_labels: edit_context.is_a?(Group)), default_label: _('Labels'))
|
||||||
|
|
||||||
- dropdown_data.merge!(data_options)
|
- dropdown_data.merge!(data_options, qa_selector: "issuable_label_dropdown")
|
||||||
- label_name = local_assigns.fetch(:label_name, _('Labels'))
|
- label_name = local_assigns.fetch(:label_name, _('Labels'))
|
||||||
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
|
- no_default_styles = local_assigns.fetch(:no_default_styles, false)
|
||||||
- classes << 'js-extra-options' if extra_options
|
- classes << 'js-extra-options' if extra_options
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
|
= hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
|
||||||
|
|
||||||
.dropdown
|
.dropdown
|
||||||
%button.dropdown-menu-toggle.js-label-select.js-multiselect.qa-issuable-label{ class: classes.join(' '), type: "button", data: dropdown_data }
|
%button.dropdown-menu-toggle.js-label-select.js-multiselect{ class: classes.join(' '), type: "button", data: dropdown_data }
|
||||||
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
|
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
|
||||||
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
|
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
|
||||||
= multi_label_name(selected, label_name)
|
= multi_label_name(selected, label_name)
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone'))
|
- dropdown_title = local_assigns.fetch(:dropdown_title, _('Filter by milestone'))
|
||||||
- if selected.present? || params[:milestone_title].present?
|
- if selected.present? || params[:milestone_title].present?
|
||||||
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
|
= hidden_field_tag(name, name == :milestone_title ? selected_text : selected.id)
|
||||||
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "qa-issuable-milestone-dropdown js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "qa-issuable-dropdown-menu-milestone dropdown-menu-selectable dropdown-menu-milestone",
|
= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: dropdown_title, toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", dropdown_qa_selector: "issuable_milestone_dropdown_content",
|
||||||
placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone') } }) do
|
placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do
|
||||||
- if project
|
- if project
|
||||||
%ul.dropdown-footer-list
|
%ul.dropdown-footer-list
|
||||||
- if can? current_user, :admin_milestone, project
|
- if can? current_user, :admin_milestone, project
|
||||||
|
|
|
@ -26,14 +26,14 @@
|
||||||
= _('To-Do')
|
= _('To-Do')
|
||||||
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
|
.js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
|
||||||
|
|
||||||
.block.assignee.qa-assignee-block{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}" }
|
.block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } }
|
||||||
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
|
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
|
||||||
|
|
||||||
- if issuable_sidebar[:supports_severity]
|
- if issuable_sidebar[:supports_severity]
|
||||||
#js-severity
|
#js-severity
|
||||||
|
|
||||||
- if reviewers
|
- if reviewers
|
||||||
.block.reviewer.qa-reviewer-block
|
.block.reviewer
|
||||||
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
|
= render "shared/issuable/sidebar_reviewers", issuable_sidebar: issuable_sidebar, reviewers: reviewers, signed_in: signed_in
|
||||||
|
|
||||||
- if issuable_sidebar[:supports_escalation]
|
- if issuable_sidebar[:supports_escalation]
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
= form.label :milestone_id, _('Milestone'), class: "col-12"
|
= form.label :milestone_id, _('Milestone'), class: "col-12"
|
||||||
.col-12
|
.col-12
|
||||||
.issuable-form-select-holder
|
.issuable-form-select-holder
|
||||||
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "qa-issuable-milestone-dropdown js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
|
= render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_upcoming: false, show_started: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input", dropdown_title: _('Select milestone')
|
||||||
|
|
||||||
.form-group.row
|
.form-group.row
|
||||||
= form.label :label_ids, _('Labels'), class: "col-12"
|
= form.label :label_ids, _('Labels'), class: "col-12"
|
||||||
|
|
|
@ -8,4 +8,4 @@
|
||||||
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
|
= hidden_field_tag "#{issuable.to_ability_name}[assignee_ids][]", 0, id: nil, data: { meta: '' }
|
||||||
|
|
||||||
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
|
= dropdown_tag(users_dropdown_label(issuable.assignees), options: assignees_dropdown_options(issuable.to_ability_name))
|
||||||
= link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 qa-assign-to-me-link #{'hide' if issuable.assignees.include?(current_user)}"
|
= link_to _('Assign to me'), '#', class: "assign-to-me-link gl-white-space-nowrap gl-pl-4 #{'hide' if issuable.assignees.include?(current_user)}", data: { qa_selector: 'assign_to_me_link' }
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
|
|
||||||
%div{ data: { testid: 'issue-title-input-field' } }
|
%div{ data: { testid: 'issue-title-input-field' } }
|
||||||
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
|
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
|
||||||
autocomplete: 'off', class: 'form-control pad qa-issuable-form-title', dir: 'auto'
|
autocomplete: 'off', class: 'form-control pad', dir: 'auto', data: { qa_selector: 'issuable_form_title_field' }
|
||||||
|
|
||||||
- if issuable.respond_to?(:draft?)
|
- if issuable.respond_to?(:draft?)
|
||||||
.form-text.text-muted
|
.form-text.text-muted
|
||||||
|
|
|
@ -4,20 +4,20 @@
|
||||||
.form-group.row
|
.form-group.row
|
||||||
.col-12
|
.col-12
|
||||||
= f.label :title
|
= f.label :title
|
||||||
= f.text_field :title, class: "gl-form-input form-control js-label-title qa-label-title", required: true, autofocus: true
|
= f.text_field :title, class: "gl-form-input form-control js-label-title", required: true, autofocus: true, data: { qa_selector: 'label_title_field' }
|
||||||
= render_if_exists 'shared/labels/create_label_help_text'
|
= render_if_exists 'shared/labels/create_label_help_text'
|
||||||
|
|
||||||
.form-group.row
|
.form-group.row
|
||||||
.col-12
|
.col-12
|
||||||
= f.label :description
|
= f.label :description
|
||||||
= f.text_field :description, class: "gl-form-input form-control js-quick-submit qa-label-description"
|
= f.text_field :description, class: "gl-form-input form-control js-quick-submit", data: { qa_selector: 'label_description_field' }
|
||||||
.form-group.row
|
.form-group.row
|
||||||
.col-12
|
.col-12
|
||||||
= f.label :color, _("Background color")
|
= f.label :color, _("Background color")
|
||||||
.input-group
|
.input-group
|
||||||
.input-group-prepend
|
.input-group-prepend
|
||||||
.input-group-text.label-color-preview
|
.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
|
.form-text.text-muted
|
||||||
= _('Choose any color.')
|
= _('Choose any color.')
|
||||||
%br
|
%br
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
- if @label.persisted?
|
- if @label.persisted?
|
||||||
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
|
= f.submit _('Save changes'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2'
|
||||||
- else
|
- else
|
||||||
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button qa-label-create-button gl-mr-2'
|
= f.submit _('Create label'), class: 'btn gl-button btn-confirm js-save-button gl-mr-2', data: { qa_selector: 'label_create_button' }
|
||||||
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
|
= link_to _('Cancel'), back_path, class: 'btn gl-button btn-default btn-cancel gl-mr-2'
|
||||||
- if @label.persisted?
|
- if @label.persisted?
|
||||||
- presented_label = @label.present
|
- presented_label = @label.present
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
|
= render Pajamas::ButtonComponent.new(icon: 'search', button_options: { type: "submit", "aria-label" => _('Submit search') })
|
||||||
= render 'shared/labels/sort_dropdown'
|
= render 'shared/labels/sort_dropdown'
|
||||||
- if labels_or_filters && can_admin_label && @project
|
- if labels_or_filters && can_admin_label && @project
|
||||||
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { class: 'qa-label-create-new' }) do
|
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_project_label_path(@project), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
|
||||||
= _('New label')
|
= _('New label')
|
||||||
- if labels_or_filters && can_admin_label && @group
|
- if labels_or_filters && can_admin_label && @group
|
||||||
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { class: 'qa-label-create-new' }) do
|
= render Pajamas::ButtonComponent.new(variant: :confirm, href: new_group_label_path(@group), button_options: { data: { qa_selector: 'create_new_label_button' } }) do
|
||||||
= _('New label')
|
= _('New label')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : ''
|
- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : ''
|
||||||
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...'
|
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : 'Filter by name...'
|
||||||
|
|
||||||
= form_tag filter_projects_path, method: :get, class: 'project-filter-form qa-project-filter-form', id: 'project-filter-form' do |f|
|
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
|
||||||
= search_field_tag :name, params[:name],
|
= search_field_tag :name, params[:name],
|
||||||
placeholder: placeholder,
|
placeholder: placeholder,
|
||||||
class: "project-filter-form-field form-control #{form_field_classes}",
|
class: "project-filter-form-field form-control #{form_field_classes}",
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
%td.merge_access_levels-container
|
%td.merge_access_levels-container
|
||||||
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
|
= hidden_field_tag "allowed_to_merge_#{protected_branch.id}", merge_access_levels.first&.access_level
|
||||||
= dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
|
= dropdown_tag( (merge_access_levels.first&.humanize || 'Select') ,
|
||||||
options: { toggle_class: 'js-allowed-to-merge qa-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
|
options: { toggle_class: 'js-allowed-to-merge', dropdown_class: 'dropdown-menu-selectable js-allowed-to-merge-container capitalize-header',
|
||||||
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
|
data: { field_name: "allowed_to_merge_#{protected_branch.id}", preselected_items: access_levels_data(merge_access_levels) }})
|
||||||
- if user_merge_access_levels.any?
|
- if user_merge_access_levels.any?
|
||||||
%p.small
|
%p.small
|
||||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370795
|
||||||
milestone: '15.3'
|
milestone: '15.3'
|
||||||
type: development
|
type: development
|
||||||
group: group::source code
|
group: group::source code
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367845
|
||||||
milestone: '15.2'
|
milestone: '15.2'
|
||||||
type: ops
|
type: ops
|
||||||
group: group::memory
|
group: group::memory
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/370708
|
||||||
milestone: '15.3'
|
milestone: '15.3'
|
||||||
type: ops
|
type: ops
|
||||||
group: group::gitaly
|
group: group::gitaly
|
||||||
default_enabled: false
|
default_enabled: true
|
||||||
|
|
|
@ -110,6 +110,12 @@
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_success'
|
- 'i_code_review_merge_request_widget_metrics_expand_success'
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_warning'
|
- 'i_code_review_merge_request_widget_metrics_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_failed'
|
- 'i_code_review_merge_request_widget_metrics_expand_failed'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_view'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_success'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
|
||||||
- name: code_review_category_monthly_active_users
|
- name: code_review_category_monthly_active_users
|
||||||
operator: OR
|
operator: OR
|
||||||
source: redis
|
source: redis
|
||||||
|
@ -208,6 +214,12 @@
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_success'
|
- 'i_code_review_merge_request_widget_metrics_expand_success'
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_warning'
|
- 'i_code_review_merge_request_widget_metrics_expand_warning'
|
||||||
- 'i_code_review_merge_request_widget_metrics_expand_failed'
|
- 'i_code_review_merge_request_widget_metrics_expand_failed'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_view'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_full_report_clicked'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_success'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_warning'
|
||||||
|
- 'i_code_review_merge_request_widget_status_checks_expand_failed'
|
||||||
- name: code_review_extension_category_monthly_active_users
|
- name: code_review_extension_category_monthly_active_users
|
||||||
operator: OR
|
operator: OR
|
||||||
source: redis
|
source: redis
|
||||||
|
|
|
@ -361,6 +361,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
|
||||||
get 'alert_management/:id', to: 'alert_management#details', as: 'alert_management_alert'
|
get 'alert_management/:id', to: 'alert_management#details', as: 'alert_management_alert'
|
||||||
|
|
||||||
get 'work_items/*work_items_path' => 'work_items#index', as: :work_items
|
get 'work_items/*work_items_path' => 'work_items#index', as: :work_items
|
||||||
|
get 'work_items/*work_items_path' => 'work_items#index', as: :work_item
|
||||||
|
|
||||||
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'
|
post 'incidents/integrations/pagerduty', to: 'incident_management/pager_duty_incidents#create'
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,7 @@ scope '-/users', module: :users do
|
||||||
end
|
end
|
||||||
|
|
||||||
resources :callouts, only: [:create]
|
resources :callouts, only: [:create]
|
||||||
|
resources :namespace_callouts, only: [:create]
|
||||||
resources :group_callouts, only: [:create]
|
resources :group_callouts, only: [:create]
|
||||||
resources :project_callouts, only: [:create]
|
resources :project_callouts, only: [:create]
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,6 +16,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
> - Custom HTTP headers API [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366524) in GitLab 15.3. [Feature flag `streaming_audit_event_headers`](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) removed.
|
> - Custom HTTP headers API [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/366524) in GitLab 15.3. [Feature flag `streaming_audit_event_headers`](https://gitlab.com/gitlab-org/gitlab/-/issues/362941) removed.
|
||||||
> - Custom HTTP headers UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default.
|
> - Custom HTTP headers UI [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/361630) in GitLab 15.2 [with a flag](feature_flags.md) named `custom_headers_streaming_audit_events_ui`. Disabled by default.
|
||||||
> - Custom HTTP headers UI [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) in GitLab 15.3. [Feature flag `custom_headers_streaming_audit_events_ui`](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) removed.
|
> - Custom HTTP headers UI [made generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) in GitLab 15.3. [Feature flag `custom_headers_streaming_audit_events_ui`](https://gitlab.com/gitlab-org/gitlab/-/issues/365259) removed.
|
||||||
|
> - [Improved user experience](https://gitlab.com/gitlab-org/gitlab/-/issues/367963) in GitLab 15.4.
|
||||||
|
|
||||||
Users can set a streaming destination for a top-level group to receive all audit events about the group, its subgroups, and
|
Users can set a streaming destination for a top-level group to receive all audit events about the group, its subgroups, and
|
||||||
projects as structured JSON.
|
projects as structured JSON.
|
||||||
|
@ -40,13 +41,12 @@ Users with the Owner role for a group can add streaming destinations for it:
|
||||||
1. On the top bar, select **Menu > Groups** and find your group.
|
1. On the top bar, select **Menu > Groups** and find your group.
|
||||||
1. On the left sidebar, select **Security & Compliance > Audit events**.
|
1. On the left sidebar, select **Security & Compliance > Audit events**.
|
||||||
1. On the main area, select **Streams** tab.
|
1. On the main area, select **Streams** tab.
|
||||||
- When the destination list is empty, select **Add stream** to show the section for adding destinations.
|
1. Select **Add streaming destination** to show the section for adding destinations.
|
||||||
- When the destination list is not empty, select **Add stream** (**{plus}**) to show the section for adding destinations.
|
|
||||||
1. Enter the destination URL to add.
|
1. Enter the destination URL to add.
|
||||||
1. Optional. Locate the **Custom HTTP headers** table.
|
1. Optional. Locate the **Custom HTTP headers** table.
|
||||||
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
|
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
|
||||||
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
|
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
|
||||||
1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
|
1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
|
||||||
20 headers per streaming destination.
|
20 headers per streaming destination.
|
||||||
1. After all headers have been filled out, select **Add** to add the new streaming destination.
|
1. After all headers have been filled out, select **Add** to add the new streaming destination.
|
||||||
|
|
||||||
|
@ -149,7 +149,7 @@ To update a streaming destinations custom HTTP headers:
|
||||||
1. Locate the header that you wish to update.
|
1. Locate the header that you wish to update.
|
||||||
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
|
1. Ignore the **Active** checkbox because it isn't functional. To track progress on adding functionality to the **Active** checkbox, see the
|
||||||
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
|
[relevant issue](https://gitlab.com/gitlab-org/gitlab/-/issues/361925).
|
||||||
1. Enter as many name and value pairs as required. When you enter a unique name and a value for a header, a new row in the table automatically appears. You can add up to
|
1. Select **Add header** to create a new name and value pair. Enter as many name and value pairs as required. You can add up to
|
||||||
20 headers per streaming destination.
|
20 headers per streaming destination.
|
||||||
1. Select **Save** to update the streaming destination.
|
1. Select **Save** to update the streaming destination.
|
||||||
|
|
||||||
|
|
|
@ -685,8 +685,12 @@ To see if GitLab can access the repository file system directly, we use the foll
|
||||||
- GitLab Rails tries to read the metadata file directly. If it exists, and if the UUID's match,
|
- GitLab Rails tries to read the metadata file directly. If it exists, and if the UUID's match,
|
||||||
assume we have direct access.
|
assume we have direct access.
|
||||||
|
|
||||||
Direct Git access is enable by default in Omnibus GitLab because it fills in the correct repository
|
Versions of GitLab 15.3 and later disable direct Git access by default.
|
||||||
paths in the GitLab configuration file `config/gitlab.yml`. This satisfies the UUID check.
|
|
||||||
|
For versions of GitLab prior to 15.3, direct Git access is enabled by
|
||||||
|
default in Omnibus GitLab because it fills in the correct repository
|
||||||
|
paths in the GitLab configuration file `config/gitlab.yml`. This
|
||||||
|
satisfies the UUID check.
|
||||||
|
|
||||||
### Transition to Gitaly Cluster
|
### Transition to Gitaly Cluster
|
||||||
|
|
||||||
|
|
|
@ -98,9 +98,20 @@ NFS performance with GitLab can in some cases be improved with
|
||||||
[direct Git access](gitaly/index.md#direct-access-to-git-in-gitlab) using
|
[direct Git access](gitaly/index.md#direct-access-to-git-in-gitlab) using
|
||||||
[Rugged](https://github.com/libgit2/rugged).
|
[Rugged](https://github.com/libgit2/rugged).
|
||||||
|
|
||||||
From GitLab 12.1, GitLab automatically detects if Rugged can and should be used per storage.
|
Versions of GitLab after 12.2 and prior to 15.3 automatically detect if
|
||||||
If you previously enabled Rugged using the feature flag and you want to use automatic detection instead,
|
Rugged can and should be used per storage.
|
||||||
you must unset the feature flag:
|
|
||||||
|
NOTE:
|
||||||
|
GitLab 15.3 and later disables this automatic detection. Auto-detection can be enabled via the
|
||||||
|
`skip_rugged_auto_detect` feature flag:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
Feature.disable(:skip_rugged_auto_detect)
|
||||||
|
```
|
||||||
|
|
||||||
|
In addition, if you previously enabled Rugged using the feature flag and
|
||||||
|
you want to use automatic detection instead, you must unset the feature
|
||||||
|
flag:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
sudo gitlab-rake gitlab:features:unset_rugged
|
sudo gitlab-rake gitlab:features:unset_rugged
|
||||||
|
|
|
@ -423,16 +423,6 @@ projects = Project.find_by_sql("SELECT * FROM projects WHERE name LIKE '%ject'")
|
||||||
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
|
=> [#<Project id:12 root/my-first-project>>, #<Project id:13 root/my-second-project>>]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Issue boards
|
|
||||||
|
|
||||||
### In case of issue boards not loading properly and it's getting time out. Call the Issue Rebalancing service to fix this
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
p = Project.find_by_full_path('<username-or-group>/<project-name>')
|
|
||||||
|
|
||||||
Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
|
|
||||||
```
|
|
||||||
|
|
||||||
## Imports and exports
|
## Imports and exports
|
||||||
|
|
||||||
### Import a project
|
### Import a project
|
||||||
|
|
|
@ -18856,7 +18856,9 @@ Represents vulnerability letter grades with associated projects.
|
||||||
|
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="workitemclosedat"></a>`closedAt` | [`Time`](#time) | Timestamp of when the work item was closed. |
|
||||||
| <a id="workitemconfidential"></a>`confidential` | [`Boolean!`](#boolean) | Indicates the work item is confidential. |
|
| <a id="workitemconfidential"></a>`confidential` | [`Boolean!`](#boolean) | Indicates the work item is confidential. |
|
||||||
|
| <a id="workitemcreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the work item was created. |
|
||||||
| <a id="workitemdescription"></a>`description` | [`String`](#string) | Description of the work item. |
|
| <a id="workitemdescription"></a>`description` | [`String`](#string) | Description of the work item. |
|
||||||
| <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
|
| <a id="workitemdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `description`. |
|
||||||
| <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
| <a id="workitemid"></a>`id` | [`WorkItemID!`](#workitemid) | Global ID of the work item. |
|
||||||
|
@ -18865,6 +18867,7 @@ Represents vulnerability letter grades with associated projects.
|
||||||
| <a id="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. |
|
| <a id="workitemstate"></a>`state` | [`WorkItemState!`](#workitemstate) | State of the work item. |
|
||||||
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
|
| <a id="workitemtitle"></a>`title` | [`String!`](#string) | Title of the work item. |
|
||||||
| <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
| <a id="workitemtitlehtml"></a>`titleHtml` | [`String`](#string) | The GitLab Flavored Markdown rendering of `title`. |
|
||||||
|
| <a id="workitemupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the work item was last updated. |
|
||||||
| <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. |
|
| <a id="workitemuserpermissions"></a>`userPermissions` | [`WorkItemPermissions!`](#workitempermissions) | Permissions for the current user on the resource. |
|
||||||
| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. |
|
| <a id="workitemwidgets"></a>`widgets` | [`[WorkItemWidget!]`](#workitemwidget) | Collection of widgets that belong to the work item. |
|
||||||
| <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |
|
| <a id="workitemworkitemtype"></a>`workItemType` | [`WorkItemType!`](#workitemtype) | Type assigned to the work item. |
|
||||||
|
|
|
@ -76,7 +76,7 @@ downstream project (`my/deployment`) too. If the downstream project is not found
|
||||||
or the user does not have [permission](../../user/permissions.md) to create a pipeline there,
|
or the user does not have [permission](../../user/permissions.md) to create a pipeline there,
|
||||||
the `staging` job is marked as _failed_.
|
the `staging` job is marked as _failed_.
|
||||||
|
|
||||||
#### Trigger job configuration keywords
|
#### Trigger job configuration limitations
|
||||||
|
|
||||||
Trigger jobs can use only a limited set of the GitLab CI/CD [configuration keywords](../yaml/index.md).
|
Trigger jobs can use only a limited set of the GitLab CI/CD [configuration keywords](../yaml/index.md).
|
||||||
The keywords available for use in trigger jobs are:
|
The keywords available for use in trigger jobs are:
|
||||||
|
@ -90,6 +90,8 @@ The keywords available for use in trigger jobs are:
|
||||||
- [`extends`](../yaml/index.md#extends)
|
- [`extends`](../yaml/index.md#extends)
|
||||||
- [`needs`](../yaml/index.md#needs), but not [`needs:project`](../yaml/index.md#needsproject)
|
- [`needs`](../yaml/index.md#needs), but not [`needs:project`](../yaml/index.md#needsproject)
|
||||||
|
|
||||||
|
Trigger jobs cannot use [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables).
|
||||||
|
|
||||||
#### Specify a downstream pipeline branch
|
#### Specify a downstream pipeline branch
|
||||||
|
|
||||||
You can specify a branch name for the downstream pipeline to use.
|
You can specify a branch name for the downstream pipeline to use.
|
||||||
|
@ -182,9 +184,12 @@ downstream-job:
|
||||||
trigger: my/project
|
trigger: my/project
|
||||||
```
|
```
|
||||||
|
|
||||||
In this scenario, the `UPSTREAM_BRANCH` variable with a value related to the
|
In this scenario, the `UPSTREAM_BRANCH` variable with the value of the upstream pipeline's
|
||||||
upstream pipeline is passed to the `downstream-job` job. It is available
|
`$CI_COMMIT_REF_NAME` is passed to `downstream-job`. It is available in the
|
||||||
in the context of all downstream builds.
|
context of all downstream builds.
|
||||||
|
|
||||||
|
You cannot use this method to forward [job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
|
||||||
|
to a downstream pipeline, as they are not available in trigger jobs.
|
||||||
|
|
||||||
Upstream pipelines take precedence over downstream ones. If there are two
|
Upstream pipelines take precedence over downstream ones. If there are two
|
||||||
variables with the same name defined in both upstream and downstream projects,
|
variables with the same name defined in both upstream and downstream projects,
|
||||||
|
|
|
@ -722,6 +722,9 @@ variables:
|
||||||
|
|
||||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28940) in GitLab Runner 15.1.
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/28940) in GitLab Runner 15.1.
|
||||||
|
|
||||||
|
NOTE:
|
||||||
|
Zip archives are the only supported artifact type. Follow [the issue for details](https://gitlab.com/gitlab-org/gitlab/-/issues/367203).
|
||||||
|
|
||||||
GitLab Runner can generate and produce attestation metadata for all build artifacts. To enable this feature, you must set the `RUNNER_GENERATE_ARTIFACTS_METADATA` environment variable to `true`. This variable can either be set globally or it can be set for individual jobs. The metadata is in rendered in a plain text `.json` file that's stored with the artifact. The file name is as follows: `{JOB_ID}-artifacts-metadata.json`.
|
GitLab Runner can generate and produce attestation metadata for all build artifacts. To enable this feature, you must set the `RUNNER_GENERATE_ARTIFACTS_METADATA` environment variable to `true`. This variable can either be set globally or it can be set for individual jobs. The metadata is in rendered in a plain text `.json` file that's stored with the artifact. The file name is as follows: `{JOB_ID}-artifacts-metadata.json`.
|
||||||
|
|
||||||
### Attestation format
|
### Attestation format
|
||||||
|
|
|
@ -132,10 +132,17 @@ These restrictions exist because `after_script` scripts are executed in a
|
||||||
|
|
||||||
## Persisted variables
|
## Persisted variables
|
||||||
|
|
||||||
The following variables are known as "persisted":
|
Some predefined variables are called "persisted".
|
||||||
|
|
||||||
|
Pipeline-level persisted variables:
|
||||||
|
|
||||||
- `CI_PIPELINE_ID`
|
- `CI_PIPELINE_ID`
|
||||||
|
- `CI_PIPELINE_URL`
|
||||||
|
|
||||||
|
Job-level persisted variables:
|
||||||
|
|
||||||
- `CI_JOB_ID`
|
- `CI_JOB_ID`
|
||||||
|
- `CI_JOB_URL`
|
||||||
- `CI_JOB_TOKEN`
|
- `CI_JOB_TOKEN`
|
||||||
- `CI_JOB_STARTED_AT`
|
- `CI_JOB_STARTED_AT`
|
||||||
- `CI_REGISTRY_USER`
|
- `CI_REGISTRY_USER`
|
||||||
|
@ -144,7 +151,7 @@ The following variables are known as "persisted":
|
||||||
- `CI_DEPLOY_USER`
|
- `CI_DEPLOY_USER`
|
||||||
- `CI_DEPLOY_PASSWORD`
|
- `CI_DEPLOY_PASSWORD`
|
||||||
|
|
||||||
They are:
|
Persisted variables are:
|
||||||
|
|
||||||
- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
|
- Supported for definitions where the ["Expansion place"](#gitlab-ciyml-file) is:
|
||||||
- Runner.
|
- Runner.
|
||||||
|
@ -153,6 +160,9 @@ They are:
|
||||||
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
|
- For definitions where the ["Expansion place"](#gitlab-ciyml-file) is GitLab.
|
||||||
- In the `only`, `except`, and `rules` [variables expressions](../jobs/job_control.md#cicd-variable-expressions).
|
- In the `only`, `except`, and `rules` [variables expressions](../jobs/job_control.md#cicd-variable-expressions).
|
||||||
|
|
||||||
|
[Pipeline trigger jobs](../yaml/index.md#trigger) cannot use job-level persisted variables,
|
||||||
|
but can use pipeline-level persisted variables.
|
||||||
|
|
||||||
Some of the persisted variables contain tokens and cannot be used by some definitions
|
Some of the persisted variables contain tokens and cannot be used by some definitions
|
||||||
due to security reasons.
|
due to security reasons.
|
||||||
|
|
||||||
|
|
|
@ -3933,6 +3933,8 @@ trigger_job:
|
||||||
and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule)
|
and [scheduled pipeline variables](../pipelines/schedules.md#add-a-pipeline-schedule)
|
||||||
are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward)
|
are not passed to downstream pipelines by default. Use [trigger:forward](#triggerforward)
|
||||||
to forward these variables to downstream pipelines.
|
to forward these variables to downstream pipelines.
|
||||||
|
- [Job-level persisted variables](../variables/where_variables_can_be_used.md#persisted-variables)
|
||||||
|
are not available in trigger jobs.
|
||||||
|
|
||||||
**Related topics**:
|
**Related topics**:
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ If the `down` method requires adding back any dropped indexes or constraints, th
|
||||||
be done within a transactional migration, then the migration would look like this:
|
be done within a transactional migration, then the migration would look like this:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[1.0]
|
class RemoveUsersUpdatedAtColumn < Gitlab::Database::Migration[2.0]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
@ -158,7 +158,7 @@ renaming. For example
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# A regular migration in db/migrate
|
# A regular migration in db/migrate
|
||||||
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[1.0]
|
class RenameUsersUpdatedAtToUpdatedAtTimestamp < Gitlab::Database::Migration[2.0]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
@ -186,7 +186,7 @@ We can perform this cleanup using
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# A post-deployment migration in db/post_migrate
|
# A post-deployment migration in db/post_migrate
|
||||||
class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[1.0]
|
class CleanupUsersUpdatedAtRename < Gitlab::Database::Migration[2.0]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
@ -233,7 +233,7 @@ as follows:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# A regular migration in db/migrate
|
# A regular migration in db/migrate
|
||||||
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[1.0]
|
class ChangeUsersUsernameStringToText < Gitlab::Database::Migration[2.0]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
@ -252,7 +252,7 @@ Next we need to clean up our changes using a post-deployment migration:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
# A post-deployment migration in db/post_migrate
|
# A post-deployment migration in db/post_migrate
|
||||||
class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[1.0]
|
class ChangeUsersUsernameStringToTextCleanup < Gitlab::Database::Migration[2.0]
|
||||||
disable_ddl_transaction!
|
disable_ddl_transaction!
|
||||||
|
|
||||||
def up
|
def up
|
||||||
|
|
|
@ -177,6 +177,7 @@ Troubleshooting can be one of three categories:
|
||||||
```
|
```
|
||||||
|
|
||||||
If multiple causes or workarounds exist, consider putting them into a table format.
|
If multiple causes or workarounds exist, consider putting them into a table format.
|
||||||
|
If you use the exact error message, surround it in backticks so it's styled as code.
|
||||||
|
|
||||||
If a page has more than five troubleshooting topics, put the content on a separate page that has troubleshooting information exclusively. Name the page `Troubleshooting <featurename>`.
|
If a page has more than five troubleshooting topics, put the content on a separate page that has troubleshooting information exclusively. Name the page `Troubleshooting <featurename>`.
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,13 @@ Also, do not use links as part of heading text.
|
||||||
|
|
||||||
See also [heading guidelines for specific topic types](../structure.md).
|
See also [heading guidelines for specific topic types](../structure.md).
|
||||||
|
|
||||||
|
### Backticks in Markdown
|
||||||
|
|
||||||
|
Use backticks for:
|
||||||
|
|
||||||
|
- [Code blocks](#code-blocks).
|
||||||
|
- Error messages.
|
||||||
|
|
||||||
### Markdown Rules
|
### Markdown Rules
|
||||||
|
|
||||||
GitLab ensures that the Markdown used across all documentation is consistent, as
|
GitLab ensures that the Markdown used across all documentation is consistent, as
|
||||||
|
|
|
@ -280,7 +280,7 @@ You can use Vale:
|
||||||
|
|
||||||
Vale returns three types of results:
|
Vale returns three types of results:
|
||||||
|
|
||||||
- **Error** - For branding and trademark issues, words or phrases with ambiguous meanings, and anything that causes content on
|
- **Error** - For branding guidelines, trademark guidelines, and anything that causes content on
|
||||||
the docs site to render incorrectly.
|
the docs site to render incorrectly.
|
||||||
- **Warning** - For Technical Writing team style preferences.
|
- **Warning** - For Technical Writing team style preferences.
|
||||||
- **Suggestion** - For basic technical writing tenets and best practices.
|
- **Suggestion** - For basic technical writing tenets and best practices.
|
||||||
|
@ -337,6 +337,29 @@ general complexity level of the page.
|
||||||
The readability score is calculated based on the number of words per sentence, and the number
|
The readability score is calculated based on the number of words per sentence, and the number
|
||||||
of syllables per word. For more information, see [the Vale documentation](https://vale.sh/docs/topics/styles/#metric).
|
of syllables per word. For more information, see [the Vale documentation](https://vale.sh/docs/topics/styles/#metric).
|
||||||
|
|
||||||
|
#### When to add a new Vale rule
|
||||||
|
|
||||||
|
It's tempting to add a Vale rule for every style guide rule. However, we should be
|
||||||
|
mindful of the effort to create and enforce a Vale rule, and the noise it creates.
|
||||||
|
|
||||||
|
In general, follow these guidelines:
|
||||||
|
|
||||||
|
- If you add an [error-level Vale rule](#vale-result-types), you must fix
|
||||||
|
the existing occurrences of the issue in the documentation before you can add the rule.
|
||||||
|
|
||||||
|
If there are too many issues to fix in a single merge request, add the rule at a
|
||||||
|
`warning` level. Then, fix the existing issues in follow-up merge requests.
|
||||||
|
When the issues are fixed, promote the rule to an `error`.
|
||||||
|
|
||||||
|
- If you add a warning-level or suggestion-level rule, consider:
|
||||||
|
|
||||||
|
- How many more warnings or suggestions it creates in the Vale output. If the
|
||||||
|
number of additional warnings is significant, the rule might be too broad.
|
||||||
|
|
||||||
|
- How often an author might ignore it because it's acceptable in the context.
|
||||||
|
If the rule is too subjective, it cannot be adequately enforced and creates
|
||||||
|
unnecessary additional warnings.
|
||||||
|
|
||||||
### Install linters
|
### Install linters
|
||||||
|
|
||||||
At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in
|
At a minimum, install [markdownlint](#markdownlint) and [Vale](#vale) to match the checks run in
|
||||||
|
|
|
@ -25,7 +25,7 @@ To ensure access to your cluster is safe:
|
||||||
- Each agent has a separate context (`kubecontext`).
|
- Each agent has a separate context (`kubecontext`).
|
||||||
- Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
|
- Only the project where the agent is configured, and any additional projects you authorize, can access the agent in your cluster.
|
||||||
|
|
||||||
You do not need to have a runner in the cluster with the agent.
|
The CI/CD workflow requires runners to be registered with GitLab, but these runners do not have to be in the cluster where the agent is.
|
||||||
|
|
||||||
## GitLab CI/CD workflow steps
|
## GitLab CI/CD workflow steps
|
||||||
|
|
||||||
|
|
|
@ -642,3 +642,20 @@ A few things to remember:
|
||||||
- For performance and visibility reasons, each list shows the first 20 issues
|
- For performance and visibility reasons, each list shows the first 20 issues
|
||||||
by default. If you have more than 20 issues, start scrolling down and the next
|
by default. If you have more than 20 issues, start scrolling down and the next
|
||||||
20 appear.
|
20 appear.
|
||||||
|
|
||||||
|
## Troubleshooting issue boards
|
||||||
|
|
||||||
|
### Use Rails console to fix issue boards not loading and timing out
|
||||||
|
|
||||||
|
If you see issue board not loading and timing out in UI, use Rails console to call the Issue Rebalancing service to fix it:
|
||||||
|
|
||||||
|
1. [Start a Rails console session](../../administration/operations/rails_console.md#starting-a-rails-console-session).
|
||||||
|
1. Run these commands:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
p = Project.find_by_full_path('<username-or-group>/<project-name>')
|
||||||
|
|
||||||
|
Issues::RelativePositionRebalancingService.new(p.root_namespace.all_projects).execute
|
||||||
|
```
|
||||||
|
|
||||||
|
1. To exit the Rails console, type `quit`.
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB |
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -37,7 +37,7 @@ To link one issue to another:
|
||||||
- **[is blocked by](#blocking-issues)**
|
- **[is blocked by](#blocking-issues)**
|
||||||
1. Input the issue number or paste in the full URL of the issue.
|
1. Input the issue number or paste in the full URL of the issue.
|
||||||
|
|
||||||
![Adding a related issue](img/related_issues_add_v12_8.png)
|
![Adding a related issue](img/related_issues_add_v15_3.png)
|
||||||
|
|
||||||
Issues of the same project can be specified just by the reference number.
|
Issues of the same project can be specified just by the reference number.
|
||||||
Issues from a different project require additional information like the
|
Issues from a different project require additional information like the
|
||||||
|
@ -54,7 +54,7 @@ To link one issue to another:
|
||||||
When you have finished adding all linked issues, you can see
|
When you have finished adding all linked issues, you can see
|
||||||
them categorized so their relationships can be better understood visually.
|
them categorized so their relationships can be better understood visually.
|
||||||
|
|
||||||
![Related issue block](img/related_issue_block_v12_8.png)
|
![Related issue block](img/related_issue_block_v15_3.png)
|
||||||
|
|
||||||
You can also add a linked issue from a commit message or the description in another issue or MR.
|
You can also add a linked issue from a commit message or the description in another issue or MR.
|
||||||
[Learn more about crosslinking issues](crosslinking_issues.md).
|
[Learn more about crosslinking issues](crosslinking_issues.md).
|
||||||
|
@ -66,7 +66,7 @@ right-side of each issue token to remove.
|
||||||
|
|
||||||
Due to the bi-directional relationship, the relationship no longer appears in either issue.
|
Due to the bi-directional relationship, the relationship no longer appears in either issue.
|
||||||
|
|
||||||
![Removing a related issue](img/related_issues_remove_v12_8.png)
|
![Removing a related issue](img/related_issues_remove_v15_3.png)
|
||||||
|
|
||||||
Access our [permissions](../../permissions.md) page for more information.
|
Access our [permissions](../../permissions.md) page for more information.
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
## Namespace storage limit
|
## Namespace storage limit
|
||||||
|
|
||||||
Namespaces on a GitLab SaaS Free tier have a 5 GB storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
|
Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
|
||||||
This limit is not visible on the storage quota page, but we plan to make it visible and enforced starting October 19, 2022.
|
This limit is not visible on the Usage quotas page, but will be prior to [enforcement](#namespace-storage-limit-enforcement-schedule). Self-managed deployments are not affected.
|
||||||
|
|
||||||
Storage types that add to the total namespace storage are:
|
Storage types that add to the total namespace storage are:
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ Storage types that add to the total namespace storage are:
|
||||||
- Artifacts
|
- Artifacts
|
||||||
- Container registry
|
- Container registry
|
||||||
- Package registry
|
- Package registry
|
||||||
- Dependecy proxy
|
- Dependency proxy
|
||||||
- Wiki
|
- Wiki
|
||||||
- Snippets
|
- Snippets
|
||||||
|
|
||||||
|
@ -30,30 +30,18 @@ If your total namespace storage exceeds the available namespace storage quota, a
|
||||||
|
|
||||||
To prevent exceeding the namespace storage quota, you can:
|
To prevent exceeding the namespace storage quota, you can:
|
||||||
|
|
||||||
1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
|
1. Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page.
|
||||||
1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier).
|
1. Apply for [GitLab for Education](https://about.gitlab.com/solutions/education/join/), [GitLab for Open Source](https://about.gitlab.com/solutions/open-source/join/), or [GitLab for Startups](https://about.gitlab.com/solutions/startups/) if you meet the eligibility requirements.
|
||||||
1. [Reduce storage usage](#manage-your-storage-usage).
|
1. Consider using a [self-managed instance](../subscriptions/self_managed/) of GitLab which does not have these limits on the free tier.
|
||||||
|
1. [Purchase additional storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) units at $60/year for 10GB of storage.
|
||||||
|
1. [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality.
|
||||||
|
1. [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) to learn more about your options and ask questions.
|
||||||
|
|
||||||
### Namespace storage limit enforcement schedule
|
### Namespace storage limit enforcement schedule
|
||||||
|
|
||||||
Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces.
|
Storage limits for GitLab SaaS Free tier namespaces will not be enforced prior to 2022-10-19. Storage limits for GitLab SaaS Paid tier namespaces will not be enforced for prior to 2023-02-15.
|
||||||
We will start with a large limit enforcement and eventually reduce it to 5 GB.
|
|
||||||
|
|
||||||
Impacted users are notified via email and in-app notifications will begin 2022-08-22.
|
Impacted users are notified via email and in-app notifications at least 60 days prior to enforcement.
|
||||||
Only GitLab SaaS users are impacted - the limits are not applicable to self-managed users.
|
|
||||||
|
|
||||||
The following table describes the enforcement schedule, which is subject to change.
|
|
||||||
|
|
||||||
| Planned enforcement date | Limit | Status |
|
|
||||||
| ----------------------- | ----- | ------ |
|
|
||||||
| October 19, 2022 | 45,000 GB | Not enforced |
|
|
||||||
| October 20, 2022 | 7,500 GB | Not enforced |
|
|
||||||
| October 24, 2022 | 500 GB | Not enforced |
|
|
||||||
| October 27, 2022 | 75 GB | Not enforced |
|
|
||||||
| November 2, 2022 | 10 GB | Not enforced |
|
|
||||||
| November 9, 2022 | 5 GB | Not enforced |
|
|
||||||
|
|
||||||
Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage).
|
|
||||||
|
|
||||||
### Project storage limit
|
### Project storage limit
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ module Gitlab
|
||||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent
|
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Projects::ProjectTransferedEvent
|
||||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
|
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupTransferedEvent
|
||||||
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
|
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupPathChangedEvent
|
||||||
|
store.subscribe ::Pages::InvalidateDomainCacheWorker, to: ::Groups::GroupDeletedEvent
|
||||||
|
|
||||||
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent
|
store.subscribe ::MergeRequests::CreateApprovalEventWorker, to: ::MergeRequests::ApprovedEvent
|
||||||
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent
|
store.subscribe ::MergeRequests::CreateApprovalNoteWorker, to: ::MergeRequests::ApprovedEvent
|
||||||
|
|
|
@ -425,3 +425,28 @@
|
||||||
redis_slot: code_review
|
redis_slot: code_review
|
||||||
category: code_review
|
category: code_review
|
||||||
aggregation: weekly
|
aggregation: weekly
|
||||||
|
## Status Checks
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_view
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_full_report_clicked
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_expand
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_expand_success
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_expand_warning
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
- name: i_code_review_merge_request_widget_status_checks_expand_failed
|
||||||
|
redis_slot: code_review
|
||||||
|
category: code_review
|
||||||
|
aggregation: weekly
|
||||||
|
|
|
@ -5,7 +5,7 @@ module Gitlab
|
||||||
class MergeRequestWidgetExtensionCounter < BaseCounter
|
class MergeRequestWidgetExtensionCounter < BaseCounter
|
||||||
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
|
KNOWN_EVENTS = %w[view full_report_clicked expand expand_success expand_warning expand_failed].freeze
|
||||||
PREFIX = 'i_code_review_merge_request_widget'
|
PREFIX = 'i_code_review_merge_request_widget'
|
||||||
WIDGETS = %w[accessibility code_quality terraform test_summary metrics].freeze
|
WIDGETS = %w[accessibility code_quality status_checks terraform test_summary metrics].freeze
|
||||||
|
|
||||||
class << self
|
class << self
|
||||||
private
|
private
|
||||||
|
|
|
@ -5329,6 +5329,11 @@ msgstr ""
|
||||||
msgid "AuditLogs|User Events"
|
msgid "AuditLogs|User Events"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AuditStreams|%d destination"
|
||||||
|
msgid_plural "AuditStreams|%d destinations"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
msgid "AuditStreams|A header with this name already exists."
|
msgid "AuditStreams|A header with this name already exists."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5344,10 +5349,16 @@ msgstr ""
|
||||||
msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems."
|
msgid "AuditStreams|Add an HTTP endpoint to manage audit logs in third-party systems."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AuditStreams|Add another custom header"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Add external stream destination"
|
msgid "AuditStreams|Add external stream destination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Add stream"
|
msgid "AuditStreams|Add header"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AuditStreams|Add streaming destination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|An error occurred when creating external audit event stream destination. Please try it again."
|
msgid "AuditStreams|An error occurred when creating external audit event stream destination. Please try it again."
|
||||||
|
@ -5365,7 +5376,7 @@ msgstr ""
|
||||||
msgid "AuditStreams|Cancel editing"
|
msgid "AuditStreams|Cancel editing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Custom HTTP headers"
|
msgid "AuditStreams|Custom HTTP headers (optional)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Delete %{link}"
|
msgid "AuditStreams|Delete %{link}"
|
||||||
|
@ -5386,6 +5397,9 @@ msgstr ""
|
||||||
msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached."
|
msgid "AuditStreams|Maximum of %{number} HTTP headers has been reached."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "AuditStreams|Remove custom header"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Save external stream destination"
|
msgid "AuditStreams|Save external stream destination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5395,9 +5409,6 @@ msgstr ""
|
||||||
msgid "AuditStreams|Stream added successfully"
|
msgid "AuditStreams|Stream added successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "AuditStreams|Stream count icon"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "AuditStreams|Stream deleted successfully"
|
msgid "AuditStreams|Stream deleted successfully"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -19443,9 +19454,6 @@ msgstr ""
|
||||||
msgid "How do I configure Akismet?"
|
msgid "How do I configure Akismet?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "How do I configure runners?"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "How do I configure this integration?"
|
msgid "How do I configure this integration?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -43987,6 +43995,9 @@ msgstr ""
|
||||||
msgid "What does this command do?"
|
msgid "What does this command do?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "What is GitLab Runner?"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "What is Markdown?"
|
msgid "What is Markdown?"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
||||||
module Alert
|
module Alert
|
||||||
class AutoDevopsAlert < Page::Base
|
class AutoDevopsAlert < Page::Base
|
||||||
view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do
|
view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do
|
||||||
element :auto_devops_banner
|
element :auto_devops_banner_content
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -23,6 +23,10 @@ module QA
|
||||||
element :create_token_button
|
element :create_token_button
|
||||||
end
|
end
|
||||||
|
|
||||||
|
base.view 'app/views/shared/access_tokens/_table.html.haml' do
|
||||||
|
element :revoke_button
|
||||||
|
end
|
||||||
|
|
||||||
base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
|
base.view 'app/views/shared/tokens/_scopes_form.html.haml' do
|
||||||
element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
|
element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ module QA
|
||||||
super
|
super
|
||||||
|
|
||||||
base.view 'app/views/shared/groups/_search_form.html.haml' do
|
base.view 'app/views/shared/groups/_search_form.html.haml' do
|
||||||
element :groups_filter
|
element :groups_filter_field
|
||||||
end
|
end
|
||||||
|
|
||||||
base.view 'app/assets/javascripts/groups/components/groups.vue' do
|
base.view 'app/assets/javascripts/groups/components/groups.vue' do
|
||||||
|
@ -22,7 +22,7 @@ module QA
|
||||||
|
|
||||||
def has_filtered_group?(name)
|
def has_filtered_group?(name)
|
||||||
# Filter and submit to reload the page and only retrieve the filtered results
|
# Filter and submit to reload the page and only retrieve the filtered results
|
||||||
find_element(:groups_filter).set(name).send_keys(:return)
|
find_element(:groups_filter_field).set(name).send_keys(:return)
|
||||||
|
|
||||||
# Since we submitted after filtering, the presence of
|
# Since we submitted after filtering, the presence of
|
||||||
# groups_list_tree_container means we have the complete filtered list
|
# groups_list_tree_container means we have the complete filtered list
|
||||||
|
|
|
@ -35,7 +35,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
base.view 'app/views/shared/issuable/_sidebar.html.haml' do
|
base.view 'app/views/shared/issuable/_sidebar.html.haml' do
|
||||||
element :assignee_block
|
element :assignee_block_container
|
||||||
element :milestone_block
|
element :milestone_block
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -127,7 +127,7 @@ module QA
|
||||||
private
|
private
|
||||||
|
|
||||||
def wait_assignees_block_finish_loading
|
def wait_assignees_block_finish_loading
|
||||||
within_element(:assignee_block) do
|
within_element(:assignee_block_container) do
|
||||||
wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
|
wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
|
||||||
finished_loading_block?
|
finished_loading_block?
|
||||||
yield
|
yield
|
||||||
|
|
|
@ -6,13 +6,8 @@ module QA
|
||||||
class Groups < Page::Base
|
class Groups < Page::Base
|
||||||
include Page::Component::GroupsFilter
|
include Page::Component::GroupsFilter
|
||||||
|
|
||||||
view 'app/views/shared/groups/_search_form.html.haml' do
|
|
||||||
element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern
|
|
||||||
element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern
|
|
||||||
end
|
|
||||||
|
|
||||||
view 'app/views/dashboard/_groups_head.html.haml' do
|
view 'app/views/dashboard/_groups_head.html.haml' do
|
||||||
element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern
|
element :new_group_button
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_group?(name)
|
def has_group?(name)
|
||||||
|
@ -26,7 +21,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_new_group
|
def click_new_group
|
||||||
click_on 'New group'
|
click_element(:new_group_button)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,7 +5,7 @@ module QA
|
||||||
module Dashboard
|
module Dashboard
|
||||||
class Projects < Page::Base
|
class Projects < Page::Base
|
||||||
view 'app/views/shared/projects/_search_form.html.haml' do
|
view 'app/views/shared/projects/_search_form.html.haml' do
|
||||||
element :project_filter_form, required: true
|
element :project_filter_form_container, required: true
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/projects/_project.html.haml' do
|
view 'app/views/shared/projects/_project.html.haml' do
|
||||||
|
@ -24,7 +24,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_by_name(name)
|
def filter_by_name(name)
|
||||||
within_element(:project_filter_form) do
|
within_element(:project_filter_form_container) do
|
||||||
fill_in :name, with: name
|
fill_in :name, with: name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -44,7 +44,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def clear_project_filter
|
def clear_project_filter
|
||||||
fill_element(:project_filter_form, "")
|
fill_element(:project_filter_form_container, "")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,11 +5,7 @@ module QA
|
||||||
module Issuable
|
module Issuable
|
||||||
class New < Page::Base
|
class New < Page::Base
|
||||||
view 'app/views/shared/issuable/form/_title.html.haml' do
|
view 'app/views/shared/issuable/form/_title.html.haml' do
|
||||||
element :issuable_form_title
|
element :issuable_form_title_field
|
||||||
end
|
|
||||||
|
|
||||||
view 'app/views/shared/issuable/form/_metadata.html.haml' do
|
|
||||||
element :issuable_milestone_dropdown
|
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/form_elements/_description.html.haml' do
|
view 'app/views/shared/form_elements/_description.html.haml' do
|
||||||
|
@ -17,11 +13,12 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do
|
view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do
|
||||||
element :issuable_dropdown_menu_milestone
|
element :issuable_milestone_dropdown
|
||||||
|
element :issuable_milestone_dropdown_content
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/issuable/_label_dropdown.html.haml' do
|
view 'app/views/shared/issuable/_label_dropdown.html.haml' do
|
||||||
element :issuable_label
|
element :issuable_label_dropdown
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
|
view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do
|
||||||
|
@ -33,7 +30,7 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_title(title)
|
def fill_title(title)
|
||||||
fill_element :issuable_form_title, title
|
fill_element :issuable_form_title_field, title
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_description(description)
|
def fill_description(description)
|
||||||
|
@ -42,7 +39,7 @@ module QA
|
||||||
|
|
||||||
def choose_milestone(milestone)
|
def choose_milestone(milestone)
|
||||||
click_element :issuable_milestone_dropdown
|
click_element :issuable_milestone_dropdown
|
||||||
within_element(:issuable_dropdown_menu_milestone) do
|
within_element(:issuable_milestone_dropdown_content) do
|
||||||
click_on milestone.title
|
click_on milestone.title
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -55,11 +52,11 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def select_label(label)
|
def select_label(label)
|
||||||
click_element :issuable_label
|
click_element :issuable_label_dropdown
|
||||||
|
|
||||||
click_link label.title
|
click_link label.title
|
||||||
|
|
||||||
click_element :issuable_label # So that the dropdown goes away(click away action)
|
click_element :issuable_label_dropdown # So that the dropdown goes away(click away action)
|
||||||
end
|
end
|
||||||
|
|
||||||
def assign_to_me
|
def assign_to_me
|
||||||
|
|
|
@ -7,26 +7,26 @@ module QA
|
||||||
include Component::LazyLoader
|
include Component::LazyLoader
|
||||||
|
|
||||||
view 'app/views/shared/labels/_nav.html.haml' do
|
view 'app/views/shared/labels/_nav.html.haml' do
|
||||||
element :label_create_new
|
element :create_new_label_button
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/empty_states/_labels.html.haml' do
|
view 'app/views/shared/empty_states/_labels.html.haml' do
|
||||||
element :label_svg
|
element :label_svg_content
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/empty_states/_priority_labels.html.haml' do
|
view 'app/views/shared/empty_states/_priority_labels.html.haml' do
|
||||||
element :label_svg
|
element :label_svg_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def click_new_label_button
|
def click_new_label_button
|
||||||
# The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit
|
# The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit
|
||||||
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
|
# This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?)
|
||||||
# before clicking the button.
|
# before clicking the button.
|
||||||
within_element(:label_svg) do
|
within_element(:label_svg_content) do
|
||||||
has_element?(:js_lazy_loaded)
|
has_element?(:js_lazy_loaded)
|
||||||
end
|
end
|
||||||
|
|
||||||
click_element :label_create_new
|
click_element :create_new_label_button
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,9 +5,9 @@ module QA
|
||||||
module Label
|
module Label
|
||||||
class New < Page::Base
|
class New < Page::Base
|
||||||
view 'app/views/shared/labels/_form.html.haml' do
|
view 'app/views/shared/labels/_form.html.haml' do
|
||||||
element :label_title
|
element :label_title_field
|
||||||
element :label_description
|
element :label_description_field
|
||||||
element :label_color
|
element :label_color_field
|
||||||
element :label_create_button
|
element :label_create_button
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -16,15 +16,15 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_title(title)
|
def fill_title(title)
|
||||||
fill_element :label_title, title
|
fill_element :label_title_field, title
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_description(description)
|
def fill_description(description)
|
||||||
fill_element :label_description, description
|
fill_element :label_description_field, description
|
||||||
end
|
end
|
||||||
|
|
||||||
def fill_color(color)
|
def fill_color(color)
|
||||||
fill_element :label_color, color
|
fill_element :label_color_field, color
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -17,10 +17,6 @@ module QA
|
||||||
element :allowed_to_merge_dropdown
|
element :allowed_to_merge_dropdown
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/projects/protected_branches/_update_protected_branch.html.haml' do
|
|
||||||
element :allowed_to_merge
|
|
||||||
end
|
|
||||||
|
|
||||||
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
|
view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do
|
||||||
element :protected_branches_list
|
element :protected_branches_list
|
||||||
end
|
end
|
||||||
|
|
|
@ -67,8 +67,8 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/shared/_ref_switcher.html.haml' do
|
view 'app/views/shared/_ref_switcher.html.haml' do
|
||||||
element :branches_select
|
|
||||||
element :branches_dropdown
|
element :branches_dropdown
|
||||||
|
element :branches_dropdown_content
|
||||||
end
|
end
|
||||||
|
|
||||||
view 'app/views/projects/blob/viewers/_loading.html.haml' do
|
view 'app/views/projects/blob/viewers/_loading.html.haml' do
|
||||||
|
@ -176,9 +176,9 @@ module QA
|
||||||
end
|
end
|
||||||
|
|
||||||
def switch_to_branch(branch_name)
|
def switch_to_branch(branch_name)
|
||||||
find_element(:branches_select).click
|
find_element(:branches_dropdown).click
|
||||||
|
|
||||||
within_element(:branches_dropdown) do
|
within_element(:branches_dropdown_content) do
|
||||||
click_on branch_name
|
click_on branch_name
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
end
|
||||||
|
|
||||||
it 'does not hide the milestone select' do
|
it 'does not hide the milestone select' do
|
||||||
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
|
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ RSpec.describe "User creates issue" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows the milestone select' do
|
it 'shows the milestone select' do
|
||||||
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
|
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'hides the incident help text' do
|
it 'hides the incident help text' do
|
||||||
|
@ -265,7 +265,7 @@ RSpec.describe "User creates issue" do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'shows the milestone select' do
|
it 'shows the milestone select' do
|
||||||
expect(page).to have_selector('.qa-issuable-milestone-dropdown') # rubocop:disable QA/SelectorUsage
|
expect(page).to have_selector('[data-testid="issuable-milestone-dropdown"]')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'hides the weight input' do
|
it 'hides the weight input' do
|
||||||
|
|
|
@ -137,7 +137,7 @@ RSpec.describe 'File blob', :js do
|
||||||
|
|
||||||
context 'when ref switch' do
|
context 'when ref switch' do
|
||||||
def switch_ref_to(ref_name)
|
def switch_ref_to(ref_name)
|
||||||
first('.qa-branches-select').click # rubocop:disable QA/SelectorUsage
|
first('[data-testid="branches-select"]').click
|
||||||
|
|
||||||
page.within '.project-refs-form' do
|
page.within '.project-refs-form' do
|
||||||
click_link ref_name
|
click_link ref_name
|
||||||
|
|
|
@ -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_LABEL,
|
||||||
EVENT_ACTION,
|
EVENT_ACTION,
|
||||||
ENVIRONMENT_SCOPE_LINK_TITLE,
|
ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||||
|
instanceString,
|
||||||
} from '~/ci_variable_list/constants';
|
} from '~/ci_variable_list/constants';
|
||||||
import { mockVariablesWithScopes } from '../mocks';
|
import { mockVariablesWithScopes } from '../mocks';
|
||||||
import ModalStub from '../stubs';
|
import ModalStub from '../stubs';
|
||||||
|
@ -19,6 +20,7 @@ describe('Ci variable modal', () => {
|
||||||
let trackingSpy;
|
let trackingSpy;
|
||||||
|
|
||||||
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
|
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
|
||||||
|
const mockVariables = mockVariablesWithScopes(instanceString);
|
||||||
|
|
||||||
const defaultProvide = {
|
const defaultProvide = {
|
||||||
awsLogoSvgPath: '/logo',
|
awsLogoSvgPath: '/logo',
|
||||||
|
@ -38,6 +40,7 @@ describe('Ci variable modal', () => {
|
||||||
environments: [],
|
environments: [],
|
||||||
mode: ADD_VARIABLE_ACTION,
|
mode: ADD_VARIABLE_ACTION,
|
||||||
selectedVariable: {},
|
selectedVariable: {},
|
||||||
|
variable: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
|
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
|
||||||
|
@ -81,22 +84,22 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the submit button as disabled ', () => {
|
it('shows the submit button as disabled ', () => {
|
||||||
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
|
expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when a key/value pair is present', () => {
|
describe('when a key/value pair is present', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ props: { selectedVariable: mockVariablesWithScopes[0] } });
|
createComponent({ props: { selectedVariable: mockVariables[0] } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the submit button as enabled ', () => {
|
it('shows the submit button as enabled ', () => {
|
||||||
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
|
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('events', () => {
|
describe('events', () => {
|
||||||
const [currentVariable] = mockVariablesWithScopes;
|
const [currentVariable] = mockVariables;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ props: { selectedVariable: currentVariable } });
|
createComponent({ props: { selectedVariable: currentVariable } });
|
||||||
|
@ -123,9 +126,9 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates the protected value to true', () => {
|
it('updates the protected value to true', () => {
|
||||||
expect(
|
expect(findProtectedVariableCheckbox().attributes('data-is-protected-checked')).toBe(
|
||||||
findProtectedVariableCheckbox().attributes('data-is-protected-checked'),
|
'true',
|
||||||
).toBeTruthy();
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -151,7 +154,7 @@ describe('Ci variable modal', () => {
|
||||||
|
|
||||||
describe('Adding a new non-AWS variable', () => {
|
describe('Adding a new non-AWS variable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
|
createComponent({ mountFn: mountExtended, props: { selectedVariable: variable } });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -164,7 +167,7 @@ describe('Ci variable modal', () => {
|
||||||
|
|
||||||
describe('Adding a new AWS variable', () => {
|
describe('Adding a new AWS variable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
const AWSKeyVariable = {
|
const AWSKeyVariable = {
|
||||||
...variable,
|
...variable,
|
||||||
key: AWS_ACCESS_KEY_ID,
|
key: AWS_ACCESS_KEY_ID,
|
||||||
|
@ -183,7 +186,7 @@ describe('Ci variable modal', () => {
|
||||||
describe('Reference warning when adding a variable', () => {
|
describe('Reference warning when adding a variable', () => {
|
||||||
describe('with a $ character', () => {
|
describe('with a $ character', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
const variableWithDollarSign = {
|
const variableWithDollarSign = {
|
||||||
...variable,
|
...variable,
|
||||||
value: 'valueWith$',
|
value: 'valueWith$',
|
||||||
|
@ -201,7 +204,7 @@ describe('Ci variable modal', () => {
|
||||||
|
|
||||||
describe('without a $ character', () => {
|
describe('without a $ character', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
createComponent({
|
createComponent({
|
||||||
mountFn: mountExtended,
|
mountFn: mountExtended,
|
||||||
props: { selectedVariable: variable },
|
props: { selectedVariable: variable },
|
||||||
|
@ -215,7 +218,7 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Editing a variable', () => {
|
describe('Editing a variable', () => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
|
createComponent({ props: { selectedVariable: variable, mode: EDIT_VARIABLE_ACTION } });
|
||||||
|
@ -286,7 +289,7 @@ describe('Ci variable modal', () => {
|
||||||
|
|
||||||
describe('when the mask state is invalid', () => {
|
describe('when the mask state is invalid', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
const invalidMaskVariable = {
|
const invalidMaskVariable = {
|
||||||
...variable,
|
...variable,
|
||||||
value: 'd:;',
|
value: 'd:;',
|
||||||
|
@ -301,7 +304,7 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('disables the submit button', () => {
|
it('disables the submit button', () => {
|
||||||
expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
|
expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the correct error text', () => {
|
it('shows the correct error text', () => {
|
||||||
|
@ -326,7 +329,7 @@ describe('Ci variable modal', () => {
|
||||||
${'unsupported|char'} | ${false} | ${0} | ${null}
|
${'unsupported|char'} | ${false} | ${0} | ${null}
|
||||||
`('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
|
`('Adding a new variable', ({ value, masked, eventSent, trackingErrorProperty }) => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
const invalidKeyVariable = {
|
const invalidKeyVariable = {
|
||||||
...variable,
|
...variable,
|
||||||
value: '',
|
value: '',
|
||||||
|
@ -359,7 +362,7 @@ describe('Ci variable modal', () => {
|
||||||
|
|
||||||
describe('when masked variable has acceptable value', () => {
|
describe('when masked variable has acceptable value', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const [variable] = mockVariablesWithScopes;
|
const [variable] = mockVariables;
|
||||||
const validMaskandKeyVariable = {
|
const validMaskandKeyVariable = {
|
||||||
...variable,
|
...variable,
|
||||||
key: AWS_ACCESS_KEY_ID,
|
key: AWS_ACCESS_KEY_ID,
|
||||||
|
@ -373,7 +376,7 @@ describe('Ci variable modal', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not disable the submit button', () => {
|
it('does not disable the submit button', () => {
|
||||||
expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
|
expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,8 +3,12 @@ import { shallowMount } from '@vue/test-utils';
|
||||||
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
|
import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
|
||||||
import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
|
import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
|
||||||
import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
|
import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
|
||||||
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION } from '~/ci_variable_list/constants';
|
import {
|
||||||
import { createJoinedEnvironments, mapEnvironmentNames } from '~/ci_variable_list/utils';
|
ADD_VARIABLE_ACTION,
|
||||||
|
EDIT_VARIABLE_ACTION,
|
||||||
|
projectString,
|
||||||
|
} from '~/ci_variable_list/constants';
|
||||||
|
import { mapEnvironmentNames } from '~/ci_variable_list/utils';
|
||||||
|
|
||||||
import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
|
import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
|
||||||
|
|
||||||
|
@ -15,7 +19,7 @@ describe('Ci variable table', () => {
|
||||||
areScopedVariablesAvailable: true,
|
areScopedVariablesAvailable: true,
|
||||||
environments: mapEnvironmentNames(mockEnvs),
|
environments: mapEnvironmentNames(mockEnvs),
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
variables: mockVariablesWithScopes,
|
variables: mockVariablesWithScopes(projectString),
|
||||||
};
|
};
|
||||||
|
|
||||||
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
|
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
|
||||||
|
@ -51,7 +55,8 @@ describe('Ci variable table', () => {
|
||||||
|
|
||||||
expect(findCiVariableModal().props()).toEqual({
|
expect(findCiVariableModal().props()).toEqual({
|
||||||
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
|
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
|
||||||
environments: createJoinedEnvironments(defaultProps.variables, defaultProps.environments),
|
environments: defaultProps.environments,
|
||||||
|
variables: defaultProps.variables,
|
||||||
mode: ADD_VARIABLE_ACTION,
|
mode: ADD_VARIABLE_ACTION,
|
||||||
selectedVariable: {},
|
selectedVariable: {},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
|
import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
|
||||||
|
import { projectString } from '~/ci_variable_list/constants';
|
||||||
import { mockVariables } from '../mocks';
|
import { mockVariables } from '../mocks';
|
||||||
|
|
||||||
describe('Ci variable table', () => {
|
describe('Ci variable table', () => {
|
||||||
|
@ -7,7 +8,7 @@ describe('Ci variable table', () => {
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
variables: mockVariables,
|
variables: mockVariables(projectString),
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = ({ props = {} } = {}) => {
|
const createComponent = ({ props = {} } = {}) => {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import { variableTypes } from '~/ci_variable_list/constants';
|
import { variableTypes, instanceString } from '~/ci_variable_list/constants';
|
||||||
|
|
||||||
export const devName = 'dev';
|
export const devName = 'dev';
|
||||||
export const prodName = 'prod';
|
export const prodName = 'prod';
|
||||||
|
|
||||||
export const mockVariables = [
|
export const mockVariables = (kind) => {
|
||||||
|
return [
|
||||||
{
|
{
|
||||||
__typename: 'CiVariable',
|
__typename: `Ci${kind}Variable`,
|
||||||
id: 1,
|
id: 1,
|
||||||
key: 'my-var',
|
key: 'my-var',
|
||||||
masked: false,
|
masked: false,
|
||||||
|
@ -14,7 +15,7 @@ export const mockVariables = [
|
||||||
variableType: variableTypes.variableType,
|
variableType: variableTypes.variableType,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
__typename: 'CiVariable',
|
__typename: `Ci${kind}Variable`,
|
||||||
id: 2,
|
id: 2,
|
||||||
key: 'secret',
|
key: 'secret',
|
||||||
masked: true,
|
masked: true,
|
||||||
|
@ -23,20 +24,22 @@ export const mockVariables = [
|
||||||
variableType: variableTypes.fileType,
|
variableType: variableTypes.fileType,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
};
|
||||||
|
|
||||||
export const mockVariablesWithScopes = mockVariables.map((variable) => {
|
export const mockVariablesWithScopes = (kind) =>
|
||||||
|
mockVariables(kind).map((variable) => {
|
||||||
return { ...variable, environmentScope: '*' };
|
return { ...variable, environmentScope: '*' };
|
||||||
});
|
});
|
||||||
|
|
||||||
const createDefaultVars = ({ withScope = true } = {}) => {
|
const createDefaultVars = ({ withScope = true, kind } = {}) => {
|
||||||
let base = mockVariables;
|
let base = mockVariables(kind);
|
||||||
|
|
||||||
if (withScope) {
|
if (withScope) {
|
||||||
base = mockVariablesWithScopes;
|
base = mockVariablesWithScopes(kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
__typename: 'CiVariableConnection',
|
__typename: `Ci${kind}VariableConnection`,
|
||||||
nodes: base,
|
nodes: base,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -101,7 +104,7 @@ export const mockGroupVariables = {
|
||||||
|
|
||||||
export const mockAdminVariables = {
|
export const mockAdminVariables = {
|
||||||
data: {
|
data: {
|
||||||
ciVariables: createDefaultVars({ withScope: false }),
|
ciVariables: createDefaultVars({ withScope: false, kind: instanceString }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,13 @@ import { allEnvironments } from '~/ci_variable_list/constants';
|
||||||
|
|
||||||
describe('utils', () => {
|
describe('utils', () => {
|
||||||
const environments = ['dev', 'prod'];
|
const environments = ['dev', 'prod'];
|
||||||
|
const newEnvironments = ['staging'];
|
||||||
|
|
||||||
describe('createJoinedEnvironments', () => {
|
describe('createJoinedEnvironments', () => {
|
||||||
it('returns only `environments` if `variables` argument is undefined', () => {
|
it('returns only `environments` if `variables` argument is undefined', () => {
|
||||||
const variables = undefined;
|
const variables = undefined;
|
||||||
|
|
||||||
expect(createJoinedEnvironments(variables, environments)).toEqual(environments);
|
expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
|
it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
|
||||||
|
@ -21,7 +22,7 @@ describe('utils', () => {
|
||||||
|
|
||||||
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
|
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
|
||||||
|
|
||||||
expect(createJoinedEnvironments(variables, environments)).toEqual([
|
expect(createJoinedEnvironments(variables, environments, [])).toEqual([
|
||||||
environments[0],
|
environments[0],
|
||||||
envScope1,
|
envScope1,
|
||||||
envScope2,
|
envScope2,
|
||||||
|
@ -29,13 +30,22 @@ describe('utils', () => {
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns combined list with new environments included', () => {
|
||||||
|
const variables = undefined;
|
||||||
|
|
||||||
|
expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
|
||||||
|
...environments,
|
||||||
|
...newEnvironments,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('removes duplicate environments', () => {
|
it('removes duplicate environments', () => {
|
||||||
const envScope1 = environments[0];
|
const envScope1 = environments[0];
|
||||||
const envScope2 = 'new2';
|
const envScope2 = 'new2';
|
||||||
|
|
||||||
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
|
const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
|
||||||
|
|
||||||
expect(createJoinedEnvironments(variables, environments)).toEqual([
|
expect(createJoinedEnvironments(variables, environments, [])).toEqual([
|
||||||
environments[0],
|
environments[0],
|
||||||
envScope2,
|
envScope2,
|
||||||
environments[1],
|
environments[1],
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue