Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3775eba7c1
commit
3940f59a61
2
Gemfile
2
Gemfile
|
@ -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'
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
|
|
|
@ -14,4 +14,6 @@ export default ({
|
|||
requestFormatter,
|
||||
showError: false,
|
||||
errorMessage: '',
|
||||
removeGroupLinkModalVisible: false,
|
||||
groupLinkToRemove: null,
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -26,5 +26,3 @@ module Mutations
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Mutations::Issues::CommonMutationArguments.prepend_if_ee('::EE::Mutations::Issues::CommonMutationArguments')
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Populate blocking issues count
|
||||
merge_request: 45176
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate blocked_by issue links to blocks type by swapping source and target
|
||||
merge_request: 45262
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update to Rack v2.1.4
|
||||
merge_request: 45340
|
||||
author:
|
||||
type: fixed
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
aed103bb25b70eb8f6387d84225a8e51672a83c4586ccc65da3011ef010da4b1
|
|
@ -0,0 +1 @@
|
|||
e44ab2a7b3014b44d7d84de1f7e618d2fc89f98b8d59f5f6fa331544e206355f
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -522,7 +522,7 @@ module API
|
|||
else
|
||||
header(*Gitlab::Workhorse.send_url(file.url))
|
||||
status :ok
|
||||
body
|
||||
body ""
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(`
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -43,6 +43,7 @@ describe('MemberList', () => {
|
|||
'created-at',
|
||||
'member-action-buttons',
|
||||
'role-dropdown',
|
||||
'remove-group-link-modal',
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue