Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
c37dd28c4a
commit
5e450e9022
101 changed files with 1260 additions and 660 deletions
24
CHANGELOG.md
24
CHANGELOG.md
|
@ -2,6 +2,30 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 13.9.1 (2021-02-23)
|
||||
|
||||
### Fixed (6 changes, 1 of them is from the community)
|
||||
|
||||
- Send SIGINT instead of SIGQUIT to puma. !54446 (Jörg Behrmann @behrmann)
|
||||
- Reset description template names cache key to reload an updated templates structure. !54614
|
||||
- Restore missing horizontal scrollbar on issue boards. !54634
|
||||
- Fix keep latest artifacts checkbox being always disabled. !54669
|
||||
- Fix Metric tab not showing up on operations page. !54736
|
||||
- Fix S3 object storage failing when endpoint is not specified. !54868
|
||||
|
||||
### Changed (1 change)
|
||||
|
||||
- Updates authorization for linting endpoint. !54492
|
||||
|
||||
### Performance (1 change)
|
||||
|
||||
- Fix N+1 SQL regression in exporting issues to CSV. !54287
|
||||
|
||||
### Other (1 change)
|
||||
|
||||
- Fix creating the idx_on_issues_where_service_desk_reply_to_is_not_null index before the post migration. !54346
|
||||
|
||||
|
||||
## 13.9.0 (2021-02-22)
|
||||
|
||||
### Security (1 change)
|
||||
|
|
|
@ -1 +1 @@
|
|||
3f702fcbdb3481eade8c18815a61f97d01886cef
|
||||
40d46031c0a188ece3ed2574559321baf316a48c
|
||||
|
|
|
@ -7,15 +7,12 @@ import {
|
|||
GlSearchBoxByType,
|
||||
GlTooltipDirective as GlTooltip,
|
||||
} from '@gitlab/ui';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { cloneDeep, isEqual } from 'lodash';
|
||||
import Vue from 'vue';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { s__, __ } from '~/locale';
|
||||
import {
|
||||
getMappingData,
|
||||
getPayloadFields,
|
||||
transformForSave,
|
||||
} from '../utils/mapping_transformations';
|
||||
import { mappingFields } from '../constants';
|
||||
import { getMappingData, transformForSave } from '../utils/mapping_transformations';
|
||||
|
||||
export const i18n = {
|
||||
columns: {
|
||||
|
@ -33,6 +30,7 @@ export const i18n = {
|
|||
|
||||
export default {
|
||||
i18n,
|
||||
mappingFields,
|
||||
components: {
|
||||
GlIcon,
|
||||
GlFormInput,
|
||||
|
@ -73,18 +71,15 @@ export default {
|
|||
};
|
||||
},
|
||||
computed: {
|
||||
payloadFields() {
|
||||
return getPayloadFields(this.parsedPayload);
|
||||
},
|
||||
mappingData() {
|
||||
return getMappingData(this.gitlabFields, this.payloadFields, this.savedMapping);
|
||||
return getMappingData(this.gitlabFields, this.parsedPayload, this.savedMapping);
|
||||
},
|
||||
hasFallbackColumn() {
|
||||
return this.gitlabFields.some(({ numberOfFallbacks }) => Boolean(numberOfFallbacks));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setMapping(gitlabKey, mappingKey, valueKey) {
|
||||
setMapping(gitlabKey, mappingKey, valueKey = mappingFields.mapping) {
|
||||
const fieldIndex = this.gitlabFields.findIndex((field) => field.name === gitlabKey);
|
||||
const updatedField = { ...this.gitlabFields[fieldIndex], ...{ [valueKey]: mappingKey } };
|
||||
Vue.set(this.gitlabFields, fieldIndex, updatedField);
|
||||
|
@ -100,11 +95,11 @@ export default {
|
|||
return fields.filter((field) => field.label.toLowerCase().includes(search));
|
||||
},
|
||||
isSelected(fieldValue, mapping) {
|
||||
return fieldValue === mapping;
|
||||
return isEqual(fieldValue, mapping);
|
||||
},
|
||||
selectedValue(name) {
|
||||
selectedValue(mapping) {
|
||||
return (
|
||||
this.payloadFields.find((item) => item.name === name)?.label ||
|
||||
this.parsedPayload.find((item) => isEqual(item.path, mapping))?.label ||
|
||||
this.$options.i18n.makeSelection
|
||||
);
|
||||
},
|
||||
|
@ -150,7 +145,7 @@ export default {
|
|||
:key="gitlabField.name"
|
||||
class="gl-display-table-row"
|
||||
>
|
||||
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p gl-vertical-align-middle">
|
||||
<div class="gl-display-table-cell gl-py-3 gl-pr-3 gl-w-30p gl-vertical-align-middle">
|
||||
<gl-form-input
|
||||
aria-labelledby="gitlabFieldsHeader"
|
||||
disabled
|
||||
|
@ -164,7 +159,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-table-cell gl-py-3 gl-pr-3 w-30p gl-vertical-align-middle">
|
||||
<div class="gl-display-table-cell gl-py-3 gl-pr-3 gl-w-30p gl-vertical-align-middle">
|
||||
<gl-dropdown
|
||||
:disabled="!gitlabField.mappingFields.length"
|
||||
aria-labelledby="parsedFieldsHeader"
|
||||
|
@ -175,10 +170,10 @@ export default {
|
|||
<gl-search-box-by-type @input="setSearchTerm($event, 'searchTerm', gitlabField.name)" />
|
||||
<gl-dropdown-item
|
||||
v-for="mappingField in filterFields(gitlabField.searchTerm, gitlabField.mappingFields)"
|
||||
:key="`${mappingField.name}__mapping`"
|
||||
:is-checked="isSelected(gitlabField.mapping, mappingField.name)"
|
||||
:key="`${mappingField.path}__mapping`"
|
||||
:is-checked="isSelected(gitlabField.mapping, mappingField.path)"
|
||||
is-check-item
|
||||
@click="setMapping(gitlabField.name, mappingField.name, 'mapping')"
|
||||
@click="setMapping(gitlabField.name, mappingField.path)"
|
||||
>
|
||||
{{ mappingField.label }}
|
||||
</gl-dropdown-item>
|
||||
|
@ -188,7 +183,7 @@ export default {
|
|||
</gl-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="gl-display-table-cell gl-py-3 w-30p">
|
||||
<div class="gl-display-table-cell gl-py-3 gl-w-30p">
|
||||
<gl-dropdown
|
||||
v-if="Boolean(gitlabField.numberOfFallbacks)"
|
||||
:disabled="!gitlabField.mappingFields.length"
|
||||
|
@ -205,10 +200,12 @@ export default {
|
|||
gitlabField.fallbackSearchTerm,
|
||||
gitlabField.mappingFields,
|
||||
)"
|
||||
:key="`${mappingField.name}__fallback`"
|
||||
:is-checked="isSelected(gitlabField.fallback, mappingField.name)"
|
||||
:key="`${mappingField.path}__fallback`"
|
||||
:is-checked="isSelected(gitlabField.fallback, mappingField.path)"
|
||||
is-check-item
|
||||
@click="setMapping(gitlabField.name, mappingField.name, 'fallback')"
|
||||
@click="
|
||||
setMapping(gitlabField.name, mappingField.path, $options.mappingFields.fallback)
|
||||
"
|
||||
>
|
||||
{{ mappingField.label }}
|
||||
</gl-dropdown-item>
|
||||
|
|
|
@ -120,14 +120,17 @@ export default {
|
|||
const { category, action } = trackAlertIntegrationsViewsOptions;
|
||||
Tracking.event(category, action);
|
||||
},
|
||||
setIntegrationToDelete({ name, id }) {
|
||||
this.integrationToDelete.id = id;
|
||||
this.integrationToDelete.name = name;
|
||||
setIntegrationToDelete(integration) {
|
||||
this.integrationToDelete = integration;
|
||||
},
|
||||
deleteIntegration() {
|
||||
this.$emit('delete-integration', { id: this.integrationToDelete.id });
|
||||
const { id, type } = this.integrationToDelete;
|
||||
this.$emit('delete-integration', { id, type });
|
||||
this.integrationToDelete = { ...integrationToDeleteDefault };
|
||||
},
|
||||
editIntegration({ id, type }) {
|
||||
this.$emit('edit-integration', { id, type });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -169,7 +172,7 @@ export default {
|
|||
|
||||
<template #cell(actions)="{ item }">
|
||||
<gl-button-group class="gl-ml-3">
|
||||
<gl-button icon="pencil" @click="$emit('edit-integration', { id: item.id })" />
|
||||
<gl-button icon="pencil" @click="editIntegration(item)" />
|
||||
<gl-button
|
||||
v-gl-modal.deleteIntegration
|
||||
:disabled="item.type === $options.typeSet.prometheus"
|
||||
|
|
|
@ -12,6 +12,8 @@ import {
|
|||
GlModalDirective,
|
||||
GlToggle,
|
||||
} from '@gitlab/ui';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
import { isEmpty, omit } from 'lodash';
|
||||
import { s__ } from '~/locale';
|
||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
@ -22,12 +24,9 @@ import {
|
|||
typeSet,
|
||||
} from '../constants';
|
||||
import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
|
||||
import parseSamplePayloadQuery from '../graphql/queries/parse_sample_payload.query.graphql';
|
||||
import MappingBuilder from './alert_mapping_builder.vue';
|
||||
import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue';
|
||||
// Mocks will be removed when integrating with BE is ready
|
||||
// data format is defined and will be the same as mocked (maybe with some minor changes)
|
||||
// feature rollout plan - https://gitlab.com/gitlab-org/gitlab/-/issues/262707#note_442529171
|
||||
import mockedCustomMapping from './mocks/parsedMapping.json';
|
||||
|
||||
export const i18n = {
|
||||
integrationFormSteps: {
|
||||
|
@ -92,7 +91,6 @@ export const i18n = {
|
|||
};
|
||||
|
||||
export default {
|
||||
integrationTypes,
|
||||
placeholders: {
|
||||
prometheus: targetPrometheusUrlPlaceholder,
|
||||
},
|
||||
|
@ -128,6 +126,9 @@ export default {
|
|||
multiIntegrations: {
|
||||
default: false,
|
||||
},
|
||||
projectPath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
loading: {
|
||||
|
@ -151,18 +152,19 @@ export default {
|
|||
},
|
||||
data() {
|
||||
return {
|
||||
selectedIntegration: integrationTypes[0].value,
|
||||
integrationTypesOptions: Object.values(integrationTypes),
|
||||
selectedIntegration: integrationTypes.none.value,
|
||||
active: false,
|
||||
formVisible: false,
|
||||
integrationTestPayload: {
|
||||
json: null,
|
||||
error: null,
|
||||
},
|
||||
resetSamplePayloadConfirmed: false,
|
||||
customMapping: null,
|
||||
resetPayloadAndMappingConfirmed: false,
|
||||
mapping: [],
|
||||
parsingPayload: false,
|
||||
currentIntegration: null,
|
||||
parsedPayload: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -210,17 +212,11 @@ export default {
|
|||
this.alertFields?.length
|
||||
);
|
||||
},
|
||||
parsedSamplePayload() {
|
||||
return this.customMapping?.samplePayload?.payloadAlerFields?.nodes;
|
||||
},
|
||||
savedMapping() {
|
||||
return this.customMapping?.storedMapping?.nodes;
|
||||
},
|
||||
hasSamplePayload() {
|
||||
return Boolean(this.customMapping?.samplePayload);
|
||||
return this.isValidNonEmptyJSON(this.currentIntegration?.payloadExample);
|
||||
},
|
||||
canEditPayload() {
|
||||
return this.hasSamplePayload && !this.resetSamplePayloadConfirmed;
|
||||
return this.hasSamplePayload && !this.resetPayloadAndMappingConfirmed;
|
||||
},
|
||||
isResetAuthKeyDisabled() {
|
||||
return !this.active && !this.integrationForm.token !== '';
|
||||
|
@ -240,25 +236,52 @@ export default {
|
|||
isSelectDisabled() {
|
||||
return this.currentIntegration !== null || !this.canAddIntegration;
|
||||
},
|
||||
savedMapping() {
|
||||
return this.mapping;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentIntegration(val) {
|
||||
if (val === null) {
|
||||
return this.reset();
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
this.selectedIntegration = val.type;
|
||||
this.active = val.active;
|
||||
if (val.type === typeSet.http && this.showMappingBuilder) this.getIntegrationMapping(val.id);
|
||||
return this.integrationTypeSelect();
|
||||
const { type, active, payloadExample, payloadAlertFields, payloadAttributeMappings } = val;
|
||||
this.selectedIntegration = type;
|
||||
this.active = active;
|
||||
|
||||
if (type === typeSet.prometheus) {
|
||||
this.integrationTestPayload.json = null;
|
||||
}
|
||||
|
||||
if (type === typeSet.http && this.showMappingBuilder) {
|
||||
this.parsedPayload = payloadAlertFields;
|
||||
this.integrationTestPayload.json = this.isValidNonEmptyJSON(payloadExample)
|
||||
? payloadExample
|
||||
: null;
|
||||
const mapping = payloadAttributeMappings.map((mappingItem) =>
|
||||
omit(mappingItem, '__typename'),
|
||||
);
|
||||
this.updateMapping(mapping);
|
||||
}
|
||||
this.toggleFormVisibility();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
integrationTypeSelect() {
|
||||
if (this.selectedIntegration === integrationTypes[0].value) {
|
||||
this.formVisible = false;
|
||||
} else {
|
||||
this.formVisible = true;
|
||||
isValidNonEmptyJSON(JSONString) {
|
||||
if (JSONString) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(JSONString);
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
}
|
||||
if (parsed) return !isEmpty(parsed);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
toggleFormVisibility() {
|
||||
this.formVisible = this.selectedIntegration !== integrationTypes.none.value;
|
||||
},
|
||||
submitWithTestPayload() {
|
||||
this.$emit('set-test-alert-payload', this.testAlertPayload);
|
||||
|
@ -269,20 +292,15 @@ export default {
|
|||
const customMappingVariables = this.glFeatures.multipleHttpIntegrationsCustomMapping
|
||||
? {
|
||||
payloadAttributeMappings: this.mapping,
|
||||
payloadExample: this.integrationTestPayload.json,
|
||||
payloadExample: this.integrationTestPayload.json || '{}',
|
||||
}
|
||||
: {};
|
||||
|
||||
const variables =
|
||||
this.selectedIntegration === typeSet.http
|
||||
? {
|
||||
name,
|
||||
active: this.active,
|
||||
...customMappingVariables,
|
||||
}
|
||||
? { name, active: this.active, ...customMappingVariables }
|
||||
: { apiUrl, active: this.active };
|
||||
const integrationPayload = { type: this.selectedIntegration, variables };
|
||||
|
||||
if (this.currentIntegration) {
|
||||
return this.$emit('update-integration', integrationPayload);
|
||||
}
|
||||
|
@ -291,11 +309,12 @@ export default {
|
|||
return this.$emit('create-new-integration', integrationPayload);
|
||||
},
|
||||
reset() {
|
||||
this.selectedIntegration = integrationTypes[0].value;
|
||||
this.integrationTypeSelect();
|
||||
this.selectedIntegration = integrationTypes.none.value;
|
||||
this.toggleFormVisibility();
|
||||
this.resetPayloadAndMapping();
|
||||
|
||||
if (this.currentIntegration) {
|
||||
return this.$emit('clear-current-integration');
|
||||
return this.$emit('clear-current-integration', { type: this.currentIntegration.type });
|
||||
}
|
||||
|
||||
return this.resetFormValues();
|
||||
|
@ -332,35 +351,40 @@ export default {
|
|||
}
|
||||
},
|
||||
parseMapping() {
|
||||
// TODO: replace with real BE mutation when ready;
|
||||
this.parsingPayload = true;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => resolve(mockedCustomMapping), 1000);
|
||||
})
|
||||
.then((res) => {
|
||||
const mapping = { ...res };
|
||||
delete mapping.storedMapping;
|
||||
this.customMapping = res;
|
||||
this.integrationTestPayload.json = res?.samplePayload.body;
|
||||
this.resetSamplePayloadConfirmed = false;
|
||||
return this.$apollo
|
||||
.query({
|
||||
query: parseSamplePayloadQuery,
|
||||
variables: { projectPath: this.projectPath, payload: this.integrationTestPayload.json },
|
||||
})
|
||||
.then(
|
||||
({
|
||||
data: {
|
||||
project: { alertManagementPayloadFields },
|
||||
},
|
||||
}) => {
|
||||
this.parsedPayload = alertManagementPayloadFields;
|
||||
this.resetPayloadAndMappingConfirmed = false;
|
||||
|
||||
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg);
|
||||
this.$toast.show(this.$options.i18n.integrationFormSteps.step4.payloadParsedSucessMsg);
|
||||
},
|
||||
)
|
||||
.catch(({ message }) => {
|
||||
this.integrationTestPayload.error = message;
|
||||
})
|
||||
.finally(() => {
|
||||
this.parsingPayload = false;
|
||||
});
|
||||
},
|
||||
getIntegrationMapping() {
|
||||
// TODO: replace with real BE mutation when ready;
|
||||
return Promise.resolve(mockedCustomMapping).then((res) => {
|
||||
this.customMapping = res;
|
||||
this.integrationTestPayload.json = res?.samplePayload.body;
|
||||
});
|
||||
},
|
||||
updateMapping(mapping) {
|
||||
this.mapping = mapping;
|
||||
},
|
||||
resetPayloadAndMapping() {
|
||||
this.resetPayloadAndMappingConfirmed = true;
|
||||
this.parsedPayload = [];
|
||||
this.updateMapping([]);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -377,8 +401,8 @@ export default {
|
|||
v-model="selectedIntegration"
|
||||
:disabled="isSelectDisabled"
|
||||
class="mw-100"
|
||||
:options="$options.integrationTypes"
|
||||
@change="integrationTypeSelect"
|
||||
:options="integrationTypesOptions"
|
||||
@change="toggleFormVisibility"
|
||||
/>
|
||||
|
||||
<div v-if="!canAddIntegration" class="gl-my-4" data-testid="multi-integrations-not-supported">
|
||||
|
@ -551,7 +575,7 @@ export default {
|
|||
:title="$options.i18n.integrationFormSteps.step4.resetHeader"
|
||||
:ok-title="$options.i18n.integrationFormSteps.step4.resetOk"
|
||||
ok-variant="danger"
|
||||
@ok="resetSamplePayloadConfirmed = true"
|
||||
@ok="resetPayloadAndMapping"
|
||||
>
|
||||
{{ $options.i18n.integrationFormSteps.step4.resetBody }}
|
||||
</gl-modal>
|
||||
|
@ -566,7 +590,7 @@ export default {
|
|||
>
|
||||
<span>{{ $options.i18n.integrationFormSteps.step5.intro }}</span>
|
||||
<mapping-builder
|
||||
:parsed-payload="parsedSamplePayload"
|
||||
:parsed-payload="parsedPayload"
|
||||
:saved-mapping="savedMapping"
|
||||
:alert-fields="alertFields"
|
||||
@onMappingUpdate="updateMapping"
|
||||
|
|
|
@ -8,15 +8,18 @@ import createPrometheusIntegrationMutation from '../graphql/mutations/create_pro
|
|||
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 updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql';
|
||||
import updateCurrentHttpIntegrationMutation from '../graphql/mutations/update_current_http_integration.mutation.graphql';
|
||||
import updateCurrentPrometheusIntegrationMutation from '../graphql/mutations/update_current_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 getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql';
|
||||
import getHttpIntegrationsQuery from '../graphql/queries/get_http_integrations.query.graphql';
|
||||
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
|
||||
import service from '../services';
|
||||
import {
|
||||
updateStoreAfterIntegrationDelete,
|
||||
updateStoreAfterIntegrationAdd,
|
||||
updateStoreAfterHttpIntegrationAdd,
|
||||
} from '../utils/cache_updates';
|
||||
import {
|
||||
DELETE_INTEGRATION_ERROR,
|
||||
|
@ -84,6 +87,28 @@ export default {
|
|||
createFlash({ message: err });
|
||||
},
|
||||
},
|
||||
// TODO: we'll need to update the logic to request specific http integration by its id on edit
|
||||
// when BE adds support for it https://gitlab.com/gitlab-org/gitlab/-/issues/321674
|
||||
// currently the request for ALL http integrations is made and on specific integration edit we search it in the list
|
||||
httpIntegrations: {
|
||||
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
|
||||
query: getHttpIntegrationsQuery,
|
||||
variables() {
|
||||
return {
|
||||
projectPath: this.projectPath,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
const { alertManagementHttpIntegrations: { nodes: list = [] } = {} } = data.project || {};
|
||||
|
||||
return {
|
||||
list,
|
||||
};
|
||||
},
|
||||
error(err) {
|
||||
createFlash({ message: err });
|
||||
},
|
||||
},
|
||||
currentIntegration: {
|
||||
query: getCurrentIntegrationQuery,
|
||||
},
|
||||
|
@ -93,6 +118,7 @@ export default {
|
|||
isUpdating: false,
|
||||
testAlertPayload: null,
|
||||
integrations: {},
|
||||
httpIntegrations: {},
|
||||
currentIntegration: null,
|
||||
};
|
||||
},
|
||||
|
@ -105,22 +131,28 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
isHttp(type) {
|
||||
return type === typeSet.http;
|
||||
},
|
||||
createNewIntegration({ type, variables }) {
|
||||
const { projectPath } = this;
|
||||
|
||||
const isHttp = this.isHttp(type);
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation:
|
||||
type === this.$options.typeSet.http
|
||||
? createHttpIntegrationMutation
|
||||
: createPrometheusIntegrationMutation,
|
||||
mutation: isHttp ? createHttpIntegrationMutation : createPrometheusIntegrationMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
projectPath,
|
||||
},
|
||||
update(store, { data }) {
|
||||
updateStoreAfterIntegrationAdd(store, getIntegrationsQuery, data, { projectPath });
|
||||
if (isHttp) {
|
||||
updateStoreAfterHttpIntegrationAdd(store, getHttpIntegrationsQuery, data, {
|
||||
projectPath,
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
|
||||
|
@ -157,10 +189,9 @@ export default {
|
|||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation:
|
||||
type === this.$options.typeSet.http
|
||||
? updateHttpIntegrationMutation
|
||||
: updatePrometheusIntegrationMutation,
|
||||
mutation: this.isHttp(type)
|
||||
? updateHttpIntegrationMutation
|
||||
: updatePrometheusIntegrationMutation,
|
||||
variables: {
|
||||
...variables,
|
||||
id: this.currentIntegration.id,
|
||||
|
@ -176,7 +207,7 @@ export default {
|
|||
return this.validateAlertPayload();
|
||||
}
|
||||
|
||||
this.clearCurrentIntegration();
|
||||
this.clearCurrentIntegration({ type });
|
||||
|
||||
return createFlash({
|
||||
message: this.$options.i18n.changesSaved,
|
||||
|
@ -195,16 +226,13 @@ export default {
|
|||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation:
|
||||
type === this.$options.typeSet.http
|
||||
? resetHttpTokenMutation
|
||||
: resetPrometheusTokenMutation,
|
||||
mutation: this.isHttp(type) ? resetHttpTokenMutation : resetPrometheusTokenMutation,
|
||||
variables,
|
||||
})
|
||||
.then(
|
||||
({ data: { httpIntegrationResetToken, prometheusIntegrationResetToken } = {} } = {}) => {
|
||||
const error =
|
||||
httpIntegrationResetToken?.errors[0] || prometheusIntegrationResetToken?.errors[0];
|
||||
const [error] =
|
||||
httpIntegrationResetToken?.errors || prometheusIntegrationResetToken?.errors;
|
||||
if (error) {
|
||||
return createFlash({ message: error });
|
||||
}
|
||||
|
@ -214,10 +242,10 @@ export default {
|
|||
prometheusIntegrationResetToken?.integration;
|
||||
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCurrentIntergrationMutation,
|
||||
variables: {
|
||||
...integration,
|
||||
},
|
||||
mutation: this.isHttp(type)
|
||||
? updateCurrentHttpIntegrationMutation
|
||||
: updateCurrentPrometheusIntegrationMutation,
|
||||
variables: integration,
|
||||
});
|
||||
|
||||
return createFlash({
|
||||
|
@ -233,33 +261,30 @@ export default {
|
|||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
editIntegration({ id }) {
|
||||
const currentIntegration = this.integrations.list.find(
|
||||
(integration) => integration.id === id,
|
||||
);
|
||||
editIntegration({ id, type }) {
|
||||
let currentIntegration = this.integrations.list.find((integration) => integration.id === id);
|
||||
if (this.isHttp(type)) {
|
||||
const httpIntegrationMappingData = this.httpIntegrations.list.find(
|
||||
(integration) => integration.id === id,
|
||||
);
|
||||
currentIntegration = { ...currentIntegration, ...httpIntegrationMappingData };
|
||||
}
|
||||
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCurrentIntergrationMutation,
|
||||
variables: {
|
||||
id: currentIntegration.id,
|
||||
name: currentIntegration.name,
|
||||
active: currentIntegration.active,
|
||||
token: currentIntegration.token,
|
||||
type: currentIntegration.type,
|
||||
url: currentIntegration.url,
|
||||
apiUrl: currentIntegration.apiUrl,
|
||||
},
|
||||
mutation: this.isHttp(type)
|
||||
? updateCurrentHttpIntegrationMutation
|
||||
: updateCurrentPrometheusIntegrationMutation,
|
||||
variables: currentIntegration,
|
||||
});
|
||||
},
|
||||
deleteIntegration({ id }) {
|
||||
deleteIntegration({ id, type }) {
|
||||
const { projectPath } = this;
|
||||
|
||||
this.isUpdating = true;
|
||||
this.$apollo
|
||||
.mutate({
|
||||
mutation: destroyHttpIntegrationMutation,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
variables: { id },
|
||||
update(store, { data }) {
|
||||
updateStoreAfterIntegrationDelete(store, getIntegrationsQuery, data, { projectPath });
|
||||
},
|
||||
|
@ -269,7 +294,7 @@ export default {
|
|||
if (error) {
|
||||
return createFlash({ message: error });
|
||||
}
|
||||
this.clearCurrentIntegration();
|
||||
this.clearCurrentIntegration({ type });
|
||||
return createFlash({
|
||||
message: this.$options.i18n.integrationRemoved,
|
||||
type: FLASH_TYPES.SUCCESS,
|
||||
|
@ -282,9 +307,11 @@ export default {
|
|||
this.isUpdating = false;
|
||||
});
|
||||
},
|
||||
clearCurrentIntegration() {
|
||||
clearCurrentIntegration({ type }) {
|
||||
this.$apollo.mutate({
|
||||
mutation: updateCurrentIntergrationMutation,
|
||||
mutation: this.isHttp(type)
|
||||
? updateCurrentHttpIntegrationMutation
|
||||
: updateCurrentPrometheusIntegrationMutation,
|
||||
variables: {},
|
||||
});
|
||||
},
|
||||
|
|
|
@ -1,95 +0,0 @@
|
|||
{
|
||||
"samplePayload": {
|
||||
"body": "{\n \"dashboardId\":1,\n \"evalMatches\":[\n {\n \"value\":1,\n \"metric\":\"Count\",\n \"tags\":{}\n }\n ],\n \"imageUrl\":\"https://grafana.com/static/assets/img/blog/mixed_styles.png\",\n \"message\":\"Notification Message\",\n \"orgId\":1,\n \"panelId\":2,\n \"ruleId\":1,\n \"ruleName\":\"Panel Title alert\",\n \"ruleUrl\":\"http://localhost:3000/d/hZ7BuVbWz/test-dashboard?fullscreen\\u0026edit\\u0026tab=alert\\u0026panelId=2\\u0026orgId=1\",\n \"state\":\"alerting\",\n \"tags\":{\n \"tag name\":\"tag value\"\n },\n \"title\":\"[Alerting] Panel Title alert\"\n}\n",
|
||||
"payloadAlerFields": {
|
||||
"nodes": [
|
||||
{
|
||||
"path": ["dashboardId"],
|
||||
"label": "Dashboard Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["evalMatches"],
|
||||
"label": "Eval Matches",
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"path": ["createdAt"],
|
||||
"label": "Created At",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"path": ["imageUrl"],
|
||||
"label": "Image Url",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["message"],
|
||||
"label": "Message",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["orgId"],
|
||||
"label": "Org Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["panelId"],
|
||||
"label": "Panel Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["ruleId"],
|
||||
"label": "Rule Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["ruleName"],
|
||||
"label": "Rule Name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["ruleUrl"],
|
||||
"label": "Rule Url",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["state"],
|
||||
"label": "State",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["title"],
|
||||
"label": "Title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": ["tags", "tag"],
|
||||
"label": "Tags",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"storedMapping": {
|
||||
"nodes": [
|
||||
{
|
||||
"alertFieldName": "title",
|
||||
"payloadAlertPaths": "title",
|
||||
"fallbackAlertPaths": "ruleUrl"
|
||||
},
|
||||
{
|
||||
"alertFieldName": "description",
|
||||
"payloadAlertPaths": "message"
|
||||
},
|
||||
{
|
||||
"alertFieldName": "hosts",
|
||||
"payloadAlertPaths": "evalMatches"
|
||||
},
|
||||
{
|
||||
"alertFieldName": "startTime",
|
||||
"payloadAlertPaths": "createdAt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -40,11 +40,11 @@ export const i18n = {
|
|||
integration: s__('AlertSettings|Integration'),
|
||||
};
|
||||
|
||||
export const integrationTypes = [
|
||||
{ value: '', text: s__('AlertSettings|Select integration type') },
|
||||
{ value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
|
||||
{ value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
|
||||
];
|
||||
export const integrationTypes = {
|
||||
none: { value: '', text: s__('AlertSettings|Select integration type') },
|
||||
http: { value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
|
||||
prometheus: { value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
|
||||
};
|
||||
|
||||
export const typeSet = {
|
||||
http: 'HTTP',
|
||||
|
@ -68,3 +68,8 @@ export const trackAlertIntegrationsViewsOptions = {
|
|||
category: 'Alert Integrations',
|
||||
action: 'view_alert_integrations_list',
|
||||
};
|
||||
|
||||
export const mappingFields = {
|
||||
mapping: 'mapping',
|
||||
fallback: 'fallback',
|
||||
};
|
||||
|
|
|
@ -10,7 +10,18 @@ const resolvers = {
|
|||
Mutation: {
|
||||
updateCurrentIntegration: (
|
||||
_,
|
||||
{ id = null, name, active, token, type, url, apiUrl },
|
||||
{
|
||||
id = null,
|
||||
name,
|
||||
active,
|
||||
token,
|
||||
type,
|
||||
url,
|
||||
apiUrl,
|
||||
payloadExample,
|
||||
payloadAttributeMappings,
|
||||
payloadAlertFields,
|
||||
},
|
||||
{ cache },
|
||||
) => {
|
||||
const sourceData = cache.readQuery({ query: getCurrentIntegrationQuery });
|
||||
|
@ -28,6 +39,9 @@ const resolvers = {
|
|||
type,
|
||||
url,
|
||||
apiUrl,
|
||||
payloadExample,
|
||||
payloadAttributeMappings,
|
||||
payloadAlertFields,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
#import "./integration_item.fragment.graphql"
|
||||
#import "./http_integration_payload_data.fragment.graphql"
|
||||
|
||||
fragment HttpIntegrationItem on AlertManagementHttpIntegration {
|
||||
...IntegrationItem
|
||||
...HttpIntegrationPayloadData
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
fragment HttpIntegrationPayloadData on AlertManagementHttpIntegration {
|
||||
payloadExample
|
||||
payloadAttributeMappings {
|
||||
fieldName
|
||||
path
|
||||
type
|
||||
label
|
||||
}
|
||||
payloadAlertFields {
|
||||
path
|
||||
type
|
||||
label
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
#import "../fragments/integration_item.fragment.graphql"
|
||||
#import "../fragments/http_integration_item.fragment.graphql"
|
||||
|
||||
mutation createHttpIntegration(
|
||||
$projectPath: ID!
|
||||
|
@ -18,7 +18,7 @@ mutation createHttpIntegration(
|
|||
) {
|
||||
errors
|
||||
integration {
|
||||
...IntegrationItem
|
||||
...HttpIntegrationItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#import "../fragments/integration_item.fragment.graphql"
|
||||
#import "../fragments/http_integration_item.fragment.graphql"
|
||||
|
||||
mutation destroyHttpIntegration($id: ID!) {
|
||||
httpIntegrationDestroy(input: { id: $id }) {
|
||||
errors
|
||||
integration {
|
||||
...IntegrationItem
|
||||
...HttpIntegrationItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#import "../fragments/integration_item.fragment.graphql"
|
||||
#import "../fragments/http_integration_item.fragment.graphql"
|
||||
|
||||
mutation resetHttpIntegrationToken($id: ID!) {
|
||||
httpIntegrationResetToken(input: { id: $id }) {
|
||||
errors
|
||||
integration {
|
||||
...IntegrationItem
|
||||
...HttpIntegrationItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
mutation updateCurrentHttpIntegration(
|
||||
$id: String
|
||||
$name: String
|
||||
$active: Boolean
|
||||
$token: String
|
||||
$type: String
|
||||
$url: String
|
||||
$apiUrl: String
|
||||
$payloadExample: JsonString
|
||||
$payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
|
||||
$payloadAlertFields: [AlertManagementPayloadAlertField!]
|
||||
) {
|
||||
updateCurrentIntegration(
|
||||
id: $id
|
||||
name: $name
|
||||
active: $active
|
||||
token: $token
|
||||
type: $type
|
||||
url: $url
|
||||
apiUrl: $apiUrl
|
||||
payloadExample: $payloadExample
|
||||
payloadAttributeMappings: $payloadAttributeMappings
|
||||
payloadAlertFields: $payloadAlertFields
|
||||
) @client
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
mutation updateCurrentIntegration(
|
||||
mutation updateCurrentPrometheusIntegration(
|
||||
$id: String
|
||||
$name: String
|
||||
$active: Boolean
|
||||
|
@ -6,6 +6,7 @@ mutation updateCurrentIntegration(
|
|||
$type: String
|
||||
$url: String
|
||||
$apiUrl: String
|
||||
$samplePayload: String
|
||||
) {
|
||||
updateCurrentIntegration(
|
||||
id: $id
|
||||
|
@ -15,5 +16,6 @@ mutation updateCurrentIntegration(
|
|||
type: $type
|
||||
url: $url
|
||||
apiUrl: $apiUrl
|
||||
samplePayload: $samplePayload
|
||||
) @client
|
||||
}
|
|
@ -1,10 +1,24 @@
|
|||
#import "../fragments/integration_item.fragment.graphql"
|
||||
#import "../fragments/http_integration_item.fragment.graphql"
|
||||
|
||||
mutation updateHttpIntegration($id: ID!, $name: String!, $active: Boolean!) {
|
||||
httpIntegrationUpdate(input: { id: $id, name: $name, active: $active }) {
|
||||
mutation updateHttpIntegration(
|
||||
$id: ID!
|
||||
$name: String!
|
||||
$active: Boolean!
|
||||
$payloadExample: JsonString
|
||||
$payloadAttributeMappings: [AlertManagementPayloadAlertFieldInput!]
|
||||
) {
|
||||
httpIntegrationUpdate(
|
||||
input: {
|
||||
id: $id
|
||||
name: $name
|
||||
active: $active
|
||||
payloadExample: $payloadExample
|
||||
payloadAttributeMappings: $payloadAttributeMappings
|
||||
}
|
||||
) {
|
||||
errors
|
||||
integration {
|
||||
...IntegrationItem
|
||||
...HttpIntegrationItem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
#import "../fragments/http_integration_payload_data.fragment.graphql"
|
||||
|
||||
# TODO: this query need to accept http integration id to request a sepcific integration
|
||||
query getHttpIntegrations($projectPath: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
alertManagementHttpIntegrations {
|
||||
nodes {
|
||||
id
|
||||
...HttpIntegrationPayloadData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
query parsePayloadFields($projectPath: ID!, $payload: String!) {
|
||||
project(fullPath: $projectPath) {
|
||||
alertManagementPayloadFields(payloadExample: $payload) {
|
||||
path
|
||||
label
|
||||
type
|
||||
}
|
||||
}
|
||||
}
|
|
@ -60,6 +60,32 @@ const addIntegrationToStore = (
|
|||
});
|
||||
};
|
||||
|
||||
const addHttpIntegrationToStore = (store, query, { httpIntegrationCreate }, variables) => {
|
||||
const integration = httpIntegrationCreate?.integration;
|
||||
if (!integration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceData = store.readQuery({
|
||||
query,
|
||||
variables,
|
||||
});
|
||||
|
||||
const data = produce(sourceData, (draftData) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
draftData.project.alertManagementHttpIntegrations.nodes = [
|
||||
integration,
|
||||
...draftData.project.alertManagementHttpIntegrations.nodes,
|
||||
];
|
||||
});
|
||||
|
||||
store.writeQuery({
|
||||
query,
|
||||
variables,
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
const onError = (data, message) => {
|
||||
createFlash({ message });
|
||||
throw new Error(data.errors);
|
||||
|
@ -82,3 +108,11 @@ export const updateStoreAfterIntegrationAdd = (store, query, data, variables) =>
|
|||
addIntegrationToStore(store, query, data, variables);
|
||||
}
|
||||
};
|
||||
|
||||
export const updateStoreAfterHttpIntegrationAdd = (store, query, data, variables) => {
|
||||
if (hasErrors(data)) {
|
||||
onError(data, ADD_INTEGRATION_ERROR);
|
||||
} else {
|
||||
addHttpIntegrationToStore(store, query, data, variables);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { isEqual } from 'lodash';
|
||||
/**
|
||||
* Given data for GitLab alert fields, parsed payload fields data and previously stored mapping (if any)
|
||||
* creates an object in a form convenient to build UI && interact with it
|
||||
|
@ -10,16 +11,19 @@
|
|||
export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
|
||||
return gitlabFields.map((gitlabField) => {
|
||||
// find fields from payload that match gitlab alert field by type
|
||||
const mappingFields = payloadFields.filter(({ type }) => gitlabField.types.includes(type));
|
||||
const mappingFields = payloadFields.filter(({ type }) =>
|
||||
gitlabField.types.includes(type.toLowerCase()),
|
||||
);
|
||||
|
||||
// find the mapping that was previously stored
|
||||
const foundMapping = savedMapping.find(({ fieldName }) => fieldName === gitlabField.name);
|
||||
|
||||
const { fallbackAlertPaths, payloadAlertPaths } = foundMapping || {};
|
||||
const foundMapping = savedMapping.find(
|
||||
({ fieldName }) => fieldName.toLowerCase() === gitlabField.name,
|
||||
);
|
||||
const { path: mapping, fallbackPath: fallback } = foundMapping || {};
|
||||
|
||||
return {
|
||||
mapping: payloadAlertPaths,
|
||||
fallback: fallbackAlertPaths,
|
||||
mapping,
|
||||
fallback,
|
||||
searchTerm: '',
|
||||
fallbackSearchTerm: '',
|
||||
mappingFields,
|
||||
|
@ -36,7 +40,7 @@ export const getMappingData = (gitlabFields, payloadFields, savedMapping) => {
|
|||
*/
|
||||
export const transformForSave = (mappingData) => {
|
||||
return mappingData.reduce((acc, field) => {
|
||||
const mapped = field.mappingFields.find(({ name }) => name === field.mapping);
|
||||
const mapped = field.mappingFields.find(({ path }) => isEqual(path, field.mapping));
|
||||
if (mapped) {
|
||||
const { path, type, label } = mapped;
|
||||
acc.push({
|
||||
|
@ -49,13 +53,3 @@ export const transformForSave = (mappingData) => {
|
|||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds `name` prop to each provided by BE parsed payload field
|
||||
* @param {Object} payload - parsed sample payload
|
||||
*
|
||||
* @return {Object} same as input with an extra `name` property which basically serves as a key to make a match
|
||||
*/
|
||||
export const getPayloadFields = (payload) => {
|
||||
return payload.map((field) => ({ ...field, name: field.path.join('_') }));
|
||||
};
|
||||
|
|
|
@ -123,7 +123,7 @@ export default {
|
|||
<gl-button
|
||||
data-testid="action-delete"
|
||||
icon="remove"
|
||||
category="primary"
|
||||
category="secondary"
|
||||
variant="danger"
|
||||
:title="s__('PackageRegistry|Remove package')"
|
||||
:aria-label="s__('PackageRegistry|Remove package')"
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script>
|
||||
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
},
|
||||
props: {
|
||||
error: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
wikiPagePath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-alert variant="danger" :dismissible="false">
|
||||
<gl-sprintf :message="error">
|
||||
<template #wikiLink="{ content }">
|
||||
<gl-link :href="wikiPagePath" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
</template>
|
|
@ -6,9 +6,10 @@ import Translate from '~/vue_shared/translate';
|
|||
import GLForm from '../../../gl_form';
|
||||
import ZenMode from '../../../zen_mode';
|
||||
import deleteWikiModal from './components/delete_wiki_modal.vue';
|
||||
import wikiAlert from './components/wiki_alert.vue';
|
||||
import Wikis from './wikis';
|
||||
|
||||
export default () => {
|
||||
const createModalVueApp = () => {
|
||||
new Wikis(); // eslint-disable-line no-new
|
||||
new ShortcutsWiki(); // eslint-disable-line no-new
|
||||
new ZenMode(); // eslint-disable-line no-new
|
||||
|
@ -39,3 +40,28 @@ export default () => {
|
|||
});
|
||||
}
|
||||
};
|
||||
|
||||
const createAlertVueApp = () => {
|
||||
const el = document.getElementById('js-wiki-error');
|
||||
if (el) {
|
||||
const { error, wikiPagePath } = el.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(wikiAlert, {
|
||||
props: {
|
||||
error,
|
||||
wikiPagePath,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default () => {
|
||||
createModalVueApp();
|
||||
createAlertVueApp();
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
|
|||
import { __ } from '~/locale';
|
||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||
import CommitComponent from '~/vue_shared/components/commit.vue';
|
||||
import { PIPELINES_TABLE } from '../../constants';
|
||||
import eventHub from '../../event_hub';
|
||||
import PipelineTriggerer from './pipeline_triggerer.vue';
|
||||
import PipelineUrl from './pipeline_url.vue';
|
||||
|
@ -57,7 +56,6 @@ export default {
|
|||
default: null,
|
||||
},
|
||||
},
|
||||
pipelinesTable: PIPELINES_TABLE,
|
||||
data() {
|
||||
return {
|
||||
isRetrying: false,
|
||||
|
@ -173,6 +171,10 @@ export default {
|
|||
this.isRetrying = true;
|
||||
eventHub.$emit('retryPipeline', this.pipeline.retry_path);
|
||||
},
|
||||
handlePipelineActionRequestComplete() {
|
||||
// warn the pipelines table to update
|
||||
eventHub.$emit('refreshPipelinesTable');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -220,9 +222,9 @@ export default {
|
|||
data-testid="widget-mini-pipeline-graph"
|
||||
>
|
||||
<pipeline-stage
|
||||
:type="$options.pipelinesTable"
|
||||
:stage="stage"
|
||||
:update-dropdown="updateGraphDropdown"
|
||||
@pipelineActionRequestComplete="handlePipelineActionRequestComplete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -15,7 +15,6 @@ import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/u
|
|||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { __ } from '~/locale';
|
||||
import { PIPELINES_TABLE } from '../../constants';
|
||||
import eventHub from '../../event_hub';
|
||||
import JobItem from '../graph/job_item.vue';
|
||||
|
||||
|
@ -39,11 +38,6 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -90,13 +84,11 @@ export default {
|
|||
return this.$el.classList.contains('show');
|
||||
},
|
||||
pipelineActionRequestComplete() {
|
||||
if (this.type === PIPELINES_TABLE) {
|
||||
// warn the pipelines table to update
|
||||
eventHub.$emit('refreshPipelinesTable');
|
||||
return;
|
||||
}
|
||||
// close the dropdown in MR widget
|
||||
this.$refs.stageGlDropdown.hide();
|
||||
|
||||
// warn the pipelines table to update
|
||||
this.$emit('pipelineActionRequestComplete');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { s__, __ } from '~/locale';
|
||||
|
||||
export const CANCEL_REQUEST = 'CANCEL_REQUEST';
|
||||
export const PIPELINES_TABLE = 'PIPELINES_TABLE';
|
||||
export const LAYOUT_CHANGE_DELAY = 300;
|
||||
export const FILTER_PIPELINES_SEARCH_DELAY = 200;
|
||||
export const ANY_TRIGGER_AUTHOR = 'Any';
|
||||
|
|
|
@ -48,6 +48,7 @@ export default {
|
|||
:title="title"
|
||||
:aria-label="title"
|
||||
variant="danger"
|
||||
category="secondary"
|
||||
icon="remove"
|
||||
@click="$emit('delete')"
|
||||
/>
|
||||
|
|
|
@ -112,10 +112,11 @@ module WikiActions
|
|||
wiki_page_path(wiki, page)
|
||||
)
|
||||
else
|
||||
@error = response.message
|
||||
render 'shared/wikis/edit'
|
||||
end
|
||||
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
|
||||
@error = e
|
||||
@error = e.message
|
||||
render 'shared/wikis/edit'
|
||||
end
|
||||
# rubocop:enable Gitlab/ModuleWithInstanceVariables
|
||||
|
|
|
@ -50,24 +50,6 @@ module WikiHelper
|
|||
end
|
||||
end
|
||||
|
||||
def wiki_page_errors(error)
|
||||
return unless error
|
||||
|
||||
content_tag(:div, class: 'alert alert-danger') do
|
||||
case error
|
||||
when WikiPage::PageChangedError
|
||||
page_link = link_to s_("WikiPageConflictMessage|the page"), wiki_page_path(@wiki, @page), target: "_blank"
|
||||
concat(
|
||||
(s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
|
||||
)
|
||||
when WikiPage::PageRenameError
|
||||
s_("WikiEdit|There is already a page with the same title in that path.")
|
||||
else
|
||||
error.message
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def wiki_attachment_upload_url
|
||||
case @wiki.container
|
||||
when Project
|
||||
|
|
|
@ -13,6 +13,7 @@ class Iteration < ApplicationRecord
|
|||
}.with_indifferent_access.freeze
|
||||
|
||||
include AtomicInternalId
|
||||
include Timebox
|
||||
|
||||
belongs_to :project
|
||||
belongs_to :group
|
||||
|
@ -23,22 +24,22 @@ class Iteration < ApplicationRecord
|
|||
|
||||
validates :start_date, presence: true
|
||||
validates :due_date, presence: true
|
||||
validates :iterations_cadence, presence: true, unless: -> { project_id.present? }
|
||||
|
||||
validate :dates_do_not_overlap, if: :start_or_due_dates_changed?
|
||||
validate :future_date, if: :start_or_due_dates_changed?, unless: :skip_future_date_validation
|
||||
validate :no_project, unless: :skip_project_validation
|
||||
validate :validate_group
|
||||
|
||||
before_create :set_iterations_cadence
|
||||
before_validation :set_iterations_cadence, unless: -> { project_id.present? }
|
||||
before_create :set_past_iteration_state
|
||||
|
||||
scope :upcoming, -> { with_state(:upcoming) }
|
||||
scope :started, -> { with_state(:started) }
|
||||
scope :closed, -> { with_state(:closed) }
|
||||
|
||||
scope :within_timeframe, -> (start_date, end_date) do
|
||||
where('start_date IS NOT NULL OR due_date IS NOT NULL')
|
||||
.where('start_date IS NULL OR start_date <= ?', end_date)
|
||||
.where('due_date IS NULL OR due_date >= ?', start_date)
|
||||
where('start_date <= ?', end_date).where('due_date >= ?', start_date)
|
||||
end
|
||||
|
||||
scope :start_date_passed, -> { where('start_date <= ?', Date.current).where('due_date >= ?', Date.current) }
|
||||
|
@ -106,30 +107,20 @@ class Iteration < ApplicationRecord
|
|||
start_date_changed? || due_date_changed?
|
||||
end
|
||||
|
||||
# ensure dates do not overlap with other Iterations in the same group/project tree
|
||||
# ensure dates do not overlap with other Iterations in the same cadence tree
|
||||
def dates_do_not_overlap
|
||||
iterations = if parent_group.present? && resource_parent.is_a?(Project)
|
||||
Iteration.where(group: parent_group.self_and_ancestors).or(project.iterations)
|
||||
elsif parent_group.present?
|
||||
Iteration.where(group: parent_group.self_and_ancestors)
|
||||
else
|
||||
project.iterations
|
||||
end
|
||||
return unless iterations_cadence
|
||||
return unless iterations_cadence.iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
|
||||
|
||||
return unless iterations.where.not(id: self.id).within_timeframe(start_date, due_date).exists?
|
||||
|
||||
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations"))
|
||||
# for now we only have a single default cadence within a group just to wrap the iterations into a set.
|
||||
# once we introduce multiple cadences per group we need to change this message.
|
||||
# related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/299312
|
||||
errors.add(:base, s_("Iteration|Dates cannot overlap with other existing Iterations within this group"))
|
||||
end
|
||||
|
||||
# ensure dates are in the future
|
||||
def future_date
|
||||
if start_date_changed?
|
||||
errors.add(:start_date, s_("Iteration|cannot be in the past")) if start_date < Date.current
|
||||
if start_or_due_dates_changed?
|
||||
errors.add(:start_date, s_("Iteration|cannot be more than 500 years in the future")) if start_date > 500.years.from_now
|
||||
end
|
||||
|
||||
if due_date_changed?
|
||||
errors.add(:due_date, s_("Iteration|cannot be in the past")) if due_date < Date.current
|
||||
errors.add(:due_date, s_("Iteration|cannot be more than 500 years in the future")) if due_date > 500.years.from_now
|
||||
end
|
||||
end
|
||||
|
@ -140,6 +131,12 @@ class Iteration < ApplicationRecord
|
|||
errors.add(:project_id, s_("is not allowed. We do not currently support project-level iterations"))
|
||||
end
|
||||
|
||||
def set_past_iteration_state
|
||||
# if we create an iteration in the past, we set the state to closed right away,
|
||||
# no need to wait for IterationsUpdateStatusWorker to do so.
|
||||
self.state = :closed if due_date < Date.current
|
||||
end
|
||||
|
||||
# TODO: this method should be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296099
|
||||
def set_iterations_cadence
|
||||
return if iterations_cadence
|
||||
|
@ -147,13 +144,20 @@ class Iteration < ApplicationRecord
|
|||
# issue to clarify project iterations: https://gitlab.com/gitlab-org/gitlab/-/issues/299864
|
||||
return unless group
|
||||
|
||||
self.iterations_cadence = group.iterations_cadences.first || create_default_cadence
|
||||
# we need this as we use the cadence to validate the dates overlap for this iteration,
|
||||
# so in the case this runs before background migration we need to first set all iterations
|
||||
# in this group to a cadence before we can validate the dates overlap.
|
||||
default_cadence = find_or_create_default_cadence
|
||||
group.iterations.where(iterations_cadence_id: nil).update_all(iterations_cadence_id: default_cadence.id)
|
||||
|
||||
self.iterations_cadence = default_cadence
|
||||
end
|
||||
|
||||
def create_default_cadence
|
||||
def find_or_create_default_cadence
|
||||
cadence_title = "#{group.name} Iterations"
|
||||
start_date = self.start_date || Date.today
|
||||
|
||||
Iterations::Cadence.create!(group: group, title: cadence_title, start_date: start_date)
|
||||
::Iterations::Cadence.create_with(title: cadence_title, start_date: start_date).safe_find_or_create_by!(group: group)
|
||||
end
|
||||
|
||||
# TODO: remove this as part of https://gitlab.com/gitlab-org/gitlab/-/issues/296100
|
||||
|
|
|
@ -82,6 +82,8 @@ class Namespace < ApplicationRecord
|
|||
before_destroy(prepend: true) { prepare_for_destroy }
|
||||
after_destroy :rm_dir
|
||||
|
||||
before_save :ensure_delayed_project_removal_assigned_to_namespace_settings, if: :delayed_project_removal_changed?
|
||||
|
||||
scope :for_user, -> { where('type IS NULL') }
|
||||
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
|
||||
scope :include_route, -> { includes(:route) }
|
||||
|
@ -408,6 +410,13 @@ class Namespace < ApplicationRecord
|
|||
|
||||
private
|
||||
|
||||
def ensure_delayed_project_removal_assigned_to_namespace_settings
|
||||
return if Feature.disabled?(:migrate_delayed_project_removal, default_enabled: true)
|
||||
|
||||
self.namespace_settings || build_namespace_settings
|
||||
namespace_settings.delayed_project_removal = delayed_project_removal
|
||||
end
|
||||
|
||||
def all_projects_with_pages
|
||||
if all_projects.pages_metadata_not_migrated.exists?
|
||||
Gitlab::BackgroundMigration::MigratePagesMetadata.new.perform_on_relation(
|
||||
|
|
|
@ -205,14 +205,15 @@ class WikiPage
|
|||
last_commit_sha = attrs.delete(:last_commit_sha)
|
||||
|
||||
if last_commit_sha && last_commit_sha != self.last_commit_sha
|
||||
raise PageChangedError
|
||||
raise PageChangedError, s_(
|
||||
'WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs.')
|
||||
end
|
||||
|
||||
update_attributes(attrs)
|
||||
|
||||
if title.present? && title_changed? && wiki.find_page(title).present?
|
||||
attributes[:title] = page.title
|
||||
raise PageRenameError
|
||||
raise PageRenameError, s_('WikiEdit|There is already a page with the same title in that path.')
|
||||
end
|
||||
|
||||
save do
|
||||
|
|
|
@ -33,7 +33,7 @@ module Groups
|
|||
Group.transaction do
|
||||
if @group.save
|
||||
@group.add_owner(current_user)
|
||||
@group.create_namespace_settings
|
||||
@group.create_namespace_settings unless @group.namespace_settings
|
||||
Service.create_from_active_default_integrations(@group, :group_id)
|
||||
OnboardingProgress.onboard(@group)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
- wiki_page_title @page, @page.persisted? ? _('Edit') : _('New')
|
||||
- add_page_specific_style 'page_bundles/wiki'
|
||||
|
||||
= wiki_page_errors(@error)
|
||||
- if @error
|
||||
#js-wiki-error{ data: { error: @error, wiki_page_path: wiki_page_path(@wiki, @page) } }
|
||||
|
||||
.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
|
||||
= wiki_sidebar_toggle_button
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Move wiki helper alert to Vue
|
||||
merge_request: 54517
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: 'Registry: make delete icon buttons secondary'
|
||||
merge_request: 54545
|
||||
author:
|
||||
type: changed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix Metric tab not showing up on operations page
|
||||
merge_request: 54736
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Migrate namespaces delayed_project_removal to namespace_settings
|
||||
merge_request: 53916
|
||||
author:
|
||||
type: changed
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Fix creating the idx_on_issues_where_service_desk_reply_to_is_not_null index
|
||||
before the post migration
|
||||
merge_request: 54346
|
||||
author:
|
||||
type: other
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix keep latest artifacts checkbox being always disabled
|
||||
merge_request: 54669
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove backup_labels table
|
||||
merge_request: 54856
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/iterations-dates-validations.yml
Normal file
5
changelogs/unreleased/iterations-dates-validations.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow overlapping iteration dates with ancestor group iterations and restrict dates overlapping for iterations within same group
|
||||
merge_request: 52403
|
||||
author:
|
||||
type: changed
|
5
changelogs/unreleased/iterations-in-the-past.yml
Normal file
5
changelogs/unreleased/iterations-in-the-past.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Allow creation of iterations in the past
|
||||
merge_request: 52403
|
||||
author:
|
||||
type: changed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Updates authorization for linting endpoint
|
||||
merge_request: 54492
|
||||
author:
|
||||
type: changed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Restore missing horizontal scrollbar on issue boards
|
||||
merge_request: 54634
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Send SIGINT instead of SIGQUIT to puma
|
||||
merge_request: 54446
|
||||
author: Jörg Behrmann @behrmann
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Reset description template names cache key to reload an updated templates structure
|
||||
merge_request: 54614
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix S3 object storage failing when endpoint is not specified
|
||||
merge_request: 54868
|
||||
author:
|
||||
type: fixed
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Fix N+1 SQL regression in exporting issues to CSV
|
||||
merge_request: 54287
|
||||
author:
|
||||
type: performance
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: migrate_delayed_project_removal
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53916
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300207
|
||||
milestone: '13.9'
|
||||
type: development
|
||||
group: group::access
|
||||
default_enabled: true
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIterationsCadenceDateRangeConstraint < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
execute <<~SQL
|
||||
ALTER TABLE sprints
|
||||
ADD CONSTRAINT iteration_start_and_due_date_iterations_cadence_id_constraint
|
||||
EXCLUDE USING gist
|
||||
( iterations_cadence_id WITH =,
|
||||
daterange(start_date, due_date, '[]') WITH &&
|
||||
)
|
||||
WHERE (group_id IS NOT NULL)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
execute <<~SQL
|
||||
ALTER TABLE sprints
|
||||
DROP CONSTRAINT IF EXISTS iteration_start_and_due_date_iterations_cadence_id_constraint
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveIterationGroupDateRangeConstraint < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
execute <<~SQL
|
||||
ALTER TABLE sprints
|
||||
DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_group_id_constraint
|
||||
SQL
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
with_lock_retries do
|
||||
execute <<~SQL
|
||||
ALTER TABLE sprints
|
||||
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint
|
||||
EXCLUDE USING gist
|
||||
( group_id WITH =,
|
||||
daterange(start_date, due_date, '[]') WITH &&
|
||||
)
|
||||
WHERE (group_id IS NOT NULL)
|
||||
SQL
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDelayedProjectRemovalToNamespaceSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :namespace_settings, :delayed_project_removal, :boolean, default: false, null: false
|
||||
end
|
||||
end
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddIndexToNamespacesDelayedProjectRemoval < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
INDEX_NAME = 'tmp_idx_on_namespaces_delayed_project_removal'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_concurrent_index :namespaces, :id, name: INDEX_NAME, where: 'delayed_project_removal = TRUE'
|
||||
end
|
||||
|
||||
def down
|
||||
remove_concurrent_index_by_name :namespaces, INDEX_NAME
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSprintsStartDateNotNullCheckConstraint < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint(:sprints, :start_date, validate: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint(:sprints, :start_date)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddSprintsDueDateNotNullCheckConstraint < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
add_not_null_constraint(:sprints, :due_date, validate: false)
|
||||
end
|
||||
|
||||
def down
|
||||
remove_not_null_constraint(:sprints, :due_date)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
class Namespace < ActiveRecord::Base
|
||||
self.table_name = 'namespaces'
|
||||
|
||||
include ::EachBatch
|
||||
end
|
||||
|
||||
def up
|
||||
Namespace.select(:id).where(delayed_project_removal: true).each_batch do |batch|
|
||||
values = batch.map { |record| "(#{record.id}, TRUE, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" }
|
||||
|
||||
execute <<-EOF.strip_heredoc
|
||||
INSERT INTO namespace_settings (namespace_id, delayed_project_removal, created_at, updated_at)
|
||||
VALUES #{values.join(', ')}
|
||||
ON CONFLICT (namespace_id) DO UPDATE
|
||||
SET delayed_project_removal = TRUE
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveBackupLabelsForeignKeys < ActiveRecord::Migration[6.0]
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
with_lock_retries do
|
||||
remove_foreign_key_if_exists(:backup_labels, :projects)
|
||||
remove_foreign_key_if_exists(:backup_labels, :namespaces)
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
add_concurrent_foreign_key(:backup_labels, :projects, column: :project_id, on_delete: :cascade)
|
||||
add_concurrent_foreign_key(:backup_labels, :namespaces, column: :group_id, on_delete: :cascade)
|
||||
end
|
||||
end
|
36
db/post_migrate/20210222192144_remove_backup_labels_table.rb
Normal file
36
db/post_migrate/20210222192144_remove_backup_labels_table.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveBackupLabelsTable < ActiveRecord::Migration[6.0]
|
||||
DOWNTIME = false
|
||||
|
||||
def up
|
||||
drop_table :backup_labels
|
||||
end
|
||||
|
||||
def down
|
||||
create_table :backup_labels, id: false do |t|
|
||||
t.integer :id, null: false
|
||||
t.string :title
|
||||
t.string :color
|
||||
t.integer :project_id
|
||||
t.timestamps null: true # rubocop:disable Migration/Timestamps
|
||||
t.boolean :template, default: false
|
||||
t.string :description
|
||||
t.text :description_html
|
||||
t.string :type
|
||||
t.integer :group_id
|
||||
t.integer :cached_markdown_version
|
||||
t.integer :restore_action
|
||||
t.string :new_title
|
||||
end
|
||||
|
||||
execute 'ALTER TABLE backup_labels ADD PRIMARY KEY (id)'
|
||||
|
||||
add_index :backup_labels, [:group_id, :project_id, :title], name: 'backup_labels_group_id_project_id_title_idx', unique: true
|
||||
add_index :backup_labels, [:group_id, :title], where: 'project_id = NULL::integer', name: 'backup_labels_group_id_title_idx'
|
||||
add_index :backup_labels, :project_id, name: 'backup_labels_project_id_idx'
|
||||
add_index :backup_labels, :template, name: 'backup_labels_template_idx', where: 'template'
|
||||
add_index :backup_labels, :title, name: 'backup_labels_title_idx'
|
||||
add_index :backup_labels, [:type, :project_id], name: 'backup_labels_type_project_id_idx'
|
||||
end
|
||||
end
|
1
db/schema_migrations/20210127152613
Normal file
1
db/schema_migrations/20210127152613
Normal file
|
@ -0,0 +1 @@
|
|||
32f636ffad4d2c6a002129d6e3eaeaf5d8f420dcc1273665129dc4d23f2e0dbe
|
1
db/schema_migrations/20210127202613
Normal file
1
db/schema_migrations/20210127202613
Normal file
|
@ -0,0 +1 @@
|
|||
951f46f88c1b07505e0b560e802a8bd701db7d379342d97b0bff3ad90e81fb02
|
1
db/schema_migrations/20210214201118
Normal file
1
db/schema_migrations/20210214201118
Normal file
|
@ -0,0 +1 @@
|
|||
8c1da1c7edba16993da93d9075ad2a3624b8c12ccf73a241e1a166014a99e254
|
1
db/schema_migrations/20210214205155
Normal file
1
db/schema_migrations/20210214205155
Normal file
|
@ -0,0 +1 @@
|
|||
7678d97de752e7a9a571d80febc74eb44c699c7b1967690d9a2391036caea5d2
|
1
db/schema_migrations/20210215095328
Normal file
1
db/schema_migrations/20210215095328
Normal file
|
@ -0,0 +1 @@
|
|||
25820a3d060826a082565f12a3ac96deafbbde750f5756d71e34d14801ec6148
|
1
db/schema_migrations/20210218144056
Normal file
1
db/schema_migrations/20210218144056
Normal file
|
@ -0,0 +1 @@
|
|||
545747e86481c74832a6df55764ab97ecfefc4446df9cc2366a8ce9d9c400ea4
|
1
db/schema_migrations/20210218144656
Normal file
1
db/schema_migrations/20210218144656
Normal file
|
@ -0,0 +1 @@
|
|||
91969bfc791cd7bc78b940aa6fed345b13a3186db0b89828428b798aa4f7949e
|
1
db/schema_migrations/20210222185538
Normal file
1
db/schema_migrations/20210222185538
Normal file
|
@ -0,0 +1 @@
|
|||
0bccf1ff356a4b9c08d472e8b63070b497f331c2dfaded1bdb2cf01860df8903
|
1
db/schema_migrations/20210222192144
Normal file
1
db/schema_migrations/20210222192144
Normal file
|
@ -0,0 +1 @@
|
|||
b2508d46edbfbba24df65731f6e285886acbb6352a900dd1c6a985a686252ef0
|
|
@ -9747,23 +9747,6 @@ CREATE SEQUENCE background_migration_jobs_id_seq
|
|||
|
||||
ALTER SEQUENCE background_migration_jobs_id_seq OWNED BY background_migration_jobs.id;
|
||||
|
||||
CREATE TABLE backup_labels (
|
||||
id integer NOT NULL,
|
||||
title character varying,
|
||||
color character varying,
|
||||
project_id integer,
|
||||
created_at timestamp without time zone,
|
||||
updated_at timestamp without time zone,
|
||||
template boolean DEFAULT false,
|
||||
description character varying,
|
||||
description_html text,
|
||||
type character varying,
|
||||
group_id integer,
|
||||
cached_markdown_version integer,
|
||||
restore_action integer,
|
||||
new_title character varying
|
||||
);
|
||||
|
||||
CREATE TABLE badges (
|
||||
id integer NOT NULL,
|
||||
link_url character varying NOT NULL,
|
||||
|
@ -14370,6 +14353,7 @@ CREATE TABLE namespace_settings (
|
|||
allow_mfa_for_subgroups boolean DEFAULT true NOT NULL,
|
||||
default_branch_name text,
|
||||
repository_read_only boolean DEFAULT false NOT NULL,
|
||||
delayed_project_removal boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT check_0ba93c78c7 CHECK ((char_length(default_branch_name) <= 255))
|
||||
);
|
||||
|
||||
|
@ -19823,9 +19807,6 @@ ALTER TABLE ONLY aws_roles
|
|||
ALTER TABLE ONLY background_migration_jobs
|
||||
ADD CONSTRAINT background_migration_jobs_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY backup_labels
|
||||
ADD CONSTRAINT backup_labels_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY badges
|
||||
ADD CONSTRAINT badges_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -19889,9 +19870,15 @@ ALTER TABLE ONLY chat_teams
|
|||
ALTER TABLE vulnerability_scanners
|
||||
ADD CONSTRAINT check_37608c9db5 CHECK ((char_length(vendor) <= 255)) NOT VALID;
|
||||
|
||||
ALTER TABLE sprints
|
||||
ADD CONSTRAINT check_ccd8a1eae0 CHECK ((start_date IS NOT NULL)) NOT VALID;
|
||||
|
||||
ALTER TABLE group_import_states
|
||||
ADD CONSTRAINT check_cda75c7c3f CHECK ((user_id IS NOT NULL)) NOT VALID;
|
||||
|
||||
ALTER TABLE sprints
|
||||
ADD CONSTRAINT check_df3816aed7 CHECK ((due_date IS NOT NULL)) NOT VALID;
|
||||
|
||||
ALTER TABLE ONLY ci_build_needs
|
||||
ADD CONSTRAINT ci_build_needs_pkey PRIMARY KEY (id);
|
||||
|
||||
|
@ -20400,7 +20387,7 @@ ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
|
|||
ADD CONSTRAINT issues_self_managed_prometheus_alert_events_pkey PRIMARY KEY (issue_id, self_managed_prometheus_alert_event_id);
|
||||
|
||||
ALTER TABLE ONLY sprints
|
||||
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint EXCLUDE USING gist (group_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL));
|
||||
ADD CONSTRAINT iteration_start_and_due_date_iterations_cadence_id_constraint EXCLUDE USING gist (iterations_cadence_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL));
|
||||
|
||||
ALTER TABLE ONLY sprints
|
||||
ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint EXCLUDE USING gist (project_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((project_id IS NOT NULL));
|
||||
|
@ -21271,18 +21258,6 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON approval_proj
|
|||
|
||||
CREATE INDEX approval_mr_rule_index_merge_request_id ON approval_merge_request_rules USING btree (merge_request_id);
|
||||
|
||||
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON backup_labels USING btree (group_id, project_id, title);
|
||||
|
||||
CREATE INDEX backup_labels_group_id_title_idx ON backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
|
||||
|
||||
CREATE INDEX backup_labels_project_id_idx ON backup_labels USING btree (project_id);
|
||||
|
||||
CREATE INDEX backup_labels_template_idx ON backup_labels USING btree (template) WHERE template;
|
||||
|
||||
CREATE INDEX backup_labels_title_idx ON backup_labels USING btree (title);
|
||||
|
||||
CREATE INDEX backup_labels_type_project_id_idx ON backup_labels USING btree (type, project_id);
|
||||
|
||||
CREATE UNIQUE INDEX bulk_import_trackers_uniq_relation_by_entity ON bulk_import_trackers USING btree (bulk_import_entity_id, relation);
|
||||
|
||||
CREATE INDEX ci_builds_gitlab_monitor_metrics ON ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
|
||||
|
@ -23869,6 +23844,8 @@ CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree
|
|||
|
||||
CREATE INDEX tmp_idx_deduplicate_vulnerability_occurrences ON vulnerability_occurrences USING btree (project_id, report_type, location_fingerprint, primary_identifier_id, id);
|
||||
|
||||
CREATE INDEX tmp_idx_on_namespaces_delayed_project_removal ON namespaces USING btree (id) WHERE (delayed_project_removal = true);
|
||||
|
||||
CREATE INDEX tmp_index_on_security_findings_scan_id ON security_findings USING btree (scan_id) WHERE (uuid IS NULL);
|
||||
|
||||
CREATE INDEX tmp_index_on_vulnerabilities_non_dismissed ON vulnerabilities USING btree (id) WHERE (state <> 2);
|
||||
|
@ -24461,9 +24438,6 @@ ALTER TABLE ONLY vulnerabilities
|
|||
ALTER TABLE ONLY labels
|
||||
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY backup_labels
|
||||
ADD CONSTRAINT fk_7de4989a69 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY merge_requests
|
||||
ADD CONSTRAINT fk_7e85395a64 FOREIGN KEY (sprint_id) REFERENCES sprints(id) ON DELETE CASCADE;
|
||||
|
||||
|
@ -25979,9 +25953,6 @@ ALTER TABLE ONLY serverless_domain_cluster
|
|||
ALTER TABLE ONLY labels
|
||||
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY backup_labels
|
||||
ADD CONSTRAINT fk_rails_c1ac5161d8 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE ONLY project_feature_usages
|
||||
ADD CONSTRAINT fk_rails_c22a50024b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@ Read more on [pagination](README.md#pagination).
|
|||
|
||||
Get all Merge Trains of the requested project:
|
||||
|
||||
```txt
|
||||
```shell
|
||||
GET /projects/:id/merge_trains
|
||||
GET /projects/:id/merge_trains?scope=complete
|
||||
```
|
||||
|
|
|
@ -10,41 +10,20 @@ disqus_identifier: 'https://docs.gitlab.com/ee/ci/environments.html'
|
|||
|
||||
> Introduced in GitLab 8.9.
|
||||
|
||||
Environments allow control of the continuous deployment of your software,
|
||||
all within GitLab.
|
||||
Environments describe where code is deployed.
|
||||
|
||||
## Introduction
|
||||
|
||||
There are many stages required in the software development process before the software is ready
|
||||
for public consumption.
|
||||
|
||||
For example:
|
||||
|
||||
1. Develop your code.
|
||||
1. Test your code.
|
||||
1. Deploy your code into a testing or staging environment before you release it to the public.
|
||||
|
||||
This helps find bugs in your software, and also in the deployment process as well.
|
||||
|
||||
GitLab CI/CD is capable of not only testing or building your projects, but also
|
||||
deploying them in your infrastructure, with the added benefit of giving you a
|
||||
way to track your deployments. In other words, you always know what is
|
||||
currently being deployed or has been deployed on your servers.
|
||||
|
||||
It's important to know that:
|
||||
|
||||
- Environments are like tags for your CI jobs, describing where code gets deployed.
|
||||
- Deployments are created when [GitLab CI/CD](../yaml/README.md) is used to deploy versions of code to environments.
|
||||
Each time [GitLab CI/CD](../yaml/README.md) deploys a version of code to an environment,
|
||||
a deployment is created.
|
||||
|
||||
GitLab:
|
||||
|
||||
- Provides a full history of your deployments for each environment.
|
||||
- Keeps track of your deployments, so you always know what is currently being deployed on your
|
||||
- Provides a full history of deployments to each environment.
|
||||
- Tracks your deployments, so you always know what is currently deployed on your
|
||||
servers.
|
||||
|
||||
If you have a deployment service such as [Kubernetes](../../user/project/clusters/index.md)
|
||||
associated with your project, you can use it to assist with your deployments, and
|
||||
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
|
||||
If you have a deployment service like [Kubernetes](../../user/project/clusters/index.md)
|
||||
associated with your project, you can use it to assist with your deployments.
|
||||
You can even access a [web terminal](#web-terminals) for your environment from within GitLab.
|
||||
|
||||
## Configuring environments
|
||||
|
||||
|
@ -69,6 +48,10 @@ In the scenario:
|
|||
|
||||
### Defining environments
|
||||
|
||||
You can create environments manually in the web interface, but we recommend
|
||||
that you define your environments in the `.gitlab-ci.yml` file. After the first
|
||||
deploy, the environments are automatically created.
|
||||
|
||||
Let's consider the following `.gitlab-ci.yml` example:
|
||||
|
||||
```yaml
|
||||
|
@ -539,8 +522,8 @@ So now, every branch:
|
|||
|
||||
- Gets its own environment.
|
||||
- Is deployed to its own unique location, with the added benefit of:
|
||||
- Having a [history of deployments](#viewing-deployment-history).
|
||||
- Being able to [rollback changes](#retrying-and-rolling-back) if needed.
|
||||
- Having a [history of deployments](#view-the-deployment-history).
|
||||
- Being able to [roll back changes](#retry-or-roll-back-a-deployment) if needed.
|
||||
|
||||
For more information, see [Using the environment URL](#using-the-environment-url).
|
||||
|
||||
|
@ -555,72 +538,46 @@ For more information, see [Protected environments](protected_environments.md).
|
|||
Once environments are configured, GitLab provides many features for working with them,
|
||||
as documented below.
|
||||
|
||||
### Viewing environments and deployments
|
||||
### View environments and deployments
|
||||
|
||||
A list of environments and deployment statuses is available on each project's **Operations > Environments** page.
|
||||
Prerequisites:
|
||||
|
||||
For example:
|
||||
- You must have a minimum of [Reporter permission](../../user/permissions.md#project-members-permissions).
|
||||
|
||||
![Environment view](../img/environments_available_13_7.png)
|
||||
To view a list of environments and deployment statuses:
|
||||
|
||||
This example shows:
|
||||
- Go to the project's **Operations > Environments** page.
|
||||
|
||||
- The environment's name with a link to its deployments.
|
||||
- The last deployment ID number and who performed it.
|
||||
- The job ID of the last deployment with its respective job name.
|
||||
- The commit information of the last deployment, such as who committed it, to what
|
||||
branch, and the Git SHA of the commit.
|
||||
- The exact time the last deployment was performed.
|
||||
- The upcoming deployment, if a deployment for the environment is in progress.
|
||||
- When the environment stops automatically.
|
||||
- A button that takes you to the URL that you defined under the `environment` keyword
|
||||
in `.gitlab-ci.yml`.
|
||||
- A number of deployment actions, including:
|
||||
- Prevent the environment from [stopping automatically](#automatically-stopping-an-environment).
|
||||
- [Open the live environment](#using-the-environment-url).
|
||||
- Trigger [a manual deployment to a different environment](#configuring-manual-deployments).
|
||||
- [Retry the deployment](#retrying-and-rolling-back).
|
||||
- [Stop the environment](#stopping-an-environment).
|
||||
The **Environments** page shows the latest deployments.
|
||||
|
||||
The information shown in the **Environments** page is limited to the latest
|
||||
deployments, but an environment can have multiple deployments.
|
||||
- An environment can have multiple deployments. Some deployments may not be listed on the page.
|
||||
- Only deploys that happen after your `.gitlab-ci.yml` is properly configured
|
||||
show up in the **Environment** and **Last deployment** lists.
|
||||
|
||||
> **Notes:**
|
||||
>
|
||||
> - While you can create environments manually in the web interface, we recommend
|
||||
> that you define your environments in `.gitlab-ci.yml` first. They will
|
||||
> be automatically created for you after the first deploy.
|
||||
> - The environments page can only be viewed by users with [Reporter permission](../../user/permissions.md#project-members-permissions)
|
||||
> and above. For more information on permissions, see the [permissions documentation](../../user/permissions.md).
|
||||
> - Only deploys that happen after your `.gitlab-ci.yml` is properly configured
|
||||
> show up in the **Environment** and **Last deployment** lists.
|
||||
### View the deployment history
|
||||
|
||||
### Viewing deployment history
|
||||
GitLab tracks your deployments, so you:
|
||||
|
||||
GitLab keeps track of your deployments, so you:
|
||||
- Always know what is currently deployed on your servers.
|
||||
- Have the full history of your deployments for every environment.
|
||||
|
||||
- Always know what is currently being deployed on your servers.
|
||||
- Can have the full history of your deployments for every environment.
|
||||
|
||||
Clicking on an environment shows the history of its deployments. Here's an example **Environments** page
|
||||
with multiple deployments:
|
||||
- Go to the project's **Operations > Environments** page.
|
||||
|
||||
![Deployments](../img/deployments_view.png)
|
||||
|
||||
This view is similar to the **Environments** page, but all deployments are shown. Also in this view
|
||||
is a **Rollback** button. For more information, see [Retrying and rolling back](#retrying-and-rolling-back).
|
||||
This view is similar to the **Environments** page, but all deployments are shown.
|
||||
|
||||
### Retrying and rolling back
|
||||
### Retry or roll back a deployment
|
||||
|
||||
If there is a problem with a deployment, you can retry it or roll it back.
|
||||
|
||||
To retry or rollback a deployment:
|
||||
|
||||
1. Navigate to **Operations > Environments**.
|
||||
1. Click on the environment.
|
||||
1. In the deployment history list for the environment, click the:
|
||||
- **Retry** button next to the last deployment, to retry that deployment.
|
||||
- **Rollback** button next to a previously successful deployment, to roll back to that deployment.
|
||||
1. Go to the project's **Operations > Environments**.
|
||||
1. Select the environment.
|
||||
1. In the deployment history list for the environment:
|
||||
- To retry a deployment, select **Retry**.
|
||||
- to roll back to a deployment, next to a previously successful deployment, select **Rollback**.
|
||||
|
||||
#### What to expect with a rollback
|
||||
|
||||
|
@ -662,7 +619,7 @@ from source files to public pages in the environment set for Review Apps.
|
|||
Stopping an environment:
|
||||
|
||||
- Moves it from the list of **Available** environments to the list of **Stopped**
|
||||
environments on the [**Environments** page](#viewing-environments-and-deployments).
|
||||
environments on the [**Environments** page](#view-environments-and-deployments).
|
||||
- Executes an [`on_stop` action](../yaml/README.md#environmenton_stop), if defined.
|
||||
|
||||
This is often used when multiple developers are working on a project at the same time,
|
||||
|
@ -819,7 +776,7 @@ Environments can also be deleted by using the [Environments API](../../api/envir
|
|||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/208655) in GitLab 13.2.
|
||||
|
||||
By default, GitLab creates a [deployment](#viewing-deployment-history) every time a
|
||||
By default, GitLab creates a [deployment](#view-the-deployment-history) every time a
|
||||
build with the specified environment runs. Newer deployments can also
|
||||
[cancel older ones](deployment_safety.md#skip-outdated-deployment-jobs).
|
||||
|
||||
|
@ -897,7 +854,7 @@ severity is shown, so you can identify which environments need immediate attenti
|
|||
When the issue that triggered the alert is resolved, it is removed and is no
|
||||
longer visible on the environment page.
|
||||
|
||||
If the alert requires a [rollback](#retrying-and-rolling-back), you can select the
|
||||
If the alert requires a [rollback](#retry-or-roll-back-a-deployment), you can select the
|
||||
deployment tab from the environment page and select which deployment to roll back to.
|
||||
|
||||
#### Auto Rollback **(ULTIMATE)**
|
||||
|
|
|
@ -187,7 +187,7 @@ For example, if you start rolling out new code and:
|
|||
|
||||
- Users do not experience trouble, GitLab can automatically complete the deployment from 0% to 100%.
|
||||
- Users experience trouble with the new code, you can stop the timed incremental rollout by canceling the pipeline
|
||||
and [rolling](../environments/index.md#retrying-and-rolling-back) back to the last stable version.
|
||||
and [rolling](../environments/index.md#retry-or-roll-back-a-deployment) back to the last stable version.
|
||||
|
||||
![Pipelines example](img/pipeline_incremental_rollout.png)
|
||||
|
||||
|
|
|
@ -84,7 +84,7 @@ displayed by GitLab:
|
|||
![pipeline status](img/pipeline_status.png)
|
||||
|
||||
If anything goes wrong, you can
|
||||
[roll back](../environments/index.md#retrying-and-rolling-back) the changes:
|
||||
[roll back](../environments/index.md#retry-or-roll-back-a-deployment) the changes:
|
||||
|
||||
![rollback button](img/rollback.png)
|
||||
|
||||
|
|
|
@ -593,7 +593,7 @@ required to go from `10%` to `100%`, you can jump to whatever job you want.
|
|||
You can also scale down by running a lower percentage job, just before hitting
|
||||
`100%`. Once you get to `100%`, you can't scale down, and you'd have to roll
|
||||
back by redeploying the old version using the
|
||||
[rollback button](../../ci/environments/index.md#retrying-and-rolling-back) in the
|
||||
[rollback button](../../ci/environments/index.md#retry-or-roll-back-a-deployment) in the
|
||||
environment page.
|
||||
|
||||
Below, you can see how the pipeline appears if the rollout or staging
|
||||
|
|
|
@ -222,7 +222,7 @@ you to common environment tasks:
|
|||
- **Terminal** (**{terminal}**) - Opens a [web terminal](../../ci/environments/index.md#web-terminals)
|
||||
session inside the container where the application is running
|
||||
- **Re-deploy to environment** (**{repeat}**) - For more information, see
|
||||
[Retrying and rolling back](../../ci/environments/index.md#retrying-and-rolling-back)
|
||||
[Retrying and rolling back](../../ci/environments/index.md#retry-or-roll-back-a-deployment)
|
||||
- **Stop environment** (**{stop}**) - For more information, see
|
||||
[Stopping an environment](../../ci/environments/index.md#stopping-an-environment)
|
||||
|
||||
|
|
|
@ -475,7 +475,7 @@ URLs to scan can be specified by either of the following methods:
|
|||
|
||||
To define the URLs to scan in a file, create a plain text file with one path per line.
|
||||
|
||||
```txt
|
||||
```plaintext
|
||||
page1.html
|
||||
/page2.html
|
||||
category/shoes/page1.html
|
||||
|
|
|
@ -11,7 +11,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
Custom project templates are useful for organizations that need to create many similar types of [projects](../project/index.md) and want to start from the same jumping-off point.
|
||||
|
||||
## Setting up Group-level Project Templates
|
||||
## Setting up group-level project templates
|
||||
|
||||
To use a custom project template for a new project you need to:
|
||||
|
||||
|
@ -30,7 +30,7 @@ To use a custom project template for a new project you need to:
|
|||
|
||||
Here is a sample group/project structure for a hypothetical "Acme Co" for project templates:
|
||||
|
||||
```txt
|
||||
```plaintext
|
||||
# GitLab instance and group
|
||||
gitlab.com/acmeco/
|
||||
# Subgroups
|
||||
|
|
|
@ -362,6 +362,11 @@ the user gets the highest access level from the groups. For example, if one grou
|
|||
is linked as `Guest` and another `Maintainer`, a user in both groups gets `Maintainer`
|
||||
access.
|
||||
|
||||
Users who are not members of any mapped SAML groups are removed from the GitLab group.
|
||||
|
||||
You can prevent accidental member removal. For example, if you have a SAML group link for `Owner` level access
|
||||
in a top-level group, you should also set up a group link for all other members.
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Description |
|
||||
|
|
|
@ -16849,10 +16849,7 @@ msgstr ""
|
|||
msgid "Iterations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration|Dates cannot overlap with other existing Iterations"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration|cannot be in the past"
|
||||
msgid "Iteration|Dates cannot overlap with other existing Iterations within this group"
|
||||
msgstr ""
|
||||
|
||||
msgid "Iteration|cannot be more than 500 years in the future"
|
||||
|
@ -33642,10 +33639,7 @@ msgstr ""
|
|||
msgid "WikiPageConfirmDelete|Delete page %{pageTitle}?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs."
|
||||
msgstr ""
|
||||
|
||||
msgid "WikiPageConflictMessage|the page"
|
||||
msgid "WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{wikiLinkStart}the page%{wikiLinkEnd} and make sure your changes will not unintentionally remove theirs."
|
||||
msgstr ""
|
||||
|
||||
msgid "WikiPageCreate|Create %{pageTitle}"
|
||||
|
|
|
@ -5,16 +5,6 @@ module QA
|
|||
module Project
|
||||
module_function
|
||||
|
||||
def add_member(project:, username:)
|
||||
project.visit!
|
||||
|
||||
Page::Project::Menu.perform(&:click_members)
|
||||
|
||||
Page::Project::Members.perform do |member_settings|
|
||||
member_settings.add_member(username)
|
||||
end
|
||||
end
|
||||
|
||||
def go_to_create_project_from_template
|
||||
if Page::Project::NewExperiment.perform(&:shown?)
|
||||
Page::Project::NewExperiment.perform(&:click_create_from_template_link)
|
||||
|
|
|
@ -15,7 +15,7 @@ FactoryBot.define do
|
|||
raise "Don't set owner for groups, use `group.add_owner(user)` instead"
|
||||
end
|
||||
|
||||
create(:namespace_settings, namespace: group)
|
||||
create(:namespace_settings, namespace: group) unless group.namespace_settings
|
||||
end
|
||||
|
||||
trait :public do
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
|
||||
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
|
||||
import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import alertFields from '../mocks/alertFields.json';
|
||||
import alertFields from '../mocks/alert_fields.json';
|
||||
import parsedMapping from '../mocks/parsed_mapping.json';
|
||||
|
||||
describe('AlertMappingBuilder', () => {
|
||||
let wrapper;
|
||||
|
@ -12,8 +12,8 @@ describe('AlertMappingBuilder', () => {
|
|||
function mountComponent() {
|
||||
wrapper = shallowMount(AlertMappingBuilder, {
|
||||
propsData: {
|
||||
parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
|
||||
savedMapping: parsedMapping.storedMapping.nodes,
|
||||
parsedPayload: parsedMapping.payloadAlerFields,
|
||||
savedMapping: parsedMapping.payloadAttributeMappings,
|
||||
alertFields,
|
||||
},
|
||||
});
|
||||
|
@ -33,6 +33,15 @@ describe('AlertMappingBuilder', () => {
|
|||
const findColumnInRow = (row, column) =>
|
||||
wrapper.findAll('.gl-display-table-row').at(row).findAll('.gl-display-table-cell ').at(column);
|
||||
|
||||
const getDropdownContent = (dropdown, types) => {
|
||||
const searchBox = dropdown.findComponent(GlSearchBoxByType);
|
||||
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
|
||||
const mappingOptions = parsedMapping.payloadAlerFields.filter(({ type }) =>
|
||||
types.includes(type),
|
||||
);
|
||||
return { searchBox, dropdownItems, mappingOptions };
|
||||
};
|
||||
|
||||
it('renders column captions', () => {
|
||||
expect(findColumnInRow(0, 0).text()).toContain(i18n.columns.gitlabKeyTitle);
|
||||
expect(findColumnInRow(0, 2).text()).toContain(i18n.columns.payloadKeyTitle);
|
||||
|
@ -63,10 +72,7 @@ describe('AlertMappingBuilder', () => {
|
|||
it('renders mapping dropdown for each field', () => {
|
||||
alertFields.forEach(({ types }, index) => {
|
||||
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
|
||||
const searchBox = dropdown.findComponent(GlSearchBoxByType);
|
||||
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
|
||||
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
|
||||
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
|
||||
const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types);
|
||||
|
||||
expect(dropdown.exists()).toBe(true);
|
||||
expect(searchBox.exists()).toBe(true);
|
||||
|
@ -80,11 +86,7 @@ describe('AlertMappingBuilder', () => {
|
|||
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
|
||||
|
||||
if (numberOfFallbacks) {
|
||||
const searchBox = dropdown.findComponent(GlSearchBoxByType);
|
||||
const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
|
||||
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
|
||||
const mappingOptions = nodes.filter(({ type }) => types.includes(type));
|
||||
|
||||
const { searchBox, dropdownItems, mappingOptions } = getDropdownContent(dropdown, types);
|
||||
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
|
||||
expect(dropdownItems).toHaveLength(mappingOptions.length);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,8 @@ import waitForPromises from 'helpers/wait_for_promises';
|
|||
import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
|
||||
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
|
||||
import { typeSet } from '~/alerts_settings/constants';
|
||||
import alertFields from '../mocks/alertFields.json';
|
||||
import alertFields from '../mocks/alert_fields.json';
|
||||
import parsedMapping from '../mocks/parsed_mapping.json';
|
||||
import { defaultAlertSettingsConfig } from './util';
|
||||
|
||||
describe('AlertsSettingsForm', () => {
|
||||
|
@ -39,6 +40,9 @@ describe('AlertsSettingsForm', () => {
|
|||
multiIntegrations,
|
||||
},
|
||||
mocks: {
|
||||
$apollo: {
|
||||
query: jest.fn(),
|
||||
},
|
||||
$toast: {
|
||||
show: mockToastShow,
|
||||
},
|
||||
|
@ -146,7 +150,7 @@ describe('AlertsSettingsForm', () => {
|
|||
|
||||
enableIntegration(0, integrationName);
|
||||
|
||||
const sampleMapping = { field: 'test' };
|
||||
const sampleMapping = parsedMapping.payloadAttributeMappings;
|
||||
findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
|
||||
findForm().trigger('submit');
|
||||
|
||||
|
@ -157,7 +161,7 @@ describe('AlertsSettingsForm', () => {
|
|||
name: integrationName,
|
||||
active: true,
|
||||
payloadAttributeMappings: sampleMapping,
|
||||
payloadExample: null,
|
||||
payloadExample: '{}',
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
@ -275,34 +279,47 @@ describe('AlertsSettingsForm', () => {
|
|||
});
|
||||
|
||||
describe('Test payload section for HTTP integration', () => {
|
||||
const validSamplePayload = JSON.stringify(alertFields);
|
||||
const emptySamplePayload = '{}';
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
multipleHttpIntegrationsCustomMapping: true,
|
||||
props: {
|
||||
data: {
|
||||
currentIntegration: {
|
||||
type: typeSet.http,
|
||||
payloadExample: validSamplePayload,
|
||||
payloadAttributeMappings: [],
|
||||
},
|
||||
alertFields,
|
||||
active: false,
|
||||
resetPayloadAndMappingConfirmed: false,
|
||||
},
|
||||
props: { alertFields },
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
active | resetSamplePayloadConfirmed | disabled
|
||||
${true} | ${true} | ${undefined}
|
||||
${false} | ${true} | ${'disabled'}
|
||||
${true} | ${false} | ${'disabled'}
|
||||
${false} | ${false} | ${'disabled'}
|
||||
`('', ({ active, resetSamplePayloadConfirmed, disabled }) => {
|
||||
const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
|
||||
active | resetPayloadAndMappingConfirmed | disabled
|
||||
${true} | ${true} | ${undefined}
|
||||
${false} | ${true} | ${'disabled'}
|
||||
${true} | ${false} | ${'disabled'}
|
||||
${false} | ${false} | ${'disabled'}
|
||||
`('', ({ active, resetPayloadAndMappingConfirmed, disabled }) => {
|
||||
const payloadResetMsg = resetPayloadAndMappingConfirmed
|
||||
? 'was confirmed'
|
||||
: 'was not confirmed';
|
||||
const enabledState = disabled === 'disabled' ? 'disabled' : 'enabled';
|
||||
const activeState = active ? 'active' : 'not active';
|
||||
|
||||
it(`textarea should be ${enabledState} when payload reset ${payloadResetMsg} and current integration is ${activeState}`, async () => {
|
||||
wrapper.setData({
|
||||
customMapping: { samplePayload: true },
|
||||
currentIntegration: {
|
||||
type: typeSet.http,
|
||||
payloadExample: validSamplePayload,
|
||||
payloadAttributeMappings: [],
|
||||
},
|
||||
active,
|
||||
resetSamplePayloadConfirmed,
|
||||
resetPayloadAndMappingConfirmed,
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findTestPayloadSection().find(GlFormTextarea).attributes('disabled')).toBe(disabled);
|
||||
|
@ -311,20 +328,27 @@ describe('AlertsSettingsForm', () => {
|
|||
|
||||
describe('action buttons for sample payload', () => {
|
||||
describe.each`
|
||||
resetSamplePayloadConfirmed | samplePayload | caption
|
||||
${false} | ${true} | ${'Edit payload'}
|
||||
${true} | ${false} | ${'Submit payload'}
|
||||
${true} | ${true} | ${'Submit payload'}
|
||||
${false} | ${false} | ${'Submit payload'}
|
||||
`('', ({ resetSamplePayloadConfirmed, samplePayload, caption }) => {
|
||||
const samplePayloadMsg = samplePayload ? 'was provided' : 'was not provided';
|
||||
const payloadResetMsg = resetSamplePayloadConfirmed ? 'was confirmed' : 'was not confirmed';
|
||||
resetPayloadAndMappingConfirmed | payloadExample | caption
|
||||
${false} | ${validSamplePayload} | ${'Edit payload'}
|
||||
${true} | ${emptySamplePayload} | ${'Submit payload'}
|
||||
${true} | ${validSamplePayload} | ${'Submit payload'}
|
||||
${false} | ${emptySamplePayload} | ${'Submit payload'}
|
||||
`('', ({ resetPayloadAndMappingConfirmed, payloadExample, caption }) => {
|
||||
const samplePayloadMsg = payloadExample ? 'was provided' : 'was not provided';
|
||||
const payloadResetMsg = resetPayloadAndMappingConfirmed
|
||||
? 'was confirmed'
|
||||
: 'was not confirmed';
|
||||
|
||||
it(`shows ${caption} button when sample payload ${samplePayloadMsg} and payload reset ${payloadResetMsg}`, async () => {
|
||||
wrapper.setData({
|
||||
selectedIntegration: typeSet.http,
|
||||
customMapping: { samplePayload },
|
||||
resetSamplePayloadConfirmed,
|
||||
currentIntegration: {
|
||||
payloadExample,
|
||||
type: typeSet.http,
|
||||
active: true,
|
||||
payloadAttributeMappings: [],
|
||||
},
|
||||
resetPayloadAndMappingConfirmed,
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(findActionBtn().text()).toBe(caption);
|
||||
|
@ -333,16 +357,20 @@ describe('AlertsSettingsForm', () => {
|
|||
});
|
||||
|
||||
describe('Parsing payload', () => {
|
||||
it('displays a toast message on successful parse', async () => {
|
||||
jest.useFakeTimers();
|
||||
beforeEach(() => {
|
||||
wrapper.setData({
|
||||
selectedIntegration: typeSet.http,
|
||||
customMapping: { samplePayload: false },
|
||||
resetPayloadAndMappingConfirmed: true,
|
||||
});
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
it('displays a toast message on successful parse', async () => {
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockResolvedValue({
|
||||
data: {
|
||||
project: { alertManagementPayloadFields: [] },
|
||||
},
|
||||
});
|
||||
findActionBtn().vm.$emit('click');
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
|
@ -350,6 +378,16 @@ describe('AlertsSettingsForm', () => {
|
|||
'Sample payload has been parsed. You can now map the fields.',
|
||||
);
|
||||
});
|
||||
|
||||
it('displays an error message under payload field on unsuccessful parse', async () => {
|
||||
const errorMessage = 'Error parsing paylod';
|
||||
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({ message: errorMessage });
|
||||
findActionBtn().vm.$emit('click');
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
expect(findTestPayloadSection().find('.invalid-feedback').text()).toBe(errorMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutat
|
|||
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 updateCurrentHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_http_integration.mutation.graphql';
|
||||
import updateCurrentPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/update_current_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 getIntegrationsQuery from '~/alerts_settings/graphql/queries/get_integrations.query.graphql';
|
||||
|
@ -31,7 +33,8 @@ import {
|
|||
updateHttpVariables,
|
||||
createPrometheusVariables,
|
||||
updatePrometheusVariables,
|
||||
ID,
|
||||
HTTP_ID,
|
||||
PROMETHEUS_ID,
|
||||
errorMsg,
|
||||
getIntegrationsQueryResponse,
|
||||
destroyIntegrationResponse,
|
||||
|
@ -50,8 +53,30 @@ describe('AlertsSettingsWrapper', () => {
|
|||
let fakeApollo;
|
||||
let destroyIntegrationHandler;
|
||||
useMockIntersectionObserver();
|
||||
const httpMappingData = {
|
||||
payloadExample: '{"test: : "field"}',
|
||||
payloadAttributeMappings: [],
|
||||
payloadAlertFields: [],
|
||||
};
|
||||
const httpIntegrations = {
|
||||
list: [
|
||||
{
|
||||
id: mockIntegrations[0].id,
|
||||
...httpMappingData,
|
||||
},
|
||||
{
|
||||
id: mockIntegrations[1].id,
|
||||
...httpMappingData,
|
||||
},
|
||||
{
|
||||
id: mockIntegrations[2].id,
|
||||
httpMappingData,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const findLoader = () => wrapper.find(IntegrationsList).find(GlLoadingIcon);
|
||||
const findLoader = () => wrapper.findComponent(IntegrationsList).findComponent(GlLoadingIcon);
|
||||
const findIntegrationsList = () => wrapper.findComponent(IntegrationsList);
|
||||
const findIntegrations = () => wrapper.find(IntegrationsList).findAll('table tbody tr');
|
||||
|
||||
async function destroyHttpIntegration(localWrapper) {
|
||||
|
@ -197,13 +222,13 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
|
||||
type: typeSet.http,
|
||||
variables: { id: ID },
|
||||
variables: { id: HTTP_ID },
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: resetHttpTokenMutation,
|
||||
variables: {
|
||||
id: ID,
|
||||
id: HTTP_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -232,7 +257,7 @@ describe('AlertsSettingsWrapper', () => {
|
|||
|
||||
it('calls `$apollo.mutate` with `updatePrometheusIntegrationMutation`', () => {
|
||||
createComponent({
|
||||
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[0] },
|
||||
data: { integrations: { list: mockIntegrations }, currentIntegration: mockIntegrations[3] },
|
||||
loading: false,
|
||||
});
|
||||
|
||||
|
@ -261,13 +286,13 @@ describe('AlertsSettingsWrapper', () => {
|
|||
});
|
||||
wrapper.find(AlertsSettingsForm).vm.$emit('reset-token', {
|
||||
type: typeSet.prometheus,
|
||||
variables: { id: ID },
|
||||
variables: { id: PROMETHEUS_ID },
|
||||
});
|
||||
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: resetPrometheusTokenMutation,
|
||||
variables: {
|
||||
id: ID,
|
||||
id: PROMETHEUS_ID,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -328,6 +353,42 @@ describe('AlertsSettingsWrapper', () => {
|
|||
mock.restore();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `$apollo.mutate` with `updateCurrentHttpIntegrationMutation` on HTTP integration edit', () => {
|
||||
createComponent({
|
||||
data: {
|
||||
integrations: { list: mockIntegrations },
|
||||
currentIntegration: mockIntegrations[0],
|
||||
httpIntegrations,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
findIntegrationsList().vm.$emit('edit-integration', updateHttpVariables);
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateCurrentHttpIntegrationMutation,
|
||||
variables: { ...mockIntegrations[0], ...httpMappingData },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls `$apollo.mutate` with `updateCurrentPrometheusIntegrationMutation` on PROMETHEUS integration edit', () => {
|
||||
createComponent({
|
||||
data: {
|
||||
integrations: { list: mockIntegrations },
|
||||
currentIntegration: mockIntegrations[3],
|
||||
httpIntegrations,
|
||||
},
|
||||
loading: false,
|
||||
});
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate');
|
||||
findIntegrationsList().vm.$emit('edit-integration', updatePrometheusVariables);
|
||||
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
|
||||
mutation: updateCurrentPrometheusIntegrationMutation,
|
||||
variables: mockIntegrations[3],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with mocked Apollo client', () => {
|
||||
|
|
|
@ -1,29 +1,34 @@
|
|||
const projectPath = '';
|
||||
export const ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
|
||||
export const HTTP_ID = 'gid://gitlab/AlertManagement::HttpIntegration/7';
|
||||
export const PROMETHEUS_ID = 'gid://gitlab/PrometheusService/12';
|
||||
export const errorMsg = 'Something went wrong';
|
||||
|
||||
export const createHttpVariables = {
|
||||
name: 'Test Pre',
|
||||
active: true,
|
||||
projectPath,
|
||||
type: 'HTTP',
|
||||
};
|
||||
|
||||
export const updateHttpVariables = {
|
||||
name: 'Test Pre',
|
||||
active: true,
|
||||
id: ID,
|
||||
id: HTTP_ID,
|
||||
type: 'HTTP',
|
||||
};
|
||||
|
||||
export const createPrometheusVariables = {
|
||||
apiUrl: 'https://test-pre.com',
|
||||
active: true,
|
||||
projectPath,
|
||||
type: 'PROMETHEUS',
|
||||
};
|
||||
|
||||
export const updatePrometheusVariables = {
|
||||
apiUrl: 'https://test-pre.com',
|
||||
active: true,
|
||||
id: ID,
|
||||
id: PROMETHEUS_ID,
|
||||
type: 'PROMETHEUS',
|
||||
};
|
||||
|
||||
export const getIntegrationsQueryResponse = {
|
||||
|
@ -99,6 +104,9 @@ export const destroyIntegrationResponse = {
|
|||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
payloadExample: '{"field": "value"}',
|
||||
payloadAttributeMappings: [],
|
||||
payloadAlertFields: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -117,6 +125,9 @@ export const destroyIntegrationResponseWithErrors = {
|
|||
'http://127.0.0.1:3000/h5bp/html5-boilerplate/alerts/notify/test-5/d4875758e67334f3.json',
|
||||
token: '89eb01df471d990ff5162a1c640408cf',
|
||||
apiUrl: null,
|
||||
payloadExample: '{"field": "value"}',
|
||||
payloadAttributeMappings: [],
|
||||
payloadAlertFields: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
122
spec/frontend/alerts_settings/mocks/parsed_mapping.json
Normal file
122
spec/frontend/alerts_settings/mocks/parsed_mapping.json
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"payloadAlerFields": [
|
||||
{
|
||||
"path": [
|
||||
"dashboardId"
|
||||
],
|
||||
"label": "Dashboard Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"evalMatches"
|
||||
],
|
||||
"label": "Eval Matches",
|
||||
"type": "array"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"createdAt"
|
||||
],
|
||||
"label": "Created At",
|
||||
"type": "datetime"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"imageUrl"
|
||||
],
|
||||
"label": "Image Url",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"message"
|
||||
],
|
||||
"label": "Message",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"orgId"
|
||||
],
|
||||
"label": "Org Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"panelId"
|
||||
],
|
||||
"label": "Panel Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"ruleId"
|
||||
],
|
||||
"label": "Rule Id",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"ruleName"
|
||||
],
|
||||
"label": "Rule Name",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"ruleUrl"
|
||||
],
|
||||
"label": "Rule Url",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"state"
|
||||
],
|
||||
"label": "State",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"title"
|
||||
],
|
||||
"label": "Title",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"path": [
|
||||
"tags",
|
||||
"tag"
|
||||
],
|
||||
"label": "Tags",
|
||||
"type": "string"
|
||||
}
|
||||
],
|
||||
"payloadAttributeMappings": [
|
||||
{
|
||||
"fieldName": "title",
|
||||
"label": "Title",
|
||||
"type": "STRING",
|
||||
"path": ["title"]
|
||||
},
|
||||
{
|
||||
"fieldName": "description",
|
||||
"label": "description",
|
||||
"type": "STRING",
|
||||
"path": ["description"]
|
||||
},
|
||||
{
|
||||
"fieldName": "hosts",
|
||||
"label": "Host",
|
||||
"type": "ARRAY",
|
||||
"path": ["hosts", "host"]
|
||||
},
|
||||
{
|
||||
"fieldName": "startTime",
|
||||
"label": "Created Atd",
|
||||
"type": "STRING",
|
||||
"path": ["time", "createdAt"]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,29 +1,25 @@
|
|||
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
|
||||
import {
|
||||
getMappingData,
|
||||
getPayloadFields,
|
||||
transformForSave,
|
||||
} from '~/alerts_settings/utils/mapping_transformations';
|
||||
import alertFields from '../mocks/alertFields.json';
|
||||
import { getMappingData, transformForSave } from '~/alerts_settings/utils/mapping_transformations';
|
||||
import alertFields from '../mocks/alert_fields.json';
|
||||
import parsedMapping from '../mocks/parsed_mapping.json';
|
||||
|
||||
describe('Mapping Transformation Utilities', () => {
|
||||
const nameField = {
|
||||
label: 'Name',
|
||||
path: ['alert', 'name'],
|
||||
type: 'string',
|
||||
type: 'STRING',
|
||||
};
|
||||
const dashboardField = {
|
||||
label: 'Dashboard Id',
|
||||
path: ['alert', 'dashboardId'],
|
||||
type: 'string',
|
||||
type: 'STRING',
|
||||
};
|
||||
|
||||
describe('getMappingData', () => {
|
||||
it('should return mapping data', () => {
|
||||
const result = getMappingData(
|
||||
alertFields,
|
||||
getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
|
||||
parsedMapping.storedMapping.nodes.slice(0, 3),
|
||||
parsedMapping.payloadAlerFields.slice(0, 3),
|
||||
parsedMapping.payloadAttributeMappings.slice(0, 3),
|
||||
);
|
||||
|
||||
result.forEach((data, index) => {
|
||||
|
@ -44,8 +40,8 @@ describe('Mapping Transformation Utilities', () => {
|
|||
const mockMappingData = [
|
||||
{
|
||||
name: fieldName,
|
||||
mapping: 'alert_name',
|
||||
mappingFields: getPayloadFields([dashboardField, nameField]),
|
||||
mapping: ['alert', 'name'],
|
||||
mappingFields: [dashboardField, nameField],
|
||||
},
|
||||
];
|
||||
const result = transformForSave(mockMappingData);
|
||||
|
@ -61,21 +57,11 @@ describe('Mapping Transformation Utilities', () => {
|
|||
{
|
||||
name: fieldName,
|
||||
mapping: null,
|
||||
mappingFields: getPayloadFields([nameField, dashboardField]),
|
||||
mappingFields: [nameField, dashboardField],
|
||||
},
|
||||
];
|
||||
const result = transformForSave(mockMappingData);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPayloadFields', () => {
|
||||
it('should add name field to each payload field', () => {
|
||||
const result = getPayloadFields([nameField, dashboardField]);
|
||||
expect(result).toEqual([
|
||||
{ ...nameField, name: 'alert_name' },
|
||||
{ ...dashboardField, name: 'alert_dashboardId' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -102,7 +102,7 @@ exports[`packages_list_row renders 1`] = `
|
|||
<gl-button-stub
|
||||
aria-label="Remove package"
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
category="secondary"
|
||||
data-testid="action-delete"
|
||||
icon="remove"
|
||||
size="medium"
|
||||
|
|
|
@ -60,11 +60,9 @@ describe('packages_list_row', () => {
|
|||
});
|
||||
|
||||
describe('when is is group', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ isGroup: true });
|
||||
});
|
||||
|
||||
it('has a package path component', () => {
|
||||
mountComponent({ isGroup: true });
|
||||
|
||||
expect(findPackagePath().exists()).toBe(true);
|
||||
expect(findPackagePath().props()).toMatchObject({ path: 'foo/bar/baz' });
|
||||
});
|
||||
|
@ -92,10 +90,22 @@ describe('packages_list_row', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('delete event', () => {
|
||||
beforeEach(() => mountComponent({ packageEntity: packageWithoutTags }));
|
||||
describe('delete button', () => {
|
||||
it('exists and has the correct props', () => {
|
||||
mountComponent({ packageEntity: packageWithoutTags });
|
||||
|
||||
expect(findDeleteButton().exists()).toBe(true);
|
||||
expect(findDeleteButton().attributes()).toMatchObject({
|
||||
icon: 'remove',
|
||||
category: 'secondary',
|
||||
variant: 'danger',
|
||||
title: 'Remove package',
|
||||
});
|
||||
});
|
||||
|
||||
it('emits the packageToDelete event when the delete button is clicked', async () => {
|
||||
mountComponent({ packageEntity: packageWithoutTags });
|
||||
|
||||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
|
40
spec/frontend/pages/shared/wikis/wiki_alert_spec.js
Normal file
40
spec/frontend/pages/shared/wikis/wiki_alert_spec.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import WikiAlert from '~/pages/shared/wikis/components/wiki_alert.vue';
|
||||
|
||||
describe('WikiAlert', () => {
|
||||
let wrapper;
|
||||
const ERROR = 'There is already a page with the same title in that path.';
|
||||
const ERROR_WITH_LINK = 'Before text %{wikiLinkStart}the page%{wikiLinkEnd} after text.';
|
||||
const PATH = '/test';
|
||||
|
||||
function createWrapper(propsData = {}, stubs = {}) {
|
||||
wrapper = shallowMount(WikiAlert, {
|
||||
propsData: { wikiPagePath: PATH, ...propsData },
|
||||
stubs,
|
||||
});
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
const findGlAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findGlLink = () => wrapper.findComponent(GlLink);
|
||||
const findGlSprintf = () => wrapper.findComponent(GlSprintf);
|
||||
|
||||
describe('Wiki Alert', () => {
|
||||
it('shows an alert when there is an error', () => {
|
||||
createWrapper({ error: ERROR });
|
||||
expect(findGlAlert().exists()).toBe(true);
|
||||
expect(findGlSprintf().exists()).toBe(true);
|
||||
expect(findGlSprintf().attributes('message')).toBe(ERROR);
|
||||
});
|
||||
|
||||
it('shows a the link to the help path', () => {
|
||||
createWrapper({ error: ERROR_WITH_LINK }, { GlAlert, GlSprintf });
|
||||
expect(findGlLink().attributes('href')).toBe(PATH);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -142,6 +142,8 @@ describe('Pipelines stage component', () => {
|
|||
beforeEach(() => {
|
||||
mock.onGet(dropdownPath).reply(200, stageReply);
|
||||
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
const clickCiAction = async () => {
|
||||
|
@ -152,34 +154,22 @@ describe('Pipelines stage component', () => {
|
|||
await axios.waitForAll();
|
||||
};
|
||||
|
||||
describe('within pipeline table', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ type: 'PIPELINES_TABLE' });
|
||||
});
|
||||
it('closes dropdown when job item action is clicked', async () => {
|
||||
const hidden = jest.fn();
|
||||
|
||||
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
await clickCiAction();
|
||||
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
|
||||
|
||||
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
|
||||
});
|
||||
expect(hidden).toHaveBeenCalledTimes(0);
|
||||
|
||||
await clickCiAction();
|
||||
|
||||
expect(hidden).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('in MR widget', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
it('emits `pipelineActionRequestComplete` when job item action is clicked', async () => {
|
||||
await clickCiAction();
|
||||
|
||||
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
|
||||
const hidden = jest.fn();
|
||||
|
||||
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
|
||||
|
||||
expect(hidden).toHaveBeenCalledTimes(0);
|
||||
|
||||
await clickCiAction();
|
||||
|
||||
expect(hidden).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(wrapper.emitted('pipelineActionRequestComplete')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -58,6 +58,7 @@ describe('delete_button', () => {
|
|||
title: 'Foo title',
|
||||
variant: 'danger',
|
||||
disabled: 'true',
|
||||
category: 'secondary',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -58,12 +58,10 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema:
|
|||
context 'when an iteration cadence exists for a group' do
|
||||
let!(:group) { namespaces.create!(name: 'group', path: 'group') }
|
||||
|
||||
let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 5.days.ago, title: 'Cadence 1') }
|
||||
let!(:iterations_cadence_2) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 2') }
|
||||
let!(:iterations_cadence_1) { iterations_cadences.create!(group_id: group.id, start_date: 2.days.ago, title: 'Cadence 1') }
|
||||
|
||||
let!(:iteration_1) { iterations.create!(group_id: group.id, iid: 1, title: 'Iteration 1', start_date: 10.days.ago, due_date: 8.days.ago) }
|
||||
let!(:iteration_2) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_1.id, iid: 2, title: 'Iteration 2', start_date: 5.days.ago, due_date: 3.days.ago) }
|
||||
let!(:iteration_3) { iterations.create!(group_id: group.id, iterations_cadence_id: iterations_cadence_2.id, iid: 3, title: 'Iteration 3', start_date: 2.days.ago, due_date: 1.day.ago) }
|
||||
|
||||
subject { described_class.new.perform(group.id) }
|
||||
|
||||
|
@ -76,7 +74,6 @@ RSpec.describe Gitlab::BackgroundMigration::SetDefaultIterationCadences, schema:
|
|||
|
||||
expect(iteration_1.reload.iterations_cadence_id).to eq(iterations_cadence_1.id)
|
||||
expect(iteration_2.reload.iterations_cadence_id).to eq(iterations_cadence_1.id)
|
||||
expect(iteration_3.reload.iterations_cadence_id).to eq(iterations_cadence_2.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require Rails.root.join('db', 'post_migrate', '20210215095328_migrate_delayed_project_removal_from_namespaces_to_namespace_settings.rb')
|
||||
|
||||
RSpec.describe MigrateDelayedProjectRemovalFromNamespacesToNamespaceSettings, :migration do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:namespace_settings) { table(:namespace_settings) }
|
||||
|
||||
let!(:namespace_wo_settings) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) }
|
||||
let!(:namespace_wo_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) }
|
||||
let!(:namespace_w_settings_delay_true) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: true) }
|
||||
let!(:namespace_w_settings_delay_false) { namespaces.create!(name: generate(:name), path: generate(:name), delayed_project_removal: false) }
|
||||
|
||||
let!(:namespace_settings_delay_true) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_true.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) }
|
||||
let!(:namespace_settings_delay_false) { namespace_settings.create!(namespace_id: namespace_w_settings_delay_false.id, delayed_project_removal: false, created_at: DateTime.now, updated_at: DateTime.now) }
|
||||
|
||||
it 'migrates delayed_project_removal to namespace_settings' do
|
||||
disable_migrations_output { migrate! }
|
||||
|
||||
expect(namespace_settings.count).to eq(3)
|
||||
|
||||
expect(namespace_settings.find_by(namespace_id: namespace_wo_settings.id).delayed_project_removal).to eq(true)
|
||||
expect(namespace_settings.find_by(namespace_id: namespace_wo_settings_delay_false.id)).to be_nil
|
||||
|
||||
expect(namespace_settings_delay_true.reload.delayed_project_removal).to eq(true)
|
||||
expect(namespace_settings_delay_false.reload.delayed_project_removal).to eq(false)
|
||||
end
|
||||
end
|
|
@ -16,13 +16,13 @@ RSpec.describe RescheduleSetDefaultIterationCadences do
|
|||
let(:group_7) { namespaces.create!(name: 'test_7', path: 'test_7') }
|
||||
let(:group_8) { namespaces.create!(name: 'test_8', path: 'test_8') }
|
||||
|
||||
let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id) }
|
||||
let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id) }
|
||||
let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id) }
|
||||
let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id) }
|
||||
let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id) }
|
||||
let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id) }
|
||||
let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id) }
|
||||
let!(:iteration_1) { iterations.create!(iid: 1, title: 'iteration 1', group_id: group_1.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_2) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_3.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_3) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_4.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_4) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_5.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_5) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_6.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_6) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_7.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
let!(:iteration_7) { iterations.create!(iid: 1, title: 'iteration 2', group_id: group_8.id, start_date: 2.days.from_now, due_date: 3.days.from_now) }
|
||||
|
||||
around do |example|
|
||||
freeze_time { Sidekiq::Testing.fake! { example.run } }
|
||||
|
|
|
@ -3,10 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Iteration do
|
||||
let_it_be(:project) { create(:project) }
|
||||
let_it_be(:group) { create(:group) }
|
||||
let(:set_cadence) { nil }
|
||||
|
||||
let_it_be(:group) { create(:group) }
|
||||
let_it_be(:project) { create(:project, group: group) }
|
||||
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:project) }
|
||||
it { is_expected.to belong_to(:group) }
|
||||
|
@ -67,7 +68,7 @@ RSpec.describe Iteration do
|
|||
expect { iteration.save! }.to change { Iterations::Cadence.count }.by(1)
|
||||
end
|
||||
|
||||
it 'sets the newly created iterations_cadence to the reecord' do
|
||||
it 'sets the newly created iterations_cadence to the record' do
|
||||
iteration.save!
|
||||
|
||||
expect(iteration.iterations_cadence).to eq(Iterations::Cadence.last)
|
||||
|
@ -148,7 +149,7 @@ RSpec.describe Iteration do
|
|||
context 'Validations' do
|
||||
subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) }
|
||||
|
||||
describe '#not_belonging_to_project' do
|
||||
describe 'when iteration belongs to project' do
|
||||
subject { build(:iteration, project: project, start_date: Time.current, due_date: 1.day.from_now) }
|
||||
|
||||
it 'is invalid' do
|
||||
|
@ -180,13 +181,13 @@ RSpec.describe Iteration do
|
|||
let(:due_date) { 6.days.from_now }
|
||||
|
||||
shared_examples_for 'overlapping dates' do |skip_constraint_test: false|
|
||||
context 'when start_date is in range' do
|
||||
context 'when start_date overlaps' do
|
||||
let(:start_date) { 5.days.from_now }
|
||||
let(:due_date) { 3.weeks.from_now }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group')
|
||||
end
|
||||
|
||||
unless skip_constraint_test
|
||||
|
@ -197,13 +198,13 @@ RSpec.describe Iteration do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when end_date is in range' do
|
||||
context 'when due_date overlaps' do
|
||||
let(:start_date) { Time.current }
|
||||
let(:due_date) { 6.days.from_now }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group')
|
||||
end
|
||||
|
||||
unless skip_constraint_test
|
||||
|
@ -217,7 +218,7 @@ RSpec.describe Iteration do
|
|||
context 'when both overlap' do
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
|
||||
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations within this group')
|
||||
end
|
||||
|
||||
unless skip_constraint_test
|
||||
|
@ -231,7 +232,7 @@ RSpec.describe Iteration do
|
|||
|
||||
context 'group' do
|
||||
it_behaves_like 'overlapping dates' do
|
||||
let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' }
|
||||
let(:constraint_name) { 'iteration_start_and_due_date_iterations_cadence_id_constraint' }
|
||||
end
|
||||
|
||||
context 'different group' do
|
||||
|
@ -249,11 +250,12 @@ RSpec.describe Iteration do
|
|||
|
||||
subject { build(:iteration, group: subgroup, start_date: start_date, due_date: due_date) }
|
||||
|
||||
it_behaves_like 'overlapping dates', skip_constraint_test: true
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
end
|
||||
|
||||
context 'project' do
|
||||
# Skipped. Pending https://gitlab.com/gitlab-org/gitlab/-/issues/299864
|
||||
xcontext 'project' do
|
||||
let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
|
||||
|
||||
subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) }
|
||||
|
@ -283,16 +285,16 @@ RSpec.describe Iteration do
|
|||
expect { subject.save! }.not_to raise_exception
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'project in a group' do
|
||||
let_it_be(:project) { create(:project, group: create(:group)) }
|
||||
let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
|
||||
context 'project in a group' do
|
||||
let_it_be(:project) { create(:project, group: create(:group)) }
|
||||
let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) }
|
||||
|
||||
subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) }
|
||||
subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) }
|
||||
|
||||
it_behaves_like 'overlapping dates' do
|
||||
let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' }
|
||||
it_behaves_like 'overlapping dates' do
|
||||
let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -310,19 +312,23 @@ RSpec.describe Iteration do
|
|||
let(:start_date) { 1.week.ago }
|
||||
let(:due_date) { 1.week.from_now }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:start_date]).to include('cannot be in the past')
|
||||
end
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when due_date is in the past' do
|
||||
let(:start_date) { 2.weeks.ago }
|
||||
let(:due_date) { 1.week.ago }
|
||||
|
||||
it { is_expected.to be_valid }
|
||||
end
|
||||
|
||||
context 'when due_date is before start date' do
|
||||
let(:start_date) { Time.current }
|
||||
let(:due_date) { 1.week.ago }
|
||||
|
||||
it 'is not valid' do
|
||||
expect(subject).not_to be_valid
|
||||
expect(subject.errors[:due_date]).to include('cannot be in the past')
|
||||
expect(subject.errors[:due_date]).to include('must be greater than start date')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe Namespace do
|
|||
include ProjectForksHelper
|
||||
include GitHelpers
|
||||
|
||||
let!(:namespace) { create(:namespace) }
|
||||
let!(:namespace) { create(:namespace, :with_namespace_settings) }
|
||||
let(:gitlab_shell) { Gitlab::Shell.new }
|
||||
let(:repository_storage) { 'default' }
|
||||
|
||||
|
@ -116,6 +116,28 @@ RSpec.describe Namespace do
|
|||
it { is_expected.to include_module(Namespaces::Traversal::Recursive) }
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
describe 'before_save :ensure_delayed_project_removal_assigned_to_namespace_settings' do
|
||||
it 'sets the matching value in namespace_settings' do
|
||||
expect { namespace.update!(delayed_project_removal: true) }.to change {
|
||||
namespace.namespace_settings.delayed_project_removal
|
||||
}.from(false).to(true)
|
||||
end
|
||||
|
||||
context 'when the feature flag is disabled' do
|
||||
before do
|
||||
stub_feature_flags(migrate_delayed_project_removal: false)
|
||||
end
|
||||
|
||||
it 'does not set the matching value in namespace_settings' do
|
||||
expect { namespace.update!(delayed_project_removal: true) }.not_to change {
|
||||
namespace.namespace_settings.delayed_project_removal
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#visibility_level_field' do
|
||||
it { expect(namespace.visibility_level_field).to eq(:visibility_level) }
|
||||
end
|
||||
|
|
|
@ -48,6 +48,6 @@ RSpec.shared_examples 'a mutation that returns errors in the response' do |error
|
|||
it do
|
||||
post_graphql_mutation(mutation, current_user: current_user)
|
||||
|
||||
expect(mutation_response['errors']).to eq(errors)
|
||||
expect(mutation_response['errors']).to match_array(errors)
|
||||
end
|
||||
end
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue