Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1c898dc5c1
commit
e04431d29e
|
@ -0,0 +1,55 @@
|
|||
<script>
|
||||
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
|
||||
|
||||
export default {
|
||||
components: { InputCopyToggleVisibility },
|
||||
props: {
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
inputLabel: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
copyButtonTitle: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formInputGroupProps() {
|
||||
return { id: this.inputId };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row">
|
||||
<div class="col-lg-12">
|
||||
<hr />
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<h4 class="gl-mt-0"><slot name="title"></slot></h4>
|
||||
<slot name="description"></slot>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
<input-copy-toggle-visibility
|
||||
:label="inputLabel"
|
||||
:label-for="inputId"
|
||||
:form-input-group-props="formInputGroupProps"
|
||||
:value="token"
|
||||
:copy-button-title="copyButtonTitle"
|
||||
>
|
||||
<template #description>
|
||||
<slot name="input-description"></slot>
|
||||
</template>
|
||||
</input-copy-toggle-visibility>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,111 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { pickBy } from 'lodash';
|
||||
|
||||
import { s__ } from '~/locale';
|
||||
|
||||
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '../constants';
|
||||
import Token from './token.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
canNotAccessOtherData: s__('AccessTokens|It cannot be used to access any other data.'),
|
||||
[FEED_TOKEN]: {
|
||||
label: s__('AccessTokens|Feed token'),
|
||||
copyButtonTitle: s__('AccessTokens|Copy feed token'),
|
||||
description: s__(
|
||||
'AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.',
|
||||
),
|
||||
inputDescription: s__(
|
||||
'AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
|
||||
),
|
||||
resetConfirmMessage: s__(
|
||||
'AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.',
|
||||
),
|
||||
},
|
||||
[INCOMING_EMAIL_TOKEN]: {
|
||||
label: s__('AccessTokens|Incoming email token'),
|
||||
copyButtonTitle: s__('AccessTokens|Copy incoming email token'),
|
||||
description: s__(
|
||||
'AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.',
|
||||
),
|
||||
inputDescription: s__(
|
||||
'AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}.',
|
||||
),
|
||||
resetConfirmMessage: s__(
|
||||
'AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.',
|
||||
),
|
||||
},
|
||||
[STATIC_OBJECT_TOKEN]: {
|
||||
label: s__('AccessTokens|Static object token'),
|
||||
copyButtonTitle: s__('AccessTokens|Copy static object token'),
|
||||
description: s__(
|
||||
'AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.',
|
||||
),
|
||||
inputDescription: s__(
|
||||
'AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}.',
|
||||
),
|
||||
resetConfirmMessage: s__('AccessTokens|Are you sure?'),
|
||||
},
|
||||
},
|
||||
htmlAttributes: {
|
||||
[FEED_TOKEN]: {
|
||||
inputId: 'feed_token',
|
||||
containerTestId: 'feed-token-container',
|
||||
},
|
||||
[INCOMING_EMAIL_TOKEN]: {
|
||||
inputId: 'incoming_email_token',
|
||||
containerTestId: 'incoming-email-token-container',
|
||||
},
|
||||
[STATIC_OBJECT_TOKEN]: {
|
||||
inputId: 'static_object_token',
|
||||
containerTestId: 'static-object-token-container',
|
||||
},
|
||||
},
|
||||
components: { Token, GlSprintf, GlLink },
|
||||
inject: ['tokenTypes'],
|
||||
computed: {
|
||||
enabledTokenTypes() {
|
||||
return pickBy(this.tokenTypes, (tokenData, tokenType) => {
|
||||
return (
|
||||
tokenData?.enabled &&
|
||||
this.$options.i18n[tokenType] &&
|
||||
this.$options.htmlAttributes[tokenType]
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<token
|
||||
v-for="(tokenData, tokenType) in enabledTokenTypes"
|
||||
:key="tokenType"
|
||||
:token="tokenData.token"
|
||||
:input-id="$options.htmlAttributes[tokenType].inputId"
|
||||
:input-label="$options.i18n[tokenType].label"
|
||||
:copy-button-title="$options.i18n[tokenType].copyButtonTitle"
|
||||
:data-testid="$options.htmlAttributes[tokenType].containerTestId"
|
||||
>
|
||||
<template #title>{{ $options.i18n[tokenType].label }}</template>
|
||||
<template #description>
|
||||
<p>{{ $options.i18n[tokenType].description }}</p>
|
||||
<p>{{ $options.i18n.canNotAccessOtherData }}</p>
|
||||
</template>
|
||||
<template #input-description>
|
||||
<gl-sprintf :message="$options.i18n[tokenType].inputDescription">
|
||||
<template #link="{ content }">
|
||||
<gl-link
|
||||
:href="tokenData.resetPath"
|
||||
:data-confirm="$options.i18n[tokenType].resetConfirmMessage"
|
||||
data-method="put"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</template>
|
||||
</token>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,4 @@
|
|||
// Token types
|
||||
export const FEED_TOKEN = 'feedToken';
|
||||
export const INCOMING_EMAIL_TOKEN = 'incomingEmailToken';
|
||||
export const STATIC_OBJECT_TOKEN = 'staticObjectToken';
|
|
@ -1,9 +1,13 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import createFlash from '~/flash';
|
||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||
import { parseRailsFormFields } from '~/lib/utils/forms';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
import ExpiresAtField from './components/expires_at_field.vue';
|
||||
import TokensApp from './components/tokens_app.vue';
|
||||
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from './constants';
|
||||
|
||||
export const initExpiresAtField = () => {
|
||||
const el = document.querySelector('.js-access-tokens-expires-at');
|
||||
|
@ -81,3 +85,29 @@ export const initProjectsField = () => {
|
|||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const initTokensApp = () => {
|
||||
const el = document.getElementById('js-tokens-app');
|
||||
|
||||
if (!el) return false;
|
||||
|
||||
const tokensData = convertObjectPropsToCamelCase(JSON.parse(el.dataset.tokensData), {
|
||||
deep: true,
|
||||
});
|
||||
|
||||
const tokenTypes = {
|
||||
[FEED_TOKEN]: tokensData[FEED_TOKEN],
|
||||
[INCOMING_EMAIL_TOKEN]: tokensData[INCOMING_EMAIL_TOKEN],
|
||||
[STATIC_OBJECT_TOKEN]: tokensData[STATIC_OBJECT_TOKEN],
|
||||
};
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
tokenTypes,
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(TokensApp);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -33,7 +33,7 @@ class GlEmoji extends HTMLElement {
|
|||
this.dataset.unicodeVersion = unicodeVersion;
|
||||
|
||||
emojiUnicode = emojiInfo.e;
|
||||
this.innerHTML = emojiInfo.e;
|
||||
this.textContent = emojiInfo.e;
|
||||
|
||||
this.title = emojiInfo.d;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,8 @@ import { produce } from 'immer';
|
|||
import { __, s__ } from '~/locale';
|
||||
import { convertToGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_GROUP } from '~/graphql_shared/constants';
|
||||
import createContact from './queries/create_contact.mutation.graphql';
|
||||
import createContactMutation from './queries/create_contact.mutation.graphql';
|
||||
import updateContactMutation from './queries/update_contact.mutation.graphql';
|
||||
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
|
||||
|
||||
export default {
|
||||
|
@ -21,6 +22,11 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
contact: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -35,66 +41,111 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
invalid() {
|
||||
return this.firstName === '' || this.lastName === '' || this.email === '';
|
||||
const { firstName, lastName, email } = this;
|
||||
|
||||
return firstName.trim() === '' || lastName.trim() === '' || email.trim() === '';
|
||||
},
|
||||
editMode() {
|
||||
return Boolean(this.contact);
|
||||
},
|
||||
title() {
|
||||
return this.editMode ? this.$options.i18n.editTitle : this.$options.i18n.newTitle;
|
||||
},
|
||||
buttonLabel() {
|
||||
return this.editMode
|
||||
? this.$options.i18n.editButtonLabel
|
||||
: this.$options.i18n.createButtonLabel;
|
||||
},
|
||||
mutation() {
|
||||
return this.editMode ? updateContactMutation : createContactMutation;
|
||||
},
|
||||
variables() {
|
||||
const { contact, firstName, lastName, phone, email, description, editMode, groupId } = this;
|
||||
|
||||
const variables = {
|
||||
input: {
|
||||
firstName,
|
||||
lastName,
|
||||
phone,
|
||||
email,
|
||||
description,
|
||||
},
|
||||
};
|
||||
|
||||
if (editMode) {
|
||||
variables.input.id = contact.id;
|
||||
} else {
|
||||
variables.input.groupId = convertToGraphQLId(TYPE_GROUP, groupId);
|
||||
}
|
||||
|
||||
return variables;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.editMode) {
|
||||
const { contact } = this;
|
||||
|
||||
this.firstName = contact.firstName || '';
|
||||
this.lastName = contact.lastName || '';
|
||||
this.phone = contact.phone || '';
|
||||
this.email = contact.email || '';
|
||||
this.description = contact.description || '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
save() {
|
||||
const { mutation, variables, updateCache, close } = this;
|
||||
|
||||
this.submitting = true;
|
||||
|
||||
return this.$apollo
|
||||
.mutate({
|
||||
mutation: createContact,
|
||||
variables: {
|
||||
input: {
|
||||
groupId: convertToGraphQLId(TYPE_GROUP, this.groupId),
|
||||
firstName: this.firstName,
|
||||
lastName: this.lastName,
|
||||
phone: this.phone,
|
||||
email: this.email,
|
||||
description: this.description,
|
||||
},
|
||||
},
|
||||
update: this.updateCache,
|
||||
mutation,
|
||||
variables,
|
||||
update: updateCache,
|
||||
})
|
||||
.then(({ data }) => {
|
||||
if (data.customerRelationsContactCreate.errors.length === 0) this.close(true);
|
||||
if (
|
||||
data.customerRelationsContactCreate?.errors.length === 0 ||
|
||||
data.customerRelationsContactUpdate?.errors.length === 0
|
||||
) {
|
||||
close(true);
|
||||
}
|
||||
|
||||
this.submitting = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.errorMessages = [__('Something went wrong. Please try again.')];
|
||||
this.errorMessages = [this.$options.i18n.somethingWentWrong];
|
||||
this.submitting = false;
|
||||
});
|
||||
},
|
||||
close(success) {
|
||||
this.$emit('close', success);
|
||||
},
|
||||
updateCache(store, { data: { customerRelationsContactCreate } }) {
|
||||
if (customerRelationsContactCreate.errors.length > 0) {
|
||||
this.errorMessages = customerRelationsContactCreate.errors;
|
||||
updateCache(store, { data }) {
|
||||
const mutationData =
|
||||
data.customerRelationsContactCreate || data.customerRelationsContactUpdate;
|
||||
|
||||
if (mutationData?.errors.length > 0) {
|
||||
this.errorMessages = mutationData.errors;
|
||||
return;
|
||||
}
|
||||
|
||||
const variables = {
|
||||
groupFullPath: this.groupFullPath,
|
||||
};
|
||||
const sourceData = store.readQuery({
|
||||
const queryArgs = {
|
||||
query: getGroupContactsQuery,
|
||||
variables,
|
||||
});
|
||||
variables: { groupFullPath: this.groupFullPath },
|
||||
};
|
||||
|
||||
const data = produce(sourceData, (draftState) => {
|
||||
const sourceData = store.readQuery(queryArgs);
|
||||
|
||||
queryArgs.data = produce(sourceData, (draftState) => {
|
||||
draftState.group.contacts.nodes = [
|
||||
...sourceData.group.contacts.nodes,
|
||||
customerRelationsContactCreate.contact,
|
||||
...sourceData.group.contacts.nodes.filter(({ id }) => id !== this.contact?.id),
|
||||
mutationData.contact,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: getGroupContactsQuery,
|
||||
variables,
|
||||
data,
|
||||
});
|
||||
store.writeQuery(queryArgs);
|
||||
},
|
||||
getDrawerHeaderHeight() {
|
||||
const wrapperEl = document.querySelector('.content-wrapper');
|
||||
|
@ -107,14 +158,17 @@ export default {
|
|||
},
|
||||
},
|
||||
i18n: {
|
||||
buttonLabel: s__('Crm|Create new contact'),
|
||||
createButtonLabel: s__('Crm|Create new contact'),
|
||||
editButtonLabel: __('Save changes'),
|
||||
cancel: __('Cancel'),
|
||||
firstName: s__('Crm|First name'),
|
||||
lastName: s__('Crm|Last name'),
|
||||
email: s__('Crm|Email'),
|
||||
phone: s__('Crm|Phone number (optional)'),
|
||||
description: s__('Crm|Description (optional)'),
|
||||
title: s__('Crm|New Contact'),
|
||||
newTitle: s__('Crm|New contact'),
|
||||
editTitle: s__('Crm|Edit contact'),
|
||||
somethingWentWrong: __('Something went wrong. Please try again.'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -127,7 +181,7 @@ export default {
|
|||
@close="close(false)"
|
||||
>
|
||||
<template #title>
|
||||
<h4>{{ $options.i18n.title }}</h4>
|
||||
<h3>{{ title }}</h3>
|
||||
</template>
|
||||
<gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
|
||||
<ul class="gl-mb-0! gl-ml-5">
|
||||
|
@ -160,9 +214,9 @@ export default {
|
|||
variant="confirm"
|
||||
:disabled="invalid"
|
||||
:loading="submitting"
|
||||
data-testid="create-new-contact-button"
|
||||
data-testid="save-contact-button"
|
||||
type="submit"
|
||||
>{{ $options.i18n.buttonLabel }}</gl-button
|
||||
>{{ buttonLabel }}</gl-button
|
||||
>
|
||||
</span>
|
||||
</form>
|
|
@ -2,9 +2,11 @@
|
|||
import { GlAlert, GlButton, GlLoadingIcon, GlTable, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
import { s__, __ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { TYPE_CRM_CONTACT } from '~/graphql_shared/constants';
|
||||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '../constants';
|
||||
import getGroupContactsQuery from './queries/get_group_contacts.query.graphql';
|
||||
import NewContactForm from './new_contact_form.vue';
|
||||
import ContactForm from './contact_form.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -12,7 +14,7 @@ export default {
|
|||
GlButton,
|
||||
GlLoadingIcon,
|
||||
GlTable,
|
||||
NewContactForm,
|
||||
ContactForm,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -47,11 +49,19 @@ export default {
|
|||
return this.$apollo.queries.contacts.loading;
|
||||
},
|
||||
showNewForm() {
|
||||
return this.$route.path.startsWith('/new');
|
||||
return this.$route.name === NEW_ROUTE_NAME;
|
||||
},
|
||||
canCreateNew() {
|
||||
showEditForm() {
|
||||
return !this.isLoading && this.$route.name === EDIT_ROUTE_NAME;
|
||||
},
|
||||
canAdmin() {
|
||||
return parseBoolean(this.canAdminCrmContact);
|
||||
},
|
||||
editingContact() {
|
||||
return this.contacts.find(
|
||||
(contact) => contact.id === convertToGraphQLId(TYPE_CRM_CONTACT, this.$route.params.id),
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
extractContacts(data) {
|
||||
|
@ -61,16 +71,28 @@ export default {
|
|||
displayNewForm() {
|
||||
if (this.showNewForm) return;
|
||||
|
||||
this.$router.push({ path: '/new' });
|
||||
this.$router.push({ name: NEW_ROUTE_NAME });
|
||||
},
|
||||
hideNewForm(success) {
|
||||
if (success) this.$toast.show(s__('Crm|Contact has been added'));
|
||||
|
||||
this.$router.replace({ path: '/' });
|
||||
this.$router.replace({ name: INDEX_ROUTE_NAME });
|
||||
},
|
||||
hideEditForm(success) {
|
||||
if (success) this.$toast.show(s__('Crm|Contact has been updated'));
|
||||
|
||||
this.editingContactId = 0;
|
||||
this.$router.replace({ name: INDEX_ROUTE_NAME });
|
||||
},
|
||||
getIssuesPath(path, value) {
|
||||
return `${path}?scope=all&state=opened&crm_contact_id=${value}`;
|
||||
},
|
||||
edit(value) {
|
||||
if (this.showEditForm) return;
|
||||
|
||||
this.editingContactId = value;
|
||||
this.$router.push({ name: EDIT_ROUTE_NAME, params: { id: value } });
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ key: 'firstName', sortable: true },
|
||||
|
@ -87,7 +109,7 @@ export default {
|
|||
},
|
||||
{
|
||||
key: 'id',
|
||||
label: __('Issues'),
|
||||
label: '',
|
||||
formatter: (id) => {
|
||||
return getIdFromGraphQLId(id);
|
||||
},
|
||||
|
@ -96,6 +118,7 @@ export default {
|
|||
i18n: {
|
||||
emptyText: s__('Crm|No contacts found'),
|
||||
issuesButtonLabel: __('View issues'),
|
||||
editButtonLabel: __('Edit'),
|
||||
title: s__('Crm|Customer Relations Contacts'),
|
||||
newContact: s__('Crm|New contact'),
|
||||
errorText: __('Something went wrong. Please try again.'),
|
||||
|
@ -116,7 +139,7 @@ export default {
|
|||
</h2>
|
||||
<div class="gl-display-none gl-md-display-flex gl-align-items-center gl-justify-content-end">
|
||||
<gl-button
|
||||
v-if="canCreateNew"
|
||||
v-if="canAdmin"
|
||||
variant="confirm"
|
||||
data-testid="new-contact-button"
|
||||
@click="displayNewForm"
|
||||
|
@ -125,7 +148,13 @@ export default {
|
|||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
<new-contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
|
||||
<contact-form v-if="showNewForm" :drawer-open="showNewForm" @close="hideNewForm" />
|
||||
<contact-form
|
||||
v-if="showEditForm"
|
||||
:contact="editingContact"
|
||||
:drawer-open="showEditForm"
|
||||
@close="hideEditForm"
|
||||
/>
|
||||
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
|
||||
<gl-table
|
||||
v-else
|
||||
|
@ -138,11 +167,20 @@ export default {
|
|||
<template #cell(id)="data">
|
||||
<gl-button
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.issuesButtonLabel"
|
||||
class="gl-mr-3"
|
||||
data-testid="issues-link"
|
||||
icon="issues"
|
||||
:aria-label="$options.i18n.issuesButtonLabel"
|
||||
:href="getIssuesPath(groupIssuesPath, data.value)"
|
||||
/>
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
v-gl-tooltip.hover.bottom="$options.i18n.editButtonLabel"
|
||||
data-testid="edit-contact-button"
|
||||
icon="pencil"
|
||||
:aria-label="$options.i18n.editButtonLabel"
|
||||
@click="edit(data.value)"
|
||||
/>
|
||||
</template>
|
||||
</gl-table>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#import "./crm_contact_fields.fragment.graphql"
|
||||
|
||||
mutation updateContact($input: CustomerRelationsContactUpdateInput!) {
|
||||
customerRelationsContactUpdate(input: $input) {
|
||||
contact {
|
||||
...ContactFragment
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export const INDEX_ROUTE_NAME = 'index';
|
||||
export const NEW_ROUTE_NAME = 'new';
|
||||
export const EDIT_ROUTE_NAME = 'edit';
|
|
@ -4,6 +4,7 @@ import VueApollo from 'vue-apollo';
|
|||
import VueRouter from 'vue-router';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import CrmContactsRoot from './components/contacts_root.vue';
|
||||
import routes from './routes';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(VueRouter);
|
||||
|
@ -25,14 +26,7 @@ export default () => {
|
|||
const router = new VueRouter({
|
||||
base: basePath,
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'Contacts List',
|
||||
path: '/',
|
||||
component: CrmContactsRoot,
|
||||
},
|
||||
],
|
||||
routes,
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { INDEX_ROUTE_NAME, NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from './constants';
|
||||
import CrmContactsRoot from './components/contacts_root.vue';
|
||||
|
||||
export default [
|
||||
{
|
||||
name: INDEX_ROUTE_NAME,
|
||||
path: '/',
|
||||
component: CrmContactsRoot,
|
||||
},
|
||||
{
|
||||
name: NEW_ROUTE_NAME,
|
||||
path: '/new',
|
||||
component: CrmContactsRoot,
|
||||
},
|
||||
{
|
||||
name: EDIT_ROUTE_NAME,
|
||||
path: '/:id/edit',
|
||||
component: CrmContactsRoot,
|
||||
},
|
||||
];
|
|
@ -16,3 +16,6 @@ export const CATEGORY_ICON_MAP = {
|
|||
export const EMOJIS_PER_ROW = 9;
|
||||
export const EMOJI_ROW_HEIGHT = 34;
|
||||
export const CATEGORY_ROW_HEIGHT = 37;
|
||||
|
||||
export const CACHE_VERSION_KEY = 'gl-emoji-map-version';
|
||||
export const CACHE_KEY = 'gl-emoji-map';
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { escape, minBy } from 'lodash';
|
||||
import emojiRegexFactory from 'emoji-regex';
|
||||
import emojiAliases from 'emojis/aliases.json';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
import AccessorUtilities from '../lib/utils/accessor';
|
||||
import axios from '../lib/utils/axios_utils';
|
||||
import { CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
||||
import { CACHE_KEY, CACHE_VERSION_KEY, CATEGORY_ICON_MAP, FREQUENTLY_USED_KEY } from './constants';
|
||||
|
||||
let emojiMap = null;
|
||||
let validEmojiNames = null;
|
||||
|
@ -17,10 +17,15 @@ const isLocalStorageAvailable = AccessorUtilities.canUseLocalStorage();
|
|||
async function loadEmoji() {
|
||||
if (
|
||||
isLocalStorageAvailable &&
|
||||
window.localStorage.getItem('gl-emoji-map-version') === EMOJI_VERSION &&
|
||||
window.localStorage.getItem('gl-emoji-map')
|
||||
window.localStorage.getItem(CACHE_VERSION_KEY) === EMOJI_VERSION &&
|
||||
window.localStorage.getItem(CACHE_KEY)
|
||||
) {
|
||||
return JSON.parse(window.localStorage.getItem('gl-emoji-map'));
|
||||
const emojis = JSON.parse(window.localStorage.getItem(CACHE_KEY));
|
||||
// Workaround because the pride flag is broken in EMOJI_VERSION = '1'
|
||||
if (emojis.gay_pride_flag) {
|
||||
emojis.gay_pride_flag.e = '🏳️🌈';
|
||||
}
|
||||
return emojis;
|
||||
}
|
||||
|
||||
// We load the JSON file direct from the server
|
||||
|
@ -29,15 +34,19 @@ async function loadEmoji() {
|
|||
const { data } = await axios.get(
|
||||
`${gon.relative_url_root || ''}/-/emojis/${EMOJI_VERSION}/emojis.json`,
|
||||
);
|
||||
window.localStorage.setItem('gl-emoji-map-version', EMOJI_VERSION);
|
||||
window.localStorage.setItem('gl-emoji-map', JSON.stringify(data));
|
||||
window.localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
|
||||
window.localStorage.setItem(CACHE_KEY, JSON.stringify(data));
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadEmojiWithNames() {
|
||||
return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
|
||||
acc[key] = { ...value, name: key, e: sanitize(value.e) };
|
||||
const emojiRegex = emojiRegexFactory();
|
||||
|
||||
return Object.entries(await loadEmoji()).reduce((acc, [key, value]) => {
|
||||
// Filter out entries which aren't emojis
|
||||
if (value.e.match(emojiRegex)?.[0] === value.e) {
|
||||
acc[key] = { ...value, name: key };
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
export const MINIMUM_SEARCH_LENGTH = 3;
|
||||
|
||||
export const TYPE_CI_RUNNER = 'Ci::Runner';
|
||||
export const TYPE_CRM_CONTACT = 'CustomerRelations::Contact';
|
||||
export const TYPE_DISCUSSION = 'Discussion';
|
||||
export const TYPE_EPIC = 'Epic';
|
||||
export const TYPE_GROUP = 'Group';
|
||||
export const TYPE_ISSUE = 'Issue';
|
||||
|
@ -8,11 +10,10 @@ export const TYPE_ITERATION = 'Iteration';
|
|||
export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
|
||||
export const TYPE_MERGE_REQUEST = 'MergeRequest';
|
||||
export const TYPE_MILESTONE = 'Milestone';
|
||||
export const TYPE_NOTE = 'Note';
|
||||
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
|
||||
export const TYPE_PROJECT = 'Project';
|
||||
export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
|
||||
export const TYPE_SITE_PROFILE = 'DastSiteProfile';
|
||||
export const TYPE_USER = 'User';
|
||||
export const TYPE_VULNERABILITY = 'Vulnerability';
|
||||
export const TYPE_NOTE = 'Note';
|
||||
export const TYPE_DISCUSSION = 'Discussion';
|
||||
export const TYPE_PACKAGES_PACKAGE = 'Packages::Package';
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { initExpiresAtField, initProjectsField } from '~/access_tokens';
|
||||
import { initExpiresAtField, initProjectsField, initTokensApp } from '~/access_tokens';
|
||||
|
||||
initExpiresAtField();
|
||||
initProjectsField();
|
||||
initTokensApp();
|
||||
|
|
|
@ -23,9 +23,9 @@ module AuthenticatesWithTwoFactor
|
|||
|
||||
session[:otp_user_id] = user.id
|
||||
session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
|
||||
push_frontend_feature_flag(:webauthn)
|
||||
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
|
||||
|
||||
if Feature.enabled?(:webauthn)
|
||||
if Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
setup_webauthn_authentication(user)
|
||||
else
|
||||
setup_u2f_authentication(user)
|
||||
|
|
|
@ -11,7 +11,7 @@ module AuthenticatesWithTwoFactorForAdminMode
|
|||
return handle_locked_user(user) unless user.can?(:log_in)
|
||||
|
||||
session[:otp_user_id] = user.id
|
||||
push_frontend_feature_flag(:webauthn)
|
||||
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
|
||||
|
||||
if user.two_factor_webauthn_enabled?
|
||||
setup_webauthn_authentication(user)
|
||||
|
|
|
@ -9,6 +9,10 @@ class Groups::Crm::ContactsController < Groups::ApplicationController
|
|||
render action: "index"
|
||||
end
|
||||
|
||||
def edit
|
||||
render action: "index"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize_read_crm_contact!
|
||||
|
|
|
@ -8,7 +8,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
helper_method :current_password_required?
|
||||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:webauthn)
|
||||
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
|
@ -44,7 +44,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
@qr_code = build_qr_code
|
||||
@account_string = account_string
|
||||
|
||||
if Feature.enabled?(:webauthn)
|
||||
if Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
setup_webauthn_registration
|
||||
else
|
||||
setup_u2f_registration
|
||||
|
@ -69,7 +69,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
|
|||
@error = { message: _('Invalid pin code.') }
|
||||
@qr_code = build_qr_code
|
||||
|
||||
if Feature.enabled?(:webauthn)
|
||||
if Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
setup_webauthn_registration
|
||||
else
|
||||
setup_u2f_registration
|
||||
|
|
|
@ -8,7 +8,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
before_action :authorize_change_username!, only: :update_username
|
||||
skip_before_action :require_email, only: [:show, :update]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:webauthn)
|
||||
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
feature_category :users
|
||||
|
|
|
@ -32,7 +32,7 @@ class SessionsController < Devise::SessionsController
|
|||
before_action :load_recaptcha
|
||||
before_action :set_invite_params, only: [:new]
|
||||
before_action do
|
||||
push_frontend_feature_flag(:webauthn)
|
||||
push_frontend_feature_flag(:webauthn, default_enabled: :yaml)
|
||||
end
|
||||
|
||||
after_action :log_failed_login, if: :action_new_and_failed_login?
|
||||
|
@ -305,9 +305,9 @@ class SessionsController < Devise::SessionsController
|
|||
def authentication_method
|
||||
if user_params[:otp_attempt]
|
||||
AuthenticationEvent::TWO_FACTOR
|
||||
elsif user_params[:device_response] && Feature.enabled?(:webauthn)
|
||||
elsif user_params[:device_response] && Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
AuthenticationEvent::TWO_FACTOR_WEBAUTHN
|
||||
elsif user_params[:device_response] && !Feature.enabled?(:webauthn)
|
||||
elsif user_params[:device_response] && !Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
AuthenticationEvent::TWO_FACTOR_U2F
|
||||
else
|
||||
AuthenticationEvent::STANDARD
|
||||
|
|
|
@ -1,7 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module AccessTokensHelper
|
||||
include AccountsHelper
|
||||
include ApplicationHelper
|
||||
|
||||
def scope_description(prefix)
|
||||
prefix == :project_access_token ? [:doorkeeper, :project_access_token_scope_desc] : [:doorkeeper, :scope_desc]
|
||||
end
|
||||
|
||||
def tokens_app_data
|
||||
{
|
||||
feed_token: {
|
||||
enabled: !Gitlab::CurrentSettings.disable_feed_token,
|
||||
token: current_user.feed_token,
|
||||
reset_path: reset_feed_token_profile_path
|
||||
},
|
||||
incoming_email_token: {
|
||||
enabled: incoming_email_token_enabled?,
|
||||
token: current_user.enabled_incoming_email_token,
|
||||
reset_path: reset_incoming_email_token_profile_path
|
||||
},
|
||||
static_object_token: {
|
||||
enabled: static_objects_external_storage_enabled?,
|
||||
token: current_user.enabled_static_object_token,
|
||||
reset_path: reset_static_object_token_profile_path
|
||||
}
|
||||
}.to_json
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,7 +13,7 @@ class MembersPreloader
|
|||
ActiveRecord::Associations::Preloader.new.preload(members, :created_by)
|
||||
ActiveRecord::Associations::Preloader.new.preload(members, user: :status)
|
||||
ActiveRecord::Associations::Preloader.new.preload(members, user: :u2f_registrations)
|
||||
ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn)
|
||||
ActiveRecord::Associations::Preloader.new.preload(members, user: :webauthn_registrations) if Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -105,27 +105,32 @@ module Namespaces
|
|||
:traversal_ids,
|
||||
'LEAD (namespaces.traversal_ids, 1) OVER (ORDER BY namespaces.traversal_ids ASC) next_traversal_ids'
|
||||
)
|
||||
cte = Gitlab::SQL::CTE.new(:base_cte, base)
|
||||
base_cte = Gitlab::SQL::CTE.new(:base_cte, base)
|
||||
|
||||
namespaces = Arel::Table.new(:namespaces)
|
||||
records = unscoped
|
||||
.with(cte.to_arel)
|
||||
.from([cte.table, namespaces])
|
||||
|
||||
# Bound the search space to ourselves (optional) and descendants.
|
||||
#
|
||||
# WHERE (base_cte.next_traversal_ids IS NULL OR base_cte.next_traversal_ids > namespaces.traversal_ids)
|
||||
# AND next_traversal_ids_sibling(base_cte.traversal_ids) > namespaces.traversal_ids
|
||||
records = records
|
||||
.where(cte.table[:next_traversal_ids].eq(nil).or(cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
|
||||
.where(next_sibling_func(cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
|
||||
records = unscoped
|
||||
.from([base_cte.table, namespaces])
|
||||
.where(base_cte.table[:next_traversal_ids].eq(nil).or(base_cte.table[:next_traversal_ids].gt(namespaces[:traversal_ids])))
|
||||
.where(next_sibling_func(base_cte.table[:traversal_ids]).gt(namespaces[:traversal_ids]))
|
||||
|
||||
# AND base_cte.traversal_ids <= namespaces.traversal_ids
|
||||
if include_self
|
||||
records.where(cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
|
||||
else
|
||||
records.where(cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
|
||||
end
|
||||
records = if include_self
|
||||
records.where(base_cte.table[:traversal_ids].lteq(namespaces[:traversal_ids]))
|
||||
else
|
||||
records.where(base_cte.table[:traversal_ids].lt(namespaces[:traversal_ids]))
|
||||
end
|
||||
|
||||
records_cte = Gitlab::SQL::CTE.new(:descendants_cte, records)
|
||||
|
||||
unscoped
|
||||
.unscope(where: [:type])
|
||||
.with(base_cte.to_arel, records_cte.to_arel)
|
||||
.from(records_cte.alias_to(namespaces))
|
||||
end
|
||||
|
||||
def next_sibling_func(*args)
|
||||
|
|
|
@ -187,8 +187,8 @@ class User < ApplicationRecord
|
|||
has_one :abuse_report, dependent: :destroy, foreign_key: :user_id # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :reported_abuse_reports, dependent: :destroy, foreign_key: :reporter_id, class_name: "AbuseReport" # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
|
||||
has_many :builds, class_name: 'Ci::Build'
|
||||
has_many :pipelines, class_name: 'Ci::Pipeline'
|
||||
has_many :todos
|
||||
has_many :notification_settings
|
||||
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
||||
|
@ -911,7 +911,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def two_factor_u2f_enabled?
|
||||
return false if Feature.enabled?(:webauthn)
|
||||
return false if Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
|
||||
if u2f_registrations.loaded?
|
||||
u2f_registrations.any?
|
||||
|
@ -925,7 +925,7 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def two_factor_webauthn_enabled?
|
||||
return false unless Feature.enabled?(:webauthn)
|
||||
return false unless Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
|
||||
(webauthn_registrations.loaded? && webauthn_registrations.any?) || (!webauthn_registrations.loaded? && webauthn_registrations.exists?)
|
||||
end
|
||||
|
@ -1790,7 +1790,7 @@ class User < ApplicationRecord
|
|||
# we do this on read since migrating all existing users is not a feasible
|
||||
# solution.
|
||||
def feed_token
|
||||
Gitlab::CurrentSettings.disable_feed_token ? nil : ensure_feed_token!
|
||||
ensure_feed_token! unless Gitlab::CurrentSettings.disable_feed_token
|
||||
end
|
||||
|
||||
# Each existing user needs to have a `static_object_token`.
|
||||
|
@ -1800,6 +1800,14 @@ class User < ApplicationRecord
|
|||
ensure_static_object_token!
|
||||
end
|
||||
|
||||
def enabled_static_object_token
|
||||
static_object_token if Gitlab::CurrentSettings.static_objects_external_storage_enabled?
|
||||
end
|
||||
|
||||
def enabled_incoming_email_token
|
||||
incoming_email_token if Gitlab::IncomingEmail.supports_issue_creation?
|
||||
end
|
||||
|
||||
def sync_attribute?(attribute)
|
||||
return true if ldap_user? && attribute == :email
|
||||
|
||||
|
|
|
@ -63,6 +63,12 @@ class MemberEntity < Grape::Entity
|
|||
member.respond_to?(:invited_user_state) ? member.invited_user_state : ""
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def current_user
|
||||
options[:current_user]
|
||||
end
|
||||
end
|
||||
|
||||
MemberEntity.prepend_mod_with('MemberEntity')
|
||||
|
|
|
@ -32,62 +32,64 @@
|
|||
type_plural: type_plural,
|
||||
active_tokens: @active_personal_access_tokens,
|
||||
revoke_route_helper: ->(token) { revoke_profile_personal_access_token_path(token) }
|
||||
- if Feature.enabled?(:hide_access_tokens)
|
||||
#js-tokens-app{ data: { tokens_data: tokens_app_data } }
|
||||
- else
|
||||
- unless Gitlab::CurrentSettings.disable_feed_token
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Feed token')
|
||||
%p
|
||||
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.feed-token-reset
|
||||
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
|
||||
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
|
||||
- unless Gitlab::CurrentSettings.disable_feed_token
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Feed token')
|
||||
%p
|
||||
= s_('AccessTokens|Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.feed-token-reset
|
||||
= label_tag :feed_token, s_('AccessTokens|Feed token'), class: 'label-bold'
|
||||
= text_field_tag :feed_token, current_user.feed_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :feed_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any RSS or calendar URLs currently in use will stop working.'), testid: :reset_feed_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
- if incoming_email_token_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Incoming email token')
|
||||
%p
|
||||
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.incoming-email-token-reset
|
||||
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
|
||||
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
|
||||
- if incoming_email_token_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4.profile-settings-sidebar
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Incoming email token')
|
||||
%p
|
||||
= s_('AccessTokens|Your incoming email token authenticates you when you create a new issue by email, and is included in your personal project-specific email addresses.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8.incoming-email-token-reset
|
||||
= label_tag :incoming_email_token, s_('AccessTokens|Incoming email token'), class: 'label-bold'
|
||||
= text_field_tag :incoming_email_token, current_user.incoming_email_token, class: 'form-control gl-form-input js-select-on-focus', readonly: true
|
||||
%p.form-text.text-muted
|
||||
- reset_link = link_to s_('AccessTokens|reset this token'), [:reset, :incoming_email_token, :profile], method: :put, data: { confirm: s_('AccessTokens|Are you sure? Any issue email addresses currently in use will stop working.'), testid: :reset_email_token_link }
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}.') % { link_reset_it: reset_link }
|
||||
= reset_message.html_safe
|
||||
|
||||
- if static_objects_external_storage_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Static object token')
|
||||
%p
|
||||
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8
|
||||
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
|
||||
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
|
||||
%p.form-text.text-muted
|
||||
- reset_link = url_for [:reset, :static_object_token, :profile]
|
||||
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
|
||||
- reset_link_end = '</a>'.html_safe
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
|
||||
= reset_message.html_safe
|
||||
- if static_objects_external_storage_enabled?
|
||||
.col-lg-12
|
||||
%hr
|
||||
.row.gl-mt-3.js-search-settings-section
|
||||
.col-lg-4
|
||||
%h4.gl-mt-0
|
||||
= s_('AccessTokens|Static object token')
|
||||
%p
|
||||
= s_('AccessTokens|Your static object token authenticates you when repository static objects (such as archives or blobs) are served from an external storage.')
|
||||
%p
|
||||
= s_('AccessTokens|It cannot be used to access any other data.')
|
||||
.col-lg-8
|
||||
= label_tag :static_object_token, s_('AccessTokens|Static object token'), class: "label-bold"
|
||||
= text_field_tag :static_object_token, current_user.static_object_token, class: 'form-control gl-form-input', readonly: true, onclick: 'this.select()'
|
||||
%p.form-text.text-muted
|
||||
- reset_link = url_for [:reset, :static_object_token, :profile]
|
||||
- reset_link_start = '<a data-confirm="%{confirm}" rel="nofollow" data-method="put" href="%{url}">'.html_safe % { confirm: s_('AccessTokens|Are you sure?'), url: reset_link }
|
||||
- reset_link_end = '</a>'.html_safe
|
||||
- reset_message = s_('AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}.') % { reset_link_start: reset_link_start, reset_link_end: reset_link_end }
|
||||
= reset_message.html_safe
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
- page_title _('Two-Factor Authentication'), _('Account')
|
||||
- add_to_breadcrumbs _('Account'), profile_account_path
|
||||
- @content_class = "limit-container-width" unless fluid_layout
|
||||
- webauthn_enabled = Feature.enabled?(:webauthn)
|
||||
- webauthn_enabled = Feature.enabled?(:webauthn, default_enabled: :yaml)
|
||||
|
||||
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
|
||||
.row.gl-mt-3
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
%span.cgray= starrer.user.to_reference
|
||||
|
||||
- if starrer.user == current_user
|
||||
%span.badge-pill.badge-success.gl-badge.gl-ml-2.sm= _("It's you")
|
||||
= gl_badge_tag _("It's you"), variant: :success, size: :sm, class: 'gl-ml-2'
|
||||
|
||||
.block-truncated
|
||||
= time_ago_with_tooltip(starrer.starred_since)
|
||||
|
|
|
@ -3,14 +3,10 @@
|
|||
|
||||
require_relative '../metrics_server/metrics_server'
|
||||
|
||||
begin
|
||||
target = ENV['METRICS_SERVER_TARGET']
|
||||
raise "Required: METRICS_SERVER_TARGET=[sidekiq]" unless target == 'sidekiq'
|
||||
target = ENV['METRICS_SERVER_TARGET']
|
||||
raise "METRICS_SERVER_TARGET cannot be blank" if target.blank?
|
||||
|
||||
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
|
||||
wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
|
||||
metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/#{target}")
|
||||
wipe_metrics_dir = Gitlab::Utils.to_boolean(ENV['WIPE_METRICS_DIR']) || false
|
||||
|
||||
# Re-raise exceptions in threads on the main thread.
|
||||
Thread.abort_on_exception = true
|
||||
MetricsServer.new(target, metrics_dir, wipe_metrics_dir).start
|
||||
end
|
||||
Process.wait(MetricsServer.spawn(target, metrics_dir: metrics_dir, wipe_metrics_dir: wipe_metrics_dir))
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
---
|
||||
name: geo_pages_deployment_verification
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74905
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/346754
|
||||
name: hide_access_tokens
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/76280
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347490
|
||||
milestone: '14.6'
|
||||
type: development
|
||||
group: group::geo
|
||||
group: group::access
|
||||
default_enabled: false
|
|
@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/232671
|
|||
milestone: '13.4'
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
|
@ -127,7 +127,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
|
|||
end
|
||||
|
||||
namespace :crm do
|
||||
resources :contacts, only: [:index, :new]
|
||||
resources :contacts, only: [:index, :new, :edit]
|
||||
resources :organizations, only: [:index]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ChangePackageIndexOnCorpus < Gitlab::Database::Migration[1.0]
|
||||
INDEX_NAME = 'index_coverage_fuzzing_corpuses_on_package_id'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
# Changing this index is safe.
|
||||
# The table does not have any data in it as it's behind a feature flag.
|
||||
def up
|
||||
remove_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
|
||||
add_concurrent_index :coverage_fuzzing_corpuses, :package_id, unique: true, name: INDEX_NAME
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
|
||||
add_concurrent_index :coverage_fuzzing_corpuses, :package_id, name: INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
8960c0a2b7e621e466fde3bde6a252119008579c058046a16d57a6f6bff42008
|
|
@ -25870,7 +25870,7 @@ CREATE INDEX index_container_repository_on_name_trigram ON container_repositorie
|
|||
|
||||
CREATE UNIQUE INDEX index_content_blocked_states_on_container_id_commit_sha_path ON content_blocked_states USING btree (container_identifier, commit_sha, path);
|
||||
|
||||
CREATE INDEX index_coverage_fuzzing_corpuses_on_package_id ON coverage_fuzzing_corpuses USING btree (package_id);
|
||||
CREATE UNIQUE INDEX index_coverage_fuzzing_corpuses_on_package_id ON coverage_fuzzing_corpuses USING btree (package_id);
|
||||
|
||||
CREATE INDEX index_coverage_fuzzing_corpuses_on_project_id ON coverage_fuzzing_corpuses USING btree (project_id);
|
||||
|
||||
|
|
|
@ -54,8 +54,8 @@ verification methods:
|
|||
| Blobs | External Merge Request Diffs _(file system)_ | Geo with API | SHA256 checksum |
|
||||
| Blobs | External Merge Request Diffs _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
| Blobs | Pipeline artifacts _(file system)_ | Geo with API | SHA256 checksum |
|
||||
| Blobs | Pipeline artifacts _(object storage)_ | Geo with API/Managed (*2*) | SHA256 checksum |
|
||||
| Blobs | Pages _(file system)_ | Geo with API | _Not implemented_ |
|
||||
| Blobs | Pipeline artifacts _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
| Blobs | Pages _(file system)_ | Geo with API | SHA256 checksum |
|
||||
| Blobs | Pages _(object storage)_ | Geo with API/Managed (*2*) | _Not implemented_ |
|
||||
|
||||
- (*1*): Redis replication can be used as part of HA with Redis sentinel. It's not used between Geo sites.
|
||||
|
@ -147,7 +147,6 @@ these epics/issues:
|
|||
- [Geo: Improve the self-service Geo replication framework](https://gitlab.com/groups/gitlab-org/-/epics/3761)
|
||||
- [Geo: Move existing blobs to framework](https://gitlab.com/groups/gitlab-org/-/epics/3588)
|
||||
- [Geo: Add unreplicated data types](https://gitlab.com/groups/gitlab-org/-/epics/893)
|
||||
- [Geo: Support GitLab Pages](https://gitlab.com/groups/gitlab-org/-/epics/589)
|
||||
|
||||
### Replicated data types behind a feature flag
|
||||
|
||||
|
@ -190,7 +189,7 @@ successfully, you must replicate their data using some other means.
|
|||
|[Project wiki repository](../../../user/project/wiki/) | **Yes** (10.2) | **Yes** (10.7) | No | |
|
||||
|[Group wiki repository](../../../user/project/wiki/group.md) | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No | No | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. |
|
||||
|[Uploads](../../uploads.md) | **Yes** (10.2) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817) | No | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. |
|
||||
|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes**(14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is under development behind the feature flag `geo_lfs_object_verification` introduced in 14.6. |
|
||||
|[LFS objects](../../lfs/index.md) | **Yes** (10.2) | **Yes**(14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is behind the feature flag `geo_lfs_object_verification` enabled by default in 14.6. |
|
||||
|[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|
||||
|[Project snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|
||||
|[CI job artifacts](../../../ci/pipelines/job_artifacts.md) | **Yes** (10.4) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923) | Via Object Storage provider if supported. Native Geo support (Beta). | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. Job logs also verified on transfer. |
|
||||
|
@ -203,7 +202,7 @@ successfully, you must replicate their data using some other means.
|
|||
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.12) | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0. |
|
||||
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | **Yes** (14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification is behind the feature flag `geo_merge_request_diff_verification`, enabled by default in 14.6.|
|
||||
|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|
||||
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_pages_deployment_replication`, enabled by default. |
|
||||
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (14.6) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_pages_deployment_replication`, enabled by default. Verification is behind the feature flag `geo_pages_deployment_verification`, enabled by default in 14.6. |
|
||||
|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
|
||||
|[Elasticsearch integration](../../../integration/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries use the same ES cluster as the primary. |
|
||||
|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked by [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for disaster recovery purposes because it can be recreated from external sources. |
|
||||
|
|
|
@ -275,24 +275,24 @@ GET /groups/:id/projects
|
|||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ----------------------------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `archived` | boolean | no | Limit by archived status |
|
||||
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
|
||||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of authorized projects matching the search criteria |
|
||||
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
|
||||
| `owned` | boolean | no | Limit by projects owned by the current user |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
|
||||
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
|
||||
| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
|
||||
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
|
||||
| `min_access_level` | integer | no | Limit to projects where current user has at least this [access level](members.md#valid-access-levels) |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
|
||||
| `with_security_reports` | boolean | no | **(ULTIMATE)** Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------------------------------- | -------------- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `archived` | boolean | no | Limit by archived status |
|
||||
| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
|
||||
| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, `similarity` (1), or `last_activity_at` fields. Default is `created_at` |
|
||||
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
|
||||
| `search` | string | no | Return list of authorized projects matching the search criteria |
|
||||
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
|
||||
| `owned` | boolean | no | Limit by projects owned by the current user |
|
||||
| `starred` | boolean | no | Limit by projects starred by the current user |
|
||||
| `with_issues_enabled` | boolean | no | Limit by projects with issues feature enabled. Default is `false` |
|
||||
| `with_merge_requests_enabled` | boolean | no | Limit by projects with merge requests feature enabled. Default is `false` |
|
||||
| `with_shared` | boolean | no | Include projects shared to this group. Default is `true` |
|
||||
| `include_subgroups` | boolean | no | Include projects in subgroups of this group. Default is `false` |
|
||||
| `min_access_level` | integer | no | Limit to projects where current user has at least this [access level](members.md#valid-access-levels) |
|
||||
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (administrators only) |
|
||||
| `with_security_reports` **(ULTIMATE)** | boolean | no | Return only projects that have security reports artifacts present in any of their builds. This means "projects with security reports enabled". Default is `false` |
|
||||
|
||||
1. Order by similarity: Orders the results by a similarity score calculated from the provided `search`
|
||||
URL parameter. When using `order_by=similarity`, the `sort` parameter is ignored. When the `search`
|
||||
|
@ -783,28 +783,28 @@ POST /groups
|
|||
|
||||
Parameters:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------------------------ | ------- | -------- | ----------- |
|
||||
| `name` | string | yes | The name of the group. |
|
||||
| `path` | string | yes | The path of the group. |
|
||||
| `description` | string | no | The group's description. |
|
||||
| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to projects within this group. |
|
||||
| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
|
||||
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
|
||||
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
|
||||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
| `request_access_enabled` | boolean | no | Allow users to request member access. |
|
||||
| `parent_id` | integer | no | The parent group ID for creating nested group. |
|
||||
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
|
||||
| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
|
||||
| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------------------------------------------- | ------- | -------- | ----------- |
|
||||
| `name` | string | yes | The name of the group. |
|
||||
| `path` | string | yes | The path of the group. |
|
||||
| `description` | string | no | The group's description. |
|
||||
| `membership_lock` **(PREMIUM)** | boolean | no | Prevent adding new members to projects within this group. |
|
||||
| `visibility` | string | no | The group's visibility. Can be `private`, `internal`, or `public`. |
|
||||
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
|
||||
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
|
||||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
| `request_access_enabled` | boolean | no | Allow users to request member access. |
|
||||
| `parent_id` | integer | no | The parent group ID for creating nested group. |
|
||||
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. |
|
||||
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
|
||||
| `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
|
||||
|
||||
### Options for `default_branch_protection`
|
||||
|
||||
|
@ -884,32 +884,32 @@ Updates the project group. Only available to group owners and administrators.
|
|||
PUT /groups/:id
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------------------------------ | ------- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of the group. |
|
||||
| `name` | string | no | The name of the group. |
|
||||
| `path` | string | no | The path of the group. |
|
||||
| `description` | string | no | The description of the group. |
|
||||
| `membership_lock` | boolean | no | **(PREMIUM)** Prevent adding new members to projects within this group. |
|
||||
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
|
||||
| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
|
||||
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
|
||||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
| `request_access_enabled` | boolean | no | Allow users to request member access. |
|
||||
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
|
||||
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
|
||||
| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
|
||||
| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
|
||||
| `prevent_forking_outside_group` | boolean | no | **(PREMIUM)** When enabled, users can **not** fork projects from this group to external namespaces
|
||||
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
|
||||
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/index.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |
|
||||
| Attribute | Type | Required | Description |
|
||||
| ------------------------------------------------------- | ------- | -------- | ----------- |
|
||||
| `id` | integer | yes | The ID of the group. |
|
||||
| `name` | string | no | The name of the group. |
|
||||
| `path` | string | no | The path of the group. |
|
||||
| `description` | string | no | The description of the group. |
|
||||
| `membership_lock` **(PREMIUM)** | boolean | no | Prevent adding new members to projects within this group. |
|
||||
| `share_with_group_lock` | boolean | no | Prevent sharing a project with another group within this group. |
|
||||
| `visibility` | string | no | The visibility level of the group. Can be `private`, `internal`, or `public`. |
|
||||
| `require_two_factor_authentication` | boolean | no | Require all users in this group to setup Two-factor authentication. |
|
||||
| `two_factor_grace_period` | integer | no | Time before Two-factor authentication is enforced (in hours). |
|
||||
| `project_creation_level` | string | no | Determine if developers can create projects in the group. Can be `noone` (No one), `maintainer` (users with the Maintainer role), or `developer` (users with the Developer or Maintainer role). |
|
||||
| `auto_devops_enabled` | boolean | no | Default to Auto DevOps pipeline for all projects within this group. |
|
||||
| `subgroup_creation_level` | string | no | Allowed to [create subgroups](../user/group/subgroups/index.md#creating-a-subgroup). Can be `owner` (Owners), or `maintainer` (users with the Maintainer role). |
|
||||
| `emails_disabled` | boolean | no | Disable email notifications |
|
||||
| `avatar` | mixed | no | Image file for avatar of the group. [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/-/issues/36681) |
|
||||
| `mentions_disabled` | boolean | no | Disable the capability of a group from getting mentioned |
|
||||
| `lfs_enabled` (optional) | boolean | no | Enable/disable Large File Storage (LFS) for the projects in this group. |
|
||||
| `request_access_enabled` | boolean | no | Allow users to request member access. |
|
||||
| `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). |
|
||||
| `file_template_project_id` **(PREMIUM)** | integer | no | The ID of a project to load custom file templates from. |
|
||||
| `shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` |
|
||||
| `extra_shared_runners_minutes_limit` **(PREMIUM SELF)** | integer | no | Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). |
|
||||
| `prevent_forking_outside_group` **(PREMIUM)** | boolean | no | When enabled, users can **not** fork projects from this group to external namespaces
|
||||
| `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. |
|
||||
| `prevent_sharing_groups_outside_hierarchy` | boolean | no | See [Prevent group sharing outside the group hierarchy](../user/group/index.md#prevent-group-sharing-outside-the-group-hierarchy). This attribute is only available on top-level groups. [Introduced in GitLab 14.1](https://gitlab.com/gitlab-org/gitlab/-/issues/333721) |
|
||||
|
||||
NOTE:
|
||||
The `projects` and `shared_projects` attributes in the response are deprecated and [scheduled for removal in API v5](https://gitlab.com/gitlab-org/gitlab/-/issues/213797).
|
||||
|
|
|
@ -197,18 +197,18 @@ POST /projects/:id/protected_branches
|
|||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | yes | The name of the branch or wildcard |
|
||||
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
|
||||
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
|
||||
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
|
||||
| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
|
||||
| `allowed_to_push` | array | no | **(PREMIUM)** Array of access levels allowed to push, with each described by a hash |
|
||||
| `allowed_to_merge` | array | no | **(PREMIUM)** Array of access levels allowed to merge, with each described by a hash |
|
||||
| `allowed_to_unprotect` | array | no | **(PREMIUM)** Array of access levels allowed to unprotect, with each described by a hash |
|
||||
| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) |
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------------------------------------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | yes | The name of the branch or wildcard |
|
||||
| `push_access_level` | string | no | Access levels allowed to push (defaults: `40`, Maintainer role) |
|
||||
| `merge_access_level` | string | no | Access levels allowed to merge (defaults: `40`, Maintainer role) |
|
||||
| `unprotect_access_level` | string | no | Access levels allowed to unprotect (defaults: `40`, Maintainer role) |
|
||||
| `allow_force_push` | boolean | no | Allow all users with push access to force push. (default: `false`) |
|
||||
| `allowed_to_push` **(PREMIUM)** | array | no | Array of access levels allowed to push, with each described by a hash |
|
||||
| `allowed_to_merge` **(PREMIUM)** | array | no | Array of access levels allowed to merge, with each described by a hash |
|
||||
| `allowed_to_unprotect` **(PREMIUM)** | array | no | Array of access levels allowed to unprotect, with each described by a hash |
|
||||
| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false) |
|
||||
|
||||
Example response:
|
||||
|
||||
|
@ -414,8 +414,8 @@ PATCH /projects/:id/protected_branches/:name
|
|||
curl --request PATCH --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch"
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| --------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | yes | The name of the branch |
|
||||
| `code_owner_approval_required` | boolean | no | **(PREMIUM)** Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false)|
|
||||
| Attribute | Type | Required | Description |
|
||||
| -------------------------------------------- | ---- | -------- | ----------- |
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `name` | string | yes | The name of the branch |
|
||||
| `code_owner_approval_required` **(PREMIUM)** | boolean | no | Prevent pushes to this branch if it matches an item in the [`CODEOWNERS` file](../user/project/code_owners.md). (defaults: false)|
|
||||
|
|
|
@ -228,7 +228,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `after_sign_up_text` | string | no | Text shown to the user after signing up. |
|
||||
| `akismet_api_key` | string | required by: `akismet_enabled` | API key for Akismet spam protection. |
|
||||
| `akismet_enabled` | boolean | no | (**If enabled, requires:** `akismet_api_key`) Enable or disable Akismet spam protection. |
|
||||
| `allow_group_owners_to_manage_ldap` | boolean | no | **(PREMIUM)** Set to `true` to allow group owners to manage LDAP. |
|
||||
| `allow_group_owners_to_manage_ldap` **(PREMIUM)** | boolean | no | Set to `true` to allow group owners to manage LDAP. |
|
||||
| `allow_local_requests_from_hooks_and_services` | boolean | no | (Deprecated: Use `allow_local_requests_from_web_hooks_and_services` instead) Allow requests to the local network from hooks and services. |
|
||||
| `allow_local_requests_from_system_hooks` | boolean | no | Allow requests to the local network from system hooks. |
|
||||
| `allow_local_requests_from_web_hooks_and_services` | boolean | no | Allow requests to the local network from web hooks and services. |
|
||||
|
@ -242,7 +242,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `auto_devops_domain` | string | no | Specify a domain to use by default for every project's Auto Review Apps and Auto Deploy stages. |
|
||||
| `auto_devops_enabled` | boolean | no | Enable Auto DevOps for projects by default. It automatically builds, tests, and deploys applications based on a predefined CI/CD configuration. |
|
||||
| `automatic_purchased_storage_allocation` | boolean | no | Enabling this permits automatic allocation of purchased storage in a namespace. |
|
||||
| `check_namespace_plan` | boolean | no | **(PREMIUM)** Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
|
||||
| `check_namespace_plan` **(PREMIUM)** | boolean | no | Enabling this makes only licensed EE features available to projects if the project namespace's plan includes the feature or if the project is public. |
|
||||
| `commit_email_hostname` | string | no | Custom hostname (for private commit emails). |
|
||||
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes. |
|
||||
| `deactivate_dormant_users` | boolean | no | Enable [automatic deactivation of dormant users](../user/admin_area/moderate_users.md#automatically-deactivate-dormant-users). |
|
||||
|
@ -255,8 +255,8 @@ listed in the descriptions of the relevant settings.
|
|||
| `default_project_visibility` | string | no | What visibility level new projects receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
|
||||
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
|
||||
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
|
||||
| `delayed_project_deletion` | boolean | no | **(PREMIUM SELF)** Enable delayed project deletion by default in new groups. Default is `false`. |
|
||||
| `deletion_adjourned_period` | integer | no | **(PREMIUM SELF)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
|
||||
| `delayed_project_deletion` **(PREMIUM SELF)** | boolean | no | Enable delayed project deletion by default in new groups. Default is `false`. |
|
||||
| `deletion_adjourned_period` **(PREMIUM SELF)** | integer | no | The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
|
||||
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
|
||||
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |
|
||||
| `diff_max_lines` | integer | no | Maximum [lines in a diff](../user/admin_area/diff_limits.md). |
|
||||
|
@ -273,23 +273,23 @@ listed in the descriptions of the relevant settings.
|
|||
| `eks_account_id` | string | no | Amazon account ID. |
|
||||
| `eks_integration_enabled` | boolean | no | Enable integration with Amazon EKS. |
|
||||
| `eks_secret_access_key` | string | no | AWS IAM secret access key. |
|
||||
| `elasticsearch_aws_access_key` | string | no | **(PREMIUM)** AWS IAM access key. |
|
||||
| `elasticsearch_aws_region` | string | no | **(PREMIUM)** The AWS region the Elasticsearch domain is configured. |
|
||||
| `elasticsearch_aws_secret_access_key` | string | no | **(PREMIUM)** AWS IAM secret access key. |
|
||||
| `elasticsearch_aws` | boolean | no | **(PREMIUM)** Enable the use of AWS hosted Elasticsearch. |
|
||||
| `elasticsearch_indexed_field_length_limit` | integer | no | **(PREMIUM)** Maximum size of text fields to index by Elasticsearch. 0 value means no limit. This does not apply to repository and wiki indexing. |
|
||||
| `elasticsearch_indexed_file_size_limit_kb` | integer | no | **(PREMIUM)** Maximum size of repository and wiki files that are indexed by Elasticsearch. |
|
||||
| `elasticsearch_indexing` | boolean | no | **(PREMIUM)** Enable Elasticsearch indexing. |
|
||||
| `elasticsearch_limit_indexing` | boolean | no | **(PREMIUM)** Limit Elasticsearch to index certain namespaces and projects. |
|
||||
| `elasticsearch_max_bulk_concurrency` | integer | no | **(PREMIUM)** Maximum concurrency of Elasticsearch bulk requests per indexing operation. This only applies to repository indexing operations. |
|
||||
| `elasticsearch_max_bulk_size_mb` | integer | no | **(PREMIUM)** Maximum size of Elasticsearch bulk indexing requests in MB. This only applies to repository indexing operations. |
|
||||
| `elasticsearch_namespace_ids` | array of integers | no | **(PREMIUM)** The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
|
||||
| `elasticsearch_project_ids` | array of integers | no | **(PREMIUM)** The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
|
||||
| `elasticsearch_search` | boolean | no | **(PREMIUM)** Enable Elasticsearch search. |
|
||||
| `elasticsearch_url` | string | no | **(PREMIUM)** The URL to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). |
|
||||
| `elasticsearch_username` | string | no | **(PREMIUM)** The `username` of your Elasticsearch instance. |
|
||||
| `elasticsearch_password` | string | no | **(PREMIUM)** The password of your Elasticsearch instance. |
|
||||
| `email_additional_text` | string | no | **(PREMIUM)** Additional text added to the bottom of every email for legal/auditing/compliance reasons. |
|
||||
| `elasticsearch_aws_access_key` **(PREMIUM)** | string | no | AWS IAM access key. |
|
||||
| `elasticsearch_aws_region` **(PREMIUM)** | string | no | The AWS region the Elasticsearch domain is configured. |
|
||||
| `elasticsearch_aws_secret_access_key` **(PREMIUM)** | string | no | AWS IAM secret access key. |
|
||||
| `elasticsearch_aws` **(PREMIUM)** | boolean | no | Enable the use of AWS hosted Elasticsearch. |
|
||||
| `elasticsearch_indexed_field_length_limit` **(PREMIUM)** | integer | no | Maximum size of text fields to index by Elasticsearch. 0 value means no limit. This does not apply to repository and wiki indexing. |
|
||||
| `elasticsearch_indexed_file_size_limit_kb` **(PREMIUM)** | integer | no | Maximum size of repository and wiki files that are indexed by Elasticsearch. |
|
||||
| `elasticsearch_indexing` **(PREMIUM)** | boolean | no | Enable Elasticsearch indexing. |
|
||||
| `elasticsearch_limit_indexing` **(PREMIUM)** | boolean | no | Limit Elasticsearch to index certain namespaces and projects. |
|
||||
| `elasticsearch_max_bulk_concurrency` **(PREMIUM)** | integer | no | Maximum concurrency of Elasticsearch bulk requests per indexing operation. This only applies to repository indexing operations. |
|
||||
| `elasticsearch_max_bulk_size_mb` **(PREMIUM)** | integer | no | Maximum size of Elasticsearch bulk indexing requests in MB. This only applies to repository indexing operations. |
|
||||
| `elasticsearch_namespace_ids` **(PREMIUM)** | array of integers | no | The namespaces to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
|
||||
| `elasticsearch_project_ids` **(PREMIUM)** | array of integers | no | The projects to index via Elasticsearch if `elasticsearch_limit_indexing` is enabled. |
|
||||
| `elasticsearch_search` **(PREMIUM)** | boolean | no | Enable Elasticsearch search. |
|
||||
| `elasticsearch_url` **(PREMIUM)** | string | no | The URL to use for connecting to Elasticsearch. Use a comma-separated list to support cluster (for example, `http://localhost:9200, http://localhost:9201"`). |
|
||||
| `elasticsearch_username` **(PREMIUM)** | string | no | The `username` of your Elasticsearch instance. |
|
||||
| `elasticsearch_password` **(PREMIUM)** | string | no | The password of your Elasticsearch instance. |
|
||||
| `email_additional_text` **(PREMIUM)** | string | no | Additional text added to the bottom of every email for legal/auditing/compliance reasons. |
|
||||
| `email_author_in_body` | boolean | no | Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead. |
|
||||
| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
|
||||
| `enforce_namespace_storage_limit` | boolean | no | Enabling this permits enforcement of namespace storage limits. |
|
||||
|
@ -304,11 +304,11 @@ listed in the descriptions of the relevant settings.
|
|||
| `external_pipeline_validation_service_url` | string | no | URL to use for pipeline validation requests. |
|
||||
| `external_pipeline_validation_service_token` | string | no | Optional. Token to include as the `X-Gitlab-Token` header in requests to the URL in `external_pipeline_validation_service_url`. |
|
||||
| `external_pipeline_validation_service_timeout` | integer | no | How long to wait for a response from the pipeline validation service. Assumes `OK` if it times out. |
|
||||
| `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. |
|
||||
| `file_template_project_id` **(PREMIUM)** | integer | no | The ID of a project to load custom file templates from. |
|
||||
| `first_day_of_week` | integer | no | Start day of the week for calendar views and date pickers. Valid values are `0` (default) for Sunday, `1` for Monday, and `6` for Saturday. |
|
||||
| `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
|
||||
| `geo_status_timeout` | integer | no | **(PREMIUM)** The amount of seconds after which a request to get a secondary node status times out. |
|
||||
| `git_two_factor_session_expiry` | integer | no | **(PREMIUM)** Maximum duration (in minutes) of a session for Git operations when 2FA is enabled. |
|
||||
| `geo_node_allowed_ips` **(PREMIUM)** | string | yes | Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. |
|
||||
| `geo_status_timeout` **(PREMIUM)** | integer | no | The amount of seconds after which a request to get a secondary node status times out. |
|
||||
| `git_two_factor_session_expiry` **(PREMIUM)** | integer | no | Maximum duration (in minutes) of a session for Git operations when 2FA is enabled. |
|
||||
| `gitaly_timeout_default` | integer | no | Default Gitaly timeout, in seconds. This timeout is not enforced for Git fetch/push operations or Sidekiq jobs. Set to `0` to disable timeouts. |
|
||||
| `gitaly_timeout_fast` | integer | no | Gitaly fast operation timeout, in seconds. Some Gitaly operations are expected to be fast. If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' can help maintain the stability of the GitLab instance. Set to `0` to disable timeouts. |
|
||||
| `gitaly_timeout_medium` | integer | no | Medium Gitaly timeout, in seconds. This should be a value between the Fast and the Default timeout. Set to `0` to disable timeouts. |
|
||||
|
@ -319,7 +319,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `help_page_hide_commercial_content` | boolean | no | Hide marketing-related entries from help. |
|
||||
| `help_page_support_url` | string | no | Alternate support URL for help page and help dropdown. |
|
||||
| `help_page_text` | string | no | Custom text displayed on the help page. |
|
||||
| `help_text` | string | no | **(PREMIUM)** GitLab server administrator information. |
|
||||
| `help_text` **(PREMIUM)** | string | no | GitLab server administrator information. |
|
||||
| `hide_third_party_offers` | boolean | no | Do not display offers from third parties in GitLab. |
|
||||
| `home_page_url` | string | no | Redirect to this URL when not logged in. |
|
||||
| `housekeeping_bitmaps_enabled` | boolean | required by: `housekeeping_enabled` | Enable Git pack file bitmap creation. |
|
||||
|
@ -336,21 +336,21 @@ listed in the descriptions of the relevant settings.
|
|||
| `local_markdown_version` | integer | no | Increase this value when any cached Markdown should be invalidated. |
|
||||
| `mailgun_signing_key` | string | no | The Mailgun HTTP webhook signing key for receiving events from webhook. |
|
||||
| `mailgun_events_enabled` | boolean | no | Enable Mailgun event receiver. |
|
||||
| `maintenance_mode_message` | string | no | **(PREMIUM)** Message displayed when instance is in maintenance mode. |
|
||||
| `maintenance_mode` | boolean | no | **(PREMIUM)** When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |
|
||||
| `maintenance_mode_message` **(PREMIUM)** | string | no | Message displayed when instance is in maintenance mode. |
|
||||
| `maintenance_mode` **(PREMIUM)** | boolean | no | When instance is in maintenance mode, non-administrative users can sign in with read-only access and make read-only API requests. |
|
||||
| `max_artifacts_size` | integer | no | Maximum artifacts size in MB. |
|
||||
| `max_attachment_size` | integer | no | Limit attachment size in MB. |
|
||||
| `max_import_size` | integer | no | Maximum import size in MB. 0 for unlimited. Default = 0 (unlimited) [Modified](https://gitlab.com/gitlab-org/gitlab/-/issues/251106) from 50MB to 0 in GitLab 13.8. |
|
||||
| `max_pages_size` | integer | no | Maximum size of pages repositories in MB. |
|
||||
| `max_personal_access_token_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for personal access tokens in days. |
|
||||
| `max_ssh_key_lifetime` | integer | no | **(ULTIMATE SELF)** Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
|
||||
| `max_personal_access_token_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for personal access tokens in days. |
|
||||
| `max_ssh_key_lifetime` **(ULTIMATE SELF)** | integer | no | Maximum allowable lifetime for SSH keys in days. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1007) in GitLab 14.6. |
|
||||
| `metrics_method_call_threshold` | integer | no | A method call is only tracked when it takes longer than the given amount of milliseconds. |
|
||||
| `mirror_available` | boolean | no | Allow repository mirroring to configured by project Maintainers. If disabled, only Administrators can configure repository mirroring. |
|
||||
| `mirror_capacity_threshold` | integer | no | **(PREMIUM)** Minimum capacity to be available before scheduling more mirrors preemptively. |
|
||||
| `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. |
|
||||
| `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
|
||||
| `npm_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
|
||||
| `pypi_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
|
||||
| `mirror_capacity_threshold` **(PREMIUM)** | integer | no | Minimum capacity to be available before scheduling more mirrors preemptively. |
|
||||
| `mirror_max_capacity` **(PREMIUM)** | integer | no | Maximum number of mirrors that can be synchronizing at the same time. |
|
||||
| `mirror_max_delay` **(PREMIUM)** | integer | no | Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
|
||||
| `npm_package_requests_forwarding` **(PREMIUM)** | boolean | no | Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
|
||||
| `pypi_package_requests_forwarding` **(PREMIUM)** | boolean | no | Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
|
||||
| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or IP addresses to which local requests are allowed when local requests for hooks and services are disabled.
|
||||
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
|
||||
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
|
||||
|
@ -365,7 +365,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `project_export_enabled` | boolean | no | Enable project export. |
|
||||
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
|
||||
| `protected_ci_variables` | boolean | no | CI/CD variables are protected by default. |
|
||||
| `pseudonymizer_enabled` | boolean | no | **(PREMIUM)** When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
|
||||
| `pseudonymizer_enabled` **(PREMIUM)** | boolean | no | When enabled, GitLab runs a background job that produces pseudonymized CSVs of the GitLab database to upload to your configured object storage directory.
|
||||
| `push_event_activities_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push events are created. [Bulk push events are created](../user/admin_area/settings/push_event_activities_limit.md) if it surpasses that value. |
|
||||
| `push_event_hooks_limit` | integer | no | Number of changes (branches or tags) in a single push to determine whether webhooks and services fire or not. Webhooks and services aren't submitted if it surpasses that value. |
|
||||
| `rate_limiting_response_text` | string | no | When rate limiting is enabled via the `throttle_*` settings, send this plain text response when a rate limit is exceeded. 'Retry later' is sent if this is blank. |
|
||||
|
@ -375,7 +375,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `recaptcha_site_key` | string | required by: `recaptcha_enabled` | Site key for reCAPTCHA. |
|
||||
| `receive_max_input_size` | integer | no | Maximum push size (MB). |
|
||||
| `repository_checks_enabled` | boolean | no | GitLab periodically runs `git fsck` in all project and wiki repositories to look for silent disk corruption issues. |
|
||||
| `repository_size_limit` | integer | no | **(PREMIUM)** Size limit per repository (MB) |
|
||||
| `repository_size_limit` **(PREMIUM)** | integer | no | Size limit per repository (MB) |
|
||||
| `repository_storages_weighted` | hash of strings to integers | no | (GitLab 13.1 and later) Hash of names of taken from `gitlab.yml` to [weights](../administration/repository_storage_paths.md#configure-where-new-repositories-are-stored). New projects are created in one of these stores, chosen by a weighted random selection. |
|
||||
| `repository_storages` | array of strings | no | (GitLab 13.0 and earlier) List of names of enabled storage paths, taken from `gitlab.yml`. New projects are created in one of these stores, chosen at random. |
|
||||
| `require_admin_approval_after_user_signup` | boolean | no | When enabled, any user that signs up for an account using the registration form is placed under a **Pending approval** state and has to be explicitly [approved](../user/admin_area/moderate_users.md) by an administrator. |
|
||||
|
@ -385,7 +385,7 @@ listed in the descriptions of the relevant settings.
|
|||
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up. |
|
||||
| `session_expire_delay` | integer | no | Session duration in minutes. GitLab restart is required to apply changes. |
|
||||
| `shared_runners_enabled` | boolean | no | (**If enabled, requires:** `shared_runners_text` and `shared_runners_minutes`) Enable shared runners for new projects. |
|
||||
| `shared_runners_minutes` | integer | required by: `shared_runners_enabled` | **(PREMIUM)** Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
|
||||
| `shared_runners_minutes` **(PREMIUM)** | integer | required by: `shared_runners_enabled` | Set the maximum number of pipeline minutes that a group can use on shared runners per month. |
|
||||
| `shared_runners_text` | string | required by: `shared_runners_enabled` | Shared runners text. |
|
||||
| `sidekiq_job_limiter_mode` | string | no | `track` or `compress`. Sets the behavior for [Sidekiq job size limits](../user/admin_area/settings/sidekiq_job_limits.md). Default: 'compress'. |
|
||||
| `sidekiq_job_limiter_compression_threshold_bytes` | integer | no | The threshold in bytes at which Sidekiq jobs are compressed before being stored in Redis. Default: 100 000 bytes (100KB). |
|
||||
|
@ -393,10 +393,10 @@ listed in the descriptions of the relevant settings.
|
|||
| `sign_in_text` | string | no | Text on the login page. |
|
||||
| `signin_enabled` | string | no | (Deprecated: Use `password_authentication_enabled_for_web` instead) Flag indicating if password authentication is enabled for the web interface. |
|
||||
| `signup_enabled` | boolean | no | Enable registration. Default is `true`. |
|
||||
| `slack_app_enabled` | boolean | no | **(PREMIUM)** (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
|
||||
| `slack_app_id` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app ID of the Slack-app. |
|
||||
| `slack_app_secret` | string | required by: `slack_app_enabled` | **(PREMIUM)** The app secret of the Slack-app. |
|
||||
| `slack_app_verification_token` | string | required by: `slack_app_enabled` | **(PREMIUM)** The verification token of the Slack-app. |
|
||||
| `slack_app_enabled` **(PREMIUM)** | boolean | no | (**If enabled, requires:** `slack_app_id`, `slack_app_secret` and `slack_app_secret`) Enable Slack app. |
|
||||
| `slack_app_id` **(PREMIUM)** | string | required by: `slack_app_enabled` | The app ID of the Slack-app. |
|
||||
| `slack_app_secret` **(PREMIUM)** | string | required by: `slack_app_enabled` | The app secret of the Slack-app. |
|
||||
| `slack_app_verification_token` **(PREMIUM)** | string | required by: `slack_app_enabled` | The verification token of the Slack-app. |
|
||||
| `snippet_size_limit` | integer | no | Max snippet content size in **bytes**. Default: 52428800 Bytes (50MB).|
|
||||
| `snowplow_app_id` | string | no | The Snowplow site name / application ID. (for example, `gitlab`) |
|
||||
| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (for example, `snowplow.trx.gitlab.net`) |
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
---
|
||||
type: reference, dev
|
||||
stage: none
|
||||
group: Development
|
||||
info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
|
||||
description: "Writing styles, markup, formatting, and other standards for the GitLab RESTful APIs."
|
||||
info: For assistance with this Style Guide page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-other-projects-and-subjects.
|
||||
description: 'Writing styles, markup, formatting, and other standards for the GitLab RESTful APIs.'
|
||||
---
|
||||
|
||||
# RESTful API
|
||||
|
@ -30,6 +27,10 @@ In the Markdown doc for a resource (AKA endpoint):
|
|||
- Every method must have a detailed [description of the parameters](#method-description).
|
||||
- Every method must have a cURL example.
|
||||
- Every method must have a response body (in JSON format).
|
||||
- If an attribute is available only to higher level tiers than the other
|
||||
parameters, add the appropriate inline [tier badge](styleguide/index.md#product-tier-badges).
|
||||
Put the badge in the **Attribute** column, like the
|
||||
`**(<tier>)**` code in the following template.
|
||||
|
||||
## API topic template
|
||||
|
||||
|
@ -49,12 +50,12 @@ METHOD /endpoint
|
|||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| :---------- | :------- | :--------------------- | :-------------------- |
|
||||
| `attribute` | datatype | **{check-circle}** Yes | Detailed description. |
|
||||
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-------------------------|:---------|:-----------------------|:----------------------|
|
||||
| `attribute` | datatype | **{check-circle}** Yes | Detailed description. |
|
||||
| `attribute` **(<tier>)** | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
| `attribute` | datatype | **{dotted-circle}** No | Detailed description. |
|
||||
|
||||
Example request:
|
||||
|
||||
|
@ -83,20 +84,20 @@ always be in code blocks using backticks (`` ` ``).
|
|||
Sort the attributes in the table: first, required, then alphabetically.
|
||||
|
||||
```markdown
|
||||
| Attribute | Type | Required | Description |
|
||||
| :------------- | :------------ | :--------------------- | :--------------------------------------------------- |
|
||||
| `user` | string | **{check-circle}** Yes | The GitLab username. |
|
||||
| `assignee_ids` | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
|
||||
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-----------------------------|:--------------|:-----------------------|:-----------------------------------------------------|
|
||||
| `user` | string | **{check-circle}** Yes | The GitLab username. |
|
||||
| `assignee_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
|
||||
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
|
||||
```
|
||||
|
||||
Rendered example:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| :------------- | :------------ | :--------------------- | :--------------------------------------------------- |
|
||||
| `user` | string | **{check-circle}** Yes | The GitLab username. |
|
||||
| `assignee_ids` | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
|
||||
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
|
||||
| Attribute | Type | Required | Description |
|
||||
|:-----------------------------|:--------------|:-----------------------|:-----------------------------------------------------|
|
||||
| `user` | string | **{check-circle}** Yes | The GitLab username. |
|
||||
| `assignee_ids` **(PREMIUM)** | integer array | **{dotted-circle}** No | The IDs of the users to assign the issue to. |
|
||||
| `confidential` | boolean | **{dotted-circle}** No | Set an issue to be confidential. Default is `false`. |
|
||||
|
||||
## cURL commands
|
||||
|
||||
|
@ -109,7 +110,7 @@ Rendered example:
|
|||
username and password.
|
||||
|
||||
| Methods | Description |
|
||||
| :---------------------------------------------- | :----------------------------------------------------- |
|
||||
|:------------------------------------------------|:-------------------------------------------------------|
|
||||
| `--header "PRIVATE-TOKEN: <your_access_token>"` | Use this method as is, whenever authentication needed. |
|
||||
| `--request POST` | Use this method when creating new objects |
|
||||
| `--request PUT` | Use this method when updating existing objects |
|
||||
|
|
|
@ -1806,7 +1806,9 @@ after the heading text. For example:
|
|||
# Heading title **(FREE)**
|
||||
```
|
||||
|
||||
Do not add tier badges inline with other text. The single source of truth for a feature should be the heading where the functionality is described.
|
||||
Do not add tier badges inline with other text, except for [API attributes](../restful_api_styleguide.md).
|
||||
The single source of truth for a feature should be the heading where the
|
||||
functionality is described.
|
||||
|
||||
#### Available product tier badges
|
||||
|
||||
|
|
|
@ -107,19 +107,19 @@ The following options are available:
|
|||
| Push rule | Description |
|
||||
|---------------------------------|-------------|
|
||||
| Removal of tags with `git push` | Forbid users to remove Git tags with `git push`. Tags can be deleted through the web UI. |
|
||||
| Check whether the commit author is a GitLab user | Restrict commits to existing GitLab users (checked against their emails). |
|
||||
| Reject unverified users | GitLab rejects any commit that was not committed by an authenticated user. |
|
||||
| Check whether the commit author is a GitLab user | Restrict commits to existing GitLab users (checked against their emails). <sup>1</sup> |
|
||||
| Reject unverified users | GitLab rejects any commit that was not committed by the same user as the user who pushed it, or where the committer's email address is not [confirmed](../security/user_email_confirmation.md). |
|
||||
| Check whether commit is signed through GPG | Reject commit when it is not signed through GPG. Read [signing commits with GPG](../user/project/repository/gpg_signed_commits/index.md). |
|
||||
| Prevent pushing secret files | GitLab rejects any files that are likely to contain secrets. See the [forbidden file names](#prevent-pushing-secrets-to-the-repository). |
|
||||
| Require expression in commit messages | Only commit messages that match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
|
||||
| Reject expression in commit messages | Only commit messages that do not match this regular expression are allowed to be pushed. Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
|
||||
| Restrict by branch name | Only branch names that match this regular expression are allowed to be pushed. Leave empty to allow all branch names. |
|
||||
| Restrict by commit author's email | Only commit author's email that match this regular expression are allowed to be pushed. Leave empty to allow any email. |
|
||||
| Prohibited file names | Any committed filenames that match this regular expression and do not already exist in the repository are not allowed to be pushed. Leave empty to allow any filenames. See [common examples](#prohibited-file-names). |
|
||||
| Require expression in commit messages | Only commit messages that match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
|
||||
| Reject expression in commit messages | Only commit messages that do not match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow any commit message. Uses multiline mode, which can be disabled using `(?-m)`. |
|
||||
| Restrict by branch name | Only branch names that match this regular expression are allowed to be pushed. <sup>2</sup> Leave empty to allow all branch names. |
|
||||
| Restrict by commit author's email | Only commit author's email that match this regular expression are allowed to be pushed. <sup>1</sup> <sup>2</sup> Leave empty to allow any email. |
|
||||
| Prohibited file names | Any committed filenames that match this regular expression and do not already exist in the repository are not allowed to be pushed. <sup>2</sup> Leave empty to allow any filenames. See [common examples](#prohibited-file-names). |
|
||||
| Maximum file size | Pushes that contain added or updated files that exceed this file size (in MB) are rejected. Set to 0 to allow files of any size. Files tracked by Git LFS are exempted. |
|
||||
|
||||
NOTE:
|
||||
GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular expressions in push rules, and you can test them at the [regex101 regex tester](https://regex101.com/).
|
||||
1. Checks both the commit author and committer.
|
||||
1. GitLab uses [RE2 syntax](https://github.com/google/re2/wiki/Syntax) for regular expressions in push rules, and you can test them at the [regex101 regex tester](https://regex101.com/).
|
||||
|
||||
### Caveat to "Reject unsigned commits" push rule
|
||||
|
||||
|
|
|
@ -283,6 +283,30 @@ It also displays the following important statistics:
|
|||
| Maximum users | The highest number of billable users on your system during the term of the loaded license. |
|
||||
| Users over license | Calculated as `Maximum users` - `Users in License` for the current license term. This number incurs a retroactive charge that needs to be paid for at renewal. |
|
||||
|
||||
## Export your license usage
|
||||
|
||||
> Introduced in GitLab 14.6.
|
||||
|
||||
If you are an administrator, you can export your license usage into a CSV:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Subscription**.
|
||||
1. In the top right, select **Export license usage file**.
|
||||
|
||||
This file contains all the information GitLab needs to manually process quarterly reconciliations or renewals. If your instance is firewalled or air-gapped, you can provide GitLab with this information.
|
||||
|
||||
The **License Usage** CSV includes the following details:
|
||||
|
||||
- License key
|
||||
- Email
|
||||
- License start date
|
||||
- License end date
|
||||
- Company
|
||||
- Generated at (the timestamp for when the file was exported)
|
||||
- Table of historical user counts for each day in the period:
|
||||
- Date the count was recorded
|
||||
- Active user count
|
||||
|
||||
## Renew your subscription
|
||||
|
||||
To renew your subscription,
|
||||
|
|
|
@ -75,21 +75,21 @@ If you choose a size larger than the configured value for the web server,
|
|||
you may receive errors. See the [troubleshooting section](#troubleshooting) for more
|
||||
details.
|
||||
|
||||
## Personal Access Token prefix
|
||||
## Personal access token prefix
|
||||
|
||||
> [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5. Default prefix added.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20968) in GitLab 13.7.
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/342327) in GitLab 14.5, a default prefix.
|
||||
|
||||
You can set a global prefix for all generated Personal Access Tokens.
|
||||
You can specify a prefix for personal access tokens. You might use a prefix
|
||||
to find tokens more quickly, or for use with automation tools.
|
||||
|
||||
A prefix can help you identify PATs visually, as well as with automation tools.
|
||||
The default prefix is `glpat-` but administrators can change it.
|
||||
|
||||
NOTE:
|
||||
For GitLab.com and self-managed instances, the default prefix is `glpat-`.
|
||||
[Project access tokens](../../project/settings/project_access_tokens.md) also inherit this prefix.
|
||||
|
||||
### Set a prefix
|
||||
|
||||
Only a GitLab administrator can set the prefix, which is a global setting applied
|
||||
to any PAT generated in the system by any user:
|
||||
To change the default global prefix:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Settings > General**.
|
||||
|
@ -97,8 +97,8 @@ to any PAT generated in the system by any user:
|
|||
1. Fill in the **Personal Access Token prefix** field.
|
||||
1. Click **Save changes**.
|
||||
|
||||
It is also possible to configure the prefix via the [settings API](../../../api/settings.md)
|
||||
using the `personal_access_token_prefix` field.
|
||||
You can also configure the prefix by using the
|
||||
[settings API](../../../api/settings.md).
|
||||
|
||||
## Repository size limit **(PREMIUM SELF)**
|
||||
|
||||
|
|
|
@ -168,6 +168,36 @@ container_scanning:
|
|||
CS_DISABLE_DEPENDENCY_LIST: "true"
|
||||
```
|
||||
|
||||
#### Report language-specific findings
|
||||
|
||||
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/7277) in GitLab 14.6.
|
||||
|
||||
The `CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` CI/CD variable controls whether the scan reports
|
||||
findings related to programming languages. The languages supported depend on the
|
||||
[scanner used](#change-scanners):
|
||||
|
||||
- [Trivy](https://aquasecurity.github.io/trivy/latest/vulnerability/detection/language/).
|
||||
- [Grype](https://github.com/anchore/grype#features).
|
||||
|
||||
By default, the report only includes packages managed by the Operating System (OS) package manager
|
||||
(for example, `yum`, `apt`, `apk`, `tdnf`). To report security findings in non-OS packages, set
|
||||
`CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN` to `"false"`:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Security/Container-Scanning.gitlab-ci.yml
|
||||
|
||||
container_scanning:
|
||||
variables:
|
||||
CS_DISABLE_LANGUAGE_VULNERABILITY_SCAN: "false"
|
||||
```
|
||||
|
||||
When you enable this feature, you may see [duplicate findings](../terminology/#duplicate-finding)
|
||||
in the [Vulnerability Report](../vulnerability_report/)
|
||||
if [Dependency Scanning](../dependency_scanning/)
|
||||
is enabled for your project. This happens because GitLab can't automatically deduplicate the
|
||||
findings reported by the two different analyzers.
|
||||
|
||||
#### Available CI/CD variables
|
||||
|
||||
You can [configure](#customizing-the-container-scanning-settings) analyzers by using the following CI/CD variables:
|
||||
|
|
|
@ -119,7 +119,7 @@ The following table lists project permissions available for each role:
|
|||
| [Merge requests](project/merge_requests/index.md):<br>Apply code change suggestions | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Approve (*9*) | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Assign | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Create | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Create (*18*) | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Add labels | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Lock threads | | | ✓ | ✓ | ✓ |
|
||||
| [Merge requests](project/merge_requests/index.md):<br>Manage or accept | | | ✓ | ✓ | ✓ |
|
||||
|
@ -233,6 +233,7 @@ The following table lists project permissions available for each role:
|
|||
1. Guest users can only set metadata (for example, labels, assignees, or milestones)
|
||||
when creating an issue. They cannot change the metadata on existing issues.
|
||||
1. In GitLab 14.5 or later, Guests are not allowed to [create incidents](../operations/incident_management/incidents.md#incident-creation).
|
||||
1. In projects that accept contributions from external members, users can create, edit, and close their own merge requests.
|
||||
|
||||
## Project features permissions
|
||||
|
||||
|
|
|
@ -20,8 +20,7 @@ password secret.
|
|||
NOTE:
|
||||
When you enable 2FA, don't forget to back up your [recovery codes](#recovery-codes)!
|
||||
|
||||
In addition to time-based one time passwords (TOTP), GitLab supports U2F
|
||||
(universal 2nd factor) and WebAuthn (experimental) devices as the second factor
|
||||
In addition to time-based one time passwords (TOTP), GitLab supports WebAuthn devices as the second factor
|
||||
of authentication. After being enabled, in addition to supplying your username
|
||||
and password to sign in, you're prompted to activate your U2F / WebAuthn device
|
||||
(usually by pressing a button on it) which performs secure authentication on
|
||||
|
@ -269,11 +268,11 @@ Click on **Register U2F Device** to complete the process.
|
|||
|
||||
### WebAuthn device
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22506) in GitLab 13.4.
|
||||
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-webauthn).
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22506) in GitLab 13.4 [with a flag](../../../administration/feature_flags.md) named `webauthn`. Disabled by default.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/232671) in GitLab 14.6.
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is available. To disable the feature, ask an administrator to [disable the feature flag](../../../administration/feature_flags.md) named `webauthn`. If you disable the WebAuthn feature flag after WebAuthn devices have been registered, these devices are not usable until you re-enable this feature. On GitLab.com, this feature is available.
|
||||
|
||||
The WebAuthn workflow is [supported by](https://caniuse.com/#search=webauthn) the
|
||||
following desktop browsers:
|
||||
|
@ -350,7 +349,7 @@ request, and you're automatically signed in.
|
|||
### Sign in by using a WebAuthn device
|
||||
|
||||
In supported browsers you should be automatically prompted to activate your WebAuthn device
|
||||
(for example, by touching/pressing its button) after entering your credentials.
|
||||
(for example, by touching or pressing its button) after entering your credentials.
|
||||
|
||||
A message displays, indicating that your device responded to the authentication
|
||||
request and you're automatically signed in.
|
||||
|
@ -495,25 +494,6 @@ request a GitLab global administrator disable two-factor authentication for your
|
|||
|
||||
- To enforce 2FA at the system or group levels see [Enforce Two-factor Authentication](../../../security/two_factor_authentication.md).
|
||||
|
||||
## Enable or disable WebAuthn **(FREE SELF)**
|
||||
|
||||
Support for WebAuthn is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:webauthn)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:webauthn)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you are receiving an `invalid pin code` error, this may indicate that there is a time sync issue between the authentication application and the GitLab instance itself.
|
||||
|
|
|
@ -74,7 +74,7 @@ change and whether you need access to a development environment:
|
|||
|
||||
If you decide to permanently stop work on a merge request,
|
||||
GitLab recommends you close the merge request rather than
|
||||
[delete it](#delete-a-merge-request). Users with
|
||||
[delete it](#delete-a-merge-request). The author and assignees of a merge request, and users with
|
||||
Developer, Maintainer, or Owner [roles](../../permissions.md) in a project
|
||||
can close merge requests in the project:
|
||||
|
||||
|
|
|
@ -32,6 +32,9 @@ You can use project access tokens:
|
|||
- Consider [disabling project access tokens](#enable-or-disable-project-access-token-creation) to
|
||||
lower potential abuse.
|
||||
|
||||
Project access tokens inherit the [default prefix setting](../../admin_area/settings/account_and_limit_settings.md#personal-access-token-prefix)
|
||||
configured for personal access tokens.
|
||||
|
||||
## Create a project access token
|
||||
|
||||
To create a project access token:
|
||||
|
|
|
@ -22,6 +22,20 @@ ci_namespace_mirrors:
|
|||
- table: namespaces
|
||||
column: namespace_id
|
||||
on_delete: async_delete
|
||||
ci_builds:
|
||||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_nullify
|
||||
ci_pipelines:
|
||||
- table: merge_requests
|
||||
column: merge_request_id
|
||||
on_delete: async_delete
|
||||
- table: external_pull_requests
|
||||
column: external_pull_request_id
|
||||
on_delete: async_nullify
|
||||
- table: users
|
||||
column: user_id
|
||||
on_delete: async_nullify
|
||||
ci_project_mirrors:
|
||||
- table: projects
|
||||
column: project_id
|
||||
|
@ -49,3 +63,7 @@ merge_request_metrics:
|
|||
- table: ci_pipelines
|
||||
column: pipeline_id
|
||||
on_delete: async_delete
|
||||
project_pages_metadata:
|
||||
- table: ci_job_artifacts
|
||||
column: artifacts_archive_id
|
||||
on_delete: async_nullify
|
||||
|
|
|
@ -2,12 +2,6 @@
|
|||
|
||||
module Gitlab
|
||||
module ProcessManagement
|
||||
# The signals that should terminate both the master and workers.
|
||||
TERMINATE_SIGNALS = %i(INT TERM).freeze
|
||||
|
||||
# The signals that should simply be forwarded to the workers.
|
||||
FORWARD_SIGNALS = %i(TTIN USR1 USR2 HUP).freeze
|
||||
|
||||
# Traps the given signals and yields the block whenever these signals are
|
||||
# received.
|
||||
#
|
||||
|
@ -26,12 +20,13 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def self.trap_terminate(&block)
|
||||
trap_signals(TERMINATE_SIGNALS, &block)
|
||||
end
|
||||
|
||||
def self.trap_forward(&block)
|
||||
trap_signals(FORWARD_SIGNALS, &block)
|
||||
# Traps the given signals with the given command.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# modify_signals(%i(HUP TERM), 'DEFAULT')
|
||||
def self.modify_signals(signals, command)
|
||||
signals.each { |signal| trap(signal, command) }
|
||||
end
|
||||
|
||||
def self.signal(pid, signal)
|
||||
|
|
|
@ -1805,6 +1805,15 @@ msgstr ""
|
|||
msgid "AccessTokens|Are you sure? Any issue email addresses currently in use will stop working."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Copy feed token"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Copy incoming email token"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Copy static object token"
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Created"
|
||||
msgstr ""
|
||||
|
||||
|
@ -1817,12 +1826,21 @@ msgstr ""
|
|||
msgid "AccessTokens|It cannot be used to access any other data."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can access repository static objects as if they were you. If that ever happens, %{reset_link_start}reset this token%{reset_link_end}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can create issues as if they were you. If that happens, %{link_reset_it}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{linkStart}reset this token%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
msgid "AccessTokens|Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you. If that happens, %{link_reset_it}."
|
||||
msgstr ""
|
||||
|
||||
|
@ -10264,6 +10282,9 @@ msgstr ""
|
|||
msgid "Crm|Contact has been added"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Contact has been updated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Create new contact"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10273,6 +10294,9 @@ msgstr ""
|
|||
msgid "Crm|Description (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Edit contact"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Email"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10282,9 +10306,6 @@ msgstr ""
|
|||
msgid "Crm|Last name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|New Contact"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|New contact"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -22,5 +22,6 @@ require_relative '../lib/gitlab/metrics/exporter/base_exporter'
|
|||
require_relative '../lib/gitlab/metrics/exporter/sidekiq_exporter'
|
||||
require_relative '../lib/gitlab/health_checks/probes/collection'
|
||||
require_relative '../lib/gitlab/health_checks/probes/status'
|
||||
require_relative '../lib/gitlab/process_management'
|
||||
|
||||
# rubocop:enable Naming/FileName
|
||||
|
|
|
@ -6,17 +6,28 @@ require_relative 'dependencies'
|
|||
|
||||
class MetricsServer # rubocop:disable Gitlab/NamespacedClass
|
||||
class << self
|
||||
def spawn(target, gitlab_config: nil, wipe_metrics_dir: false)
|
||||
cmd = "#{Rails.root}/bin/metrics-server"
|
||||
env = {
|
||||
'METRICS_SERVER_TARGET' => target,
|
||||
'GITLAB_CONFIG' => gitlab_config,
|
||||
'WIPE_METRICS_DIR' => wipe_metrics_dir.to_s
|
||||
}
|
||||
def spawn(target, metrics_dir:, wipe_metrics_dir: false, trapped_signals: [])
|
||||
raise "The only valid target is 'sidekiq' currently" unless target == 'sidekiq'
|
||||
|
||||
Process.spawn(env, cmd, err: $stderr, out: $stdout).tap do |pid|
|
||||
pid = Process.fork
|
||||
|
||||
if pid.nil? # nil means we're inside the fork
|
||||
# Remove any custom signal handlers the parent process had registered, since we do
|
||||
# not want to inherit them, and Ruby forks with a `clone` that has the `CLONE_SIGHAND`
|
||||
# flag set.
|
||||
Gitlab::ProcessManagement.modify_signals(trapped_signals, 'DEFAULT')
|
||||
|
||||
server = MetricsServer.new(target, metrics_dir, wipe_metrics_dir)
|
||||
# This rewrites /proc/cmdline, since otherwise tools like `top` will show the
|
||||
# parent process `cmdline` which is really confusing.
|
||||
$0 = server.name
|
||||
|
||||
server.start
|
||||
else
|
||||
Process.detach(pid)
|
||||
end
|
||||
|
||||
pid
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -34,10 +45,15 @@ class MetricsServer # rubocop:disable Gitlab/NamespacedClass
|
|||
FileUtils.mkdir_p(@metrics_dir, mode: 0700)
|
||||
::Prometheus::CleanupMultiprocDirService.new.execute if @wipe_metrics_dir
|
||||
|
||||
settings = Settings.monitoring.sidekiq_exporter
|
||||
settings = Settings.new(Settings.monitoring[name])
|
||||
|
||||
exporter_class = "Gitlab::Metrics::Exporter::#{@target.camelize}Exporter".constantize
|
||||
server = exporter_class.instance(settings, synchronous: true)
|
||||
|
||||
server.start
|
||||
end
|
||||
|
||||
def name
|
||||
"#{@target}_exporter"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -55,9 +55,9 @@
|
|||
"@babel/preset-env": "^7.10.1",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "1.226.0",
|
||||
"@gitlab/svgs": "1.229.0",
|
||||
"@gitlab/tributejs": "1.0.0",
|
||||
"@gitlab/ui": "32.43.2",
|
||||
"@gitlab/ui": "32.49.0",
|
||||
"@gitlab/visual-review-tools": "1.6.1",
|
||||
"@rails/actioncable": "6.1.4-1",
|
||||
"@rails/ujs": "6.1.4-1",
|
||||
|
@ -115,7 +115,7 @@
|
|||
"codesandbox-api": "0.0.23",
|
||||
"compression-webpack-plugin": "^5.0.2",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"core-js": "^3.19.3",
|
||||
"core-js": "^3.20.0",
|
||||
"cron-validator": "^1.1.1",
|
||||
"cronstrue": "^1.122.0",
|
||||
"cropper": "^2.3.0",
|
||||
|
@ -129,7 +129,7 @@
|
|||
"dompurify": "^2.3.4",
|
||||
"dropzone": "^4.2.0",
|
||||
"editorconfig": "^0.15.3",
|
||||
"emoji-regex": "^7.0.3",
|
||||
"emoji-regex": "^10.0.0",
|
||||
"fast-mersenne-twister": "1.0.2",
|
||||
"file-loader": "^6.2.0",
|
||||
"fuzzaldrin-plus": "^0.6.0",
|
||||
|
|
|
@ -20,6 +20,14 @@ require_relative 'sidekiq_cluster'
|
|||
module Gitlab
|
||||
module SidekiqCluster
|
||||
class CLI
|
||||
THREAD_NAME = 'supervisor'
|
||||
|
||||
# The signals that should terminate both the master and workers.
|
||||
TERMINATE_SIGNALS = %i(INT TERM).freeze
|
||||
|
||||
# The signals that should simply be forwarded to the workers.
|
||||
FORWARD_SIGNALS = %i(TTIN USR1 USR2 HUP).freeze
|
||||
|
||||
CommandError = Class.new(StandardError)
|
||||
|
||||
def initialize(log_output = $stderr)
|
||||
|
@ -27,6 +35,7 @@ module Gitlab
|
|||
@max_concurrency = 50
|
||||
@min_concurrency = 0
|
||||
@environment = ENV['RAILS_ENV'] || 'development'
|
||||
@metrics_dir = ENV["prometheus_multiproc_dir"] || File.absolute_path("tmp/prometheus_multiproc_dir/sidekiq")
|
||||
@pid = nil
|
||||
@interval = 5
|
||||
@alive = true
|
||||
|
@ -39,6 +48,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def run(argv = ARGV)
|
||||
Thread.current.name = THREAD_NAME
|
||||
|
||||
if argv.empty?
|
||||
raise CommandError,
|
||||
'You must specify at least one queue to start a worker for'
|
||||
|
@ -144,13 +155,13 @@ module Gitlab
|
|||
end
|
||||
|
||||
def trap_signals
|
||||
ProcessManagement.trap_terminate do |signal|
|
||||
ProcessManagement.trap_signals(TERMINATE_SIGNALS) do |signal|
|
||||
@alive = false
|
||||
ProcessManagement.signal_processes(@processes, signal)
|
||||
wait_for_termination
|
||||
end
|
||||
|
||||
ProcessManagement.trap_forward do |signal|
|
||||
ProcessManagement.trap_signals(FORWARD_SIGNALS) do |signal|
|
||||
ProcessManagement.signal_processes(@processes, signal)
|
||||
end
|
||||
end
|
||||
|
@ -180,7 +191,12 @@ module Gitlab
|
|||
return unless metrics_server_enabled?
|
||||
|
||||
@logger.info("Starting metrics server on port #{sidekiq_exporter_port}")
|
||||
@metrics_server_pid = MetricsServer.spawn('sidekiq', wipe_metrics_dir: wipe_metrics_dir)
|
||||
@metrics_server_pid = MetricsServer.spawn(
|
||||
'sidekiq',
|
||||
metrics_dir: @metrics_dir,
|
||||
wipe_metrics_dir: wipe_metrics_dir,
|
||||
trapped_signals: TERMINATE_SIGNALS + FORWARD_SIGNALS
|
||||
)
|
||||
end
|
||||
|
||||
def sidekiq_exporter_enabled?
|
||||
|
|
|
@ -29,17 +29,27 @@ RSpec.describe 'bin/metrics-server', :aggregate_failures do
|
|||
|
||||
config_file.write(YAML.dump(config))
|
||||
config_file.close
|
||||
@pid = MetricsServer.spawn('sidekiq', gitlab_config: config_file.path, wipe_metrics_dir: true)
|
||||
|
||||
env = {
|
||||
'GITLAB_CONFIG' => config_file.path,
|
||||
'METRICS_SERVER_TARGET' => 'sidekiq',
|
||||
'WIPE_METRICS_DIR' => '1'
|
||||
}
|
||||
@pid = Process.spawn(env, 'bin/metrics-server', pgroup: true)
|
||||
end
|
||||
|
||||
after do
|
||||
webmock_enable!
|
||||
|
||||
if @pid
|
||||
pgrp = Process.getpgid(@pid)
|
||||
|
||||
Timeout.timeout(5) do
|
||||
Process.kill('TERM', @pid)
|
||||
Process.kill('TERM', -pgrp)
|
||||
Process.waitpid(@pid)
|
||||
end
|
||||
|
||||
expect(Gitlab::ProcessManagement.process_alive?(@pid)).to be(false)
|
||||
end
|
||||
rescue Errno::ESRCH => _
|
||||
# 'No such process' means the process died before
|
||||
|
|
|
@ -258,6 +258,17 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
|
|||
end
|
||||
|
||||
context 'metrics server' do
|
||||
let(:trapped_signals) { described_class::TERMINATE_SIGNALS + described_class::FORWARD_SIGNALS }
|
||||
let(:metrics_dir) { Dir.mktmpdir }
|
||||
|
||||
before do
|
||||
stub_env('prometheus_multiproc_dir', metrics_dir)
|
||||
end
|
||||
|
||||
after do
|
||||
FileUtils.rm_rf(metrics_dir, secure: true)
|
||||
end
|
||||
|
||||
context 'starting the server' do
|
||||
context 'without --dryrun' do
|
||||
context 'when there are no sidekiq_health_checks settings set' do
|
||||
|
@ -342,31 +353,33 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
|
|||
end
|
||||
end
|
||||
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
context 'with valid settings' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
where(:sidekiq_exporter_enabled, :sidekiq_exporter_port, :sidekiq_health_checks_port, :start_metrics_server) do
|
||||
true | '3807' | '3907' | true
|
||||
true | '3807' | '3807' | false
|
||||
false | '3807' | '3907' | false
|
||||
false | '3807' | '3907' | false
|
||||
end
|
||||
|
||||
with_them do
|
||||
before do
|
||||
allow(Gitlab::SidekiqCluster).to receive(:start)
|
||||
allow(cli).to receive(:write_pid)
|
||||
allow(cli).to receive(:trap_signals)
|
||||
allow(cli).to receive(:start_loop)
|
||||
where(:sidekiq_exporter_enabled, :sidekiq_exporter_port, :sidekiq_health_checks_port, :start_metrics_server) do
|
||||
true | '3807' | '3907' | true
|
||||
true | '3807' | '3807' | false
|
||||
false | '3807' | '3907' | false
|
||||
false | '3807' | '3907' | false
|
||||
end
|
||||
|
||||
specify do
|
||||
if start_metrics_server
|
||||
expect(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: true)
|
||||
else
|
||||
expect(MetricsServer).not_to receive(:spawn)
|
||||
with_them do
|
||||
before do
|
||||
allow(Gitlab::SidekiqCluster).to receive(:start)
|
||||
allow(cli).to receive(:write_pid)
|
||||
allow(cli).to receive(:trap_signals)
|
||||
allow(cli).to receive(:start_loop)
|
||||
end
|
||||
|
||||
cli.run(%w(foo))
|
||||
specify do
|
||||
if start_metrics_server
|
||||
expect(MetricsServer).to receive(:spawn).with('sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: true, trapped_signals: trapped_signals)
|
||||
else
|
||||
expect(MetricsServer).not_to receive(:spawn)
|
||||
end
|
||||
|
||||
cli.run(%w(foo))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -388,7 +401,7 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
|
|||
|
||||
before do
|
||||
allow(cli).to receive(:sleep).with(a_kind_of(Numeric))
|
||||
allow(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: false).and_return(99)
|
||||
allow(MetricsServer).to receive(:spawn).and_return(99)
|
||||
cli.start_metrics_server
|
||||
end
|
||||
|
||||
|
@ -407,7 +420,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
|
|||
allow(Gitlab::ProcessManagement).to receive(:all_alive?).with(an_instance_of(Array)).and_return(false)
|
||||
allow(cli).to receive(:stop_metrics_server)
|
||||
|
||||
expect(MetricsServer).to receive(:spawn).with('sidekiq', wipe_metrics_dir: false)
|
||||
expect(MetricsServer).to receive(:spawn).with(
|
||||
'sidekiq', metrics_dir: metrics_dir, wipe_metrics_dir: false, trapped_signals: trapped_signals
|
||||
)
|
||||
|
||||
cli.start_loop
|
||||
end
|
||||
|
@ -484,9 +499,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI do # rubocop:disable RSpec/FilePath
|
|||
end
|
||||
|
||||
describe '#trap_signals' do
|
||||
it 'traps the termination and forwarding signals' do
|
||||
expect(Gitlab::ProcessManagement).to receive(:trap_terminate)
|
||||
expect(Gitlab::ProcessManagement).to receive(:trap_forward)
|
||||
it 'traps termination and sidekiq specific signals' do
|
||||
expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[INT TERM])
|
||||
expect(Gitlab::ProcessManagement).to receive(:trap_signals).with(%i[TTIN USR1 USR2 HUP])
|
||||
|
||||
cli.trap_signals
|
||||
end
|
||||
|
|
|
@ -26,9 +26,8 @@ RSpec.describe 'Database schema' do
|
|||
boards: %w[milestone_id iteration_id],
|
||||
chat_names: %w[chat_id team_id user_id],
|
||||
chat_teams: %w[team_id],
|
||||
ci_builds: %w[erased_by_id runner_id trigger_request_id user_id],
|
||||
ci_builds: %w[erased_by_id runner_id trigger_request_id],
|
||||
ci_namespace_monthly_usages: %w[namespace_id],
|
||||
ci_pipelines: %w[user_id],
|
||||
ci_runner_projects: %w[runner_id],
|
||||
ci_trigger_requests: %w[commit_id],
|
||||
cluster_providers_aws: %w[security_group_id vpc_id access_key_id],
|
||||
|
|
|
@ -323,6 +323,14 @@ FactoryBot.define do
|
|||
size { 1149.bytes }
|
||||
end
|
||||
|
||||
trait(:generic_zip) do
|
||||
package
|
||||
file_fixture { 'spec/fixtures/packages/generic/myfile.zip' }
|
||||
file_name { "#{package.name}.zip" }
|
||||
file_sha256 { '3559e770bd493b326e8ec5e6242f7206d3fbf94fa47c16f82d34a037daa113e5' }
|
||||
size { 3989.bytes }
|
||||
end
|
||||
|
||||
trait(:object_storage) do
|
||||
file_store { Packages::PackageFileUploader::Store::REMOTE }
|
||||
end
|
||||
|
|
|
@ -247,6 +247,12 @@ FactoryBot.define do
|
|||
sequence(:name) { |n| "generic-package-#{n}" }
|
||||
version { '1.0.0' }
|
||||
package_type { :generic }
|
||||
|
||||
trait(:with_zip_file) do
|
||||
after :create do |package|
|
||||
create :package_file, :generic_zip, package: package
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -57,6 +57,14 @@ RSpec.describe 'Global search' do
|
|||
expect(page).to have_selector('.search-form')
|
||||
expect(page).to have_no_selector('#js-header-search')
|
||||
end
|
||||
|
||||
it 'focuses search input when shortcut "s" is pressed', :js do
|
||||
expect(page).not_to have_selector('#search:focus')
|
||||
|
||||
find('body').native.send_key('s')
|
||||
|
||||
expect(page).to have_selector('#search:focus')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when new_header_search feature is enabled' do
|
||||
|
@ -70,5 +78,13 @@ RSpec.describe 'Global search' do
|
|||
expect(page).to have_no_selector('.search-form')
|
||||
expect(page).to have_selector('#js-header-search')
|
||||
end
|
||||
|
||||
it 'focuses search input when shortcut "s" is pressed', :js do
|
||||
expect(page).not_to have_selector('#search:focus')
|
||||
|
||||
find('body').native.send_key('s')
|
||||
|
||||
expect(page).to have_selector('#search:focus')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -63,11 +63,24 @@ RSpec.describe 'Profile account page', :js do
|
|||
end
|
||||
|
||||
describe 'when I reset feed token' do
|
||||
before do
|
||||
it 'resets feed token with `hide_access_tokens` feature flag enabled' do
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('[data-testid="feed-token-container"]') do
|
||||
previous_token = find_field('Feed token').value
|
||||
|
||||
accept_confirm { click_link('reset this token') }
|
||||
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(find_field('Feed token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets feed token' do
|
||||
it 'resets feed token with `hide_access_tokens` feature flag disabled' do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('.feed-token-reset') do
|
||||
previous_token = find("#feed_token").value
|
||||
|
||||
|
@ -82,10 +95,26 @@ RSpec.describe 'Profile account page', :js do
|
|||
before do
|
||||
allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true)
|
||||
stub_feature_flags(bootstrap_confirmation_modals: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
end
|
||||
|
||||
it 'resets incoming email token' do
|
||||
it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('[data-testid="incoming-email-token-container"]') do
|
||||
previous_token = find_field('Incoming email token').value
|
||||
|
||||
accept_confirm { click_link('reset this token') }
|
||||
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(find_field('Incoming email token').value).not_to eq(previous_token)
|
||||
end
|
||||
end
|
||||
|
||||
it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
within('.incoming-email-token-reset') do
|
||||
previous_token = find('#incoming_email_token').value
|
||||
|
||||
|
|
|
@ -18,10 +18,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
find("#created-personal-access-token").value
|
||||
end
|
||||
|
||||
def feed_token
|
||||
find("#feed_token").value
|
||||
end
|
||||
|
||||
def feed_token_description
|
||||
"Your feed token authenticates you when your RSS reader loads a personalized RSS feed or when your calendar application loads a personalized calendar. It is visible in those feed URLs."
|
||||
end
|
||||
|
@ -136,12 +132,24 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
|
||||
describe "feed token" do
|
||||
context "when enabled" do
|
||||
it "displays feed token" do
|
||||
it "displays feed token with `hide_access_tokens` feature flag enabled" do
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(feed_token).to eq(user.feed_token)
|
||||
within('[data-testid="feed-token-container"]') do
|
||||
click_button('Click to reveal')
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
|
||||
it "displays feed token with `hide_access_tokens` feature flag disabled" do
|
||||
stub_feature_flags(hide_access_tokens: false)
|
||||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(page).to have_field('Feed token', with: user.feed_token)
|
||||
expect(page).to have_content(feed_token_description)
|
||||
end
|
||||
end
|
||||
|
@ -151,8 +159,8 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
|||
allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(true)
|
||||
visit profile_personal_access_tokens_path
|
||||
|
||||
expect(page).to have_no_content(feed_token_description)
|
||||
expect(page).to have_no_css("#feed_token")
|
||||
expect(page).not_to have_content(feed_token_description)
|
||||
expect(page).not_to have_field('Feed token')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Binary file not shown.
|
@ -1,8 +1,7 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants';
|
||||
|
||||
export const emojiFixtureMap = {
|
||||
export const validEmoji = {
|
||||
atom: {
|
||||
moji: '⚛',
|
||||
description: 'atom symbol',
|
||||
|
@ -49,11 +48,39 @@ export const emojiFixtureMap = {
|
|||
unicodeVersion: '5.1',
|
||||
description: 'white medium star',
|
||||
},
|
||||
gay_pride_flag: {
|
||||
moji: '🏳️🌈',
|
||||
unicodeVersion: '7.0',
|
||||
description: 'because it contains a zero width joiner',
|
||||
},
|
||||
family_mmb: {
|
||||
moji: '👨👨👦',
|
||||
unicodeVersion: '6.0',
|
||||
description: 'because it contains multiple zero width joiners',
|
||||
},
|
||||
};
|
||||
|
||||
export const invalidEmoji = {
|
||||
xss: {
|
||||
moji: '<img src=x onerror=prompt(1)>',
|
||||
unicodeVersion: '5.1',
|
||||
description: 'xss',
|
||||
},
|
||||
non_moji: {
|
||||
moji: 'I am not an emoji...',
|
||||
unicodeVersion: '9.0',
|
||||
description: '...and should be filtered out',
|
||||
},
|
||||
multiple_moji: {
|
||||
moji: '🍂🏭',
|
||||
unicodeVersion: '9.0',
|
||||
description: 'Multiple separate emoji that are not joined by a zero width joiner',
|
||||
},
|
||||
};
|
||||
|
||||
export const emojiFixtureMap = {
|
||||
...validEmoji,
|
||||
...invalidEmoji,
|
||||
};
|
||||
|
||||
export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
|
||||
|
@ -63,11 +90,14 @@ export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => {
|
|||
return acc;
|
||||
}, {});
|
||||
|
||||
export async function initEmojiMock(mockData = mockEmojiData) {
|
||||
const mock = new MockAdapter(axios);
|
||||
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(mockData));
|
||||
|
||||
await initEmojiMap();
|
||||
|
||||
return mock;
|
||||
export function clearEmojiMock() {
|
||||
localStorage.clear();
|
||||
initEmojiMap.promise = null;
|
||||
}
|
||||
|
||||
export async function initEmojiMock(mockData = mockEmojiData) {
|
||||
clearEmojiMock();
|
||||
localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION);
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(mockData));
|
||||
await initEmojiMap();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import Token from '~/access_tokens/components/token.vue';
|
||||
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
|
||||
|
||||
describe('Token', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultPropsData = {
|
||||
token: 'az4a2l5f8ssa0zvdfbhidbzlx',
|
||||
inputId: 'feed_token',
|
||||
inputLabel: 'Feed token',
|
||||
copyButtonTitle: 'Copy feed token',
|
||||
};
|
||||
|
||||
const defaultSlots = {
|
||||
title: 'Feed token title',
|
||||
description: 'Feed token description',
|
||||
'input-description': 'Feed token input description',
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots });
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders title slot', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByText(defaultSlots.title, { selector: 'h4' }).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders description slot', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByText(defaultSlots.description).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders input description slot', () => {
|
||||
createComponent();
|
||||
|
||||
expect(wrapper.findByText(defaultSlots['input-description']).exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('correctly passes props to `InputCopyToggleVisibility` component', () => {
|
||||
createComponent();
|
||||
|
||||
const inputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
|
||||
|
||||
expect(inputCopyToggleVisibilityComponent.props()).toMatchObject({
|
||||
formInputGroupProps: {
|
||||
id: defaultPropsData.inputId,
|
||||
},
|
||||
value: defaultPropsData.token,
|
||||
copyButtonTitle: defaultPropsData.copyButtonTitle,
|
||||
});
|
||||
expect(inputCopyToggleVisibilityComponent.attributes()).toMatchObject({
|
||||
label: defaultPropsData.inputLabel,
|
||||
'label-for': defaultPropsData.inputId,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,148 @@
|
|||
import { merge } from 'lodash';
|
||||
|
||||
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
|
||||
import TokensApp from '~/access_tokens/components/tokens_app.vue';
|
||||
import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants';
|
||||
|
||||
describe('TokensApp', () => {
|
||||
let wrapper;
|
||||
|
||||
const defaultProvide = {
|
||||
tokenTypes: {
|
||||
[FEED_TOKEN]: {
|
||||
enabled: true,
|
||||
token: 'DUKu345VD73Py7zz3z89',
|
||||
resetPath: '/-/profile/reset_feed_token',
|
||||
},
|
||||
[INCOMING_EMAIL_TOKEN]: {
|
||||
enabled: true,
|
||||
token: 'az4a2l5f8ssa0zvdfbhidbzlx',
|
||||
resetPath: '/-/profile/reset_incoming_email_token',
|
||||
},
|
||||
[STATIC_OBJECT_TOKEN]: {
|
||||
enabled: true,
|
||||
token: 'QHXwGHYioHTgxQnAcyZ-',
|
||||
resetPath: '/-/profile/reset_static_object_token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createComponent = (options = {}) => {
|
||||
wrapper = mountExtended(TokensApp, merge({}, { provide: defaultProvide }, options));
|
||||
};
|
||||
|
||||
const expectTokenRendered = ({
|
||||
testId,
|
||||
expectedLabel,
|
||||
expectedDescription,
|
||||
expectedInputDescription,
|
||||
expectedResetPath,
|
||||
expectedResetConfirmMessage,
|
||||
expectedProps,
|
||||
}) => {
|
||||
const container = extendedWrapper(wrapper.findByTestId(testId));
|
||||
|
||||
expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true);
|
||||
expect(container.findByText(expectedDescription).exists()).toBe(true);
|
||||
expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true);
|
||||
expect(container.findByText('reset this token').attributes()).toMatchObject({
|
||||
'data-confirm': expectedResetConfirmMessage,
|
||||
'data-method': 'put',
|
||||
href: expectedResetPath,
|
||||
});
|
||||
expect(container.props()).toMatchObject(expectedProps);
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
it('renders all enabled tokens', () => {
|
||||
createComponent();
|
||||
|
||||
expectTokenRendered({
|
||||
testId: TokensApp.htmlAttributes[FEED_TOKEN].containerTestId,
|
||||
expectedLabel: TokensApp.i18n[FEED_TOKEN].label,
|
||||
expectedDescription: TokensApp.i18n[FEED_TOKEN].description,
|
||||
expectedInputDescription:
|
||||
'Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you.',
|
||||
expectedResetPath: defaultProvide.tokenTypes[FEED_TOKEN].resetPath,
|
||||
expectedResetConfirmMessage: TokensApp.i18n[FEED_TOKEN].resetConfirmMessage,
|
||||
expectedProps: {
|
||||
token: defaultProvide.tokenTypes[FEED_TOKEN].token,
|
||||
inputId: TokensApp.htmlAttributes[FEED_TOKEN].inputId,
|
||||
inputLabel: TokensApp.i18n[FEED_TOKEN].label,
|
||||
copyButtonTitle: TokensApp.i18n[FEED_TOKEN].copyButtonTitle,
|
||||
},
|
||||
});
|
||||
|
||||
expectTokenRendered({
|
||||
testId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].containerTestId,
|
||||
expectedLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label,
|
||||
expectedDescription: TokensApp.i18n[INCOMING_EMAIL_TOKEN].description,
|
||||
expectedInputDescription:
|
||||
'Keep this token secret. Anyone who has it can create issues as if they were you.',
|
||||
expectedResetPath: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].resetPath,
|
||||
expectedResetConfirmMessage: TokensApp.i18n[INCOMING_EMAIL_TOKEN].resetConfirmMessage,
|
||||
expectedProps: {
|
||||
token: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].token,
|
||||
inputId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].inputId,
|
||||
inputLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label,
|
||||
copyButtonTitle: TokensApp.i18n[INCOMING_EMAIL_TOKEN].copyButtonTitle,
|
||||
},
|
||||
});
|
||||
|
||||
expectTokenRendered({
|
||||
testId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].containerTestId,
|
||||
expectedLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label,
|
||||
expectedDescription: TokensApp.i18n[STATIC_OBJECT_TOKEN].description,
|
||||
expectedInputDescription:
|
||||
'Keep this token secret. Anyone who has it can access repository static objects as if they were you.',
|
||||
expectedResetPath: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].resetPath,
|
||||
expectedResetConfirmMessage: TokensApp.i18n[STATIC_OBJECT_TOKEN].resetConfirmMessage,
|
||||
expectedProps: {
|
||||
token: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].token,
|
||||
inputId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].inputId,
|
||||
inputLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label,
|
||||
copyButtonTitle: TokensApp.i18n[STATIC_OBJECT_TOKEN].copyButtonTitle,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't render disabled tokens", () => {
|
||||
createComponent({
|
||||
provide: {
|
||||
tokenTypes: {
|
||||
[FEED_TOKEN]: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
describe('when there are tokens missing an `i18n` definition', () => {
|
||||
it('renders without errors', () => {
|
||||
createComponent({
|
||||
provide: {
|
||||
tokenTypes: {
|
||||
fooBar: {
|
||||
enabled: true,
|
||||
token: 'rewjoa58dfm54jfkdlsdf',
|
||||
resetPath: '/-/profile/foo_bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,6 +12,7 @@ exports[`Alert integration settings form default state should match the default
|
|||
<gl-form-group-stub
|
||||
class="gl-pl-0"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-checkbox-stub
|
||||
checked="true"
|
||||
|
@ -28,6 +29,7 @@ exports[`Alert integration settings form default state should match the default
|
|||
label-for="alert-integration-settings-issue-template"
|
||||
label-size="sm"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<label
|
||||
class="gl-display-inline-flex"
|
||||
|
@ -83,6 +85,7 @@ exports[`Alert integration settings form default state should match the default
|
|||
<gl-form-group-stub
|
||||
class="gl-pl-0 gl-mb-5"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-checkbox-stub>
|
||||
<span>
|
||||
|
@ -94,6 +97,7 @@ exports[`Alert integration settings form default state should match the default
|
|||
<gl-form-group-stub
|
||||
class="gl-pl-0 gl-mb-5"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-checkbox-stub
|
||||
checked="true"
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import $ from 'jquery';
|
||||
import Cookies from 'js-cookie';
|
||||
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
||||
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
|
||||
import loadAwardsHandler from '~/awards_handler';
|
||||
import { EMOJI_VERSION } from '~/emoji';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
window.gl = window.gl || {};
|
||||
window.gon = window.gon || {};
|
||||
|
||||
let mock;
|
||||
let awardsHandler = null;
|
||||
const urlRoot = gon.relative_url_root;
|
||||
|
||||
|
@ -76,8 +73,7 @@ describe('AwardsHandler', () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
|
||||
await initEmojiMock(emojiData);
|
||||
|
||||
loadFixtures('snippets/show.html');
|
||||
|
||||
|
@ -89,7 +85,7 @@ describe('AwardsHandler', () => {
|
|||
// restore original url root value
|
||||
gon.relative_url_root = urlRoot;
|
||||
|
||||
mock.restore();
|
||||
clearEmojiMock();
|
||||
|
||||
// Undo what we did to the shared <body>
|
||||
$('body').removeAttr('data-page');
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import installGlEmojiElement from '~/behaviors/gl_emoji';
|
||||
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
|
||||
import { EMOJI_VERSION } from '~/emoji';
|
||||
|
||||
import * as EmojiUnicodeSupport from '~/emoji/support';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
jest.mock('~/emoji/support');
|
||||
|
||||
describe('gl_emoji', () => {
|
||||
let mock;
|
||||
const emojiData = {
|
||||
grey_question: {
|
||||
c: 'symbols',
|
||||
|
@ -38,15 +36,12 @@ describe('gl_emoji', () => {
|
|||
return div.firstElementChild;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mock = new MockAdapter(axios);
|
||||
mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData);
|
||||
|
||||
return initEmojiMap().catch(() => {});
|
||||
beforeEach(async () => {
|
||||
await initEmojiMock(emojiData);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
clearEmojiMock();
|
||||
|
||||
document.body.innerHTML = '';
|
||||
});
|
||||
|
|
|
@ -4,41 +4,49 @@ import VueApollo from 'vue-apollo';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import NewContactForm from '~/crm/components/new_contact_form.vue';
|
||||
import ContactForm from '~/crm/components/contact_form.vue';
|
||||
import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql';
|
||||
import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql';
|
||||
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
|
||||
import {
|
||||
createContactMutationErrorResponse,
|
||||
createContactMutationResponse,
|
||||
getGroupContactsQueryResponse,
|
||||
updateContactMutationErrorResponse,
|
||||
updateContactMutationResponse,
|
||||
} from './mock_data';
|
||||
|
||||
describe('Customer relations contacts root app', () => {
|
||||
describe('Customer relations contact form component', () => {
|
||||
Vue.use(VueApollo);
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
let mutation;
|
||||
let queryHandler;
|
||||
|
||||
const findCreateNewContactButton = () => wrapper.findByTestId('create-new-contact-button');
|
||||
const findSaveContactButton = () => wrapper.findByTestId('save-contact-button');
|
||||
const findCancelButton = () => wrapper.findByTestId('cancel-button');
|
||||
const findForm = () => wrapper.find('form');
|
||||
const findError = () => wrapper.findComponent(GlAlert);
|
||||
|
||||
const mountComponent = ({ mountFunction = shallowMountExtended } = {}) => {
|
||||
fakeApollo = createMockApollo([[createContactMutation, queryHandler]]);
|
||||
const mountComponent = ({ mountFunction = shallowMountExtended, editForm = false } = {}) => {
|
||||
fakeApollo = createMockApollo([[mutation, queryHandler]]);
|
||||
fakeApollo.clients.defaultClient.cache.writeQuery({
|
||||
query: getGroupContactsQuery,
|
||||
variables: { groupFullPath: 'flightjs' },
|
||||
data: getGroupContactsQueryResponse.data,
|
||||
});
|
||||
wrapper = mountFunction(NewContactForm, {
|
||||
const propsData = { drawerOpen: true };
|
||||
if (editForm)
|
||||
propsData.contact = { firstName: 'First', lastName: 'Last', email: 'email@example.com' };
|
||||
wrapper = mountFunction(ContactForm, {
|
||||
provide: { groupId: 26, groupFullPath: 'flightjs' },
|
||||
apolloProvider: fakeApollo,
|
||||
propsData: { drawerOpen: true },
|
||||
propsData,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
mutation = createContactMutation;
|
||||
queryHandler = jest.fn().mockResolvedValue(createContactMutationResponse);
|
||||
});
|
||||
|
||||
|
@ -47,14 +55,14 @@ describe('Customer relations contacts root app', () => {
|
|||
fakeApollo = null;
|
||||
});
|
||||
|
||||
describe('Create new contact button', () => {
|
||||
it('should be disabled by default', () => {
|
||||
describe('Save contact button', () => {
|
||||
it('should be disabled when required fields are empty', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findCreateNewContactButton().attributes('disabled')).toBeTruthy();
|
||||
expect(findSaveContactButton().props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not be disabled when first, last and email have values', async () => {
|
||||
it('should not be disabled when required fields have values', async () => {
|
||||
mountComponent();
|
||||
|
||||
wrapper.find('#contact-first-name').vm.$emit('input', 'A');
|
||||
|
@ -62,7 +70,7 @@ describe('Customer relations contacts root app', () => {
|
|||
wrapper.find('#contact-email').vm.$emit('input', 'C');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findCreateNewContactButton().attributes('disabled')).toBeFalsy();
|
||||
expect(findSaveContactButton().props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,7 +82,7 @@ describe('Customer relations contacts root app', () => {
|
|||
expect(wrapper.emitted().close).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('when query is successful', () => {
|
||||
describe('when create mutation is successful', () => {
|
||||
it("should emit 'close'", async () => {
|
||||
mountComponent();
|
||||
|
||||
|
@ -85,7 +93,7 @@ describe('Customer relations contacts root app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when query fails', () => {
|
||||
describe('when create mutation fails', () => {
|
||||
it('should show error on reject', async () => {
|
||||
queryHandler = jest.fn().mockRejectedValue('ERROR');
|
||||
mountComponent();
|
||||
|
@ -107,4 +115,43 @@ describe('Customer relations contacts root app', () => {
|
|||
expect(findError().text()).toBe('Phone is invalid.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when update mutation is successful', () => {
|
||||
it("should emit 'close'", async () => {
|
||||
mutation = updateContactMutation;
|
||||
queryHandler = jest.fn().mockResolvedValue(updateContactMutationResponse);
|
||||
mountComponent({ editForm: true });
|
||||
|
||||
findForm().trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(wrapper.emitted().close).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when update mutation fails', () => {
|
||||
beforeEach(() => {
|
||||
mutation = updateContactMutation;
|
||||
});
|
||||
|
||||
it('should show error on reject', async () => {
|
||||
queryHandler = jest.fn().mockRejectedValue('ERROR');
|
||||
mountComponent({ editForm: true });
|
||||
findForm().trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findError().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should show error on error response', async () => {
|
||||
queryHandler = jest.fn().mockResolvedValue(updateContactMutationErrorResponse);
|
||||
mountComponent({ editForm: true });
|
||||
|
||||
findForm().trigger('submit');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findError().exists()).toBe(true);
|
||||
expect(findError().text()).toBe('Email is invalid.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,8 +6,10 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ContactsRoot from '~/crm/components/contacts_root.vue';
|
||||
import NewContactForm from '~/crm/components/new_contact_form.vue';
|
||||
import ContactForm from '~/crm/components/contact_form.vue';
|
||||
import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql';
|
||||
import { NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '~/crm/constants';
|
||||
import routes from '~/crm/routes';
|
||||
import { getGroupContactsQueryResponse } from './mock_data';
|
||||
|
||||
describe('Customer relations contacts root app', () => {
|
||||
|
@ -21,7 +23,8 @@ describe('Customer relations contacts root app', () => {
|
|||
const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName });
|
||||
const findIssuesLinks = () => wrapper.findAllByTestId('issues-link');
|
||||
const findNewContactButton = () => wrapper.findByTestId('new-contact-button');
|
||||
const findNewContactForm = () => wrapper.findComponent(NewContactForm);
|
||||
const findEditContactButton = () => wrapper.findByTestId('edit-contact-button');
|
||||
const findContactForm = () => wrapper.findComponent(ContactForm);
|
||||
const findError = () => wrapper.findComponent(GlAlert);
|
||||
const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse);
|
||||
|
||||
|
@ -49,7 +52,7 @@ describe('Customer relations contacts root app', () => {
|
|||
router = new VueRouter({
|
||||
base: basePath,
|
||||
mode: 'history',
|
||||
routes: [],
|
||||
routes,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -79,12 +82,12 @@ describe('Customer relations contacts root app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('new contact form', () => {
|
||||
describe('contact form', () => {
|
||||
it('should not exist by default', async () => {
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findNewContactForm().exists()).toBe(false);
|
||||
expect(findContactForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should exist when user clicks new contact button', async () => {
|
||||
|
@ -93,25 +96,54 @@ describe('Customer relations contacts root app', () => {
|
|||
findNewContactButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findNewContactForm().exists()).toBe(true);
|
||||
expect(findContactForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should exist when user navigates directly to /new', async () => {
|
||||
router.replace({ path: '/new' });
|
||||
it('should exist when user navigates directly to `new` route', async () => {
|
||||
router.replace({ name: NEW_ROUTE_NAME });
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findNewContactForm().exists()).toBe(true);
|
||||
expect(findContactForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not exist when form emits close', async () => {
|
||||
router.replace({ path: '/new' });
|
||||
mountComponent();
|
||||
|
||||
findNewContactForm().vm.$emit('close');
|
||||
it('should exist when user clicks edit contact button', async () => {
|
||||
mountComponent({ mountFunction: mountExtended });
|
||||
await waitForPromises();
|
||||
|
||||
expect(findNewContactForm().exists()).toBe(false);
|
||||
findEditContactButton().vm.$emit('click');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findContactForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should exist when user navigates directly to `edit` route', async () => {
|
||||
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
|
||||
expect(findContactForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('should not exist when new form emits close', async () => {
|
||||
router.replace({ name: NEW_ROUTE_NAME });
|
||||
mountComponent();
|
||||
|
||||
findContactForm().vm.$emit('close');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findContactForm().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('should not exist when edit form emits close', async () => {
|
||||
router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } });
|
||||
mountComponent();
|
||||
await waitForPromises();
|
||||
|
||||
findContactForm().vm.$emit('close');
|
||||
await waitForPromises();
|
||||
|
||||
expect(findContactForm().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -106,3 +106,31 @@ export const createContactMutationErrorResponse = {
|
|||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const updateContactMutationResponse = {
|
||||
data: {
|
||||
customerRelationsContactUpdate: {
|
||||
__typeName: 'CustomerRelationsContactCreatePayload',
|
||||
contact: {
|
||||
__typename: 'CustomerRelationsContact',
|
||||
id: 'gid://gitlab/CustomerRelations::Contact/1',
|
||||
firstName: 'First',
|
||||
lastName: 'Last',
|
||||
email: 'email@example.com',
|
||||
phone: null,
|
||||
description: null,
|
||||
organization: null,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const updateContactMutationErrorResponse = {
|
||||
data: {
|
||||
customerRelationsContactUpdate: {
|
||||
contact: null,
|
||||
errors: ['Email is invalid.'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
import { emojiFixtureMap, mockEmojiData, initEmojiMock } from 'helpers/emoji';
|
||||
import {
|
||||
emojiFixtureMap,
|
||||
mockEmojiData,
|
||||
initEmojiMock,
|
||||
validEmoji,
|
||||
invalidEmoji,
|
||||
clearEmojiMock,
|
||||
} from 'helpers/emoji';
|
||||
import { trimText } from 'helpers/text_helper';
|
||||
import { glEmojiTag, searchEmoji, getEmojiInfo, sortEmoji } from '~/emoji';
|
||||
import {
|
||||
glEmojiTag,
|
||||
searchEmoji,
|
||||
getEmojiInfo,
|
||||
sortEmoji,
|
||||
initEmojiMap,
|
||||
getAllEmoji,
|
||||
} from '~/emoji';
|
||||
|
||||
import isEmojiUnicodeSupported, {
|
||||
isFlagEmoji,
|
||||
isRainbowFlagEmoji,
|
||||
|
@ -9,7 +24,6 @@ import isEmojiUnicodeSupported, {
|
|||
isHorceRacingSkinToneComboEmoji,
|
||||
isPersonZwjEmoji,
|
||||
} from '~/emoji/support/is_emoji_unicode_supported';
|
||||
import { sanitize } from '~/lib/dompurify';
|
||||
|
||||
const emptySupportMap = {
|
||||
personZwj: false,
|
||||
|
@ -31,14 +45,55 @@ const emptySupportMap = {
|
|||
};
|
||||
|
||||
describe('emoji', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
mock = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
clearEmojiMock();
|
||||
});
|
||||
|
||||
describe('initEmojiMap', () => {
|
||||
it('should contain valid emoji', async () => {
|
||||
await initEmojiMap();
|
||||
|
||||
const allEmoji = Object.keys(getAllEmoji());
|
||||
Object.keys(validEmoji).forEach((key) => {
|
||||
expect(allEmoji.includes(key)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not contain invalid emoji', async () => {
|
||||
await initEmojiMap();
|
||||
|
||||
const allEmoji = Object.keys(getAllEmoji());
|
||||
Object.keys(invalidEmoji).forEach((key) => {
|
||||
expect(allEmoji.includes(key)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('fixes broken pride emoji', async () => {
|
||||
clearEmojiMock();
|
||||
await initEmojiMock({
|
||||
gay_pride_flag: {
|
||||
c: 'flags',
|
||||
// Without a zero-width joiner
|
||||
e: '🏳🌈',
|
||||
name: 'gay_pride_flag',
|
||||
u: '6.0',
|
||||
},
|
||||
});
|
||||
|
||||
expect(getAllEmoji()).toEqual({
|
||||
gay_pride_flag: {
|
||||
c: 'flags',
|
||||
// With a zero-width joiner
|
||||
e: '🏳️🌈',
|
||||
name: 'gay_pride_flag',
|
||||
u: '6.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('glEmojiTag', () => {
|
||||
|
@ -378,32 +433,14 @@ describe('emoji', () => {
|
|||
});
|
||||
|
||||
describe('searchEmoji', () => {
|
||||
const emojiFixture = Object.keys(mockEmojiData).reduce((acc, k) => {
|
||||
const { name, e, u, d } = mockEmojiData[k];
|
||||
acc[k] = { name, e: sanitize(e), u, d };
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => {
|
||||
const search = searchEmoji(input);
|
||||
|
||||
const expected = [
|
||||
'atom',
|
||||
'bomb',
|
||||
'construction_worker_tone5',
|
||||
'five',
|
||||
'grey_question',
|
||||
'black_heart',
|
||||
'heart',
|
||||
'custard',
|
||||
'star',
|
||||
'xss',
|
||||
].map((name) => {
|
||||
const expected = Object.keys(validEmoji).map((name) => {
|
||||
return {
|
||||
emoji: emojiFixture[name],
|
||||
emoji: mockEmojiData[name],
|
||||
field: 'd',
|
||||
fieldValue: emojiFixture[name].d,
|
||||
fieldValue: mockEmojiData[name].d,
|
||||
score: 0,
|
||||
};
|
||||
});
|
||||
|
@ -453,7 +490,7 @@ describe('emoji', () => {
|
|||
const { field, score, fieldValue, name } = item;
|
||||
|
||||
return {
|
||||
emoji: emojiFixture[name],
|
||||
emoji: mockEmojiData[name],
|
||||
field,
|
||||
fieldValue,
|
||||
score,
|
||||
|
@ -564,9 +601,9 @@ describe('emoji', () => {
|
|||
const { field, score, name } = item;
|
||||
|
||||
return {
|
||||
emoji: emojiFixture[name],
|
||||
emoji: mockEmojiData[name],
|
||||
field,
|
||||
fieldValue: emojiFixture[name][field],
|
||||
fieldValue: mockEmojiData[name][field],
|
||||
score,
|
||||
};
|
||||
});
|
||||
|
@ -622,13 +659,4 @@ describe('emoji', () => {
|
|||
expect(sortEmoji(scoredItems)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitize emojis', () => {
|
||||
it('should return sanitized emoji', () => {
|
||||
expect(getEmojiInfo('xss')).toEqual({
|
||||
...mockEmojiData.xss,
|
||||
e: '<img src="x">',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import $ from 'jquery';
|
||||
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
|
||||
import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete';
|
||||
import { initEmojiMock } from 'helpers/emoji';
|
||||
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
||||
import '~/lib/utils/jquery_at_who';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
@ -803,8 +803,6 @@ describe('GfmAutoComplete', () => {
|
|||
});
|
||||
|
||||
describe('emoji', () => {
|
||||
let mock;
|
||||
|
||||
const mockItem = {
|
||||
'atwho-at': ':',
|
||||
emoji: {
|
||||
|
@ -818,14 +816,14 @@ describe('GfmAutoComplete', () => {
|
|||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
mock = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
|
||||
await new GfmAutoComplete({}).loadEmojiData({ atwho() {}, trigger() {} }, ':');
|
||||
if (!GfmAutoComplete.glEmojiTag) throw new Error('emoji not loaded');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mock.restore();
|
||||
clearEmojiMock();
|
||||
});
|
||||
|
||||
describe('Emoji.templateFunction', () => {
|
||||
|
|
|
@ -47,6 +47,7 @@ exports[`grafana integration component default state to match the default snapsh
|
|||
label="Enable authentication"
|
||||
label-for="grafana-integration-enabled"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-checkbox-stub
|
||||
id="grafana-integration-enabled"
|
||||
|
@ -62,6 +63,7 @@ exports[`grafana integration component default state to match the default snapsh
|
|||
label="Grafana URL"
|
||||
label-for="grafana-url"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-input-stub
|
||||
id="grafana-url"
|
||||
|
@ -74,6 +76,7 @@ exports[`grafana integration component default state to match the default snapsh
|
|||
label="API token"
|
||||
label-for="grafana-token"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-input-stub
|
||||
id="grafana-token"
|
||||
|
|
|
@ -14,6 +14,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
|
|||
<gl-form-group-stub
|
||||
class="col-8 col-md-9 gl-p-0"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-toggle-stub
|
||||
id="active"
|
||||
|
@ -28,6 +29,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
|
|||
label="Webhook URL"
|
||||
label-for="url"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-input-group-stub
|
||||
data-testid="webhook-url"
|
||||
|
|
|
@ -52,6 +52,7 @@ exports[`self monitor component When the self monitor project has not been creat
|
|||
|
||||
<gl-form-group-stub
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-toggle-stub
|
||||
label="Self monitoring"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { initEmojiMock } from 'helpers/emoji';
|
||||
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
|
||||
import * as UserApi from '~/api/user_api';
|
||||
import EmojiPicker from '~/emoji/components/picker.vue';
|
||||
import createFlash from '~/flash';
|
||||
|
@ -12,7 +12,6 @@ jest.mock('~/flash');
|
|||
|
||||
describe('SetStatusModalWrapper', () => {
|
||||
let wrapper;
|
||||
let mockEmoji;
|
||||
const $toast = {
|
||||
show: jest.fn(),
|
||||
};
|
||||
|
@ -63,12 +62,12 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
mockEmoji.restore();
|
||||
clearEmojiMock();
|
||||
});
|
||||
|
||||
describe('with minimum props', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent();
|
||||
return initModal();
|
||||
});
|
||||
|
@ -112,7 +111,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('improvedEmojiPicker is true', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({}, true);
|
||||
return initModal();
|
||||
});
|
||||
|
@ -126,7 +125,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('with no currentMessage set', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentMessage: '' });
|
||||
return initModal();
|
||||
});
|
||||
|
@ -146,7 +145,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('with no currentEmoji set', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentEmoji: '' });
|
||||
return initModal();
|
||||
});
|
||||
|
@ -161,7 +160,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('with no currentMessage set', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
|
||||
return initModal();
|
||||
});
|
||||
|
@ -174,7 +173,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('with currentClearStatusAfter set', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' });
|
||||
return initModal();
|
||||
});
|
||||
|
@ -190,7 +189,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
describe('update status', () => {
|
||||
describe('succeeds', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent();
|
||||
await initModal();
|
||||
|
||||
|
@ -246,7 +245,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('success message', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
|
||||
jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue();
|
||||
return initModal({ mockOnUpdateSuccess: false });
|
||||
|
@ -262,7 +261,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('with errors', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent();
|
||||
await initModal();
|
||||
|
||||
|
@ -279,7 +278,7 @@ describe('SetStatusModalWrapper', () => {
|
|||
|
||||
describe('error message', () => {
|
||||
beforeEach(async () => {
|
||||
mockEmoji = await initEmojiMock();
|
||||
await initEmojiMock();
|
||||
wrapper = createComponent({ currentEmoji: '', currentMessage: '' });
|
||||
jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue();
|
||||
return initModal({ mockOnUpdateFailure: false });
|
||||
|
|
|
@ -25,6 +25,7 @@ describe('Shortcuts', () => {
|
|||
|
||||
jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus');
|
||||
jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus');
|
||||
jest.spyOn(document.querySelector('#search'), 'focus');
|
||||
|
||||
new Shortcuts(); // eslint-disable-line no-new
|
||||
});
|
||||
|
@ -111,4 +112,12 @@ describe('Shortcuts', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('focusSearch', () => {
|
||||
it('focuses the search bar', () => {
|
||||
Shortcuts.focusSearch(createEvent('KeyboardEvent'));
|
||||
|
||||
expect(document.querySelector('#search').focus).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
|
|||
class="gl-mb-0"
|
||||
id="visibility-level-setting"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-radio-group-stub
|
||||
checked="private"
|
||||
|
|
|
@ -5,6 +5,7 @@ exports[`Title edit field matches the snapshot 1`] = `
|
|||
label="Title"
|
||||
label-for="title-field-edit"
|
||||
labeldescription=""
|
||||
optionaltext="(optional)"
|
||||
>
|
||||
<gl-form-input-stub />
|
||||
</gl-form-group-stub>
|
||||
|
|
|
@ -15,4 +15,53 @@ RSpec.describe AccessTokensHelper do
|
|||
it { expect(helper.scope_description(prefix)).to eq(description_location) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#tokens_app_data' do
|
||||
let_it_be(:feed_token) { 'DUKu345VD73Py7zz3z89' }
|
||||
let_it_be(:incoming_email_token) { 'az4a2l5f8ssa0zvdfbhidbzlx' }
|
||||
let_it_be(:static_object_token) { 'QHXwGHYioHTgxQnAcyZ-' }
|
||||
let_it_be(:feed_token_reset_path) { '/-/profile/reset_feed_token' }
|
||||
let_it_be(:incoming_email_token_reset_path) { '/-/profile/reset_incoming_email_token' }
|
||||
let_it_be(:static_object_token_reset_path) { '/-/profile/reset_static_object_token' }
|
||||
let_it_be(:user) do
|
||||
build(
|
||||
:user,
|
||||
feed_token: feed_token,
|
||||
incoming_email_token: incoming_email_token,
|
||||
static_object_token: static_object_token
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns expected json' do
|
||||
allow(Gitlab::CurrentSettings).to receive_messages(
|
||||
disable_feed_token: false,
|
||||
static_objects_external_storage_enabled?: true
|
||||
)
|
||||
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
|
||||
allow(helper).to receive_messages(
|
||||
current_user: user,
|
||||
reset_feed_token_profile_path: feed_token_reset_path,
|
||||
reset_incoming_email_token_profile_path: incoming_email_token_reset_path,
|
||||
reset_static_object_token_profile_path: static_object_token_reset_path
|
||||
)
|
||||
|
||||
expect(helper.tokens_app_data).to eq({
|
||||
feed_token: {
|
||||
enabled: true,
|
||||
token: feed_token,
|
||||
reset_path: feed_token_reset_path
|
||||
},
|
||||
incoming_email_token: {
|
||||
enabled: true,
|
||||
token: incoming_email_token,
|
||||
reset_path: incoming_email_token_reset_path
|
||||
},
|
||||
static_object_token: {
|
||||
enabled: true,
|
||||
token: static_object_token,
|
||||
reset_path: static_object_token_reset_path
|
||||
}
|
||||
}.to_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,21 +12,12 @@ RSpec.describe Gitlab::ProcessManagement do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.trap_terminate' do
|
||||
it 'traps the termination signals' do
|
||||
expect(described_class).to receive(:trap_signals)
|
||||
.with(described_class::TERMINATE_SIGNALS)
|
||||
describe '.modify_signals' do
|
||||
it 'traps the given signals with the given command' do
|
||||
expect(described_class).to receive(:trap).ordered.with(:INT, 'DEFAULT')
|
||||
expect(described_class).to receive(:trap).ordered.with(:HUP, 'DEFAULT')
|
||||
|
||||
described_class.trap_terminate { }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.trap_forward' do
|
||||
it 'traps the signals to forward' do
|
||||
expect(described_class).to receive(:trap_signals)
|
||||
.with(described_class::FORWARD_SIGNALS)
|
||||
|
||||
described_class.trap_forward { }
|
||||
described_class.modify_signals(%i(INT HUP), 'DEFAULT')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -8,52 +8,67 @@ require_relative '../support/helpers/next_instance_of'
|
|||
RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
|
||||
include NextInstanceOf
|
||||
|
||||
before do
|
||||
# We do not want this to have knock-on effects on the test process.
|
||||
allow(Gitlab::ProcessManagement).to receive(:modify_signals)
|
||||
end
|
||||
|
||||
describe '.spawn' do
|
||||
let(:env) do
|
||||
{
|
||||
'METRICS_SERVER_TARGET' => 'sidekiq',
|
||||
'GITLAB_CONFIG' => nil,
|
||||
'WIPE_METRICS_DIR' => 'false'
|
||||
}
|
||||
context 'when in parent process' do
|
||||
it 'forks into a new process and detaches it' do
|
||||
expect(Process).to receive(:fork).and_return(99)
|
||||
expect(Process).to receive(:detach).with(99)
|
||||
|
||||
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
|
||||
end
|
||||
end
|
||||
|
||||
it 'spawns a process with the correct environment variables and detaches it' do
|
||||
expect(Process).to receive(:spawn).with(env, anything, err: $stderr, out: $stdout).and_return(99)
|
||||
expect(Process).to receive(:detach).with(99)
|
||||
context 'when in child process' do
|
||||
before do
|
||||
# This signals the process that it's "inside" the fork
|
||||
expect(Process).to receive(:fork).and_return(nil)
|
||||
expect(Process).not_to receive(:detach)
|
||||
end
|
||||
|
||||
described_class.spawn('sidekiq')
|
||||
it 'starts the metrics server with the given arguments' do
|
||||
expect_next_instance_of(MetricsServer) do |server|
|
||||
expect(server).to receive(:start)
|
||||
end
|
||||
|
||||
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics')
|
||||
end
|
||||
|
||||
it 'resets signal handlers from parent process' do
|
||||
expect(Gitlab::ProcessManagement).to receive(:modify_signals).with(%i[A B], 'DEFAULT')
|
||||
|
||||
described_class.spawn('sidekiq', metrics_dir: 'path/to/metrics', trapped_signals: %i[A B])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start' do
|
||||
let(:exporter_class) { Class.new(Gitlab::Metrics::Exporter::BaseExporter) }
|
||||
let(:exporter_double) { double('fake_exporter', start: true) }
|
||||
let(:prometheus_client_double) { double(::Prometheus::Client) }
|
||||
let(:prometheus_config) { ::Prometheus::Client::Configuration.new }
|
||||
let(:prometheus_config) { ::Prometheus::Client.configuration }
|
||||
let(:metrics_dir) { Dir.mktmpdir }
|
||||
let(:settings_double) { double(:settings, sidekiq_exporter: {}) }
|
||||
let!(:old_metrics_dir) { ::Prometheus::Client.configuration.multiprocess_files_dir }
|
||||
let(:settings) { { "fake_exporter" => { "enabled" => true } } }
|
||||
let!(:old_metrics_dir) { prometheus_config.multiprocess_files_dir }
|
||||
|
||||
subject(:metrics_server) { described_class.new('fake', metrics_dir, true)}
|
||||
|
||||
before do
|
||||
stub_env('prometheus_multiproc_dir', metrics_dir)
|
||||
stub_const('Gitlab::Metrics::Exporter::FakeExporter', exporter_class)
|
||||
allow(exporter_class).to receive(:instance).with({}, synchronous: true).and_return(exporter_double)
|
||||
allow(Settings).to receive(:monitoring).and_return(settings_double)
|
||||
expect(exporter_class).to receive(:instance).with(settings['fake_exporter'], synchronous: true).and_return(exporter_double)
|
||||
expect(Settings).to receive(:monitoring).and_return(settings)
|
||||
end
|
||||
|
||||
after do
|
||||
Gitlab::Metrics.reset_registry!
|
||||
|
||||
::Prometheus::CleanupMultiprocDirService.new.execute
|
||||
Dir.rmdir(metrics_dir)
|
||||
::Prometheus::Client.configuration.multiprocess_files_dir = old_metrics_dir
|
||||
FileUtils.rm_rf(metrics_dir, secure: true)
|
||||
prometheus_config.multiprocess_files_dir = old_metrics_dir
|
||||
end
|
||||
|
||||
it 'configures ::Prometheus::Client' do
|
||||
allow(prometheus_client_double).to receive(:configuration).and_return(prometheus_config)
|
||||
|
||||
metrics_server.start
|
||||
|
||||
expect(prometheus_config.multiprocess_files_dir).to eq metrics_dir
|
||||
|
@ -90,12 +105,5 @@ RSpec.describe MetricsServer do # rubocop:disable RSpec/FilePath
|
|||
|
||||
metrics_server.start
|
||||
end
|
||||
|
||||
it 'sends the correct Settings to the exporter instance' do
|
||||
expect(Settings).to receive(:monitoring).and_return(settings_double)
|
||||
expect(settings_double).to receive(:sidekiq_exporter)
|
||||
|
||||
metrics_server.start
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5421,4 +5421,9 @@ RSpec.describe Ci::Build do
|
|||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :ci_build }
|
||||
end
|
||||
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:model) { create(:ci_build, user: create(:user)) }
|
||||
let!(:parent) { model.user }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -700,4 +700,8 @@ RSpec.describe Ci::JobArtifact do
|
|||
when changes or new entries are made.
|
||||
MSG
|
||||
end
|
||||
|
||||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :ci_job_artifact }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4672,4 +4672,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :ci_pipeline }
|
||||
end
|
||||
|
||||
it_behaves_like 'cleanup by a loose foreign key' do
|
||||
let!(:model) { create(:ci_pipeline, user: create(:user)) }
|
||||
let!(:parent) { model.user }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -232,4 +232,8 @@ RSpec.describe ExternalPullRequest do
|
|||
'with space/README.md']
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :external_pull_request }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5042,4 +5042,8 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
expect(described_class.from_fork).to eq([fork_mr])
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :merge_request }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,8 +110,8 @@ RSpec.describe User do
|
|||
it { is_expected.to have_many(:spam_logs).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:todos) }
|
||||
it { is_expected.to have_many(:award_emoji).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:builds).dependent(:nullify) }
|
||||
it { is_expected.to have_many(:pipelines).dependent(:nullify) }
|
||||
it { is_expected.to have_many(:builds) }
|
||||
it { is_expected.to have_many(:pipelines) }
|
||||
it { is_expected.to have_many(:chat_names).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:uploads) }
|
||||
it { is_expected.to have_many(:reported_abuse_reports).dependent(:destroy).class_name('AbuseReport') }
|
||||
|
@ -1616,6 +1616,46 @@ RSpec.describe User do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'enabled_static_object_token' do
|
||||
let_it_be(:static_object_token) { 'ilqx6jm1u945macft4eff0nw' }
|
||||
|
||||
it 'returns incoming email token when supported' do
|
||||
allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(true)
|
||||
|
||||
user = create(:user, static_object_token: static_object_token)
|
||||
|
||||
expect(user.enabled_static_object_token).to eq(static_object_token)
|
||||
end
|
||||
|
||||
it 'returns `nil` when not supported' do
|
||||
allow(Gitlab::CurrentSettings).to receive(:static_objects_external_storage_enabled?).and_return(false)
|
||||
|
||||
user = create(:user, static_object_token: static_object_token)
|
||||
|
||||
expect(user.enabled_static_object_token).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe 'enabled_incoming_email_token' do
|
||||
let_it_be(:incoming_email_token) { 'ilqx6jm1u945macft4eff0nw' }
|
||||
|
||||
it 'returns incoming email token when supported' do
|
||||
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(true)
|
||||
|
||||
user = create(:user, incoming_email_token: incoming_email_token)
|
||||
|
||||
expect(user.enabled_incoming_email_token).to eq(incoming_email_token)
|
||||
end
|
||||
|
||||
it 'returns `nil` when not supported' do
|
||||
allow(Gitlab::IncomingEmail).to receive(:supports_issue_creation?).and_return(false)
|
||||
|
||||
user = create(:user, incoming_email_token: incoming_email_token)
|
||||
|
||||
expect(user.enabled_incoming_email_token).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#recently_sent_password_reset?' do
|
||||
it 'is false when reset_password_sent_at is nil' do
|
||||
user = build_stubbed(:user, reset_password_sent_at: nil)
|
||||
|
@ -6289,4 +6329,8 @@ RSpec.describe User do
|
|||
expect(user.user_readme).to be(nil)
|
||||
end
|
||||
end
|
||||
|
||||
it_behaves_like 'it has loose foreign keys' do
|
||||
let(:factory_name) { :user }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Groups::Crm::ContactsController do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
shared_examples 'response with 404 status' do
|
||||
it 'returns 404' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'ok response with index template' do
|
||||
it 'renders the index template' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
expect(response).to render_template(:index)
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'ok response with index template if authorized' do
|
||||
context 'private group' do
|
||||
let(:group) { create(:group, :private) }
|
||||
|
||||
context 'with authorized user' do
|
||||
before do
|
||||
group.add_reporter(user)
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
context 'when feature flag is enabled' do
|
||||
it_behaves_like 'ok response with index template'
|
||||
end
|
||||
|
||||
context 'when feature flag is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(customer_relations: false)
|
||||
end
|
||||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unauthorized user' do
|
||||
before do
|
||||
sign_in(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'response with 404 status'
|
||||
end
|
||||
|
||||
context 'with anonymous user' do
|
||||
it 'blah' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'public group' do
|
||||
let(:group) { create(:group, :public) }
|
||||
|
||||
context 'with anonymous user' do
|
||||
it_behaves_like 'ok response with index template'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET #index' do
|
||||
subject do
|
||||
get group_crm_contacts_path(group)
|
||||
response
|
||||
end
|
||||
|
||||
it_behaves_like 'ok response with index template if authorized'
|
||||
end
|
||||
|
||||
describe 'GET #new' do
|
||||
subject do
|
||||
get new_group_crm_contact_path(group)
|
||||
response
|
||||
end
|
||||
|
||||
it_behaves_like 'ok response with index template if authorized'
|
||||
end
|
||||
|
||||
describe 'GET #edit' do
|
||||
subject do
|
||||
get edit_group_crm_contact_path(group, id: 1)
|
||||
response
|
||||
end
|
||||
|
||||
it_behaves_like 'ok response with index template if authorized'
|
||||
end
|
||||
end
|
|
@ -213,6 +213,12 @@ RSpec.shared_examples 'namespace traversal scopes' do
|
|||
|
||||
it { is_expected.to contain_exactly(deep_nested_group_1, deep_nested_group_2) }
|
||||
end
|
||||
|
||||
context 'with offset and limit' do
|
||||
subject { described_class.where(id: [group_1, group_2]).offset(1).limit(1).self_and_descendants }
|
||||
|
||||
it { is_expected.to contain_exactly(group_2, nested_group_2, deep_nested_group_2) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.self_and_descendants' do
|
||||
|
@ -242,6 +248,19 @@ RSpec.shared_examples 'namespace traversal scopes' do
|
|||
|
||||
it { is_expected.to contain_exactly(deep_nested_group_1.id, deep_nested_group_2.id) }
|
||||
end
|
||||
|
||||
context 'with offset and limit' do
|
||||
subject do
|
||||
described_class
|
||||
.where(id: [group_1, group_2])
|
||||
.limit(1)
|
||||
.offset(1)
|
||||
.self_and_descendant_ids
|
||||
.pluck(:id)
|
||||
end
|
||||
|
||||
it { is_expected.to contain_exactly(group_2.id, nested_group_2.id, deep_nested_group_2.id) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '.self_and_descendant_ids' do
|
||||
|
|
41
yarn.lock
41
yarn.lock
|
@ -914,27 +914,28 @@
|
|||
stylelint-declaration-strict-value "1.7.7"
|
||||
stylelint-scss "3.18.0"
|
||||
|
||||
"@gitlab/svgs@1.226.0":
|
||||
version "1.226.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.226.0.tgz#f590545df4bf871a9653f2ac93029fbc1bfd2788"
|
||||
integrity sha512-oSPbwkPJ8yDttTy+zcqtA9TIPOGiTUXlgIf1XnlrMHUoQmzUUqkJMql6LDcP4xAqX0n+7Kinoxl8gmMSwMKYjw==
|
||||
"@gitlab/svgs@1.229.0":
|
||||
version "1.229.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.229.0.tgz#1ee863320ea3e0ff6b670dac59373b8b49e31388"
|
||||
integrity sha512-10OLT3gj9AQ5DmcqaRcblkIY1dwr0danjaKl+hzjcA9sjvGuTNn3P/rQZglFanM2eI6MkoHG1YP7UeSs7cFuCQ==
|
||||
|
||||
"@gitlab/tributejs@1.0.0":
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/tributejs/-/tributejs-1.0.0.tgz#672befa222aeffc83e7d799b0500a7a4418e59b8"
|
||||
integrity sha512-nmKw1+hB6MHvlmPz63yPwVs1qQkycHwsKgxpEbzmky16Y6mL4EJMk3w1b8QlOAF/AIAzjCERPhe/R4MJiohbZw==
|
||||
|
||||
"@gitlab/ui@32.43.2":
|
||||
version "32.43.2"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.43.2.tgz#df1e86493354db61da60e2eb8e9e67c79e9c402c"
|
||||
integrity sha512-BJZJeXqi9MrJ5xAQ8rA2t95udSxsRGPeeCzlb6HI26j8LAJpVj9ArbiehoMZ45aGpX0+gnElMUGNOcZ8XHlQqw==
|
||||
"@gitlab/ui@32.49.0":
|
||||
version "32.49.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-32.49.0.tgz#d899e2e2487bb0e23a408386acc04eac14808de5"
|
||||
integrity sha512-QB1M1/8vc1o0hAm5tg8tWIEcj5Isy2JxHFWKtDNnFqPvbb0QNBsoEazz9DNra3dNSRzt8zF8NJPqmuRT8WAvQA==
|
||||
dependencies:
|
||||
"@babel/standalone" "^7.0.0"
|
||||
bootstrap-vue "2.20.1"
|
||||
copy-to-clipboard "^3.0.8"
|
||||
dompurify "^2.3.3"
|
||||
dompurify "^2.3.4"
|
||||
echarts "^5.2.1"
|
||||
highlight.js "^10.6.0"
|
||||
iframe-resizer "^4.3.2"
|
||||
js-beautify "^1.8.8"
|
||||
lodash "^4.17.20"
|
||||
portal-vue "^2.1.6"
|
||||
|
@ -3789,10 +3790,10 @@ core-js-pure@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.5.tgz#c79e75f5e38dbc85a662d91eea52b8256d53b813"
|
||||
integrity sha512-lacdXOimsiD0QyNf9BC/mxivNJ/ybBGJXQFKzRekp1WTHoVUWsUHEn+2T8GJAzzIhyOuXA+gOxCVN3l+5PLPUA==
|
||||
|
||||
core-js@^3.19.3:
|
||||
version "3.19.3"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.19.3.tgz#6df8142a996337503019ff3235a7022d7cdf4559"
|
||||
integrity sha512-LeLBMgEGSsG7giquSzvgBrTS7V5UL6ks3eQlUSbN8dJStlLFiRzUm5iqsRyzUB8carhfKjkJ2vzKqE6z1Vga9g==
|
||||
core-js@^3.20.0:
|
||||
version "3.20.0"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.20.0.tgz#1c5ac07986b8d15473ab192e45a2e115a4a95b79"
|
||||
integrity sha512-KjbKU7UEfg4YPpskMtMXPhUKn7m/1OdTHTVjy09ScR2LVaoUXe8Jh0UdvN2EKUR6iKTJph52SJP95mAB0MnVLQ==
|
||||
|
||||
core-js@~2.3.0:
|
||||
version "2.3.0"
|
||||
|
@ -4923,7 +4924,7 @@ dompurify@2.3.3:
|
|||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.3.tgz#c1af3eb88be47324432964d8abc75cf4b98d634c"
|
||||
integrity sha512-dqnqRkPMAjOZE0FogZ+ceJNM2dZ3V/yNOuFB7+39qpO93hHhfRpHw3heYQC7DPK9FqbQTfBKUJhiSfz4MvXYwg==
|
||||
|
||||
dompurify@^2.3.3, dompurify@^2.3.4:
|
||||
dompurify@^2.3.4:
|
||||
version "2.3.4"
|
||||
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.3.4.tgz#1cf5cf0105ccb4debdf6db162525bd41e6ddacc6"
|
||||
integrity sha512-6BVcgOAVFXjI0JTjEvZy901Rghm+7fDQOrNIcxB4+gdhj6Kwp6T9VBhBY/AbagKHJocRkDYGd6wvI+p4/10xtQ==
|
||||
|
@ -5041,7 +5042,12 @@ emittery@^0.7.1:
|
|||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.7.1.tgz#c02375a927a40948c0345cc903072597f5270451"
|
||||
integrity sha512-d34LN4L6h18Bzz9xpoku2nPwKxCPlPMr3EEKTkoEBi+1/+b0lcRkRJ1UVyyZaKNeqGR3swcGl6s390DNO4YVgQ==
|
||||
|
||||
emoji-regex@^7.0.1, emoji-regex@^7.0.3:
|
||||
emoji-regex@^10.0.0:
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-10.0.0.tgz#96559e19f82231b436403e059571241d627c42b8"
|
||||
integrity sha512-KmJa8l6uHi1HrBI34udwlzZY1jOEuID/ft4d8BSSEdRyap7PwBEt910453PJa5MuGvxkLqlt4Uvhu7tttFHViw==
|
||||
|
||||
emoji-regex@^7.0.1:
|
||||
version "7.0.3"
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156"
|
||||
integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==
|
||||
|
@ -6613,6 +6619,11 @@ iferr@^0.1.5:
|
|||
resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501"
|
||||
integrity sha1-xg7taebY/bazEEofy8ocGS3FtQE=
|
||||
|
||||
iframe-resizer@^4.3.2:
|
||||
version "4.3.2"
|
||||
resolved "https://registry.yarnpkg.com/iframe-resizer/-/iframe-resizer-4.3.2.tgz#42dd88345d18b9e377b6044dddb98c664ab0ce6b"
|
||||
integrity sha512-gOWo2hmdPjMQsQ+zTKbses08mDfDEMh4NneGQNP4qwePYujY1lguqP6gnbeJkf154gojWlBhIltlgnMfYjGHWA==
|
||||
|
||||
ignore-by-default@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||
|
|
Loading…
Reference in New Issue