Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-16 15:08:46 +00:00
parent 3775eba7c1
commit 3940f59a61
95 changed files with 2281 additions and 401 deletions

View File

@ -172,7 +172,7 @@ gem 'diffy', '~> 3.3'
gem 'diff_match_patch', '~> 0.1.0'
# Application server
gem 'rack', '~> 2.0.9'
gem 'rack', '~> 2.1.4'
# https://github.com/sharpstone/rack-timeout/blob/master/README.md#rails-apps-manually
gem 'rack-timeout', '~> 0.5.1', require: 'rack/timeout/base'

View File

@ -855,7 +855,7 @@ GEM
public_suffix (4.0.6)
pyu-ruby-sasl (0.0.3.3)
raabro (1.1.6)
rack (2.0.9)
rack (2.1.4)
rack-accept (0.4.5)
rack (>= 0.4)
rack-attack (6.3.0)
@ -1429,7 +1429,7 @@ DEPENDENCIES
prometheus-client-mmap (~> 0.12.0)
pry-byebug (~> 3.9.0)
pry-rails (~> 0.3.9)
rack (~> 2.0.9)
rack (~> 2.1.4)
rack-attack (~> 6.3.0)
rack-cors (~> 1.0.6)
rack-oauth2 (~> 1.9.3)

View File

