Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a268b09416
commit
f3db01da50
62 changed files with 741 additions and 473 deletions
|
@ -4,12 +4,15 @@ import {
|
|||
GlButton,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlModalDirective,
|
||||
GlTable,
|
||||
GlTooltipDirective,
|
||||
GlSprintf,
|
||||
} from '@gitlab/ui';
|
||||
import { s__, __ } from '~/locale';
|
||||
import Tracking from '~/tracking';
|
||||
import { trackAlertIntegrationsViewsOptions } from '../constants';
|
||||
import { trackAlertIntegrationsViewsOptions, integrationToDeleteDefault } from '../constants';
|
||||
|
||||
export const i18n = {
|
||||
title: s__('AlertsIntegrations|Current integrations'),
|
||||
|
@ -36,10 +39,13 @@ export default {
|
|||
GlButton,
|
||||
GlIcon,
|
||||
GlLoadingIcon,
|
||||
GlModal,
|
||||
GlTable,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
GlModal: GlModalDirective,
|
||||
},
|
||||
props: {
|
||||
integrations: {
|
||||
|
@ -71,6 +77,11 @@ export default {
|
|||
label: __('Actions'),
|
||||
},
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
integrationToDelete: integrationToDeleteDefault,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
tbodyTrClass() {
|
||||
return {
|
||||
|
@ -86,6 +97,14 @@ export default {
|
|||
const { category, action } = trackAlertIntegrationsViewsOptions;
|
||||
Tracking.event(category, action);
|
||||
},
|
||||
intergrationToDelete({ name, id }) {
|
||||
this.integrationToDelete.id = id;
|
||||
this.integrationToDelete.name = name;
|
||||
},
|
||||
deleteIntergration() {
|
||||
this.$emit('delete-integration', { id: this.integrationToDelete.id });
|
||||
this.integrationToDelete = { ...integrationToDeleteDefault };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -127,7 +146,11 @@ export default {
|
|||
<template #cell(actions)="{ item }">
|
||||
<gl-button-group>
|
||||
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
|
||||
<gl-button icon="remove" @click="$emit('delete-integration', { id: item.id })" />
|
||||
<gl-button
|
||||
v-gl-modal.deleteIntegration
|
||||
icon="remove"
|
||||
@click="intergrationToDelete(item)"
|
||||
/>
|
||||
</gl-button-group>
|
||||
</template>
|
||||
|
||||
|
@ -143,5 +166,22 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
</gl-table>
|
||||
<gl-modal
|
||||
modal-id="deleteIntegration"
|
||||
:title="__('Are you sure?')"
|
||||
:ok-title="s__('AlertSettings|Delete integration')"
|
||||
ok-variant="danger"
|
||||
@ok="deleteIntergration"
|
||||
>
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #integrationName>{{ integrationToDelete.name }}</template>
|
||||
</gl-sprintf>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -22,14 +22,12 @@ import {
|
|||
JSON_VALIDATE_DELAY,
|
||||
targetPrometheusUrlPlaceholder,
|
||||
typeSet,
|
||||
defaultFormState,
|
||||
} from '../constants';
|
||||
|
||||
export default {
|
||||
targetPrometheusUrlPlaceholder,
|
||||
JSON_VALIDATE_DELAY,
|
||||
typeSet,
|
||||
defaultFormState,
|
||||
i18n: {
|
||||
integrationFormSteps: {
|
||||
step1: {
|
||||
|
@ -113,14 +111,18 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
selectedIntegration: integrationTypesNew[0].value,
|
||||
active: false,
|
||||
options: integrationTypesNew,
|
||||
active: false,
|
||||
formVisible: false,
|
||||
integrationTestPayload: {
|
||||
json: null,
|
||||
error: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
jsonIsValid() {
|
||||
return this.integrationForm.integrationTestPayload.error === null;
|
||||
return this.integrationTestPayload.error === null;
|
||||
},
|
||||
selectedIntegrationType() {
|
||||
switch (this.selectedIntegration) {
|
||||
|
@ -129,43 +131,42 @@ export default {
|
|||
case this.$options.typeSet.prometheus:
|
||||
return this.prometheus;
|
||||
default:
|
||||
return this.defaultFormState;
|
||||
return {};
|
||||
}
|
||||
},
|
||||
integrationForm() {
|
||||
return {
|
||||
name: this.currentIntegration?.name || '',
|
||||
integrationTestPayload: {
|
||||
json: null,
|
||||
error: null,
|
||||
},
|
||||
active: this.currentIntegration?.active || false,
|
||||
token: this.currentIntegration?.token || '',
|
||||
url: this.currentIntegration?.url || '',
|
||||
token: this.currentIntegration?.token || this.selectedIntegrationType.token,
|
||||
url: this.currentIntegration?.url || this.selectedIntegrationType.url,
|
||||
apiUrl: this.currentIntegration?.apiUrl || '',
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentIntegration(val) {
|
||||
if (val === null) {
|
||||
return this.reset();
|
||||
}
|
||||
this.selectedIntegration = val.type;
|
||||
this.active = val.active;
|
||||
this.onIntegrationTypeSelect();
|
||||
return this.integrationTypeSelect();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onIntegrationTypeSelect() {
|
||||
integrationTypeSelect() {
|
||||
if (this.selectedIntegration === integrationTypesNew[0].value) {
|
||||
this.formVisible = false;
|
||||
} else {
|
||||
this.formVisible = true;
|
||||
}
|
||||
},
|
||||
onSubmitWithTestPayload() {
|
||||
submitWithTestPayload() {
|
||||
// TODO: Test payload before saving via GraphQL
|
||||
this.onSubmit();
|
||||
this.submit();
|
||||
},
|
||||
onSubmit() {
|
||||
submit() {
|
||||
const { name, apiUrl } = this.integrationForm;
|
||||
const variables =
|
||||
this.selectedIntegration === this.$options.typeSet.http
|
||||
|
@ -179,27 +180,45 @@ export default {
|
|||
|
||||
return this.$emit('create-new-integration', integrationPayload);
|
||||
},
|
||||
onReset() {
|
||||
this.integrationForm = this.defaultFormState;
|
||||
reset() {
|
||||
this.selectedIntegration = integrationTypesNew[0].value;
|
||||
this.onIntegrationTypeSelect();
|
||||
this.integrationTypeSelect();
|
||||
|
||||
if (this.currentIntegration) {
|
||||
return this.$emit('clear-current-integration');
|
||||
}
|
||||
|
||||
return this.resetFormValues();
|
||||
},
|
||||
onResetAuthKey() {
|
||||
resetFormValues() {
|
||||
this.integrationForm.name = '';
|
||||
this.integrationForm.apiUrl = '';
|
||||
this.integrationTestPayload = {
|
||||
json: null,
|
||||
error: null,
|
||||
};
|
||||
this.active = false;
|
||||
},
|
||||
resetAuthKey() {
|
||||
if (!this.currentIntegration) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$emit('reset-token', {
|
||||
type: this.selectedIntegration,
|
||||
variables: { id: this.currentIntegration.id },
|
||||
});
|
||||
},
|
||||
validateJson() {
|
||||
this.integrationForm.integrationTestPayload.error = null;
|
||||
if (this.integrationForm.integrationTestPayload.json === '') {
|
||||
this.integrationTestPayload.error = null;
|
||||
if (this.integrationTestPayload.json === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
JSON.parse(this.integrationForm.integrationTestPayload.json);
|
||||
JSON.parse(this.integrationTestPayload.json);
|
||||
} catch (e) {
|
||||
this.integrationForm.integrationTestPayload.error = JSON.stringify(e.message);
|
||||
this.integrationTestPayload.error = JSON.stringify(e.message);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -207,7 +226,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form class="gl-mt-6" @submit.prevent="onSubmit" @reset.prevent="onReset">
|
||||
<gl-form class="gl-mt-6" @submit.prevent="submit" @reset.prevent="reset">
|
||||
<h5 class="gl-font-lg gl-my-5">{{ s__('AlertSettings|Add new integrations') }}</h5>
|
||||
|
||||
<gl-form-group
|
||||
|
@ -217,8 +236,9 @@ export default {
|
|||
>
|
||||
<gl-form-select
|
||||
v-model="selectedIntegration"
|
||||
:disabled="currentIntegration !== null"
|
||||
:options="options"
|
||||
@change="onIntegrationTypeSelect"
|
||||
@change="integrationTypeSelect"
|
||||
/>
|
||||
|
||||
<alert-settings-form-help-block
|
||||
|
@ -279,7 +299,11 @@ export default {
|
|||
|
||||
<gl-form-input-group id="url" readonly :value="integrationForm.url">
|
||||
<template #append>
|
||||
<clipboard-button :text="integrationForm.url" :title="__('Copy')" class="gl-m-0!" />
|
||||
<clipboard-button
|
||||
:text="integrationForm.url || ''"
|
||||
:title="__('Copy')"
|
||||
class="gl-m-0!"
|
||||
/>
|
||||
</template>
|
||||
</gl-form-input-group>
|
||||
</div>
|
||||
|
@ -296,7 +320,11 @@ export default {
|
|||
:value="integrationForm.token"
|
||||
>
|
||||
<template #append>
|
||||
<clipboard-button :text="integrationForm.token" :title="__('Copy')" class="gl-m-0!" />
|
||||
<clipboard-button
|
||||
:text="integrationForm.token || ''"
|
||||
:title="__('Copy')"
|
||||
class="gl-m-0!"
|
||||
/>
|
||||
</template>
|
||||
</gl-form-input-group>
|
||||
|
||||
|
@ -308,7 +336,7 @@ export default {
|
|||
:title="$options.i18n.integrationFormSteps.step3.reset"
|
||||
:ok-title="$options.i18n.integrationFormSteps.step3.reset"
|
||||
ok-variant="danger"
|
||||
@ok="onResetAuthKey"
|
||||
@ok="resetAuthKey"
|
||||
>
|
||||
{{ $options.i18n.integrationFormSteps.restKeyInfo.label }}
|
||||
</gl-modal>
|
||||
|
@ -318,7 +346,7 @@ export default {
|
|||
id="test-integration"
|
||||
:label="$options.i18n.integrationFormSteps.step4.label"
|
||||
label-for="test-integration"
|
||||
:invalid-feedback="integrationForm.integrationTestPayload.error"
|
||||
:invalid-feedback="integrationTestPayload.error"
|
||||
>
|
||||
<alert-settings-form-help-block
|
||||
:message="$options.i18n.integrationFormSteps.step4.help"
|
||||
|
@ -327,8 +355,8 @@ export default {
|
|||
|
||||
<gl-form-textarea
|
||||
id="test-integration"
|
||||
v-model.trim="integrationForm.integrationTestPayload.json"
|
||||
:disabled="!integrationForm.active"
|
||||
v-model.trim="integrationTestPayload.json"
|
||||
:disabled="!active"
|
||||
:state="jsonIsValid"
|
||||
:placeholder="$options.i18n.integrationFormSteps.step4.placeholder"
|
||||
class="gl-my-4"
|
||||
|
@ -354,7 +382,7 @@ export default {
|
|||
category="secondary"
|
||||
variant="success"
|
||||
class="gl-mr-1 js-no-auto-disable"
|
||||
@click="onSubmitWithTestPayload"
|
||||
@click="submitWithTestPayload"
|
||||
>{{ s__('AlertSettings|Save and test payload') }}</gl-button
|
||||
>
|
||||
<gl-button
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script>
|
||||
import produce from 'immer';
|
||||
import { s__ } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
|
@ -9,12 +8,17 @@ import createHttpIntegrationMutation from '../graphql/mutations/create_http_inte
|
|||
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
|
||||
import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql';
|
||||
import updatePrometheusIntegrationMutation from '../graphql/mutations/update_prometheus_integration.mutation.graphql';
|
||||
import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql';
|
||||
import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql';
|
||||
import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql';
|
||||
import IntegrationsList from './alerts_integrations_list.vue';
|
||||
import SettingsFormOld from './alerts_settings_form_old.vue';
|
||||
import SettingsFormNew from './alerts_settings_form_new.vue';
|
||||
import { typeSet } from '../constants';
|
||||
import {
|
||||
updateStoreAfterIntegrationDelete,
|
||||
updateStoreAfterIntegrationAdd,
|
||||
} from '../utils/cache_updates';
|
||||
|
||||
export default {
|
||||
typeSet,
|
||||
|
@ -22,6 +26,7 @@ export default {
|
|||
changesSaved: s__(
|
||||
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
|
||||
),
|
||||
integrationRemoved: s__('AlertsIntegrations|The integration has been successfully removed.'),
|
||||
},
|
||||
components: {
|
||||
IntegrationsList,
|
||||
|
@ -89,6 +94,8 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
createNewIntegration({ type, variables }) {
|
||||
const { projectPath } = this;
|
||||
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
|
@ -98,9 +105,11 @@ export default {
|
|||
: createPrometheusIntegrationMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
projectPath: this.projectPath,
|
||||
projectPath,
|
||||
},
|
||||
update(store, { data }) {
|
||||
updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
|
||||
},
|
||||
update: this.updateIntegrations,
|
||||
})
|
||||
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
|
||||
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
|
||||
|
@ -119,41 +128,6 @@ export default {
|
|||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
updateIntegrations(
|
||||
store,
|
||||
{
|
||||
data: { httpIntegrationCreate, prometheusIntegrationCreate },
|
||||
},
|
||||
) {
|
||||
const integration =
|
||||
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = store.readQuery({
|
||||
query: getIntegrationsQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
},
|
||||
});
|
||||
|
||||
const data = produce(sourceData, draftData => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
draftData.project.alertManagementIntegrations.nodes = [
|
||||
integration,
|
||||
...draftData.project.alertManagementIntegrations.nodes,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query: getIntegrationsQuery,
|
||||
variables: {
|
||||
projectPath: this.projectPath,
|
||||
},
|
||||
data,
|
||||
});
|
||||
},
|
||||
updateIntegration({ type, variables }) {
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
|
@ -201,6 +175,12 @@ export default {
|
|||
if (error) {
|
||||
return createFlash({ message: error });
|
||||
}
|
||||
|
||||
const integration =
|
||||
httpIntegrationResetToken?.integration ||
|
||||
prometheusIntegrationResetToken?.integration;
|
||||
this.currentIntegration = integration;
|
||||
|
||||
return createFlash({
|
||||
message: this.$options.i18n.changesSaved,
|
||||
type: FLASH_TYPES.SUCCESS,
|
||||
|
@ -217,8 +197,41 @@ export default {
|
|||
editIntegration({ id }) {
|
||||
this.currentIntegration = this.integrations.list.find(integration => integration.id === id);
|
||||
},
|
||||
deleteIntegration() {
|
||||
// TODO, handle delete via GraphQL
|
||||
deleteIntegration({ id }) {
|
||||
const { projectPath } = this;
|
||||
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: destroyHttpIntegrationMutation,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
update(store, { data }) {
|
||||
updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath });
|
||||
},
|
||||
})
|
||||
.then(({ data: { httpIntegrationDestroy } = {} } = {}) => {
|
||||
const error = httpIntegrationDestroy?.errors[0];
|
||||
if (error) {
|
||||
return createFlash({ message: error });
|
||||
}
|
||||
this.currentIntegration = null;
|
||||
return createFlash({
|
||||
message: this.$options.i18n.integrationRemoved,
|
||||
type: FLASH_TYPES.SUCCESS,
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.errored = true;
|
||||
createFlash({ message: err });
|
||||
})
|
||||
.finally(() => {
|
||||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
clearCurrentIntegration() {
|
||||
this.currentIntegration = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -239,6 +252,7 @@ export default {
|
|||
@create-new-integration="createNewIntegration"
|
||||
@update-integration="updateIntegration"
|
||||
@reset-token="resetToken"
|
||||
@clear-current-integration="clearCurrentIntegration"
|
||||
/>
|
||||
<settings-form-old v-else />
|
||||
</div>
|
||||
|
|
|
@ -66,6 +66,8 @@ export const defaultFormState = {
|
|||
integrationTestPayload: { json: null, error: null },
|
||||
};
|
||||
|
||||
export const integrationToDeleteDefault = { id: null, name: '' };
|
||||
|
||||
export const JSON_VALIDATE_DELAY = 250;
|
||||
|
||||
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
#import "../fragments/integration_item.fragment.graphql"
|
||||
|
||||
mutation destroyHttpIntegration($id: ID!) {
|
||||
httpIntegrationDestroy(input: { id: $id }) {
|
||||
errors
|
||||
integration {
|
||||
...IntegrationItem
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import produce from 'immer';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
import { DELETE_INTEGRATION_ERROR, ADD_INTEGRATION_ERROR } from './error_messages';
|
||||
|
||||
const deleteIntegrationFromStore = (store, query, { httpIntegrationDestroy }, variables) => {
|
||||
const integration = httpIntegrationDestroy?.integration;
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = store.readQuery({
|
||||
query,
|
||||
variables,
|
||||
});
|
||||
|
||||
const data = produce(sourceData, draftData => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
draftData.project.alertManagementIntegrations.nodes = draftData.project.alertManagementIntegrations.nodes.filter(
|
||||
({ id }) => id !== integration.id,
|
||||
);
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query,
|
||||
variables,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const addIntegrationToStore = (
|
||||
store,
|
||||
query,
|
||||
{ httpIntegrationCreate, prometheusIntegrationCreate },
|
||||
variables,
|
||||
) => {
|
||||
const integration =
|
||||
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = store.readQuery({
|
||||
query,
|
||||
variables,
|
||||
});
|
||||
|
||||
const data = produce(sourceData, draftData => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
draftData.project.alertManagementIntegrations.nodes = [
|
||||
integration,
|
||||
...draftData.project.alertManagementIntegrations.nodes,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query,
|
||||
variables,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const onError = (data, message) => {
|
||||
createFlash({ message });
|
||||
throw new Error(data.errors);
|
||||
};
|
||||
|
||||
export const hasErrors = ({ errors = [] }) => errors?.length;
|
||||
|
||||
export const updateStoreAfterIntegrationDelete = (store, query, data, variables) => {
|
||||
if (hasErrors(data)) {
|
||||
onError(data, DELETE_INTEGRATION_ERROR);
|
||||
} else {
|
||||
deleteIntegrationFromStore(store, query, data, variables);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStoreAfterIntegrationAdd = (store, query, data, variables) => {
|
||||
if (hasErrors(data)) {
|
||||
onError(data, ADD_INTEGRATION_ERROR);
|
||||
} else {
|
||||
addIntegrationToStore(store, query, data, variables);
|
||||
}
|
||||
};
|
|
@ -0,0 +1,9 @@
|
|||
import { s__ } from '~/locale';
|
||||
|
||||
export const DELETE_INTEGRATION_ERROR = s__(
|
||||
'AlertsIntegrations|The integration could not be deleted. Please try again.',
|
||||
);
|
||||
|
||||
export const ADD_INTEGRATION_ERROR = s__(
|
||||
'AlertsIntegrations|The integration could not be added. Please try again.',
|
||||
);
|
|
@ -14,9 +14,9 @@ export default {
|
|||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
isDesktop: {
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
@ -34,7 +34,7 @@ export default {
|
|||
return this.tags.some(tag => this.selectedItems[tag.name]);
|
||||
},
|
||||
showMultiDeleteButton() {
|
||||
return this.tags.some(tag => tag.destroy_path) && this.isDesktop;
|
||||
return this.tags.some(tag => tag.destroy_path) && !this.isMobile;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -68,7 +68,7 @@ export default {
|
|||
:tag="tag"
|
||||
:first="index === 0"
|
||||
:selected="selectedItems[tag.name]"
|
||||
:is-desktop="isDesktop"
|
||||
:is-mobile="isMobile"
|
||||
@select="updateSelectedItems(tag.name)"
|
||||
@delete="$emit('delete', { [tag.name]: true })"
|
||||
/>
|
||||
|
|
|
@ -40,9 +40,9 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isDesktop: {
|
||||
isMobile: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
default: true,
|
||||
required: false,
|
||||
},
|
||||
selected: {
|
||||
|
@ -69,7 +69,7 @@ export default {
|
|||
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
|
||||
},
|
||||
mobileClasses() {
|
||||
return this.isDesktop ? '' : 'mw-s';
|
||||
return this.isMobile ? 'mw-s' : '';
|
||||
},
|
||||
shortDigest() {
|
||||
// remove sha256: from the string, and show only the first 7 char
|
||||
|
|
|
@ -37,7 +37,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
itemsToBeDeleted: [],
|
||||
isDesktop: true,
|
||||
isMobile: false,
|
||||
deleteAlertType: null,
|
||||
dismissPartialCleanupWarning: false,
|
||||
};
|
||||
|
@ -110,7 +110,7 @@ export default {
|
|||
}
|
||||
},
|
||||
handleResize() {
|
||||
this.isDesktop = GlBreakpointInstance.isDesktop();
|
||||
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs';
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -137,7 +137,7 @@ export default {
|
|||
<tags-loader v-if="isLoading" />
|
||||
<template v-else>
|
||||
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
|
||||
<tags-list v-else :tags="tags" :is-desktop="isDesktop" @delete="deleteTags" />
|
||||
<tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" />
|
||||
</template>
|
||||
|
||||
<gl-pagination
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<script>
|
||||
import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
|
||||
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlDeprecatedDropdown,
|
||||
GlDeprecatedDropdownItem,
|
||||
GlDropdown,
|
||||
GlDropdownItem,
|
||||
},
|
||||
props: {
|
||||
commits: {
|
||||
|
@ -18,20 +18,20 @@ export default {
|
|||
|
||||
<template>
|
||||
<div>
|
||||
<gl-deprecated-dropdown
|
||||
<gl-dropdown
|
||||
right
|
||||
text="Use an existing commit message"
|
||||
variant="link"
|
||||
class="mr-commit-dropdown"
|
||||
>
|
||||
<gl-deprecated-dropdown-item
|
||||
<gl-dropdown-item
|
||||
v-for="commit in commits"
|
||||
:key="commit.short_id"
|
||||
class="text-nowrap text-truncate"
|
||||
@click="$emit('input', commit.message)"
|
||||
>
|
||||
<span class="monospace mr-2">{{ commit.short_id }}</span> {{ commit.title }}
|
||||
</gl-deprecated-dropdown-item>
|
||||
</gl-deprecated-dropdown>
|
||||
</gl-dropdown-item>
|
||||
</gl-dropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,10 +15,6 @@
|
|||
|
||||
.broadcast-banner-message {
|
||||
text-align: center;
|
||||
|
||||
.broadcast-message-dismiss {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.broadcast-notification-message {
|
||||
|
@ -36,10 +32,6 @@
|
|||
&.preview {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.broadcast-message-dismiss {
|
||||
color: $gray-700;
|
||||
}
|
||||
}
|
||||
|
||||
.toggle-colors {
|
||||
|
|
|
@ -150,7 +150,7 @@ module IssuableCollections
|
|||
common_attributes + [:project, project: :namespace]
|
||||
when 'MergeRequest'
|
||||
common_attributes + [
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users, :reviewers,
|
||||
:target_project, :latest_merge_request_diff, :approvals, :approved_by_users,
|
||||
source_project: :route, head_pipeline: :project, target_project: :namespace
|
||||
]
|
||||
end
|
||||
|
|
|
@ -318,8 +318,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
end
|
||||
|
||||
def export_csv
|
||||
return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true)
|
||||
|
||||
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
|
||||
|
||||
index_path = project_merge_requests_path(project)
|
||||
|
|
|
@ -406,7 +406,7 @@ class IssuableFinder
|
|||
elsif params.filter_by_any_assignee?
|
||||
items.assigned
|
||||
elsif params.assignee
|
||||
items_assigned_to(items, params.assignee)
|
||||
items.assigned_to(params.assignee)
|
||||
elsif params.assignee_id? || params.assignee_username? # assignee not found
|
||||
items.none
|
||||
else
|
||||
|
@ -414,10 +414,6 @@ class IssuableFinder
|
|||
end
|
||||
end
|
||||
|
||||
def items_assigned_to(items, user)
|
||||
items.assigned_to(user)
|
||||
end
|
||||
|
||||
def by_negated_assignee(items)
|
||||
# We want CE users to be able to say "Issues not assigned to either PersonA nor PersonB"
|
||||
if not_params.assignees.present?
|
||||
|
|
|
@ -164,13 +164,6 @@ class MergeRequestsFinder < IssuableFinder
|
|||
end
|
||||
# rubocop: enable CodeReuse/Finder
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def items_assigned_to(items, user)
|
||||
assignee_or_reviewer = MergeRequest.from_union([super, items.reviewer_assigned_to(user)])
|
||||
items.where(id: assignee_or_reviewer)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
def by_deployments(items)
|
||||
env = params[:environment]
|
||||
before = params[:deployed_before]
|
||||
|
|
|
@ -302,7 +302,7 @@ module ProjectsHelper
|
|||
end
|
||||
|
||||
def settings_operations_available?
|
||||
can?(current_user, :read_environment, @project)
|
||||
!@project.archived? && can?(current_user, :admin_operations, @project)
|
||||
end
|
||||
|
||||
def error_tracking_setting_project_json
|
||||
|
|
|
@ -303,19 +303,6 @@ class MergeRequest < ApplicationRecord
|
|||
includes(:metrics)
|
||||
end
|
||||
|
||||
scope :reviewer_assigned_to, ->(user) do
|
||||
mr_reviewers_table = MergeRequestReviewer.arel_table
|
||||
|
||||
inner_sql = mr_reviewers_table
|
||||
.project(Arel::Nodes::True.new)
|
||||
.where(
|
||||
mr_reviewers_table[:merge_request_id].eq(MergeRequest.arel_table[:id])
|
||||
.and(mr_reviewers_table[:user_id].eq(user.id))
|
||||
).exists
|
||||
|
||||
where(inner_sql)
|
||||
end
|
||||
|
||||
after_save :keep_around_commit, unless: :importing?
|
||||
|
||||
alias_attribute :project, :target_project
|
||||
|
|
|
@ -29,10 +29,10 @@ class ResourceTimeboxEvent < ResourceEvent
|
|||
case self
|
||||
when ResourceMilestoneEvent
|
||||
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_milestone_changed_action(author: user)
|
||||
when ResourceIterationEvent
|
||||
Gitlab::UsageDataCounters::IssueActivityUniqueCounter.track_issue_iteration_changed_action(author: user)
|
||||
else
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ResourceTimeboxEvent.prepend_if_ee('EE::ResourceTimeboxEvent')
|
||||
|
|
|
@ -423,7 +423,7 @@
|
|||
= link_to project_settings_ci_cd_path(@project), title: _('CI / CD') do
|
||||
%span
|
||||
= _('CI / CD')
|
||||
- if !@project.archived? && settings_operations_available?
|
||||
- if settings_operations_available?
|
||||
= nav_link(controller: [:operations]) do
|
||||
= link_to project_settings_operations_path(@project), title: _('Operations'), data: { qa_selector: 'operations_settings_link' } do
|
||||
= _('Operations')
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
|
||||
.btn-group
|
||||
.btn-group
|
||||
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
|
||||
|
||||
- if @can_bulk_update
|
||||
|
@ -8,5 +7,4 @@
|
|||
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
|
||||
New merge request
|
||||
|
||||
- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
|
||||
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- return unless can?(current_user, :read_environment, @project)
|
||||
- return unless can?(current_user, :admin_operations, @project)
|
||||
|
||||
- setting = error_tracking_setting
|
||||
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
= render_broadcast_message(message)
|
||||
.gl-flex-grow-1.gl-flex-basis-0.gl-text-right
|
||||
- if (message.notification? || message.dismissable?) && opts[:preview].blank?
|
||||
%button.broadcast-message-dismiss.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
|
||||
= sprite_icon('close', size: 16, css_class: 'gl-icon gl-text-white gl-mx-3!')
|
||||
%button.js-dismiss-current-broadcast-notification.btn.btn-link.gl-button{ 'aria-label' => _('Close'), :type => 'button', data: { id: message.id, expire_date: message.ends_at.iso8601 } }
|
||||
= sprite_icon('close', size: 16, css_class: "gl-icon gl-mx-3! #{is_banner ? 'gl-text-white' : 'gl-text-gray-700'}")
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'container registry: show delete selected button on medium viewports'
|
||||
merge_request: 46699
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Replace-GlDeprecatedDropdown-with-GlDropdown-in-app/assets/javascripts/vue_merge_request_widget
|
||||
merge_request: 41429
|
||||
author: nuwe1
|
||||
type: other
|
5
changelogs/unreleased/bvl-handle-invalid-headers.yml
Normal file
5
changelogs/unreleased/bvl-handle-invalid-headers.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Handle nullbytes in auth headers
|
||||
merge_request: 46985
|
||||
author:
|
||||
type: fixed
|
5
changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml
Normal file
5
changelogs/unreleased/nicolasdular-fix-bm-close-icon.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix broadcast notification close icon appearance
|
||||
merge_request: 46804
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix operations settings when Pipelines are disabled
|
||||
merge_request: 47062
|
||||
author:
|
||||
type: fixed
|
|
@ -1,7 +0,0 @@
|
|||
---
|
||||
name: export_merge_requests_as_csv
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45130
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/267129
|
||||
type: development
|
||||
group: group::compliance
|
||||
default_enabled: true
|
|
@ -18,12 +18,12 @@ needs.
|
|||
|
||||
## APIs
|
||||
|
||||
- `https://docs.gitlab.com/ee/api/audit_events.html`
|
||||
- `https://docs.gitlab.com/ee/api/graphql/reference/#user`
|
||||
- `https://docs.gitlab.com/ee/api/graphql/reference/#groupmember`
|
||||
- `https://docs.gitlab.com/ee/api/graphql/reference/#projectmember`
|
||||
- [Audit events](../api/audit_events.md)
|
||||
- [GraphQL - User](../api/graphql/reference/index.md#user)
|
||||
- [GraphQL - GroupMember](../api/graphql/reference/index.md#groupmember)
|
||||
- [GraphQL - ProjectMember](../api/graphql/reference/index.md#projectmember)
|
||||
|
||||
## Features
|
||||
|
||||
- `https://docs.gitlab.com/ee/administration/audit_events.html`
|
||||
- `https://docs.gitlab.com/ee/administration/logs.html`
|
||||
- [Audit events](audit_events.md)
|
||||
- [Log system](logs.md)
|
||||
|
|
|
@ -133,6 +133,9 @@ Note the following when promoting a secondary:
|
|||
```
|
||||
|
||||
1. Promote the **secondary** node to the **primary** node.
|
||||
DANGER: **Warning:**
|
||||
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
|
||||
a point-in-time recovery to the last known state.
|
||||
|
@ -167,6 +170,9 @@ conjunction with multiple servers, as it can only
|
|||
perform changes on a **secondary** with only a single machine. Instead, you must
|
||||
do this manually.
|
||||
|
||||
DANGER: **Warning:**
|
||||
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
If the secondary node [has been paused](../../geo/index.md#pausing-and-resuming-replication), this performs
|
||||
a point-in-time recovery to the last known state.
|
||||
|
|
|
@ -227,6 +227,9 @@ conjunction with multiple servers, as it can only
|
|||
perform changes on a **secondary** with only a single machine. Instead, you must
|
||||
do this manually.
|
||||
|
||||
DANGER: **Warning:**
|
||||
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
If the secondary node [has been paused](../../../geo/index.md#pausing-and-resuming-replication), this performs
|
||||
a point-in-time recovery to the last known state.
|
||||
|
|
|
@ -196,6 +196,9 @@ For information on how to update your Geo nodes to the latest GitLab version, se
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35913) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2.
|
||||
|
||||
DANGER: **Warning:**
|
||||
In GitLab 13.2 and 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. This issue has been fixed in GitLab 13.4 or later.
|
||||
|
||||
CAUTION: **Caution:**
|
||||
Pausing and resuming of replication is currently only supported for Geo installations using an
|
||||
Omnibus GitLab-managed database. External databases are currently not supported.
|
||||
|
|
|
@ -24,6 +24,13 @@ DROP SERVER gitlab_secondary CASCADE;
|
|||
DROP EXTENSION IF EXISTS postgres_fdw;
|
||||
```
|
||||
|
||||
DANGER: **Warning:**
|
||||
In GitLab 13.3, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
|
||||
|
||||
## Updating to GitLab 13.2
|
||||
|
||||
In GitLab 13.2, promoting a secondary node to a primary while the secondary is paused fails. Do not pause replication before promoting a secondary. If the node is paused, please resume before promoting. To avoid this issue, upgrade to GitLab 13.4 or later.
|
||||
|
||||
## Updating to GitLab 13.0
|
||||
|
||||
Upgrading to GitLab 13.0 requires GitLab 12.10 to already be using PostgreSQL
|
||||
|
|
|
@ -367,7 +367,7 @@ POST /projects/:id/releases
|
|||
| `assets:links` | array of hash | no | An array of assets links. |
|
||||
| `assets:links:name`| string | required by: `assets:links` | The name of the link. |
|
||||
| `assets:links:url` | string | required by: `assets:links` | The URL of the link. |
|
||||
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md).
|
||||
| `assets:links:filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
|
||||
| `assets:links:link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`.
|
||||
| `released_at` | datetime | no | The date when the release will be/was ready. Defaults to the current time. Expected in ISO 8601 format (`2019-03-15T08:00:00Z`). |
|
||||
|
||||
|
|
|
@ -97,26 +97,29 @@ POST /projects/:id/releases/:tag_name/assets/links
|
|||
| `tag_name` | string | yes | The tag associated with the Release. |
|
||||
| `name` | string | yes | The name of the link. |
|
||||
| `url` | string | yes | The URL of the link. |
|
||||
| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
|
||||
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST \
|
||||
--header "PRIVATE-TOKEN: n671WNGecHugsdEDPsyo" \
|
||||
--data name="awesome-v0.2.dmg" \
|
||||
--data url="http://192.168.10.15:3000" \
|
||||
"https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links"
|
||||
--header "PRIVATE-TOKEN: tkhfG7HgG-LiZd3zfdDC" \
|
||||
--data name="hellodarwin-amd64" \
|
||||
--data url="https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64" \
|
||||
--data filepath="/bin/hellodarwin-amd64" \
|
||||
"https://gitlab.example.com/api/v4/projects/20/releases/v1.7.0/assets/links"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"id":1,
|
||||
"name":"awesome-v0.2.dmg",
|
||||
"url":"http://192.168.10.15:3000",
|
||||
"external":true,
|
||||
"id":2,
|
||||
"name":"hellodarwin-amd64",
|
||||
"url":"https://gitlab.example.com/mynamespace/hello/-/jobs/688/artifacts/raw/bin/hello-darwin-amd64",
|
||||
"direct_asset_url":"https://gitlab.example.com/mynamespace/hello/-/releases/v1.7.0/downloads/bin/hellodarwin-amd64",
|
||||
"external":false,
|
||||
"link_type":"other"
|
||||
}
|
||||
```
|
||||
|
@ -136,6 +139,7 @@ PUT /projects/:id/releases/:tag_name/assets/links/:link_id
|
|||
| `link_id` | integer | yes | The ID of the link. |
|
||||
| `name` | string | no | The name of the link. |
|
||||
| `url` | string | no | The URL of the link. |
|
||||
| `filepath` | string | no | Optional path for a [Direct Asset link](../../user/project/releases/index.md#permanent-links-to-release-assets).
|
||||
| `link_type` | string | no | The type of the link: `other`, `runbook`, `image`, `package`. Defaults to `other`. |
|
||||
|
||||
NOTE: **Note:**
|
||||
|
|
|
@ -320,7 +320,7 @@ services:
|
|||
command: ["--registry-mirror", "https://registry-mirror.example.com"] # Specify the registry mirror to use.
|
||||
```
|
||||
|
||||
#### DinD service defined inside of GitLab Runner configuration
|
||||
##### DinD service defined inside of GitLab Runner configuration
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-runner/-/issues/27173) in GitLab Runner 13.6.
|
||||
|
||||
|
|
|
@ -234,23 +234,23 @@ There are also two edge cases worth mentioning:
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/29654) in GitLab 12.5
|
||||
|
||||
The top-level `workflow:` key applies to the entirety of a pipeline, and
|
||||
determines whether or not a pipeline is created. It accepts a single
|
||||
`rules:` key that operates similarly to [`rules:` defined within jobs](#rules),
|
||||
enabling dynamic configuration of the pipeline.
|
||||
The top-level `workflow:` keyword determines whether or not a pipeline is created.
|
||||
It accepts a single `rules:` keyword that is similar to [`rules:` defined within jobs](#rules).
|
||||
Use it to define what can trigger a new pipeline.
|
||||
|
||||
If you are new to GitLab CI/CD and `workflow: rules`, you may find the [`workflow:rules` templates](#workflowrules-templates) useful.
|
||||
You can use the [`workflow:rules` templates](#workflowrules-templates) to import
|
||||
a preconfigured `workflow: rules` entry.
|
||||
|
||||
To define your own `workflow: rules`, the available configuration options are:
|
||||
`workflow: rules` accepts these keywords:
|
||||
|
||||
- [`if`](#rulesif): Define a rule.
|
||||
- [`when`](#when): May be set to `always` or `never` only. If not provided, the default value is `always`.
|
||||
- [`if`](#rulesif): Check this rule to determine when to run a pipeline.
|
||||
- [`when`](#when): Specify what to do when the `if` rule evaluates to true.
|
||||
- To run a pipeline, set to `always`.
|
||||
- To prevent pipelines from running, set to `never`.
|
||||
|
||||
If a pipeline attempts to run but matches no rule, it's dropped and doesn't run.
|
||||
When no rules evaluate to true, the pipeline does not run.
|
||||
|
||||
Use the example rules below exactly as written to allow pipelines that match the rule
|
||||
to run. Add `when: never` to prevent pipelines that match the rule from running. See
|
||||
the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
|
||||
Some example `if` clauses for `workflow: rules`:
|
||||
|
||||
| Example rules | Details |
|
||||
|------------------------------------------------------|-----------------------------------------------------------|
|
||||
|
@ -259,9 +259,12 @@ the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more exa
|
|||
| `if: $CI_COMMIT_TAG` | Control when tag pipelines run. |
|
||||
| `if: $CI_COMMIT_BRANCH` | Control when branch pipelines run. |
|
||||
|
||||
See the [common `if` clauses for `rules`](#common-if-clauses-for-rules) for more examples.
|
||||
|
||||
For example, in the following configuration, pipelines run for all `push` events (changes to
|
||||
branches and new tags). Only push events with `-wip` in the commit message are excluded. Scheduled
|
||||
pipelines and merge request pipelines don't run, as there's no rule allowing them.
|
||||
branches and new tags). Pipelines for push events with `-wip` in the commit message
|
||||
don't run, because they are set to `when: never`. Pipelines for schedules or merge requests
|
||||
don't run either, because no rules evaluate to true for them:
|
||||
|
||||
```yaml
|
||||
workflow:
|
||||
|
@ -271,11 +274,11 @@ workflow:
|
|||
- if: '$CI_PIPELINE_SOURCE == "push"'
|
||||
```
|
||||
|
||||
This example has strict rules, and no other pipelines can run.
|
||||
This example has strict rules, and pipelines do **not** run in any other case.
|
||||
|
||||
Alternatively, you can have loose rules by using only `when: never` rules, followed
|
||||
by a final `when: always` rule. This allows all types of pipelines, except for any
|
||||
that match the `when: never` rules:
|
||||
Alternatively, all of the rules can be `when: never`, with a final
|
||||
`when: always` rule. Pipelines that match the `when: never` rules do not run.
|
||||
All other pipeline types run:
|
||||
|
||||
```yaml
|
||||
workflow:
|
||||
|
@ -287,12 +290,13 @@ workflow:
|
|||
- when: always
|
||||
```
|
||||
|
||||
This example never allows pipelines for schedules or `push` (branches and tags) pipelines,
|
||||
but does allow pipelines in **all** other cases, *including* merge request pipelines.
|
||||
This example prevents pipelines for schedules or `push` (branches and tags) pipelines.
|
||||
The final `when: always` rule lets all other pipeline types run, **including** merge
|
||||
request pipelines.
|
||||
|
||||
Be careful not to use a configuration that might run
|
||||
merge request pipelines and branch pipelines at the same time. As with `rules` defined in jobs,
|
||||
it can cause [duplicate pipelines](#prevent-duplicate-pipelines).
|
||||
Be careful not to have rules that match both branch pipelines
|
||||
and merge request pipelines. Similar to `rules` defined in jobs, this can cause
|
||||
[duplicate pipelines](#prevent-duplicate-pipelines).
|
||||
|
||||
#### `workflow:rules` templates
|
||||
|
||||
|
|
|
@ -7,14 +7,14 @@ type: howto, reference
|
|||
|
||||
# Edit files through the command line
|
||||
|
||||
When [working with Git from the command line](start-using-git.md), you will need to
|
||||
When [working with Git from the command line](start-using-git.md), you need to
|
||||
use more than just the Git commands. There are several basic commands that you should
|
||||
learn, in order to make full use of the command line.
|
||||
|
||||
## Start working on your project
|
||||
|
||||
To work on a Git project locally (from your own computer), with the command line,
|
||||
first you will need to [clone (copy) it](start-using-git.md#clone-a-repository) to
|
||||
first you need to [clone (copy) it](start-using-git.md#clone-a-repository) to
|
||||
your computer.
|
||||
|
||||
## Working with files on the command line
|
||||
|
@ -57,7 +57,7 @@ nano README.md
|
|||
|
||||
### Remove a file or directory
|
||||
|
||||
It is easy to delete (remove) a file or directory, but be careful:
|
||||
It's easy to delete (remove) a file or directory, but be careful:
|
||||
|
||||
DANGER: **Warning:**
|
||||
This will **permanently** delete a file.
|
||||
|
@ -96,7 +96,7 @@ for example) . Execute the same full command with:
|
|||
Not all commands can be executed from a basic user account on a computer, you may
|
||||
need administrator's rights to execute commands that affect the system, or try to access
|
||||
protected data, for example. You can use `sudo` to execute these commands, but you
|
||||
will likely be asked for an administrator password.
|
||||
might be asked for an administrator password.
|
||||
|
||||
```shell
|
||||
sudo RESTRICTED-COMMAND
|
||||
|
@ -108,8 +108,8 @@ damage to your data or system.
|
|||
|
||||
## Sample Git taskflow
|
||||
|
||||
If you are completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
|
||||
will help you understand the best practices for using these commands as you work.
|
||||
If you're completely new to Git, looking through some [sample taskflows](https://rogerdudler.github.io/git-guide/)
|
||||
may help you understand the best practices for using these commands as you work.
|
||||
|
||||
<!-- ## Troubleshooting
|
||||
|
||||
|
|
|
@ -6,23 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Export Merge Requests to CSV **(CORE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
|
||||
> - It was [deployed behind a feature flag](../../../administration/feature_flags.md), disabled by default.
|
||||
> - Became enabled by default in GitLab 13.6.
|
||||
> - 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-export-merge-requests-to-csv). **(CORE ONLY)**
|
||||
> - It can be enabled or disabled for a single project.
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3619) in GitLab 13.6.
|
||||
|
||||
Exporting Merge Requests CSV enables you and your team to export all the data collected from merge requests into a comma-separated values (CSV) file, which stores tabular data in plain text.
|
||||
|
||||
To export Merge Requests to CSV, navigate to your **Merge Requests** from the sidebar of a project and click **Export to CSV**.
|
||||
|
||||
Exported files are generated asynchronously and delivered as an email attachment upon generation.
|
||||
|
||||
## CSV Output
|
||||
|
||||
The following table shows what attributes will be present in the CSV.
|
||||
|
@ -54,28 +43,3 @@ The following table shows what attributes will be present in the CSV.
|
|||
|
||||
- Export merge requests to CSV is not available at the Group’s merge request list.
|
||||
- As the merge request CSV file is sent as an email attachment, the size is limited to 15MB to ensure successful delivery across a range of email providers. If you need to minimize the size of the file, you can narrow the search before export. For example, you can set up exports of open and closed merge requests in separate files.
|
||||
|
||||
### Enable or disable Export Merge Requests to CSV **(CORE ONLY)**
|
||||
|
||||
Export merge requests to CSV is 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.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
# For the instance
|
||||
Feature.enable(:export_merge_requests_as_csv)
|
||||
# For a single project
|
||||
Feature.enable(:export_merge_requests_as_csv, Project.find(<project id>))
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
# For the instance
|
||||
Feature.disable(:export_merge_requests_as_csv)
|
||||
# For a single project
|
||||
Feature.disable(:export_merge_requests_as_csv, Project.find(<project id>))
|
||||
```
|
||||
|
|
|
@ -32,6 +32,9 @@ To set up a project import/export:
|
|||
|
||||
Note the following:
|
||||
|
||||
- Before you can import a project, you need to export the data first.
|
||||
See [Exporting a project and its data](#exporting-a-project-and-its-data)
|
||||
for how you can export a project through the UI.
|
||||
- Imports from a newer version of GitLab are not supported.
|
||||
The Importing GitLab version must be greater than or equal to the Exporting GitLab version.
|
||||
- Imports will fail unless the import and export GitLab instances are
|
||||
|
@ -129,6 +132,11 @@ For more details on the specific data persisted in a project export, see the
|
|||
|
||||
## Exporting a project and its data
|
||||
|
||||
Full project export functionality is limited to project maintainers and owners.
|
||||
You can configure such functionality through [project settings](index.md):
|
||||
|
||||
To export a project and its data, follow these steps:
|
||||
|
||||
1. Go to your project's homepage.
|
||||
|
||||
1. Click **Settings** in the sidebar.
|
||||
|
|
|
@ -12,14 +12,11 @@ module Gitlab
|
|||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def find
|
||||
BatchLoader::GraphQL.for({ model: model_class, id: model_id.to_i }).batch do |loader_info, loader|
|
||||
per_model = loader_info.group_by { |info| info[:model] }
|
||||
per_model.each do |model, info|
|
||||
ids = info.map { |i| i[:id] }
|
||||
BatchLoader::GraphQL.for(model_id.to_i).batch(key: model_class) do |ids, loader, args|
|
||||
model = args[:key]
|
||||
results = model.where(id: ids)
|
||||
|
||||
results.each { |record| loader.call({ model: model, id: record.id }, record) }
|
||||
end
|
||||
results.each { |record| loader.call(record.id, record) }
|
||||
end
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
|
|
@ -5,6 +5,8 @@ module Gitlab
|
|||
# There is no valid reason for a request to contain a malformed string
|
||||
# so just return HTTP 400 (Bad Request) if we receive one
|
||||
class HandleMalformedStrings
|
||||
include ActionController::HttpAuthentication::Basic
|
||||
|
||||
NULL_BYTE_REGEX = Regexp.new(Regexp.escape("\u0000")).freeze
|
||||
|
||||
attr_reader :app
|
||||
|
@ -21,16 +23,26 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def request_contains_malformed_string?(request)
|
||||
def request_contains_malformed_string?(env)
|
||||
return false if ENV['DISABLE_REQUEST_VALIDATION'] == '1'
|
||||
|
||||
request = Rack::Request.new(request)
|
||||
# Duplicate the env, so it is not modified when accessing the parameters
|
||||
# https://github.com/rails/rails/blob/34991a6ae2fc68347c01ea7382fa89004159e019/actionpack/lib/action_dispatch/http/parameters.rb#L59
|
||||
# The modification causes problems with our multipart middleware
|
||||
request = ActionDispatch::Request.new(env.dup)
|
||||
|
||||
return true if malformed_path?(request.path)
|
||||
return true if credentials_malformed?(request)
|
||||
|
||||
request.params.values.any? do |value|
|
||||
param_has_null_byte?(value)
|
||||
end
|
||||
rescue ActionController::BadRequest
|
||||
# If we can't build an ActionDispatch::Request something's wrong
|
||||
# This would also happen if `#params` contains invalid UTF-8
|
||||
# in this case we'll return a 400
|
||||
#
|
||||
true
|
||||
end
|
||||
|
||||
def malformed_path?(path)
|
||||
|
@ -40,6 +52,13 @@ module Gitlab
|
|||
true
|
||||
end
|
||||
|
||||
def credentials_malformed?(request)
|
||||
credentials = decode_credentials(request).presence
|
||||
return false unless credentials
|
||||
|
||||
string_malformed?(credentials)
|
||||
end
|
||||
|
||||
def param_has_null_byte?(value, depth = 0)
|
||||
# Guard against possible attack sending large amounts of nested params
|
||||
# Should be safe as deeply nested params are highly uncommon.
|
||||
|
|
|
@ -9,14 +9,12 @@ module Gitlab
|
|||
ISSUE_CREATED = 'g_project_management_issue_created'
|
||||
ISSUE_CLOSED = 'g_project_management_issue_closed'
|
||||
ISSUE_DESCRIPTION_CHANGED = 'g_project_management_issue_description_changed'
|
||||
ISSUE_ITERATION_CHANGED = 'g_project_management_issue_iteration_changed'
|
||||
ISSUE_LABEL_CHANGED = 'g_project_management_issue_label_changed'
|
||||
ISSUE_MADE_CONFIDENTIAL = 'g_project_management_issue_made_confidential'
|
||||
ISSUE_MADE_VISIBLE = 'g_project_management_issue_made_visible'
|
||||
ISSUE_MILESTONE_CHANGED = 'g_project_management_issue_milestone_changed'
|
||||
ISSUE_REOPENED = 'g_project_management_issue_reopened'
|
||||
ISSUE_TITLE_CHANGED = 'g_project_management_issue_title_changed'
|
||||
ISSUE_WEIGHT_CHANGED = 'g_project_management_issue_weight_changed'
|
||||
ISSUE_CROSS_REFERENCED = 'g_project_management_issue_cross_referenced'
|
||||
ISSUE_MOVED = 'g_project_management_issue_moved'
|
||||
ISSUE_RELATED = 'g_project_management_issue_related'
|
||||
|
@ -24,9 +22,6 @@ module Gitlab
|
|||
ISSUE_MARKED_AS_DUPLICATE = 'g_project_management_issue_marked_as_duplicate'
|
||||
ISSUE_LOCKED = 'g_project_management_issue_locked'
|
||||
ISSUE_UNLOCKED = 'g_project_management_issue_unlocked'
|
||||
ISSUE_ADDED_TO_EPIC = 'g_project_management_issue_added_to_epic'
|
||||
ISSUE_REMOVED_FROM_EPIC = 'g_project_management_issue_removed_from_epic'
|
||||
ISSUE_CHANGED_EPIC = 'g_project_management_issue_changed_epic'
|
||||
ISSUE_DESIGNS_ADDED = 'g_project_management_issue_designs_added'
|
||||
ISSUE_DESIGNS_MODIFIED = 'g_project_management_issue_designs_modified'
|
||||
ISSUE_DESIGNS_REMOVED = 'g_project_management_issue_designs_removed'
|
||||
|
@ -78,14 +73,6 @@ module Gitlab
|
|||
track_unique_action(ISSUE_MILESTONE_CHANGED, author, time)
|
||||
end
|
||||
|
||||
def track_issue_iteration_changed_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_ITERATION_CHANGED, author, time)
|
||||
end
|
||||
|
||||
def track_issue_weight_changed_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_WEIGHT_CHANGED, author, time)
|
||||
end
|
||||
|
||||
def track_issue_cross_referenced_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_CROSS_REFERENCED, author, time)
|
||||
end
|
||||
|
@ -114,18 +101,6 @@ module Gitlab
|
|||
track_unique_action(ISSUE_UNLOCKED, author, time)
|
||||
end
|
||||
|
||||
def track_issue_added_to_epic_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_ADDED_TO_EPIC, author, time)
|
||||
end
|
||||
|
||||
def track_issue_removed_from_epic_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_REMOVED_FROM_EPIC, author, time)
|
||||
end
|
||||
|
||||
def track_issue_changed_epic_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_CHANGED_EPIC, author, time)
|
||||
end
|
||||
|
||||
def track_issue_designs_added_action(author:, time: Time.zone.now)
|
||||
track_unique_action(ISSUE_DESIGNS_ADDED, author, time)
|
||||
end
|
||||
|
|
|
@ -2551,6 +2551,9 @@ msgstr ""
|
|||
msgid "AlertSettings|Copy"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertSettings|Delete integration"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertSettings|Enter integration name"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2668,9 +2671,21 @@ msgstr ""
|
|||
msgid "AlertsIntegrations|Prometheus"
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertsIntegrations|The integration could not be added. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertsIntegrations|The integration could not be deleted. Please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertsIntegrations|The integration has been successfully removed."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
|
||||
msgstr ""
|
||||
|
||||
msgid "AlertsIntegrations|You have opted to delete the %{integrationName} integration. Do you want to proceed? It means you will no longer receive alerts from this endpoint in your alert list, and this action cannot be undone."
|
||||
msgstr ""
|
||||
|
||||
msgid "Algorithm"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -1998,10 +1998,6 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
describe 'POST export_csv' do
|
||||
subject { post :export_csv, params: { namespace_id: project.namespace, project_id: project } }
|
||||
|
||||
before do
|
||||
stub_feature_flags(export_merge_requests_as_csv: project)
|
||||
end
|
||||
|
||||
it 'redirects to the merge request index' do
|
||||
subject
|
||||
|
||||
|
@ -2014,17 +2010,5 @@ RSpec.describe Projects::MergeRequestsController do
|
|||
|
||||
subject
|
||||
end
|
||||
|
||||
context 'feature is disabled' do
|
||||
before do
|
||||
stub_feature_flags(export_merge_requests_as_csv: false)
|
||||
end
|
||||
|
||||
it 'expects a 404 response' do
|
||||
subject
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -52,29 +52,20 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
end
|
||||
|
||||
context 'merge requests exist' do
|
||||
let_it_be(:author_user) { create(:user) }
|
||||
let(:label) { create(:label) }
|
||||
|
||||
let!(:assigned_merge_request) do
|
||||
create(:merge_request,
|
||||
assignees: [current_user],
|
||||
source_project: project,
|
||||
author: author_user)
|
||||
end
|
||||
|
||||
let!(:review_requested_merge_request) do
|
||||
create(:merge_request,
|
||||
reviewers: [current_user],
|
||||
source_branch: 'review',
|
||||
source_project: project,
|
||||
author: author_user)
|
||||
author: create(:user))
|
||||
end
|
||||
|
||||
let!(:assigned_merge_request_from_fork) do
|
||||
create(:merge_request,
|
||||
source_branch: 'markdown', assignees: [current_user],
|
||||
target_project: public_project, source_project: forked_project,
|
||||
author: author_user)
|
||||
author: create(:user))
|
||||
end
|
||||
|
||||
let!(:authored_merge_request) do
|
||||
|
@ -103,7 +94,7 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
create(:merge_request,
|
||||
source_branch: 'fix',
|
||||
source_project: project,
|
||||
author: author_user)
|
||||
author: create(:user))
|
||||
end
|
||||
|
||||
before do
|
||||
|
@ -120,10 +111,6 @@ RSpec.describe 'Dashboard Merge Requests' do
|
|||
expect(page).not_to have_content(labeled_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows review requested merge requests' do
|
||||
expect(page).to have_content(review_requested_merge_request.title)
|
||||
end
|
||||
|
||||
it 'shows authored merge requests', :js do
|
||||
reset_filters
|
||||
input_filtered_search("author:=#{current_user.to_reference}")
|
||||
|
|
|
@ -22,13 +22,13 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
|
|||
)
|
||||
end
|
||||
|
||||
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil|
|
||||
RSpec.shared_examples 'rejecting invalid keys' do |key_name:, message: nil, status: 500|
|
||||
context "with invalid key #{key_name}" do
|
||||
let(:body) { { key_name => file, 'package[test][name]' => 'test' } }
|
||||
|
||||
it { expect { subject }.not_to change { Packages::Package.nuget.count } }
|
||||
|
||||
it { expect(subject.code).to eq(500) }
|
||||
it { expect(subject.code).to eq(status) }
|
||||
|
||||
it { expect(subject.body).to include(message.presence || "invalid field: \"#{key_name}\"") }
|
||||
end
|
||||
|
@ -45,7 +45,7 @@ RSpec.describe 'Invalid uploads that must be rejected', :api, :js do
|
|||
# These keys are rejected directly by rack itself.
|
||||
# The request will not be received by multipart.rb (can't use the 'handling file uploads' shared example)
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'x' * 11000, message: 'Puma caught this error: exceeded available parameter key space (RangeError)'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', message: 'Puma caught this error: expected Hash (got Array)'
|
||||
it_behaves_like 'rejecting invalid keys', key_name: 'package[]test', status: 400, message: 'Bad Request'
|
||||
|
||||
it_behaves_like 'handling file uploads', 'by rejecting uploads with an invalid key'
|
||||
end
|
||||
|
|
|
@ -9,25 +9,11 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
|
|||
|
||||
before do
|
||||
sign_in(user)
|
||||
visit(project_merge_requests_path(project))
|
||||
end
|
||||
|
||||
subject { page.find('.nav-controls') }
|
||||
|
||||
context 'feature is not enabled' do
|
||||
before do
|
||||
stub_feature_flags(export_merge_requests_as_csv: false)
|
||||
visit(project_merge_requests_path(project))
|
||||
end
|
||||
|
||||
it { is_expected.not_to have_button('Export as CSV') }
|
||||
end
|
||||
|
||||
context 'feature is enabled for a project' do
|
||||
before do
|
||||
stub_feature_flags(export_merge_requests_as_csv: project)
|
||||
visit(project_merge_requests_path(project))
|
||||
end
|
||||
|
||||
it { is_expected.to have_button('Export as CSV') }
|
||||
|
||||
context 'button is clicked' do
|
||||
|
@ -42,5 +28,4 @@ RSpec.describe 'Merge Requests > Exports as CSV', :js do
|
|||
expect(page).to have_content "It will be emailed to #{user.email} when complete"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -333,8 +333,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
context 'assignee filtering' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
|
||||
let(:issuables) { described_class.new(user, params).execute }
|
||||
|
||||
it_behaves_like 'assignee ID filter' do
|
||||
|
@ -353,6 +351,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
merge_request3.assignees = [user2, user3]
|
||||
end
|
||||
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:params) { { assignee_username: [user2.username, user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3] }
|
||||
end
|
||||
|
@ -367,6 +366,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
|
||||
it_behaves_like 'no assignee filter' do
|
||||
let_it_be(:user3) { create(:user) }
|
||||
let(:expected_issuables) { [merge_request4, merge_request5] }
|
||||
end
|
||||
|
||||
|
@ -374,31 +374,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
let(:expected_issuables) { [merge_request1, merge_request2, merge_request3] }
|
||||
end
|
||||
|
||||
context 'with just reviewers' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request4.reviewers = [user3]
|
||||
merge_request4.assignees = []
|
||||
end
|
||||
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request4] }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an additional reviewer' do
|
||||
it_behaves_like 'assignee username filter' do
|
||||
before do
|
||||
merge_request3.assignees = [user3]
|
||||
merge_request4.reviewers = [user3]
|
||||
end
|
||||
|
||||
let(:params) { { assignee_username: [user3.username] } }
|
||||
let(:expected_issuables) { [merge_request3, merge_request4] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by group milestone' do
|
||||
let(:group_milestone) { create(:milestone, group: group) }
|
||||
|
||||
|
@ -425,6 +400,7 @@ RSpec.describe MergeRequestsFinder do
|
|||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'filtering by created_at/updated_at' do
|
||||
let(:new_project) { create(:project, forked_from_project: project1) }
|
||||
|
@ -587,27 +563,6 @@ RSpec.describe MergeRequestsFinder do
|
|||
expect(mrs).to eq([mr2])
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not raise any exception with complex filters' do
|
||||
# available filters from MergeRequest dashboard UI
|
||||
params = {
|
||||
project_id: project1.id,
|
||||
scope: 'authored',
|
||||
state: 'opened',
|
||||
author_username: user.username,
|
||||
assignee_username: user.username,
|
||||
approver_usernames: [user.username],
|
||||
approved_by_usernames: [user.username],
|
||||
milestone_title: 'none',
|
||||
release_tag: 'none',
|
||||
label_names: 'none',
|
||||
my_reaction_emoji: 'none',
|
||||
draft: 'no'
|
||||
}
|
||||
|
||||
merge_requests = described_class.new(user, params).execute
|
||||
expect { merge_requests.load }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
describe '#row_count', :request_store do
|
||||
|
|
|
@ -128,18 +128,18 @@ describe('AlertsSettingsFormNew', () => {
|
|||
|
||||
it('allows for update-integration with the correct form values for HTTP', async () => {
|
||||
createComponent({
|
||||
data: {
|
||||
selectedIntegration: typeSet.http,
|
||||
},
|
||||
props: {
|
||||
currentIntegration: { id: '1' },
|
||||
currentIntegration: { id: '1', name: 'Test integration pre' },
|
||||
loading: false,
|
||||
},
|
||||
});
|
||||
|
||||
const options = findSelect().findAll('option');
|
||||
await options.at(1).setSelected();
|
||||
|
||||
await findFormFields()
|
||||
.at(0)
|
||||
.setValue('Test integration');
|
||||
.setValue('Test integration post');
|
||||
await findFormToggle().trigger('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -153,27 +153,27 @@ describe('AlertsSettingsFormNew', () => {
|
|||
|
||||
expect(wrapper.emitted('update-integration')).toBeTruthy();
|
||||
expect(wrapper.emitted('update-integration')[0]).toEqual([
|
||||
{ type: typeSet.http, variables: { name: 'Test integration', active: true } },
|
||||
{ type: typeSet.http, variables: { name: 'Test integration post', active: true } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
|
||||
createComponent({
|
||||
data: {
|
||||
selectedIntegration: typeSet.prometheus,
|
||||
},
|
||||
props: {
|
||||
currentIntegration: { id: '1' },
|
||||
currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
|
||||
loading: false,
|
||||
},
|
||||
});
|
||||
|
||||
const options = findSelect().findAll('option');
|
||||
await options.at(2).setSelected();
|
||||
|
||||
await findFormFields()
|
||||
.at(0)
|
||||
.setValue('Test integration');
|
||||
await findFormFields()
|
||||
.at(1)
|
||||
.setValue('https://test.com');
|
||||
.setValue('https://test-post.com');
|
||||
await findFormToggle().trigger('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
@ -187,7 +187,7 @@ describe('AlertsSettingsFormNew', () => {
|
|||
|
||||
expect(wrapper.emitted('update-integration')).toBeTruthy();
|
||||
expect(wrapper.emitted('update-integration')[0]).toEqual([
|
||||
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
|
||||
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { mount, createLocalVue } from '@vue/test-utils';
|
||||
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||
import { GlLoadingIcon } from '@gitlab/ui';
|
||||
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
|
||||
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
|
||||
import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
|
||||
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
|
||||
import getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
|
||||
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
|
||||
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
|
||||
import updateHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_http_integration.mutation.graphql';
|
||||
import updatePrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_prometheus_integration.mutation.graphql';
|
||||
import destroyHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/destroy_http_integration.mutation.graphql';
|
||||
import resetHttpTokenMutation from '~/alerts_settings/graphql/mutations/reset_http_token.mutation.graphql';
|
||||
import resetPrometheusTokenMutation from '~/alerts_settings/graphql/mutations/reset_prometheus_token.mutation.graphql';
|
||||
import { typeSet } from '~/alerts_settings/constants';
|
||||
|
@ -20,16 +24,34 @@ import {
|
|||
createPrometheusVariables,
|
||||
updatePrometheusVariables,
|
||||
ID,
|
||||
errorMsg,
|
||||
getIntegrationsQueryResponse,
|
||||
destroyIntegrationResponse,
|
||||
integrationToDestroy,
|
||||
destroyIntegrationResponseWithErrors,
|
||||
} from './mocks/apollo_mock';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const localVue = createLocalVue();
|
||||
|
||||
describe('AlertsSettingsWrapper', () => {
|
||||
let wrapper;
|
||||
let fakeApollo;
|
||||
let destroyIntegrationHandler;
|
||||
|
||||
const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
|
||||
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
|
||||
|
||||
async function destroyHttpIntegration(localWrapper) {
|
||||
await jest.runOnlyPendingTimers();
|
||||
await localWrapper.vm.$nextTick();
|
||||
|
||||
localWrapper
|
||||
.find(IntegrationsList)
|
||||
.vm.$emit('delete-integration', { id: integrationToDestroy.id });
|
||||
}
|
||||
|
||||
const createComponent = ({ data = {}, provide = {}, loading = false } = {}) => {
|
||||
wrapper = mount(AlertsSettingsWrapper, {
|
||||
data() {
|
||||
|
@ -54,6 +76,29 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
};
|
||||
|
||||
function createComponentWithApollo({
|
||||
destroyHandler = jest.fn().mockResolvedValue(destroyIntegrationResponse),
|
||||
} = {}) {
|
||||
localVue.use(VueApollo);
|
||||
destroyIntegrationHandler = destroyHandler;
|
||||
|
||||
const requestHandlers = [
|
||||
[getIntegrationsQuery, jest.fn().mockResolvedValue(getIntegrationsQueryResponse)],
|
||||
[destroyHttpIntegrationMutation, destroyIntegrationHandler],
|
||||
];
|
||||
|
||||
fakeApollo = createMockApollo(requestHandlers);
|
||||
|
||||
wrapper = mount(AlertsSettingsWrapper, {
|
||||
localVue,
|
||||
apolloProvider: fakeApollo,
|
||||
provide: {
|
||||
...defaultAlertSettingsConfig,
|
||||
glFeatures: { httpIntegrationsList: true },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
wrapper.destroy();
|
||||
|
@ -243,7 +288,6 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
|
||||
it('shows error alert when integration creation fails ', async () => {
|
||||
const errorMsg = 'Something went wrong';
|
||||
createComponent({
|
||||
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
|
||||
provide: { glFeatures: { httpIntegrationsList: true } },
|
||||
|
@ -259,7 +303,6 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
|
||||
it('shows error alert when integration token reset fails ', () => {
|
||||
const errorMsg = 'Something went wrong';
|
||||
createComponent({
|
||||
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
|
||||
provide: { glFeatures: { httpIntegrationsList: true } },
|
||||
|
@ -276,7 +319,6 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
|
||||
it('shows error alert when integration update fails ', () => {
|
||||
const errorMsg = 'Something went wrong';
|
||||
createComponent({
|
||||
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
|
||||
provide: { glFeatures: { httpIntegrationsList: true } },
|
||||
|
@ -292,4 +334,41 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mocked Apollo client', () => {
|
||||
it('has a selection of integrations loaded via the getIntegrationsQuery', async () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
await jest.runOnlyPendingTimers();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findIntegrations()).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('calls a mutation with correct parameters and destroys a integration', async () => {
|
||||
createComponentWithApollo();
|
||||
|
||||
await destroyHttpIntegration(wrapper);
|
||||
|
||||
expect(destroyIntegrationHandler).toHaveBeenCalled();
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(findIntegrations()).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('displays flash if mutation had a recoverable error', async () => {
|
||||
createComponentWithApollo({
|
||||
destroyHandler: jest.fn().mockResolvedValue(destroyIntegrationResponseWithErrors),
|
||||
});
|
||||
|
||||
await destroyHttpIntegration(wrapper);
|
||||
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update
|
||||
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
||||
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
|
||||
|
||||
expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const projectPath = '';
|
||||
export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
|
||||
export const errorMsg = 'Something went wrong';
|
||||
|
||||
export const createHttpVariables = {
|
||||
name: 'Test Pre',
|
||||
|
@ -24,3 +25,99 @@ export const updatePrometheusVariables = {
|
|||
active: true,
|
||||
id: ID,
|
||||
};
|
||||
|
||||
export const getIntegrationsQueryResponse = {
|
||||
data: {
|
||||
project: {
|
||||
alertManagementIntegrations: {
|
||||
nodes: [
|
||||
{
|
||||
id: '37',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 5',
|
||||
url:
|
||||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
},
|
||||
{
|
||||
id: '41',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 9999',
|
||||
url:
|
||||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-9999/b78a566e1776cfc2.json',
|
||||
token: 'f7579aa03844e07af3b1f0fca3f79f81',
|
||||
apiUrl: null,
|
||||
},
|
||||
{
|
||||
id: '40',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 6',
|
||||
url:
|
||||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-6/3e828ae28a240222.json',
|
||||
token: '6536102a607a5dd74fcdde921f2349ee',
|
||||
apiUrl: null,
|
||||
},
|
||||
{
|
||||
id: '12',
|
||||
type: 'PROMETHEUS',
|
||||
active: false,
|
||||
name: 'Prometheus',
|
||||
url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/prometheus/alerts/notify.json',
|
||||
token: '256f687c6225aa5d6ee50c3d68120c4c',
|
||||
apiUrl: 'https://localhost.ieeeesassadasasa',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const integrationToDestroy = {
|
||||
id: '37',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 5',
|
||||
url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
};
|
||||
|
||||
export const destroyIntegrationResponse = {
|
||||
data: {
|
||||
httpIntegrationDestroy: {
|
||||
errors: [],
|
||||
integration: {
|
||||
id: '37',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 5',
|
||||
url:
|
||||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const destroyIntegrationResponseWithErrors = {
|
||||
data: {
|
||||
httpIntegrationDestroy: {
|
||||
errors: ['Houston, we have a problem'],
|
||||
integration: {
|
||||
id: '37',
|
||||
type: 'HTTP',
|
||||
active: true,
|
||||
name: 'Test 5',
|
||||
url:
|
||||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ describe('tags list row', () => {
|
|||
let wrapper;
|
||||
const [tag] = [...tagsListResponse.data];
|
||||
|
||||
const defaultProps = { tag, isDesktop: true, index: 0 };
|
||||
const defaultProps = { tag, isMobile: false, index: 0 };
|
||||
|
||||
const findCheckbox = () => wrapper.find(GlFormCheckbox);
|
||||
const findName = () => wrapper.find('[data-testid="name"]');
|
||||
|
@ -114,7 +114,7 @@ describe('tags list row', () => {
|
|||
});
|
||||
|
||||
it('on mobile has mw-s class', () => {
|
||||
mountComponent({ ...defaultProps, isDesktop: false });
|
||||
mountComponent({ ...defaultProps, isMobile: true });
|
||||
|
||||
expect(findName().classes('mw-s')).toBe(true);
|
||||
});
|
||||
|
|
|
@ -14,7 +14,7 @@ describe('Tags List', () => {
|
|||
const findDeleteButton = () => wrapper.find(GlButton);
|
||||
const findListTitle = () => wrapper.find('[data-testid="list-title"]');
|
||||
|
||||
const mountComponent = (propsData = { tags, isDesktop: true }) => {
|
||||
const mountComponent = (propsData = { tags, isMobile: false }) => {
|
||||
wrapper = shallowMount(component, {
|
||||
propsData,
|
||||
});
|
||||
|
@ -41,15 +41,15 @@ describe('Tags List', () => {
|
|||
|
||||
describe('delete button', () => {
|
||||
it.each`
|
||||
inputTags | isDesktop | isVisible
|
||||
${tags} | ${true} | ${true}
|
||||
${tags} | ${false} | ${false}
|
||||
${readOnlyTags} | ${true} | ${false}
|
||||
inputTags | isMobile | isVisible
|
||||
${tags} | ${false} | ${true}
|
||||
${tags} | ${true} | ${false}
|
||||
${readOnlyTags} | ${false} | ${false}
|
||||
${readOnlyTags} | ${true} | ${false}
|
||||
`(
|
||||
'is $isVisible that delete button exists when tags is $inputTags and isDesktop is $isDesktop',
|
||||
({ inputTags, isDesktop, isVisible }) => {
|
||||
mountComponent({ tags: inputTags, isDesktop });
|
||||
'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile',
|
||||
({ inputTags, isMobile, isVisible }) => {
|
||||
mountComponent({ tags: inputTags, isMobile });
|
||||
|
||||
expect(findDeleteButton().exists()).toBe(isVisible);
|
||||
},
|
||||
|
@ -110,12 +110,6 @@ describe('Tags List', () => {
|
|||
|
||||
expect(rows.at(0).attributes()).toMatchObject({
|
||||
first: 'true',
|
||||
isdesktop: 'true',
|
||||
});
|
||||
|
||||
// The list has only two tags and for some reasons .at(-1) does not work
|
||||
expect(rows.at(1).attributes()).toMatchObject({
|
||||
isdesktop: 'true',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -124,7 +124,7 @@ describe('Details Page', () => {
|
|||
|
||||
it('has the correct props bound', () => {
|
||||
expect(findTagsList().props()).toMatchObject({
|
||||
isDesktop: true,
|
||||
isMobile: false,
|
||||
tags: store.state.tags,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlDeprecatedDropdownItem } from '@gitlab/ui';
|
||||
import { GlDropdownItem } from '@gitlab/ui';
|
||||
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
|
||||
|
||||
const commits = [
|
||||
|
@ -39,7 +39,7 @@ describe('Commits message dropdown component', () => {
|
|||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem);
|
||||
const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
|
||||
const findFirstDropdownElement = () => findDropdownElements().at(0);
|
||||
|
||||
it('should have 3 elements in dropdown list', () => {
|
||||
|
|
|
@ -4,8 +4,9 @@ require 'spec_helper'
|
|||
|
||||
RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
|
||||
describe '#find' do
|
||||
let(:issue) { create(:issue) }
|
||||
let(:user) { create(:user) }
|
||||
let_it_be(:issue) { create(:issue) }
|
||||
let_it_be(:other_user) { create(:user) }
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
it 'finds a model by id' do
|
||||
issue_result = described_class.new(Issue, issue.id).find
|
||||
|
@ -16,15 +17,25 @@ RSpec.describe Gitlab::Graphql::Loaders::BatchModelLoader do
|
|||
end
|
||||
|
||||
it 'only queries once per model' do
|
||||
other_user = create(:user)
|
||||
user
|
||||
issue
|
||||
|
||||
expect do
|
||||
[described_class.new(User, other_user.id).find,
|
||||
described_class.new(User, user.id).find,
|
||||
described_class.new(Issue, issue.id).find].map(&:sync)
|
||||
end.not_to exceed_query_limit(2)
|
||||
end
|
||||
|
||||
it 'does not force values unnecessarily' do
|
||||
expect do
|
||||
a = described_class.new(User, user.id).find
|
||||
b = described_class.new(Issue, issue.id).find
|
||||
|
||||
b.sync
|
||||
|
||||
c = described_class.new(User, other_user.id).find
|
||||
|
||||
a.sync
|
||||
c.sync
|
||||
end.not_to exceed_query_limit(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,6 +4,8 @@ require 'spec_helper'
|
|||
require "rack/test"
|
||||
|
||||
RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
|
||||
include GitHttpHelpers
|
||||
|
||||
let(:null_byte) { "\u0000" }
|
||||
let(:escaped_null_byte) { "%00" }
|
||||
let(:invalid_string) { "mal\xC0formed" }
|
||||
|
@ -57,6 +59,22 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
|
|||
end
|
||||
end
|
||||
|
||||
context 'in authorization headers' do
|
||||
let(:problematic_input) { null_byte }
|
||||
|
||||
it 'rejects problematic input in the password' do
|
||||
env = env_for.merge(auth_env("username", "password#{problematic_input}encoded", nil))
|
||||
|
||||
expect(subject.call(env)).to eq error_400
|
||||
end
|
||||
|
||||
it 'rejects problematic input in the password' do
|
||||
env = env_for.merge(auth_env("username#{problematic_input}", "password#{problematic_input}encoded", nil))
|
||||
|
||||
expect(subject.call(env)).to eq error_400
|
||||
end
|
||||
end
|
||||
|
||||
context 'in params' do
|
||||
shared_examples_for 'checks params' do
|
||||
it 'rejects bad params in a top level param' do
|
||||
|
@ -86,6 +104,12 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
|
|||
|
||||
expect(subject.call(env)).to eq error_400
|
||||
end
|
||||
end
|
||||
|
||||
context 'with null byte' do
|
||||
let(:problematic_input) { null_byte }
|
||||
|
||||
it_behaves_like 'checks params'
|
||||
|
||||
it "gives up and does not reject too deeply nested params" do
|
||||
env = env_for(name: [
|
||||
|
@ -98,12 +122,6 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with null byte' do
|
||||
it_behaves_like 'checks params' do
|
||||
let(:problematic_input) { null_byte }
|
||||
end
|
||||
end
|
||||
|
||||
context 'with malformed strings' do
|
||||
it_behaves_like 'checks params' do
|
||||
let(:problematic_input) { invalid_string }
|
||||
|
@ -124,4 +142,10 @@ RSpec.describe Gitlab::Middleware::HandleMalformedStrings do
|
|||
expect(subject.call(env)).not_to eq error_400
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not modify the env' do
|
||||
env = env_for
|
||||
|
||||
expect { subject.call(env) }.not_to change { env }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -168,36 +168,6 @@ RSpec.describe Gitlab::UsageDataCounters::IssueActivityUniqueCounter, :clean_git
|
|||
end
|
||||
end
|
||||
|
||||
context 'for Issue added to epic actions' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_ADDED_TO_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
described_class.track_issue_added_to_epic_action(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for Issue removed from epic actions' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_REMOVED_FROM_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
described_class.track_issue_removed_from_epic_action(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for Issue changed epic actions' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_CHANGED_EPIC}
|
||||
|
||||
def track_action(params)
|
||||
described_class.track_issue_changed_epic_action(**params)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for Issue designs added actions' do
|
||||
it_behaves_like 'a tracked issue edit event' do
|
||||
let(:action) { described_class::ISSUE_DESIGNS_ADDED }
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User sends malformed strings as params' do
|
||||
RSpec.describe 'User sends malformed strings' do
|
||||
include GitHttpHelpers
|
||||
|
||||
let(:null_byte) { "\u0000" }
|
||||
let(:invalid_string) { "mal\xC0formed" }
|
||||
|
||||
|
@ -17,4 +19,10 @@ RSpec.describe 'User sends malformed strings as params' do
|
|||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
|
||||
it 'raises a 400 error with null bytes in the auth headers' do
|
||||
clone_get("project/path", user: "hello#{null_byte}", password: "nothing to see")
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe 'projects/settings/operations/show' do
|
|||
end
|
||||
|
||||
before_all do
|
||||
project.add_reporter(user)
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
before do
|
||||
|
|
Loading…
Reference in a new issue