@ -285,10 +285,9 @@ export default {
variant="default"
class="d-sm-none gl-absolute toggle-sidebar-mobile-button"
type="button"
icon="chevron-double-lg-left"
@click="toggleSidebar"
>
<i class="fa fa-angle-double-left"></i>
</gl-button>
/>
</div>
<div
v-if="alert"

View File

@ -32,11 +32,12 @@ export default () => {
// Sidebar has an icon which corresponds to collapsing the sidebar
// only then trigger the click.
if (sidebarGutterVueToggleEl) {
const collapseIcon = sidebarGutterVueToggleEl.querySelector('i.fa-angle-double-right');
if (collapseIcon) {
collapseIcon.click();
if (
sidebarGutterVueToggleEl &&
!sidebarGutterVueToggleEl.classList.contains('js-sidebar-collapsed')
) {
if (sidebarGutterVueToggleEl) {
sidebarGutterVueToggleEl.click();
}
}
}

View File

@ -2,7 +2,7 @@
import $ from 'jquery';
import NewCommitForm from '../new_commit_form';
import EditBlob from './edit_blob';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
@ -24,6 +24,18 @@ export default () => {
const commitButton = $('.js-commit-button');
const cancelLink = $('.btn.btn-cancel');
import('./edit_blob')
.then(({ default: EditBlob } = {}) => {
new EditBlob({
assetsPath: `${urlRoot}${assetsPath}`,
filePath,
currentAction,
projectId,
isMarkdown,
});
})
.catch(e => createFlash(e));
cancelLink.on('click', () => {
window.onbeforeunload = null;
});
@ -32,13 +44,6 @@ export default () => {
window.onbeforeunload = null;
});
new EditBlob({
assetsPath: `${urlRoot}${assetsPath}`,
filePath,
currentAction,
projectId,
isMarkdown,
});
new NewCommitForm(editBlobForm);
// returning here blocks page navigation

View File

@ -478,13 +478,14 @@ export default class MergeRequestTabs {
}
shrinkView() {
const $gutterIcon = $('.js-sidebar-toggle i:visible');
const $gutterBtn = $('.js-sidebar-toggle:visible');
const $expandSvg = $gutterBtn.find('.js-sidebar-expand');
// Wait until listeners are set
setTimeout(() => {
// Only when sidebar is expanded
if ($gutterIcon.is('.fa-angle-double-right')) {
$gutterIcon.closest('a').trigger('click', [true]);
if ($expandSvg.length && $expandSvg.hasClass('hidden')) {
$gutterBtn.trigger('click', [true]);
}
}, 0);
}
@ -494,13 +495,14 @@ export default class MergeRequestTabs {
if (parseBoolean(Cookies.get('collapsed_gutter'))) {
return;
}
const $gutterIcon = $('.js-sidebar-toggle i:visible');
const $gutterBtn = $('.js-sidebar-toggle');
const $collapseSvg = $gutterBtn.find('.js-sidebar-collapse');
// Wait until listeners are set
setTimeout(() => {
// Only when sidebar is collapsed
if ($gutterIcon.is('.fa-angle-double-left')) {
$gutterIcon.closest('a').trigger('click', [true]);
if ($collapseSvg.length && !$collapseSvg.hasClass('hidden')) {
$gutterBtn.trigger('click', [true]);
}
}, 0);
}

View File

@ -1,4 +1,5 @@
<script>
import Vue from 'vue';
import { uniqueId } from 'lodash';
import {
GlAlert,
@ -50,6 +51,10 @@ export default {
type: String,
required: true,
},
configVariablesPath: {
type: String,
required: true,
},
projectId: {
type: String,
required: true,
@ -86,7 +91,7 @@ export default {
return {
searchTerm: '',
refValue: this.refParam,
variables: [],
form: {},
error: null,
warnings: [],
totalWarnings: 0,
@ -110,60 +115,122 @@ export default {
shouldShowWarning() {
return this.warnings.length > 0 && !this.isWarningDismissed;
},
variables() {
return this.form[this.refValue]?.variables ?? [];
},
descriptions() {
return this.form[this.refValue]?.descriptions ?? {};
},
},
created() {
this.addEmptyVariable();
if (this.variableParams) {
this.setVariableParams(VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(FILE_TYPE, this.fileParams);
}
this.setRefSelected(this.refValue);
},
methods: {
setVariable(type, key, value) {
const variable = this.variables.find(v => v.key === key);
addEmptyVariable(refValue) {
const { variables } = this.form[refValue];
const lastVar = variables[variables.length - 1];
if (lastVar?.key === '' && lastVar?.value === '') {
return;
}
variables.push({
uniqueId: uniqueId(`var-${refValue}`),
variable_type: VARIABLE_TYPE,
key: '',
value: '',
});
},
setVariable(refValue, type, key, value) {
const { variables } = this.form[refValue];
const variable = variables.find(v => v.key === key);
if (variable) {
variable.type = type;
variable.value = value;
} else {
// insert before the empty variable
this.variables.splice(this.variables.length - 1, 0, {
uniqueId: uniqueId('var'),
variables.push({
uniqueId: uniqueId(`var-${refValue}`),
key,
value,
variable_type: type,
});
}
},
setVariableParams(type, paramsObj) {
setVariableParams(refValue, type, paramsObj) {
Object.entries(paramsObj).forEach(([key, value]) => {
this.setVariable(type, key, value);
this.setVariable(refValue, type, key, value);
});
},
setRefSelected(ref) {
this.refValue = ref;
setRefSelected(refValue) {
this.refValue = refValue;
if (!this.form[refValue]) {
this.fetchConfigVariables(refValue)
.then(({ descriptions, params }) => {
Vue.set(this.form, refValue, {
variables: [],
descriptions,
});
// Add default variables from yml
this.setVariableParams(refValue, VARIABLE_TYPE, params);
})
.catch(() => {
Vue.set(this.form, refValue, {
variables: [],
descriptions: {},
});
})
.finally(() => {
// Add/update variables, e.g. from query string
if (this.variableParams) {
this.setVariableParams(refValue, VARIABLE_TYPE, this.variableParams);
}
if (this.fileParams) {
this.setVariableParams(refValue, FILE_TYPE, this.fileParams);
}
// Adds empty var at the end of the form
this.addEmptyVariable(refValue);
});
}
},
isSelected(ref) {
return ref === this.refValue;
},
addEmptyVariable() {
this.variables.push({
uniqueId: uniqueId('var'),
variable_type: VARIABLE_TYPE,
key: '',
value: '',
});
},
removeVariable(index) {
this.variables.splice(index, 1);
},
canRemove(index) {
return index < this.variables.length - 1;
},
fetchConfigVariables(refValue) {
if (gon?.features?.newPipelineFormPrefilledVars) {
return axios
.get(this.configVariablesPath, {
params: {
sha: refValue,
},
})
.then(({ data }) => {
const params = {};
const descriptions = {};
Object.entries(data).forEach(([key, { value, description }]) => {
if (description !== null) {
params[key] = value;
descriptions[key] = description;
}
});
return { params, descriptions };
});
}
return Promise.resolve({ params: {}, descriptions: {} });
},
createPipeline() {
const filteredVariables = this.variables
.filter(({ key, value }) => key !== '' && value !== '')
@ -261,45 +328,53 @@ export default {
<div
v-for="(variable, index) in variables"
:key="variable.uniqueId"
class="gl-display-flex gl-align-items-stretch gl-align-items-center gl-mb-4 gl-ml-n3 gl-pb-2 gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-flex-direction-column gl-md-flex-direction-row"
class="gl-mb-3 gl-ml-n3 gl-pb-2"
data-testid="ci-variable-row"
>
<gl-form-select
v-model="variable.variable_type"
:class="$options.formElementClasses"
:options="$options.typeOptions"
/>
<gl-form-input
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
@change.once="addEmptyVariable()"
/>
<gl-form-input
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
/>
<template v-if="variables.length > 1">
<gl-button
v-if="canRemove(index)"
class="gl-md-ml-3 gl-mb-3"
data-testid="remove-ci-variable-row"
variant="danger"
category="secondary"
@click="removeVariable(index)"
>
<gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" />
<span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span>
</gl-button>
<gl-button
v-else
class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden"
icon="clear"
<div
class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
>
<gl-form-select
v-model="variable.variable_type"
:class="$options.formElementClasses"
:options="$options.typeOptions"
/>
</template>
<gl-form-input
v-model="variable.key"
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
@change="addEmptyVariable(refValue)"
/>
<gl-form-input
v-model="variable.value"
:placeholder="s__('CiVariables|Input variable value')"
class="gl-mb-3"
data-testid="pipeline-form-ci-variable-value"
/>
<template v-if="variables.length > 1">
<gl-button
v-if="canRemove(index)"
class="gl-md-ml-3 gl-mb-3"
data-testid="remove-ci-variable-row"
variant="danger"
category="secondary"
@click="removeVariable(index)"
>
<gl-icon class="gl-mr-0! gl-display-none gl-display-md-block" name="clear" />
<span class="gl-display-md-none">{{ s__('CiVariables|Remove variable') }}</span>
</gl-button>
<gl-button
v-else
class="gl-md-ml-3 gl-mb-3 gl-display-none gl-display-md-block gl-visibility-hidden"
icon="clear"
/>
</template>
</div>
<div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
{{ descriptions[variable.key] }}
</div>
</div>
<template #description

View File

@ -6,6 +6,7 @@ export default () => {
const {
projectId,
pipelinesPath,
configVariablesPath,
refParam,
varParam,
fileParam,
@ -25,6 +26,7 @@ export default () => {
props: {
projectId,
pipelinesPath,
configVariablesPath,
refParam,
variableParams,
fileParams,

View File

@ -1,10 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { GlLink, GlModal } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { s__, sprintf } from '~/locale';
import { __, s__, sprintf } from '~/locale';
/**
* Pipeline Stop Modal.
@ -13,7 +12,7 @@ import { s__, sprintf } from '~/locale';
*/
export default {
components: {
GlModal: DeprecatedModal2,
GlModal,
GlLink,
CiIcon,
},
@ -46,6 +45,17 @@ export default {
hasRef() {
return !isEmpty(this.pipeline.ref);
},
primaryProps() {
return {
text: s__('Pipeline|Stop pipeline'),
attributes: [{ variant: 'danger' }],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
},
methods: {
emitSubmit(event) {
@ -56,11 +66,11 @@ export default {
</script>
<template>
<gl-modal
id="confirmation-modal"
:header-title-text="modalTitle"
:footer-primary-button-text="s__('Pipeline|Stop pipeline')"
footer-primary-button-variant="danger"
@submit="emitSubmit($event)"
modal-id="confirmation-modal"
:title="modalTitle"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="emitSubmit($event)"
>
<p v-html="modalText"></p>

View File

@ -1,5 +1,5 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
import eventHub from '../../event_hub';
import { __ } from '~/locale';
import PipelinesActionsComponent from './pipelines_actions.vue';
@ -24,6 +24,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
components: {
PipelinesActionsComponent,
@ -366,12 +367,11 @@ export default {
<gl-button
v-if="pipeline.flags.cancelable"
v-gl-tooltip.hover
v-gl-modal-directive="'confirmation-modal'"
:aria-label="$options.i18n.cancelTitle"
:title="$options.i18n.cancelTitle"
:loading="isCancelling"
:disabled="isCancelling"
data-toggle="modal"
data-target="#confirmation-modal"
icon="close"
variant="danger"
category="primary"

View File

@ -1,11 +1,12 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { n__, __ } from '~/locale';
export default {
name: 'AssigneeTitle',
components: {
GlLoadingIcon,
GlIcon,
},
props: {
loading: {
@ -64,7 +65,7 @@ export default {
href="#"
role="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
<gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" />
</a>
</div>
</template>

View File

@ -1,13 +1,14 @@
<script>
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
export default {
name: 'ReviewerTitle',
components: {
GlLoadingIcon,
GlIcon,
},
props: {
loading: {
@ -58,7 +59,7 @@ export default {
href="#"
role="button"
>
<i aria-hidden="true" data-hidden="true" class="fa fa-angle-double-right"></i>
<gl-icon aria-hidden="true" data-hidden="true" name="chevron-double-lg-right" :size="12" />
</a>
</div>
</template>

View File

@ -1,5 +1,5 @@
<script>
import tooltip from '~/vue_shared/directives/tooltip';
import { GlTooltipDirective } from '@gitlab/ui';
import MrWidgetAuthor from './mr_widget_author.vue';
export default {
@ -8,7 +8,7 @@ export default {
MrWidgetAuthor,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
actionText: {
@ -34,6 +34,7 @@ export default {
<h4 class="js-mr-widget-author">
{{ actionText }}
<mr-widget-author :author="author" />
<time v-tooltip :title="dateTitle" data-container="body"> {{ dateReadable }} </time>
<span class="sr-only">{{ dateReadable }} ({{ dateTitle }})</span>
<time v-gl-tooltip.hover aria-hidden :title="dateTitle"> {{ dateReadable }} </time>
</h4>
</template>

View File

@ -1,11 +1,27 @@
<script>
import ActionButtonGroup from './action_button_group.vue';
import RemoveGroupLinkButton from './remove_group_link_button.vue';
export default {
name: 'GroupActionButtons',
components: { ActionButtonGroup, RemoveGroupLinkButton },
props: {
member: {
type: Object,
required: true,
},
permissions: {
type: Object,
required: true,
},
},
};
</script>
<template>
<span>
<!-- Temporarily empty -->
</span>
<action-button-group>
<div v-if="permissions.canRemove" class="gl-px-1">
<remove-group-link-button :group-link="member" />
</div>
</action-button-group>
</template>

View File

@ -0,0 +1,36 @@
<script>
import { mapActions } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RemoveGroupLinkButton',
i18n: {
buttonTitle: s__('Members|Remove group'),
},
components: { GlButton },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
groupLink: {
type: Object,
required: true,
},
},
methods: {
...mapActions(['showRemoveGroupLinkModal']),
},
};
</script>
<template>
<gl-button
v-gl-tooltip.hover
variant="danger"
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
@click="showRemoveGroupLinkModal(groupLink)"
/>
</template>

View File

@ -66,3 +66,5 @@ export const MEMBER_TYPES = {
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';

View File

@ -0,0 +1,69 @@
<script>
import { mapState, mapActions } from 'vuex';
import { GlModal, GlSprintf, GlForm } from '@gitlab/ui';
import csrf from '~/lib/utils/csrf';
import { __, s__, sprintf } from '~/locale';
import { REMOVE_GROUP_LINK_MODAL_ID } from '../constants';
export default {
name: 'RemoveGroupLinkModal',
actionCancel: {
text: __('Cancel'),
},
actionPrimary: {
text: s__('Members|Remove group'),
attributes: {
variant: 'danger',
},
},
csrf,
i18n: {
modalBody: s__('Members|Are you sure you want to remove "%{groupName}"?'),
},
modalId: REMOVE_GROUP_LINK_MODAL_ID,
components: { GlModal, GlSprintf, GlForm },
computed: {
...mapState(['memberPath', 'groupLinkToRemove', 'removeGroupLinkModalVisible']),
groupLinkPath() {
return this.memberPath.replace(/:id$/, this.groupLinkToRemove?.id);
},
groupName() {
return this.groupLinkToRemove?.sharedWithGroup.fullName;
},
modalTitle() {
return sprintf(s__('Members|Remove "%{groupName}"'), { groupName: this.groupName });
},
},
methods: {
...mapActions(['hideRemoveGroupLinkModal']),
handlePrimary() {
this.$refs.form.$el.submit();
},
},
};
</script>
<template>
<gl-modal
v-bind="$attrs"
:modal-id="$options.modalId"
:visible="removeGroupLinkModalVisible"
:title="modalTitle"
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
@primary="handlePrimary"
@hide="hideRemoveGroupLinkModal"
>
<gl-form ref="form" :action="groupLinkPath" method="post">
<p>
<gl-sprintf :message="$options.i18n.modalBody">
<template #groupName>{{ groupName }}</template>
</gl-sprintf>
</p>
<input type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
</gl-form>
</gl-modal>
</template>

View File

@ -10,6 +10,7 @@ import ExpiresAt from './expires_at.vue';
import MemberActionButtons from './member_action_buttons.vue';
import MembersTableCell from './members_table_cell.vue';
import RoleDropdown from './role_dropdown.vue';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
export default {
name: 'MembersTable',
@ -23,6 +24,7 @@ export default {
MemberSource,
MemberActionButtons,
RoleDropdown,
RemoveGroupLinkModal,
},
computed: {
...mapState(['members', 'tableFields']),
@ -37,69 +39,72 @@ export default {
</script>
<template>
<gl-table
class="members-table"
head-variant="white"
stacked="lg"
:fields="filteredFields"
:items="members"
primary-key="id"
thead-class="border-bottom"
:empty-text="__('No members found')"
show-empty
>
<template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell>
</template>
<div>
<gl-table
class="members-table"
head-variant="white"
stacked="lg"
:fields="filteredFields"
:items="members"
primary-key="id"
thead-class="border-bottom"
:empty-text="__('No members found')"
show-empty
>
<template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell>
</template>
<template #cell(source)="{ item: member }">
<members-table-cell #default="{ isDirectMember }" :member="member">
<member-source :is-direct-member="isDirectMember" :member-source="member.source" />
</members-table-cell>
</template>
<template #cell(source)="{ item: member }">
<members-table-cell #default="{ isDirectMember }" :member="member">
<member-source :is-direct-member="isDirectMember" :member-source="member.source" />
</members-table-cell>
</template>
<template #cell(granted)="{ item: { createdAt, createdBy } }">
<created-at :date="createdAt" :created-by="createdBy" />
</template>
<template #cell(granted)="{ item: { createdAt, createdBy } }">
<created-at :date="createdAt" :created-by="createdBy" />
</template>
<template #cell(invited)="{ item: { createdAt, createdBy } }">
<created-at :date="createdAt" :created-by="createdBy" />
</template>
<template #cell(invited)="{ item: { createdAt, createdBy } }">
<created-at :date="createdAt" :created-by="createdBy" />
</template>
<template #cell(requested)="{ item: { createdAt } }">
<created-at :date="createdAt" />
</template>
<template #cell(requested)="{ item: { createdAt } }">
<created-at :date="createdAt" />
</template>
<template #cell(expires)="{ item: { expiresAt } }">
<expires-at :date="expiresAt" />
</template>
<template #cell(expires)="{ item: { expiresAt } }">
<expires-at :date="expiresAt" />
</template>
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :member="member" />
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
</members-table-cell>
</template>
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
<role-dropdown v-if="permissions.canUpdate" :member="member" />
<gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
</members-table-cell>
</template>
<template #cell(actions)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
<member-action-buttons
:member-type="memberType"
:is-current-user="isCurrentUser"
:permissions="permissions"
:member="member"
/>
</members-table-cell>
</template>
<template #cell(actions)="{ item: member }">
<members-table-cell #default="{ memberType, isCurrentUser, permissions }" :member="member">
<member-action-buttons
:member-type="memberType"
:is-current-user="isCurrentUser"
:permissions="permissions"
:member="member"
/>
</members-table-cell>
</template>
<template #head(actions)="{ label }">
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
</template>
</gl-table>
<template #head(actions)="{ label }">
<span data-testid="col-actions" class="gl-sr-only">{{ label }}</span>
</template>
</gl-table>
<remove-group-link-modal />
</div>
</template>

View File

@ -1,11 +1,14 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
export default {
name: 'ToggleSidebar',
components: {
GlButton,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
collapsed: {
@ -22,6 +25,12 @@ export default {
tooltipLabel() {
return this.collapsed ? __('Expand sidebar') : __('Collapse sidebar');
},
buttonIcon() {
return this.collapsed ? 'chevron-double-lg-left' : 'chevron-double-lg-right';
},
allCssClasses() {
return [this.cssClasses, { 'js-sidebar-collapsed': this.collapsed }];
},
},
methods: {
toggle() {
@ -32,25 +41,15 @@ export default {
</script>
<template>
<button
v-tooltip
<gl-button
v-gl-tooltip:body.viewport.left
:title="tooltipLabel"
:class="cssClasses"
type="button"
class="btn btn-blank gutter-toggle btn-sidebar-action js-sidebar-vue-toggle"
data-container="body"
data-placement="left"
data-boundary="viewport"
:class="allCssClasses"
class="gutter-toggle btn-sidebar-action js-sidebar-vue-toggle"
:icon="buttonIcon"
category="tertiary"
size="small"
:aria-label="__('toggle collapse')"
@click="toggle"
>
<i
:class="{
'fa-angle-double-right': !collapsed,
'fa-angle-double-left': collapsed,
}"
:aria-label="__('toggle collapse')"
class="fa"
>
</i>
</button>
/>
</template>

View File

@ -15,3 +15,11 @@ export const updateMemberRole = async ({ state, commit }, { memberId, accessLeve
throw error;
}
};
export const showRemoveGroupLinkModal = ({ commit }, groupLink) => {
commit(types.SHOW_REMOVE_GROUP_LINK_MODAL, groupLink);
};
export const hideRemoveGroupLinkModal = ({ commit }) => {
commit(types.HIDE_REMOVE_GROUP_LINK_MODAL);
};

View File

@ -2,3 +2,6 @@ export const RECEIVE_MEMBER_ROLE_SUCCESS = 'RECEIVE_MEMBER_ROLE_SUCCESS';
export const RECEIVE_MEMBER_ROLE_ERROR = 'RECEIVE_MEMBER_ROLE_ERROR';
export const HIDE_ERROR = 'HIDE_ERROR';
export const SHOW_REMOVE_GROUP_LINK_MODAL = 'SHOW_REMOVE_GROUP_LINK_MODAL';
export const HIDE_REMOVE_GROUP_LINK_MODAL = 'HIDE_REMOVE_GROUP_LINK_MODAL';

View File

@ -23,4 +23,11 @@ export default {
state.showError = false;
state.errorMessage = '';
},
[types.SHOW_REMOVE_GROUP_LINK_MODAL](state, groupLink) {
state.removeGroupLinkModalVisible = true;
state.groupLinkToRemove = groupLink;
},
[types.HIDE_REMOVE_GROUP_LINK_MODAL](state) {
state.removeGroupLinkModalVisible = false;
},
};

View File

@ -14,4 +14,6 @@ export default ({
requestFormatter,
showError: false,
errorMessage: '',
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
});

View File

@ -109,14 +109,6 @@
content: '\f110';
}
.fa-angle-double-right::before {
content: '\f101';
}
.fa-angle-double-left::before {
content: '\f100';
}
.fa-trash-o::before {
content: '\f014';
}

View File

@ -183,7 +183,7 @@ class Admin::UsersController < Admin::ApplicationController
# restore username to keep form action url.
user.username = params[:id]
format.html { render "edit" }
format.json { render json: [result[:message]], status: result[:status] }
format.json { render json: [result[:message]], status: :internal_server_error }
end
end
end

View File

@ -45,7 +45,7 @@ class Projects::MergeRequests::DraftsController < Projects::MergeRequests::Appli
if result[:status] == :success
head :ok
else
render json: { message: result[:message] }, status: result[:status]
render json: { message: result[:message] }, status: :internal_server_error
end
end

View File

@ -17,6 +17,7 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:pipelines_security_report_summary, project)
push_frontend_feature_flag(:new_pipeline_form, project)
push_frontend_feature_flag(:graphql_pipeline_header, project, type: :development, default_enabled: false)
push_frontend_feature_flag(:new_pipeline_form_prefilled_vars, project, type: :development)
end
before_action :ensure_pipeline, only: [:show]

View File

@ -26,5 +26,3 @@ module Mutations
end
end
end
Mutations::Issues::CommonMutationArguments.prepend_if_ee('::EE::Mutations::Issues::CommonMutationArguments')

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
module Resolvers
module Ci
class RunnerPlatformsResolver < BaseResolver
type Types::Ci::RunnerPlatformType, null: false
def resolve(**args)
runner_instructions.map do |platform, data|
{
name: platform, human_readable_name: data[:human_readable_name],
architectures: parse_architectures(data[:download_locations])
}
end
end
private
def runner_instructions
Gitlab::Ci::RunnerInstructions::OS.merge(Gitlab::Ci::RunnerInstructions::OTHER_ENVIRONMENTS)
end
def parse_architectures(download_locations)
download_locations&.map do |architecture, download_location|
{ name: architecture, download_location: download_location }
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerArchitectureType < BaseObject
graphql_name 'RunnerArchitecture'
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Name of the runner platform architecture'
field :download_location, GraphQL::STRING_TYPE, null: false,
description: 'Download location for the runner for the platform architecture'
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerPlatformType < BaseObject
graphql_name 'RunnerPlatform'
field :name, GraphQL::STRING_TYPE, null: false,
description: 'Name slug of the runner platform'
field :human_readable_name, GraphQL::STRING_TYPE, null: false,
description: 'Human readable name of the runner platform'
field :architectures, Types::Ci::RunnerArchitectureType.connection_type, null: true,
description: 'Runner architectures supported for the platform'
end
end
end

View File

@ -80,6 +80,10 @@ module Types
description: 'Get statistics on the instance',
resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver
field :runner_platforms, Types::Ci::RunnerPlatformType.connection_type,
null: true, description: 'Supported runner platforms',
resolver: Resolvers::Ci::RunnerPlatformsResolver
def design_management
DesignManagementObject.new(nil)
end

View File

@ -33,6 +33,7 @@ module Groups::GroupMembersHelper
def linked_groups_list_data_attributes(group)
{
members: linked_groups_data_json(group.shared_with_group_links),
member_path: group_group_link_path(group, ':id'),
group_id: group.id
}
end

View File

@ -829,16 +829,9 @@ module Ci
end
def same_family_pipeline_ids
if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project)
::Gitlab::Ci::PipelineObjectHierarchy.new(
base_and_ancestors(same_project: true), options: { same_project: true }
).base_and_descendants.select(:id)
else
# If pipeline is a child of another pipeline, include the parent
# and the siblings, otherwise return only itself and children.
parent = parent_pipeline || self
[parent.id] + parent.child_pipelines.pluck(:id)
end
::Gitlab::Ci::PipelineObjectHierarchy.new(
base_and_ancestors(same_project: true), options: { same_project: true }
).base_and_descendants.select(:id)
end
def build_with_artifacts_in_self_and_descendants(name)

View File

@ -77,16 +77,9 @@ module Ci
# TODO: Remove this condition if favour of model validation
# https://gitlab.com/gitlab-org/gitlab/issues/38338
if ::Gitlab::Ci::Features.child_of_child_pipeline_enabled?(project)
if has_max_descendants_depth?
@bridge.drop!(:reached_max_descendant_pipelines_depth)
return false
end
else
if @bridge.triggers_child_pipeline? && @bridge.pipeline.parent_pipeline.present?
@bridge.drop!(:bridge_pipeline_is_child_pipeline)
return false
end
if has_max_descendants_depth?
@bridge.drop!(:reached_max_descendant_pipelines_depth)
return false
end
unless can_create_downstream_pipeline?(target_ref)

View File

@ -7,7 +7,15 @@
%hr
- if Feature.enabled?(:new_pipeline_form, @project)
#js-new-pipeline{ data: { project_id: @project.id, pipelines_path: project_pipelines_path(@project), ref_param: params[:ref] || @project.default_branch, var_param: params[:var].to_json, file_param: params[:file_var].to_json, ref_names: @project.repository.ref_names.to_json.html_safe, settings_link: project_settings_ci_cd_path(@project), max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
#js-new-pipeline{ data: { project_id: @project.id,
pipelines_path: project_pipelines_path(@project),
config_variables_path: config_variables_namespace_project_pipelines_path(@project.namespace, @project),
ref_param: params[:ref] || @project.default_branch,
var_param: params[:var].to_json,
file_param: params[:file_var].to_json,
ref_names: @project.repository.ref_names.to_json.html_safe,
settings_link: project_settings_ci_cd_path(@project),
max_warnings: ::Gitlab::Ci::Warnings::MAX_LIMIT } }
- else
= form_for @pipeline, as: :pipeline, url: project_pipelines_path(@project), html: { id: "new-pipeline-form", class: "js-new-pipeline-form js-requires-input" } do |f|

View File

@ -9,6 +9,8 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
worker_resource_boundary :cpu
tags :requires_disk_io
ARCHIVE_TRACES_IN = 2.minutes.freeze
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
Ci::Build.find_by(id: build_id).try do |build|
@ -33,9 +35,22 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
ArchiveTraceWorker.perform_async(build.id)
ExpirePipelineCacheWorker.perform_async(build.pipeline_id) if build.pipeline.cacheable?
ChatNotificationWorker.perform_async(build.id) if build.pipeline.chat?
##
# We want to delay sending a build trace to object storage operation to
# validate that this fixes a race condition between this and flushing live
# trace chunks and chunks being removed after consolidation and putting
# them into object storage archive.
#
# TODO This is temporary fix we should improve later, after we validate
# that this is indeed the culprit.
#
# See https://gitlab.com/gitlab-org/gitlab/-/issues/267112 for more
# details.
#
ArchiveTraceWorker.perform_in(ARCHIVE_TRACES_IN, build.id)
end
end

View File

@ -0,0 +1,5 @@
---
title: Replace fa-angle-double-left and fa-angle-double-right icons with GitLab SVG
merge_request: 45251
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Populate blocking issues count
merge_request: 45176
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Migrate blocked_by issue links to blocks type by swapping source and target
merge_request: 45262
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Update to Rack v2.1.4
merge_request: 45340
author:
type: fixed

View File

@ -1,7 +1,7 @@
---
name: ci_child_of_child_pipeline
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41102
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/243747
group: group::continuous integration
name: new_pipeline_form_prefilled_vars
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44120
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263276
type: development
default_enabled: true
group: group::continuous integration
default_enabled: false

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'set'
class ScheduleSyncBlockingIssuesCount < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 50
DELAY_INTERVAL = 120.seconds.to_i
MIGRATION = 'SyncBlockingIssuesCount'.freeze
disable_ddl_transaction!
class TmpIssueLink < ActiveRecord::Base
self.table_name = 'issue_links'
include EachBatch
end
def up
return unless Gitlab.ee?
issue_link_ids = SortedSet.new
TmpIssueLink.distinct.select(:source_id).where(link_type: 1).each_batch(of: 1000, column: :source_id) do |query|
issue_link_ids.merge(query.pluck(:source_id))
end
TmpIssueLink.distinct.select(:target_id).where(link_type: 2).each_batch(of: 1000, column: :target_id) do |query|
issue_link_ids.merge(query.pluck(:target_id))
end
issue_link_ids.each_slice(BATCH_SIZE).with_index do |items, index|
start_id, *, end_id = items
arguments = [start_id, end_id]
final_delay = DELAY_INTERVAL * (index + 1)
migrate_in(final_delay, MIGRATION, arguments)
end
end
def down
# NO OP
end
end

View File

@ -0,0 +1,30 @@
# frozen_string_literal: true
class ScheduleBlockedByLinksReplacement < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INTERVAL = 2.minutes
# at the time of writing there were 47600 blocked_by issues:
# estimated time is 48 batches * 2 minutes -> 100 minutes
BATCH_SIZE = 1000
MIGRATION = 'ReplaceBlockedByLinks'
disable_ddl_transaction!
class IssueLink < ActiveRecord::Base
include EachBatch
self.table_name = 'issue_links'
end
def up
relation = IssueLink.where(link_type: 2)
queue_background_migration_jobs_by_range_at_intervals(
relation, MIGRATION, INTERVAL, batch_size: BATCH_SIZE)
end
def down
end
end

View File

@ -0,0 +1 @@
aed103bb25b70eb8f6387d84225a8e51672a83c4586ccc65da3011ef010da4b1

View File

@ -0,0 +1 @@
e44ab2a7b3014b44d7d84de1f7e618d2fc89f98b8d59f5f6fa331544e206355f

View File

@ -39,24 +39,19 @@ the swap available when needed.
## Setup instructions
For this default reference architecture, to install GitLab use the standard
To install GitLab for this default reference architecture, use the standard
[installation instructions](../../install/README.md).
NOTE: **Note:**
You can also optionally configure GitLab to use an
[external PostgreSQL service](../postgresql/external.md) or an
[external object storage service](../object_storage.md) for
added performance and reliability at a reduced complexity cost.
You can also optionally configure GitLab to use an [external PostgreSQL service](../postgresql/external.md)
or an [external object storage service](../object_storage.md) for added
performance and reliability at a reduced complexity cost.
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
You can leverage Elasticsearch and [enable Advanced Search](../../integration/elasticsearch.md)
for faster, more advanced code search across your entire GitLab instance.
Elasticsearch cluster design and requirements are dependent on your specific
data. For recommended best practices about how to set up your Elasticsearch
cluster alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)

View File

@ -80,7 +80,7 @@ The following API resources are available in the project context:
| [Vulnerability exports](vulnerability_exports.md) **(ULTIMATE)** | `/projects/:id/vulnerability_exports` |
| [Project vulnerabilities](project_vulnerabilities.md) **(ULTIMATE)** | `/projects/:id/vulnerabilities` |
| [Vulnerability findings](vulnerability_findings.md) **(ULTIMATE)** | `/projects/:id/vulnerability_findings` |
| [Wikis](wikis.md) | `/projects/:id/wikis` |
| [Project wikis](wikis.md) | `/projects/:id/wikis` |
## Group resources
@ -108,6 +108,7 @@ The following API resources are available in the group context:
| [Notification settings](notification_settings.md) | `/groups/:id/notification_settings` (also available for projects and standalone) |
| [Resource label events](resource_label_events.md) | `/groups/:id/epics/.../resource_label_events` (also available for projects) |
| [Search](search.md) | `/groups/:id/search` (also available for projects and standalone) |
| [Group wikis](group_wikis.md) **(PREMIUM)** | `/groups/:id/wikis` |
## Standalone resources

View File

@ -3471,6 +3471,11 @@ input CreateIssueInput {
"""
epicId: EpicID
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The IID (internal ID) of a project issue. Only admins and project owners can modify
"""
@ -3510,6 +3515,11 @@ input CreateIssueInput {
Title of the issue
"""
title: String!
"""
The weight of the issue
"""
weight: Int
}
"""
@ -15538,6 +15548,31 @@ type Query {
sort: String
): ProjectConnection
"""
Supported runner platforms
"""
runnerPlatforms(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): RunnerPlatformConnection
"""
Find Snippets visible to the current user
"""
@ -16712,6 +16747,125 @@ type RunDASTScanPayload {
pipelineUrl: String
}
type RunnerArchitecture {
"""
Download location for the runner for the platform architecture
"""
downloadLocation: String!
"""
Name of the runner platform architecture
"""
name: String!
}
"""
The connection type for RunnerArchitecture.
"""
type RunnerArchitectureConnection {
"""
A list of edges.
"""
edges: [RunnerArchitectureEdge]
"""
A list of nodes.
"""
nodes: [RunnerArchitecture]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type RunnerArchitectureEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: RunnerArchitecture
}
type RunnerPlatform {
"""
Runner architectures supported for the platform
"""
architectures(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): RunnerArchitectureConnection
"""
Human readable name of the runner platform
"""
humanReadableName: String!
"""
Name slug of the runner platform
"""
name: String!
}
"""
The connection type for RunnerPlatform.
"""
type RunnerPlatformConnection {
"""
A list of edges.
"""
edges: [RunnerPlatformEdge]
"""
A list of nodes.
"""
nodes: [RunnerPlatform]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type RunnerPlatformEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: RunnerPlatform
}
"""
Represents a CI configuration of SAST
"""
@ -19657,6 +19811,11 @@ input UpdateIssueInput {
"""
epicId: ID
"""
The desired health status
"""
healthStatus: HealthStatus
"""
The IID of the issue to mutate
"""
@ -19691,6 +19850,11 @@ input UpdateIssueInput {
Title of the issue
"""
title: String
"""
The weight of the issue
"""
weight: Int
}
"""

View File

@ -9376,6 +9376,26 @@
},
"defaultValue": null
},
{
"name": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "The weight of the issue",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "epicId",
"description": "The ID of an epic to associate the issue with",
@ -44968,6 +44988,59 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runnerPlatforms",
"description": "Supported runner platforms",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "RunnerPlatformConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippets",
"description": "Find Snippets visible to the current user",
@ -48213,6 +48286,381 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerArchitecture",
"description": null,
"fields": [
{
"name": "downloadLocation",
"description": "Download location for the runner for the platform architecture",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the runner platform architecture",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerArchitectureConnection",
"description": "The connection type for RunnerArchitecture.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RunnerArchitectureEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RunnerArchitecture",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerArchitectureEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "RunnerArchitecture",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerPlatform",
"description": null,
"fields": [
{
"name": "architectures",
"description": "Runner architectures supported for the platform",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "RunnerArchitectureConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "humanReadableName",
"description": "Human readable name of the runner platform",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name slug of the runner platform",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerPlatformConnection",
"description": "The connection type for RunnerPlatform.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RunnerPlatformEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "RunnerPlatform",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerPlatformEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "RunnerPlatform",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SastCiConfiguration",
@ -57105,6 +57553,26 @@
},
"defaultValue": null
},
{
"name": "healthStatus",
"description": "The desired health status",
"type": {
"kind": "ENUM",
"name": "HealthStatus",
"ofType": null
},
"defaultValue": null
},
{
"name": "weight",
"description": "The weight of the issue",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "epicId",
"description": "The ID of the parent epic. NULL when removing the association",

View File

@ -2252,6 +2252,20 @@ Autogenerated return type of RunDASTScan.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | URL of the pipeline that was created. |
### RunnerArchitecture
| Field | Type | Description |
| ----- | ---- | ----------- |
| `downloadLocation` | String! | Download location for the runner for the platform architecture |
| `name` | String! | Name of the runner platform architecture |
### RunnerPlatform
| Field | Type | Description |
| ----- | ---- | ----------- |
| `humanReadableName` | String! | Human readable name of the runner platform |
| `name` | String! | Name slug of the runner platform |
### SastCiConfigurationAnalyzersEntity
Represents an analyzer entity in SAST CI configuration.

View File

@ -5,9 +5,9 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, api
---
# Wikis API
# Group wikis API **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in GitLab 13.2.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5.
Available only in APIv4.

View File

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference, api
---
# Wikis API
# Project wikis API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/13372) in GitLab 10.0.

View File

@ -164,36 +164,13 @@ This is [resolved in GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues
## Nested child pipelines
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29651) in GitLab 13.4.
> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-nested-child-pipelines). **(CORE ONLY)**
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/243747) in GitLab 13.5.
Parent and child pipelines were introduced with a maximum depth of one level of child
pipelines, which was later increased to two. A parent pipeline can trigger many child
pipelines, and these child pipelines can trigger their own child pipelines. It's not
possible to trigger another level of child pipelines.
### Enable or disable nested child pipelines **(CORE ONLY)**
Nested child pipelines with a depth of two are under development but ready for
production use. This feature is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md)
can opt to disable it.
To enable it:
```ruby
Feature.enable(:ci_child_of_child_pipeline)
```
To disable it:
```ruby
Feature.disable(:ci_child_of_child_pipeline)
```
## Pass variables to a child pipeline
You can [pass variables to a downstream pipeline](multi_project_pipelines.md#passing-variables-to-a-downstream-pipeline).

View File

@ -121,10 +121,11 @@ versions (stable branches `X.Y` of the `gitlab-docs` project):
pipelines succeed:
NOTE: **Note:**
The `release-X-Y` branch needs to be present locally, otherwise the Rake
task will abort.
The `release-X-Y` branch needs to be present locally,
and you need to have switched to it, otherwise the Rake task will fail.
```shell
git checkout release-X-Y
./bin/rake release:dropdowns
```

View File

@ -295,13 +295,16 @@ end
Adding foreign key to `projects`:
We can use the `add_concurrenct_foreign_key` method in this case, as this helper method
has the lock retries built into it.
```ruby
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
with_lock_retries do
add_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
end
add_concurrent_foreign_key :imports, :projects, column: :project_id, on_delete: :cascade
end
def down
@ -316,10 +319,10 @@ Adding foreign key to `users`:
```ruby
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
with_lock_retries do
add_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
end
add_concurrent_foreign_key :imports, :users, column: :user_id, on_delete: :cascade
end
def down

View File

@ -215,6 +215,85 @@ From the rails console:
Feature.enable!(:disable_authorized_projects_deduplication)
```
## Limited capacity worker
It is possible to limit the number of concurrent running jobs for a worker class
by using the `LimitedCapacity::Worker` concern.
The worker must implement three methods:
- `perform_work` - the concern implements the usual `perform` method and calls
`perform_work` if there is any capacity available.
- `remaining_work_count` - number of jobs that will have work to perform.
- `max_running_jobs` - maximum number of jobs allowed to run concurrently.
```ruby
class MyDummyWorker
include ApplicationWorker
include LimitedCapacity::Worker
def perform_work(*args)
end
def remaining_work_count(*args)
5
end
def max_running_jobs
25
end
end
```
Additional to the regular worker, a cron worker must be defined as well to
backfill the queue with jobs. the arguments passed to `perform_with_capacity`
will be passed along to the `perform_work` method.
```ruby
class ScheduleMyDummyCronWorker
include ApplicationWorker
include CronjobQueue
def perform(*args)
MyDummyWorker.perform_with_capacity(*args)
end
end
```
### How many jobs are running?
It will be running `max_running_jobs` at almost all times.
The cron worker will check the remaining capacity on each execution and it
will schedule at most `max_running_jobs` jobs. Those jobs on completion will
re-enqueue themselves immediately, but not on failure. The cron worker is in
charge of replacing those failed jobs.
### Handling errors and idempotence
This concern disables Sidekiq retries, logs the errors, and sends the job to the
dead queue. This is done to have only one source that produces jobs and because
the retry would occupy a slot with a job that will be performed in the distant future.
We let the cron worker enqueue new jobs, this could be seen as our retry and
back off mechanism because the job might fail again if executed immediately.
This means that for every failed job, we will be running at a lower capacity
until the cron worker fills the capacity again. If it is important for the
worker not to get a backlog, exceptions must be handled in `#perform_work` and
the job should not raise.
The jobs are deduplicated using the `:none` strategy, but the worker is not
marked as `idempotent!`.
### Metrics
This concern exposes three Prometheus metrics of gauge type with the worker class
name as label:
- `limited_capacity_worker_running_jobs`
- `limited_capacity_worker_max_running_jobs`
- `limited_capacity_worker_remaining_work_count`
## Job urgency
Jobs can have an `urgency` attribute set, which can be `:high`,

View File

@ -4,7 +4,7 @@ group: Health
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# GitLab Status Page **(ULTIMATE)**
# Status Page
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2479) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
@ -25,7 +25,7 @@ Clicking an incident displays a detail page with more information about a partic
valid image extension. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205166) in GitLab 13.1.
- A chronological ordered list of updates to the incident.
## Set up a GitLab Status Page
## Set up a Status Page
To configure a GitLab Status Page you must:

View File

@ -391,6 +391,51 @@ milestones.
[Learn more about Epics.](epics/index.md)
## Group wikis **(PREMIUM)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13195) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5.
> - It's [deployed behind a feature flag](../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-group-wikis).
CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details.
Group wikis work the same way as [project wikis](../project/wiki/index.md), please refer to those docs for details on usage.
Group wikis can be edited by members with [Developer permissions](../../user/permissions.md#group-members-permissions)
and above.
### Group wikis limitations
There are a few limitations compared to project wikis:
- Local Git access is not supported yet.
- Group wikis are not included in global search, group exports, backups, and Geo replication.
- Changes to group wikis don't show up in the group's activity feed.
You can follow [this epic](https://gitlab.com/groups/gitlab-org/-/epics/2782) for updates.
### Enable or disable group wikis **(CORE ONLY)**
Group wikis are under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it for your instance.
To enable it:
```ruby
Feature.enable(:group_wikis)
```
To disable it:
```ruby
Feature.disable(:group_wikis)
```
## Group Security Dashboard **(ULTIMATE)**
Get an overview of the vulnerabilities of all the projects in a group and its subgroups.

View File

@ -243,6 +243,7 @@ group.
| Action | Guest | Reporter | Developer | Maintainer | Owner |
|--------------------------------------------------------|-------|----------|-----------|------------|-------|
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
| View group wiki pages **(PREMIUM)** | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View group epic **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit group epic **(ULTIMATE)** | | ✓ | ✓ | ✓ | ✓ |
@ -256,10 +257,12 @@ group.
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
| Create/edit/delete iterations | | | ✓ | ✓ | ✓ |
| Enable/disable a dependency proxy **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Create and edit group wiki pages **(PREMIUM)** | | | ✓ | ✓ | ✓ |
| Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ |
| Create/edit/delete metrics dashboard annotations | | | ✓ | ✓ | ✓ |
| View/manage group-level Kubernetes cluster | | | | ✓ | ✓ |
| Create subgroup | | | | ✓ (1) | ✓ |
| Delete group wiki pages **(PREMIUM)** | | | | ✓ | ✓ |
| Edit epic comments (posted by any user) **(ULTIMATE)** | | | | ✓ (2) | ✓ (2) |
| Edit group settings | | | | | ✓ |
| Manage group level CI/CD variables | | | | | ✓ |
@ -273,7 +276,7 @@ group.
| Disable notification emails | | | | | ✓ |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Insights **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Issue analytics **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Productivity analytics **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| View Value Stream analytics | ✓ | ✓ | ✓ | ✓ | ✓ |
| View Billing **(FREE ONLY)** | | | | | ✓ (4) |
@ -287,6 +290,7 @@ group.
- The [group level](group/index.md#default-project-creation-level).
1. Does not apply to subgroups.
1. Developers can push commits to the default branch of a new project only if the [default branch protection](group/index.md#changing-the-default-branch-protection-of-a-group) is set to "Partially protected" or "Not protected".
1. In addition, if your group is public or internal, all users who can see the group can also see group wiki pages.
### Subgroup permissions

View File

@ -28,11 +28,12 @@ source project and only lasts while the merge request is open. Once enabled,
upstream members will also be able to retry the pipelines and jobs of the
merge request:
1. Enable the contribution while creating or editing a merge request.
1. While creating or editing a merge request, select the checkbox **Allow
commits from members who can merge to the target branch**.
![Enable contribution](img/allow_collaboration.png)
1. Once the merge request is created, you'll see that commits from members who
1. Once the merge request is created, you can see that commits from members who
can merge to the target branch are allowed.
![Check that contribution is enabled](img/allow_collaboration_after_save.png)

View File

@ -103,7 +103,7 @@ Reopened issues are considered as having been opened on the day after they were
## Burnup charts
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/6903) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.5.
Burnup charts show the assigned and completed work for a milestone.

View File

@ -6,51 +6,43 @@ type: reference, how-to
description: "The static site editor enables users to edit content on static websites without prior knowledge of the underlying templating language, site architecture or Git commands."
---
# Static Site Editor
# Static Site Editor **(CORE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0.
> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1.
> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1.
> - Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2.
> - Non-Markdown content blocks not editable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3.
> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.4.
> - Non-Markdown content blocks uneditable on the WYSIWYG mode [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216836) in GitLab 13.3.
DANGER: **Danger:**
In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
to the URL structure of the Static Site Editor. Follow the instructions in this
[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
to update your project with the latest changes.
Static Site Editor enables users to edit content on static websites without
Static Site Editor (SSE) enables users to edit content on static websites without
prior knowledge of the underlying templating language, site architecture, or
Git commands. A contributor to your project can quickly edit a Markdown page
and submit the changes for review.
## Use cases
The Static Site Editors allows collaborators to submit changes to static site
The Static Site Editor allows collaborators to submit changes to static site
files seamlessly. For example:
- Non-technical collaborators can easily edit a page directly from the browser; they don't need to know Git and the details of your project to be able to contribute.
- Non-technical collaborators can easily edit a page directly from the browser;
they don't need to know Git and the details of your project to be able to contribute.
- Recently hired team members can quickly edit content.
- Temporary collaborators can jump from project to project and quickly edit pages instead of having to clone or fork every single project they need to submit changes to.
- Temporary collaborators can jump from project to project and quickly edit pages instead
of having to clone or fork every single project they need to submit changes to.
## Requirements
- In order use the Static Site Editor feature, your project needs to be
pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman).
- The editor needs to be logged into GitLab and needs to be a member of the
project (with Developer or higher permission levels).
pre-configured with the [Static Site Editor Middleman template](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman).
- You need to be logged into GitLab and be a member of the
project (with Developer or higher permission levels).
## How it works
The Static Site Editor is in an early stage of development and only works for
The Static Site Editor is in an early stage of development and only supports
Middleman sites for now. You have to use a specific site template to start
using it. The project template is configured to deploy a [Middleman](https://middlemanapp.com/)
static website with [GitLab Pages](../pages/index.md).
Once your website is up and running, you'll see a button **Edit this page** on
Once your website is up and running, an **Edit this page** button displays on
the bottom-left corner of its pages:
![Edit this page button](img/edit_this_page_button_v12_10.png)
@ -61,61 +53,66 @@ click of a button:
![Static Site Editor](img/wysiwyg_editor_v13_3.png)
You can also edit the page's front matter both in WYSIWYG mode via the side-drawer and in Markdown
mode.
![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png)
When an editor submits their changes, in the background, GitLab automatically
creates a new branch, commits their changes, and opens a merge request. The
editor lands directly on the merge request, and then they can assign it to
a colleague for review.
## Getting started
## Set up your project
First, set up the project. Once done, you can use the Static Site Editor to
easily edit your content.
easily [edit your content](#edit-content).
### Set up your project
1. To get started, create a new project from the
[Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman)
template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork)
or [create a new project from a template](../../../gitlab-basics/create-project.md#built-in-templates).
1. Edit the `data/config.yml` file adding your project's path.
1. Editing the file triggers a CI/CD pipeline to deploy your project with GitLab Pages.
1. To get started, create a new project from the [Static Site Editor - Middleman](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman)
template. You can either [fork it](../repository/forking_workflow.md#creating-a-fork)
or [create a new project from a template](../../../gitlab-basics/create-project.md#built-in-templates).
1. Edit the [`data/config.yml`](#configuration-files) configuration file
to replace `<username>` and `<project-name>` with the proper values for
your project's path. This triggers a CI/CD pipeline to deploy your project
with GitLab Pages.
1. When the pipeline finishes, from your project's left-side menu, go to **Settings > Pages** to find the URL of your new website.
1. Visit your website and look at the bottom-left corner of the screen to see the new **Edit this page** button.
Anyone satisfying the [requirements](#requirements) will be able to edit the
Anyone satisfying the [requirements](#requirements) can edit the
content of the pages without prior knowledge of Git or of your site's
codebase.
NOTE: **Note:**
From GitLab 13.1 onward, the YAML front matter of Markdown files is hidden on the
WYSIWYG editor to avoid unintended changes. To edit it, use the Markdown editing mode, the regular
GitLab file editor, or the Web IDE.
## Edit content
NOTE: **Note:**
A new configuration file for the Static Site Editor was [introduced](https://gitlab.com/groups/gitlab-org/-/epics/4267)
in GitLab 13.4. Beginning in 13.5, the `.gitlab/static-site-editor.yml` file will store additional
configuration options for the editor. When the functionality of the existing `data/config.yml` file
is replicated in the new configuration file, `data/config.yml` will be formally deprecated.
> - Support for modifying the default merge request title and description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216861) in GitLab 13.5.
### Use the Static Site Editor to edit your content
After setting up your project, you can start editing content directly from the Static Site Editor.
For instance, suppose you are a recently hired technical writer at a large
company and a new feature has been added to the company product.
To edit a file:
1. You are assigned the task of updating the documentation.
1. You visit a page and see content that needs to be edited.
1. Click the **Edit this page** button on the production site.
1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you wish to edit the raw Markdown
instead, you can toggle the **Markdown** mode in the bottom-right corner.
1. You edit the file right there and click **Submit changes**.
1. A new merge request is automatically created and you assign it to your colleague for review.
1. Visit the page you want to edit.
1. Click the **Edit this page** button.
1. The file is opened in the Static Site Editor in **WYSIWYG** mode. If you
wish to edit the raw Markdown instead, you can toggle the **Markdown** mode
in the bottom-right corner.
1. When you're done, click **Submit changes...**.
1. (Optional) Adjust the default title and description of the merge request that will be submitted with your changes.
1. Click **Submit changes**.
1. A new merge request is automatically created and you can assign a colleague for review.
## Videos
### Text
> Support for `*.md.erb` files [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223171) in GitLab 13.2.
The Static Site Editors supports Markdown files (`.md`, `.md.erb`) for editing text.
### Images
> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1.
You can add image files on the WYSIWYG mode by clicking the image icon (**{doc-image}**).
From there, link to a URL, add optional [ALT text](https://moz.com/learn/seo/alt-text),
and you're done. The link can reference images already hosted in your project, an asset hosted
externally on a content delivery network, or any other external URL. The editor renders thumbnail previews
so you can verify the correct image is included and there aren't any references to missing images.
default directory (`source/images/`).
### Videos
> - Support for embedding YouTube videos through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216642) in GitLab 13.5.
@ -126,6 +123,63 @@ The following URL/ID formats are supported:
- YouTube embed URL (e.g. `https://www.youtube.com/embed/0t1DgySidms`)
- YouTube video ID (e.g. `0t1DgySidms`)
### Front matter
> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1.
> - Ability to edit page front matter [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/235921) in GitLab 13.5.
Front matter is a flexible and convenient way to define page-specific variables in data files
intended to be parsed by a static site generator. It is commonly used for setting a page's
title, layout template, or author, but can be used to pass any kind of metadata to the
generator as the page renders out to HTML. Included at the very top of each data file, the
front matter is often formatted as YAML or JSON and requires consistent and accurate syntax.
To edit the front matter from the Static Site Editor you can use the GitLab's regular file editor,
the Web IDE, or easily update the data directly from the WYSIWYG editor:
1. Click the **Page settings** button on the bottom-right to reveal a web form with the data you
have on the page's front matter. The form is populated with the current data:
![Editing page front matter in the Static Site Editor](img/front_matter_ui_v13_4.png)
1. Update the values as you wish and close the panel.
1. When you're done, click **Submit changes...**.
1. Describe your changes (add a commit message).
1. Click **Submit changes**.
1. Click **View merge request** and GitLab will take you there.
Note that support for adding new attributes to the page's front matter from the form is not supported
yet. You can do so by editing the file locally, through the GitLab regular file editor, or through the Web IDE. Once added, the form will load the new fields.
## Configuration files
The Static Site Editor uses Middleman's configuration file, `data/config.yml`
to customize the behavior of the project itself and to control the **Edit this
page** button, rendered through the file [`layout.erb`](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/-/blob/master/source/layouts/layout.erb).
To [configure the project template to your own project](#set-up-your-project),
you must replace the `<username>` and `<project-name>` in the `data/config.yml`
file with the proper values for your project's path.
[Other Static Site Generators](#using-other-static-site-generators) used with
the Static Site Editor may use different configuration files or approaches.
## Using Other Static Site Generators
Although Middleman is the only Static Site Generator currently officially supported
by the Static Site Editor, you can configure your project's build and deployment
to use a different Static Site Generator. In this case, use the Middleman layout
as an example, and follow a similar approach to properly render an **Edit this page**
button in your Static Site Generator's layout.
## Upgrade from GitLab 12.10 to 13.0
In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
to the URL structure of the Static Site Editor. Follow the instructions in this
[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
to update your project with the 13.0 changes.
## Limitations
- The Static Site Editor still cannot be quickly added to existing Middleman sites. Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates.
- The Static Site Editor still cannot be quickly added to existing Middleman sites.
Follow this [epic](https://gitlab.com/groups/gitlab-org/-/epics/2784) for updates.

View File

@ -19,6 +19,9 @@ You can create Wiki pages in the web interface or
[locally using Git](#adding-and-editing-wiki-pages-locally) since every Wiki is
a separate Git repository.
[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13195) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5,
**group wikis** became available. Their usage is similar to project wikis, with a few [limitations](../../group/index.md#group-wikis).
## First time creating the Home page
The first time you visit a Wiki, you will be directed to create the Home page.

View File

@ -19,6 +19,7 @@ module API
end
use AdminModeMiddleware
use ResponseCoercerMiddleware
helpers HelperMethods
@ -188,6 +189,44 @@ module API
end
end
# Prior to Rack v2.1.x, returning a body of [nil] or [201] worked
# because the body was coerced to a string. However, this no longer
# works in Rack v2.1.0+. The Rack spec
# (https://github.com/rack/rack/blob/master/SPEC.rdoc#the-body-)
# says:
#
# The Body must respond to `each` and must only yield String values
#
# Because it's easy to return the wrong body type, this middleware
# will:
#
# 1. Inspect each element of the body if it is an Array.
# 2. Coerce each value to a string if necessary.
# 3. Flag a test and development error.
class ResponseCoercerMiddleware < ::Grape::Middleware::Base
def call(env)
response = super(env)
status = response[0]
body = response[2]
return response if Rack::Utils::STATUS_WITH_NO_ENTITY_BODY[status]
return response unless body.is_a?(Array)
body.map! do |part|
if part.is_a?(String)
part
else
err = ArgumentError.new("The response body should be a String, but it is of type #{part.class}")
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(err)
part.to_s
end
end
response
end
end
class AdminModeMiddleware < ::Grape::Middleware::Base
def after
# Use a Grape middleware since the Grape `after` blocks might run

View File

@ -72,6 +72,7 @@ module API
post '/verify' do
authenticate_runner!
status 200
body "200"
end
end
@ -183,6 +184,7 @@ module API
service.execute.then do |result|
header 'X-GitLab-Trace-Update-Interval', result.backoff
status result.status
body result.status.to_s
end
end
@ -293,6 +295,7 @@ module API
if result[:status] == :success
status :created
body "201"
else
render_api_error!(result[:message], result[:http_status])
end

View File

@ -522,7 +522,7 @@ module API
else
header(*Gitlab::Workhorse.send_url(file.url))
status :ok
body
body ""
end
end

View File

@ -44,7 +44,7 @@ module API
workhorse_headers = Gitlab::Workhorse.send_url(file.url)
header workhorse_headers[0], workhorse_headers[1]
env['api.format'] = :binary
body nil
body ""
end
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class ReplaceBlockedByLinks
class IssueLink < ActiveRecord::Base
self.table_name = 'issue_links'
end
def perform(start_id, stop_id)
blocked_by_links = IssueLink.where(id: start_id..stop_id).where(link_type: 2)
ActiveRecord::Base.transaction do
# if there is duplicit bi-directional relation (issue2 is blocked by issue1
# and issue1 already links issue2), then we can just delete 'blocked by'.
# This should be rare as we have a pre-create check which checks if issues are
# already linked
blocked_by_links
.joins('INNER JOIN issue_links as opposite_links ON issue_links.source_id = opposite_links.target_id AND issue_links.target_id = opposite_links.source_id')
.where('opposite_links.link_type': 1)
.delete_all
blocked_by_links.update_all('source_id=target_id,target_id=source_id,link_type=1')
end
end
end
end
end

View File

@ -46,10 +46,6 @@ module Gitlab
Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false)
end
def self.child_of_child_pipeline_enabled?(project)
::Feature.enabled?(:ci_child_of_child_pipeline, project, default_enabled: true)
end
def self.trace_overwrite?
::Feature.enabled?(:ci_trace_overwrite, type: :ops, default_enabled: false)
end

View File

@ -16193,6 +16193,9 @@ msgstr ""
msgid "Members|Are you sure you want to leave \"%{source}\"?"
msgstr ""
msgid "Members|Are you sure you want to remove \"%{groupName}\"?"
msgstr ""
msgid "Members|Are you sure you want to remove %{usersName} from \"%{source}\""
msgstr ""
@ -16214,6 +16217,12 @@ msgstr ""
msgid "Members|No expiration set"
msgstr ""
msgid "Members|Remove \"%{groupName}\""
msgstr ""
msgid "Members|Remove group"
msgstr ""
msgid "Members|Role updated successfully."
msgstr ""

View File

@ -390,7 +390,7 @@ RSpec.describe Admin::UsersController do
describe 'POST update' do
context 'when the password has changed' do
def update_password(user, password = User.random_password, password_confirmation = password)
def update_password(user, password = User.random_password, password_confirmation = password, format = :html)
params = {
id: user.to_param,
user: {
@ -399,7 +399,7 @@ RSpec.describe Admin::UsersController do
}
}
post :update, params: params
post :update, params: params, format: format
end
context 'when admin changes their own password' do
@ -498,6 +498,23 @@ RSpec.describe Admin::UsersController do
.not_to change { user.reload.encrypted_password }
end
end
context 'when the update fails' do
let(:password) { User.random_password }
before do
expect_next_instance_of(Users::UpdateService) do |service|
allow(service).to receive(:execute).and_return({ message: 'failed', status: :error })
end
end
it 'returns a 500 error' do
expect { update_password(admin, password, password, :json) }
.not_to change { admin.reload.password_expired? }
expect(response).to have_gitlab_http_status(:error)
end
end
end
context 'admin notes' do

View File

@ -118,7 +118,7 @@ RSpec.describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
find('.js-modal-primary-action').click
click_button 'Stop pipeline'
wait_for_requests
end
@ -407,7 +407,7 @@ RSpec.describe 'Pipelines', :js do
context 'when canceling' do
before do
find('.js-pipelines-cancel-button').click
find('.js-modal-primary-action').click
click_button 'Stop pipeline'
end
it 'indicates that pipeline was canceled', :sidekiq_might_not_need_inline do

View File

@ -1,10 +1,25 @@
import $ from 'jquery';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import EditorLite from '~/blob_edit/edit_blob';
jest.mock('~/blob_edit/edit_blob');
describe('BlobBundle', () => {
it('does not load EditorLite by default', () => {
blobBundle();
expect(EditorLite).not.toHaveBeenCalled();
});
it('loads EditorLite for the edit screen', async () => {
setFixtures(`<div class="js-edit-blob-form"></div>`);
blobBundle();
await waitForPromises();
expect(EditorLite).toHaveBeenCalled();
});
describe('No Suggest Popover', () => {
beforeEach(() => {
setFixtures(`

View File

@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlForm, GlSprintf } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
import { mockRefs, mockParams, mockPostParams, mockProjectId, mockError } from '../mock_data';
@ -11,7 +12,8 @@ jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
}));
const pipelinesPath = '/root/project/-/pipleines';
const pipelinesPath = '/root/project/-/pipelines';
const configVariablesPath = '/root/project/-/pipelines/config_variables';
const postResponse = { id: 1 };
describe('Pipeline New Form', () => {
@ -28,6 +30,7 @@ describe('Pipeline New Form', () => {
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
const findValueInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-value"]');
const findErrorAlert = () => wrapper.find('[data-testid="run-pipeline-error-alert"]');
const findWarningAlert = () => wrapper.find('[data-testid="run-pipeline-warning-alert"]');
const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf);
@ -39,6 +42,7 @@ describe('Pipeline New Form', () => {
propsData: {
projectId: mockProjectId,
pipelinesPath,
configVariablesPath,
refs: mockRefs,
defaultBranch: 'master',
settingsLink: '',
@ -55,6 +59,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {});
});
afterEach(() => {
@ -66,7 +71,7 @@ describe('Pipeline New Form', () => {
describe('Dropdown with branches and tags', () => {
beforeEach(() => {
mock.onPost(pipelinesPath).reply(200, postResponse);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse);
});
it('displays dropdown with all branches and tags', () => {
@ -87,19 +92,29 @@ describe('Pipeline New Form', () => {
});
describe('Form', () => {
beforeEach(() => {
beforeEach(async () => {
createComponent('', mockParams, mount);
mock.onPost(pipelinesPath).reply(200, postResponse);
mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse);
await waitForPromises();
});
it('displays the correct values for the provided query params', async () => {
expect(findDropdown().props('text')).toBe('tag-1');
await wrapper.vm.$nextTick();
expect(findVariableRows()).toHaveLength(3);
});
it('displays a variable from provided query params', () => {
expect(findKeyInputs().at(0).element.value).toBe('test_var');
expect(findValueInputs().at(0).element.value).toBe('test_var_val');
});
it('displays an empty variable for the user to fill out', async () => {
expect(findKeyInputs().at(2).element.value).toBe('');
expect(findValueInputs().at(2).element.value).toBe('');
});
it('does not display remove icon for last row', () => {
expect(findRemoveIcons()).toHaveLength(2);
});
@ -124,13 +139,143 @@ describe('Pipeline New Form', () => {
});
it('creates blank variable on input change event', async () => {
findKeyInputs()
.at(2)
.trigger('change');
const input = findKeyInputs().at(2);
input.element.value = 'test_var_2';
input.trigger('change');
await wrapper.vm.$nextTick();
expect(findVariableRows()).toHaveLength(4);
expect(findKeyInputs().at(3).element.value).toBe('');
expect(findValueInputs().at(3).element.value).toBe('');
});
describe('when the form has been modified', () => {
const selectRef = i =>
findDropdownItems()
.at(i)
.vm.$emit('click');
beforeEach(async () => {
const input = findKeyInputs().at(0);
input.element.value = 'test_var_2';
input.trigger('change');
findRemoveIcons()
.at(1)
.trigger('click');
await wrapper.vm.$nextTick();
});
it('form values are restored when the ref changes', async () => {
expect(findVariableRows()).toHaveLength(2);
selectRef(1);
await waitForPromises();
expect(findVariableRows()).toHaveLength(3);
expect(findKeyInputs().at(0).element.value).toBe('test_var');
});
it('form values are restored again when the ref is reverted', async () => {
selectRef(1);
await waitForPromises();
selectRef(2);
await waitForPromises();
expect(findVariableRows()).toHaveLength(2);
expect(findKeyInputs().at(0).element.value).toBe('test_var_2');
});
});
});
describe('when feature flag new_pipeline_form_prefilled_vars is enabled', () => {
let origGon;
const mockYmlKey = 'yml_var';
const mockYmlValue = 'yml_var_val';
const mockYmlDesc = 'A var from yml.';
beforeAll(() => {
origGon = window.gon;
window.gon = { features: { newPipelineFormPrefilledVars: true } };
});
afterAll(() => {
window.gon = origGon;
});
describe('when yml defines a variable with description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlValue,
description: mockYmlDesc,
},
});
await waitForPromises();
});
it('displays all the variables', async () => {
expect(findVariableRows()).toHaveLength(4);
});
it('displays a variable from yml', () => {
expect(findKeyInputs().at(0).element.value).toBe(mockYmlKey);
expect(findValueInputs().at(0).element.value).toBe(mockYmlValue);
});
it('displays a variable from provided query params', () => {
expect(findKeyInputs().at(1).element.value).toBe('test_var');
expect(findValueInputs().at(1).element.value).toBe('test_var_val');
});
it('adds a description to the first variable from yml', () => {
expect(
findVariableRows()
.at(0)
.text(),
).toContain(mockYmlDesc);
});
it('removes the description when a variable key changes', async () => {
findKeyInputs().at(0).element.value = 'yml_var_modified';
findKeyInputs()
.at(0)
.trigger('change');
await wrapper.vm.$nextTick();
expect(
findVariableRows()
.at(0)
.text(),
).not.toContain(mockYmlDesc);
});
});
describe('when yml defines a variable without description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
[mockYmlKey]: {
value: mockYmlValue,
description: null,
},
});
await waitForPromises();
});
it('displays all the variables', async () => {
expect(findVariableRows()).toHaveLength(3);
});
});
});
@ -138,7 +283,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
createComponent();
mock.onPost(pipelinesPath).reply(400, mockError);
mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
findForm().vm.$emit('submit', dummySubmitEvent);

View File

@ -35,9 +35,7 @@ describe('MrWidgetAuthorTime', () => {
});
it('renders provided time', () => {
expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toEqual(
'2017-03-23T23:02:00.807Z',
);
expect(vm.$el.querySelector('time').getAttribute('title')).toEqual('2017-03-23T23:02:00.807Z');
expect(vm.$el.querySelector('time').textContent.trim()).toEqual('12 hours ago');
});

View File

@ -212,8 +212,6 @@ describe('MRWidgetMerged', () => {
});
it('should use mergedEvent mergedAt as tooltip title', () => {
expect(vm.$el.querySelector('time').getAttribute('data-original-title')).toBe(
'Jan 24, 2018 1:02pm GMT+0000',
);
expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm GMT+0000');
});
});

View File

@ -0,0 +1,64 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlButton } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RemoveGroupLinkButton from '~/vue_shared/components/members/action_buttons/remove_group_link_button.vue';
import { group } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('RemoveGroupLinkButton', () => {
let wrapper;
const actions = {
showRemoveGroupLinkModal: jest.fn(),
};
const createStore = () => {
return new Vuex.Store({
actions,
});
};
const createComponent = () => {
wrapper = mount(RemoveGroupLinkButton, {
localVue,
store: createStore(),
propsData: {
groupLink: group,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
createComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays a tooltip', () => {
const button = findButton();
expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
expect(button.attributes('title')).toBe('Remove group');
});
it('sets `aria-label` attribute', () => {
expect(findButton().attributes('aria-label')).toBe('Remove group');
});
it('calls Vuex action to open remove group link modal when clicked', () => {
findButton().trigger('click');
expect(actions.showRemoveGroupLinkModal).toHaveBeenCalledWith(expect.any(Object), group);
});
});

View File

@ -0,0 +1,106 @@
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import { GlModal, GlForm } from '@gitlab/ui';
import { nextTick } from 'vue';
import { within } from '@testing-library/dom';
import Vuex from 'vuex';
import RemoveGroupLinkModal from '~/vue_shared/components/members/modals/remove_group_link_modal.vue';
import { REMOVE_GROUP_LINK_MODAL_ID } from '~/vue_shared/components/members/constants';
import { group } from '../mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
const localVue = createLocalVue();
localVue.use(Vuex);
describe('RemoveGroupLinkModal', () => {
let wrapper;
const actions = {
hideRemoveGroupLinkModal: jest.fn(),
};
const createStore = (state = {}) => {
return new Vuex.Store({
state: {
memberPath: '/groups/foo-bar/-/group_links/:id',
groupLinkToRemove: group,
removeGroupLinkModalVisible: true,
...state,
},
actions,
});
};
const createComponent = state => {
wrapper = mount(RemoveGroupLinkModal, {
localVue,
store: createStore(state),
attrs: {
static: true,
},
});
};
const findModal = () => wrapper.find(GlModal);
const findForm = () => findModal().find(GlForm);
const getByText = (text, options) =>
createWrapper(within(findModal().element).getByText(text, options));
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when modal is open', () => {
beforeEach(async () => {
createComponent();
await nextTick();
});
it('sets modal ID', () => {
expect(findModal().props('modalId')).toBe(REMOVE_GROUP_LINK_MODAL_ID);
});
it('displays modal title', () => {
expect(getByText(`Remove "${group.sharedWithGroup.fullName}"`).exists()).toBe(true);
});
it('displays modal body', () => {
expect(
getByText(`Are you sure you want to remove "${group.sharedWithGroup.fullName}"?`).exists(),
).toBe(true);
});
it('displays form with correct action and inputs', () => {
const form = findForm();
expect(form.attributes('action')).toBe(`/groups/foo-bar/-/group_links/${group.id}`);
expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
'mock-csrf-token',
);
});
it('submits the form when "Remove group" button is clicked', () => {
const submitSpy = jest.spyOn(findForm().element, 'submit');
getByText('Remove group').trigger('click');
expect(submitSpy).toHaveBeenCalled();
submitSpy.mockRestore();
});
it('calls `hideRemoveGroupLinkModal` action when modal is closed', () => {
getByText('Cancel').trigger('click');
expect(actions.hideRemoveGroupLinkModal).toHaveBeenCalled();
});
});
it('modal does not show when `removeGroupLinkModalVisible` is `false`', () => {
createComponent({ removeGroupLinkModalVisible: false });
expect(findModal().vm.$attrs.visible).toBe(false);
});
});

View File

@ -43,6 +43,7 @@ describe('MemberList', () => {
'created-at',
'member-action-buttons',
'role-dropdown',
'remove-group-link-modal',
],
});
};

View File

@ -11,15 +11,14 @@ describe('toggleSidebar', () => {
});
});
it('should render << when collapsed', () => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true);
it('should render the "chevron-double-lg-left" icon when collapsed', () => {
expect(vm.$el.querySelector('[data-testid="chevron-double-lg-left-icon"]')).not.toBeNull();
});
it('should render >> when collapsed', () => {
it('should render the "chevron-double-lg-right" icon when expanded', async () => {
vm.collapsed = false;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true);
});
await Vue.nextTick();
expect(vm.$el.querySelector('[data-testid="chevron-double-lg-right-icon"]')).not.toBeNull();
});
it('should emit toggle event when button clicked', () => {

View File

@ -1,11 +1,15 @@
import { noop } from 'lodash';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { members } from 'jest/vue_shared/components/members/mock_data';
import { members, group } from 'jest/vue_shared/components/members/mock_data';
import testAction from 'helpers/vuex_action_helper';
import httpStatusCodes from '~/lib/utils/http_status';
import * as types from '~/vuex_shared/modules/members/mutation_types';
import { updateMemberRole } from '~/vuex_shared/modules/members/actions';
import {
updateMemberRole,
showRemoveGroupLinkModal,
hideRemoveGroupLinkModal,
} from '~/vuex_shared/modules/members/actions';
describe('Vuex members actions', () => {
let mock;
@ -30,6 +34,8 @@ describe('Vuex members actions', () => {
members,
memberPath: '/groups/foo-bar/-/group_members/:id',
requestFormatter: noop,
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
};
describe('successful request', () => {
@ -73,4 +79,32 @@ describe('Vuex members actions', () => {
});
});
});
describe('Group Link Modal', () => {
const state = {
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
};
describe('showRemoveGroupLinkModal', () => {
it(`commits ${types.SHOW_REMOVE_GROUP_LINK_MODAL} mutation`, () => {
testAction(showRemoveGroupLinkModal, group, state, [
{
type: types.SHOW_REMOVE_GROUP_LINK_MODAL,
payload: group,
},
]);
});
});
describe('hideRemoveGroupLinkModal', () => {
it(`commits ${types.HIDE_REMOVE_GROUP_LINK_MODAL} mutation`, () => {
testAction(hideRemoveGroupLinkModal, group, state, [
{
type: types.HIDE_REMOVE_GROUP_LINK_MODAL,
},
]);
});
});
});
});

View File

@ -1,4 +1,4 @@
import { members } from 'jest/vue_shared/components/members/mock_data';
import { members, group } from 'jest/vue_shared/components/members/mock_data';
import mutations from '~/vuex_shared/modules/members/mutations';
import * as types from '~/vuex_shared/modules/members/mutation_types';
@ -59,4 +59,32 @@ describe('Vuex members mutations', () => {
expect(state.errorMessage).toBe('');
});
});
describe(types.SHOW_REMOVE_GROUP_LINK_MODAL, () => {
it('sets `removeGroupLinkModalVisible` and `groupLinkToRemove`', () => {
const state = {
removeGroupLinkModalVisible: false,
groupLinkToRemove: null,
};
mutations[types.SHOW_REMOVE_GROUP_LINK_MODAL](state, group);
expect(state).toEqual({
removeGroupLinkModalVisible: true,
groupLinkToRemove: group,
});
});
});
describe(types.HIDE_REMOVE_GROUP_LINK_MODAL, () => {
it('sets `removeGroupLinkModalVisible` to `false`', () => {
const state = {
removeGroupLinkModalVisible: false,
};
mutations[types.HIDE_REMOVE_GROUP_LINK_MODAL](state);
expect(state.removeGroupLinkModalVisible).toBe(false);
});
});
});

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnerPlatformsResolver do
include GraphqlHelpers
describe '#resolve' do
subject(:resolve_subject) { resolve(described_class) }
it 'returns all possible runner platforms' do
expect(resolve_subject).to include(
hash_including(name: :linux), hash_including(name: :osx),
hash_including(name: :windows), hash_including(name: :docker),
hash_including(name: :kubernetes)
)
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::RunnerArchitectureType do
specify { expect(described_class.graphql_name).to eq('RunnerArchitecture') }
it 'exposes the expected fields' do
expected_fields = %i[
name
download_location
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::RunnerPlatformType do
specify { expect(described_class.graphql_name).to eq('RunnerPlatform') }
it 'exposes the expected fields' do
expected_fields = %i[
name
human_readable_name
architectures
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -22,6 +22,7 @@ RSpec.describe GitlabSchema.types['Query'] do
users
issue
instance_statistics_measurements
runner_platforms
]
expect(described_class).to have_graphql_fields(*expected_fields).at_least
@ -67,8 +68,16 @@ RSpec.describe GitlabSchema.types['Query'] do
describe 'instance_statistics_measurements field' do
subject { described_class.fields['instanceStatisticsMeasurements'] }
it 'returns issue' do
it 'returns instance statistics measurements' do
is_expected.to have_graphql_type(Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type)
end
end
describe 'runner_platforms field' do
subject { described_class.fields['runnerPlatforms'] }
it 'returns runner platforms' do
is_expected.to have_graphql_type(Types::Ci::RunnerPlatformType.connection_type)
end
end
end

View File

@ -88,9 +88,14 @@ RSpec.describe Groups::GroupMembersHelper do
describe '#linked_groups_list_data_attributes' do
include_context 'group_group_link'
before do
allow(helper).to receive(:group_group_link_path).with(shared_group, ':id').and_return('/groups/foo-bar/-/group_links/:id')
end
it 'returns expected hash' do
expect(helper.linked_groups_list_data_attributes(shared_group)).to include({
members: helper.linked_groups_data_json(shared_group.shared_with_group_links),
member_path: '/groups/foo-bar/-/group_links/:id',
group_id: shared_group.id
})
end

View File

@ -0,0 +1,36 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::ReplaceBlockedByLinks, schema: 20201015073808 do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') }
let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') }
let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') }
let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') }
let(:issue_links) { table(:issue_links) }
let!(:blocks_link) { issue_links.create!(source_id: issue1.id, target_id: issue2.id, link_type: 1) }
let!(:bidirectional_link) { issue_links.create!(source_id: issue2.id, target_id: issue1.id, link_type: 2) }
let!(:blocked_link) { issue_links.create!(source_id: issue1.id, target_id: issue3.id, link_type: 2) }
subject { described_class.new.perform(issue_links.minimum(:id), issue_links.maximum(:id)) }
it 'deletes issue links where opposite relation already exists' do
expect { subject }.to change { issue_links.count }.by(-1)
end
it 'ignores issue links other than blocked_by' do
subject
expect(blocks_link.reload.link_type).to eq(1)
end
it 'updates blocked_by issue links' do
subject
link = blocked_link.reload
expect(link.link_type).to eq(1)
expect(link.source_id).to eq(issue3.id)
expect(link.target_id).to eq(issue1.id)
end
end

View File

@ -142,8 +142,8 @@ RSpec.describe Gitlab::Middleware::Go do
response = go
expect(response[0]).to eq(403)
expect(response[1]['Content-Length']).to eq('0')
expect(response[2].body).to eq([''])
expect(response[1]['Content-Length']).to be_nil
expect(response[2]).to eq([''])
end
end
end
@ -187,10 +187,11 @@ RSpec.describe Gitlab::Middleware::Go do
it 'returns 404' do
response = go
expect(response[0]).to eq(404)
expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><body>go get #{Gitlab.config.gitlab.url}/#{project.full_path}</body></html>}
expect(response[2].body).to eq([expected_body])
expect(response[2]).to eq([expected_body])
end
end
@ -262,7 +263,7 @@ RSpec.describe Gitlab::Middleware::Go do
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git #{repository_url}" /><meta name="go-source" content="#{Gitlab.config.gitlab.host}/#{path} #{project_url} #{project_url}/-/tree/#{branch}{/dir} #{project_url}/-/blob/#{branch}{/dir}/{file}#L{line}" /></head><body>go get #{Gitlab.config.gitlab.url}/#{path}</body></html>}
expect(response[2].body).to eq([expected_body])
expect(response[2]).to eq([expected_body])
end
end
end

View File

@ -60,12 +60,12 @@ RSpec.describe Gitlab::Middleware::SameSiteCookies do
end
context 'with no cookies' do
let(:cookies) { nil }
let(:cookies) { "" }
it 'does not add headers' do
response = do_request
expect(response['Set-Cookie']).to be_nil
expect(response['Set-Cookie']).to eq("")
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20201015073808_schedule_blocked_by_links_replacement')
RSpec.describe ScheduleBlockedByLinksReplacement do
let(:namespace) { table(:namespaces).create!(name: 'gitlab', path: 'gitlab-org') }
let(:project) { table(:projects).create!(namespace_id: namespace.id, name: 'gitlab') }
let(:issue1) { table(:issues).create!(project_id: project.id, title: 'a') }
let(:issue2) { table(:issues).create!(project_id: project.id, title: 'b') }
let(:issue3) { table(:issues).create!(project_id: project.id, title: 'c') }
let!(:issue_links) do
[
table(:issue_links).create!(source_id: issue1.id, target_id: issue2.id, link_type: 1),
table(:issue_links).create!(source_id: issue2.id, target_id: issue1.id, link_type: 2),
table(:issue_links).create!(source_id: issue1.id, target_id: issue3.id, link_type: 2)
]
end
before do
stub_const("#{described_class.name}::BATCH_SIZE", 1)
end
it 'schedules jobs for blocked_by links' do
Sidekiq::Testing.fake! do
freeze_time do
migrate!
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
2.minutes, issue_links[1].id, issue_links[1].id)
expect(described_class::MIGRATION).to be_scheduled_delayed_migration(
4.minutes, issue_links[2].id, issue_links[2].id)
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
end
end
end
end

View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::APIGuard::ResponseCoercerMiddleware do
using RSpec::Parameterized::TableSyntax
it 'is loaded' do
expect(API::API.middleware).to include([:use, described_class])
end
describe '#call' do
let(:app) do
Class.new(API::API)
end
[
nil, 201, 10.5, "test"
].each do |val|
it 'returns a String body' do
app.get 'bodytest' do
status 200
env['api.format'] = :binary
body val
end
unless val.is_a?(String)
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).with(instance_of(ArgumentError))
end
get api('/bodytest')
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq(val.to_s)
end
end
[100, 204, 304].each do |status|
it 'allows nil body' do
app.get 'statustest' do
status status
env['api.format'] = :binary
body nil
end
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
get api('/statustest')
expect(response.status).to eq(status)
expect(response.body).to eq('')
end
end
end
end

View File

@ -325,20 +325,6 @@ RSpec.describe Ci::CreateDownstreamPipelineService, '#execute' do
expect(bridge.reload).to be_success
end
context 'when FF ci_child_of_child_pipeline is disabled' do
before do
stub_feature_flags(ci_child_of_child_pipeline: false)
end
it 'does not create a further child pipeline' do
expect { service.execute(bridge) }
.not_to change { Ci::Pipeline.count }
expect(bridge.reload).to be_failed
expect(bridge.failure_reason).to eq 'bridge_pipeline_is_child_pipeline'
end
end
end
context 'when upstream pipeline has a parent pipeline, which has a parent pipeline' do

View File

@ -20,10 +20,10 @@ RSpec.describe BuildFinishedWorker do
expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform)
expect_any_instance_of(BuildCoverageWorker).to receive(:perform)
expect(BuildHooksWorker).to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_async)
expect(ExpirePipelineCacheWorker).to receive(:perform_async)
expect(ChatNotificationWorker).not_to receive(:perform_async)
expect(Ci::BuildReportResultWorker).not_to receive(:perform)
expect(ArchiveTraceWorker).to receive(:perform_in)
subject
end