Commit 2d5bfa69 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'jh-github_search' into 'master'

Fix GitHub project import filter box

See merge request gitlab-org/gitlab!45783
parents f67ed510 d22c2557
...@@ -16,7 +16,7 @@ inherit_mode: ...@@ -16,7 +16,7 @@ inherit_mode:
- Include - Include
AllCops: AllCops:
TargetRubyVersion: 2.6 TargetRubyVersion: 2.7
TargetRailsVersion: 6.0 TargetRailsVersion: 6.0
Exclude: Exclude:
- 'vendor/**/*' - 'vendor/**/*'
......
5783f980c3c83022dd5a0173186fba4158948062 020b5f709d58277c360ba409b8f8a9e81cee2781
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlLink,
GlSprintf,
},
props: {
message: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
},
};
</script>
<template>
<span class="gl-text-gray-500">
<gl-sprintf :message="message">
<template #link="{ content }">
<gl-link class="gl-display-inline-block" :href="link" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
...@@ -81,7 +81,6 @@ export default { ...@@ -81,7 +81,6 @@ export default {
<div class="incident-management-list"> <div class="incident-management-list">
<h5 class="gl-font-lg">{{ $options.i18n.title }}</h5> <h5 class="gl-font-lg">{{ $options.i18n.title }}</h5>
<gl-table <gl-table
:empty-text="$options.i18n.emptyState"
:items="integrations" :items="integrations"
:fields="$options.fields" :fields="$options.fields"
:busy="loading" :busy="loading"
...@@ -115,6 +114,14 @@ export default { ...@@ -115,6 +114,14 @@ export default {
<template #table-busy> <template #table-busy>
<gl-loading-icon size="lg" color="dark" class="mt-3" /> <gl-loading-icon size="lg" color="dark" class="mt-3" />
</template> </template>
<template #empty>
<div
class="gl-border-t-solid gl-border-b-solid gl-border-1 gl-border gl-border-gray-100 mt-n3"
>
<p class="gl-text-gray-400 gl-py-3 gl-my-3">{{ $options.i18n.emptyState }}</p>
</div>
</template>
</gl-table> </gl-table>
</div> </div>
</template> </template>
...@@ -56,7 +56,7 @@ export default { ...@@ -56,7 +56,7 @@ export default {
data() { data() {
return { return {
loading: false, loading: false,
selectedIntegration: integrationTypes[1].value, selectedIntegration: integrationTypes[0].value,
options: integrationTypes, options: integrationTypes,
active: false, active: false,
authKey: '', authKey: '',
...@@ -88,34 +88,34 @@ export default { ...@@ -88,34 +88,34 @@ export default {
]; ];
}, },
isPrometheus() { isPrometheus() {
return this.selectedIntegration === 'prometheus'; return this.selectedIntegration === 'PROMETHEUS';
}, },
isOpsgenie() { isOpsgenie() {
return this.selectedIntegration === 'opsgenie'; return this.selectedIntegration === 'OPSGENIE';
}, },
selectedIntegrationType() { selectedIntegrationType() {
switch (this.selectedIntegration) { switch (this.selectedIntegration) {
case 'generic': { case 'HTTP': {
return { return {
url: this.generic.url, url: this.generic.url,
authKey: this.generic.authorizationKey, authKey: this.generic.authKey,
activated: this.generic.activated, active: this.generic.active,
resetKey: this.resetKey.bind(this), resetKey: this.resetKey.bind(this),
}; };
} }
case 'prometheus': { case 'PROMETHEUS': {
return { return {
url: this.prometheus.prometheusUrl, url: this.prometheus.url,
authKey: this.prometheus.authorizationKey, authKey: this.prometheus.authKey,
activated: this.prometheus.activated, active: this.prometheus.active,
resetKey: this.resetKey.bind(this, 'prometheus'), resetKey: this.resetKey.bind(this, 'PROMETHEUS'),
targetUrl: this.prometheus.prometheusApiUrl, targetUrl: this.prometheus.prometheusApiUrl,
}; };
} }
case 'opsgenie': { case 'OPSGENIE': {
return { return {
targetUrl: this.opsgenie.opsgenieMvcTargetUrl, targetUrl: this.opsgenie.opsgenieMvcTargetUrl,
activated: this.opsgenie.activated, active: this.opsgenie.active,
}; };
} }
default: { default: {
...@@ -161,16 +161,12 @@ export default { ...@@ -161,16 +161,12 @@ export default {
}, },
}, },
mounted() { mounted() {
if ( if (this.prometheus.active || this.generic.active || !this.opsgenie.opsgenieMvcIsAvailable) {
this.prometheus.activated ||
this.generic.activated ||
!this.opsgenie.opsgenieMvcIsAvailable
) {
this.removeOpsGenieOption(); this.removeOpsGenieOption();
} else if (this.opsgenie.activated) { } else if (this.opsgenie.active) {
this.setOpsgenieAsDefault(); this.setOpsgenieAsDefault();
} }
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
this.authKey = this.selectedIntegrationType.authKey ?? ''; this.authKey = this.selectedIntegrationType.authKey ?? '';
}, },
methods: { methods: {
...@@ -183,19 +179,19 @@ export default { ...@@ -183,19 +179,19 @@ export default {
}, },
setOpsgenieAsDefault() { setOpsgenieAsDefault() {
this.options = this.options.map(el => { this.options = this.options.map(el => {
if (el.value !== 'opsgenie') { if (el.value !== 'OPSGENIE') {
return { ...el, disabled: true }; return { ...el, disabled: true };
} }
return { ...el, disabled: false }; return { ...el, disabled: false };
}); });
this.selectedIntegration = this.options.find(({ value }) => value === 'opsgenie').value; this.selectedIntegration = this.options.find(({ value }) => value === 'OPSGENIE').value;
if (this.targetUrl === null) { if (this.targetUrl === null) {
this.targetUrl = this.selectedIntegrationType.targetUrl; this.targetUrl = this.selectedIntegrationType.targetUrl;
} }
}, },
removeOpsGenieOption() { removeOpsGenieOption() {
this.options = this.options.map(el => { this.options = this.options.map(el => {
if (el.value !== 'opsgenie') { if (el.value !== 'OPSGENIE') {
return { ...el, disabled: false }; return { ...el, disabled: false };
} }
return { ...el, disabled: true }; return { ...el, disabled: true };
...@@ -204,7 +200,7 @@ export default { ...@@ -204,7 +200,7 @@ export default {
resetFormValues() { resetFormValues() {
this.testAlert.json = null; this.testAlert.json = null;
this.targetUrl = this.selectedIntegrationType.targetUrl; this.targetUrl = this.selectedIntegrationType.targetUrl;
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
}, },
dismissFeedback() { dismissFeedback() {
this.serverError = null; this.serverError = null;
...@@ -212,7 +208,7 @@ export default { ...@@ -212,7 +208,7 @@ export default {
this.isFeedbackDismissed = false; this.isFeedbackDismissed = false;
}, },
resetKey(key) { resetKey(key) {
const fn = key === 'prometheus' ? this.resetPrometheusKey() : this.resetGenericKey(); const fn = key === 'PROMETHEUS' ? this.resetPrometheusKey() : this.resetGenericKey();
return fn return fn
.then(({ data: { token } }) => { .then(({ data: { token } }) => {
...@@ -242,9 +238,10 @@ export default { ...@@ -242,9 +238,10 @@ export default {
}, },
toggleActivated(value) { toggleActivated(value) {
this.loading = true; this.loading = true;
const path = this.isOpsgenie ? this.opsgenie.formPath : this.generic.formPath;
return service return service
.updateGenericActive({ .updateGenericActive({
endpoint: this[this.selectedIntegration].formPath, endpoint: path,
params: this.isOpsgenie params: this.isOpsgenie
? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } } ? { service: { opsgenie_mvc_target_url: this.targetUrl, opsgenie_mvc_enabled: value } }
: { service: { active: value } }, : { service: { active: value } },
...@@ -345,7 +342,7 @@ export default { ...@@ -345,7 +342,7 @@ export default {
if (this.canSaveForm) { if (this.canSaveForm) {
this.canSaveForm = false; this.canSaveForm = false;
this.active = this.selectedIntegrationType.activated; this.active = this.selectedIntegrationType.active;
} }
}, },
}, },
...@@ -402,9 +399,9 @@ export default { ...@@ -402,9 +399,9 @@ export default {
</gl-sprintf> </gl-sprintf>
</span> </span>
</gl-form-group> </gl-form-group>
<gl-form-group :label="$options.i18n.activeLabel" label-for="activated"> <gl-form-group :label="$options.i18n.activeLabel" label-for="active">
<toggle-button <toggle-button
id="activated" id="active"
:disabled-input="loading" :disabled-input="loading"
:is-loading="loading" :is-loading="loading"
:value="active" :value="active"
......
<script> <script>
import produce from 'immer';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { fetchPolicies } from '~/lib/graphql'; import { fetchPolicies } from '~/lib/graphql';
import createFlash, { FLASH_TYPES } from '~/flash';
import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql';
import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql';
import IntegrationsList from './alerts_integrations_list.vue'; import IntegrationsList from './alerts_integrations_list.vue';
import SettingsFormOld from './alerts_settings_form_old.vue'; import SettingsFormOld from './alerts_settings_form_old.vue';
import SettingsFormNew from './alerts_settings_form_new.vue'; import SettingsFormNew from './alerts_settings_form_new.vue';
import { typeSet } from '../constants';
export default { export default {
typeSet,
i18n: {
changesSaved: s__(
'AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list.',
),
},
components: { components: {
IntegrationsList, IntegrationsList,
SettingsFormOld, SettingsFormOld,
...@@ -49,6 +60,7 @@ export default { ...@@ -49,6 +60,7 @@ export default {
data() { data() {
return { return {
errored: false, errored: false,
isUpdating: false,
integrations: {}, integrations: {},
}; };
}, },
...@@ -61,16 +73,85 @@ export default { ...@@ -61,16 +73,85 @@ export default {
{ {
name: s__('AlertSettings|HTTP endpoint'), name: s__('AlertSettings|HTTP endpoint'),
type: s__('AlertsIntegrations|HTTP endpoint'), type: s__('AlertsIntegrations|HTTP endpoint'),
active: this.generic.activated, active: this.generic.active,
}, },
{ {
name: s__('AlertSettings|External Prometheus'), name: s__('AlertSettings|External Prometheus'),
type: s__('AlertsIntegrations|Prometheus'), type: s__('AlertsIntegrations|Prometheus'),
active: this.prometheus.activated, active: this.prometheus.active,
}, },
]; ];
}, },
}, },
methods: {
onCreateNewIntegration({ type, variables }) {
this.isUpdating = true;
this.$apollo
.mutate({
mutation:
type === this.$options.typeSet.http
? createHttpIntegrationMutation
: createPrometheusIntegrationMutation,
variables: {
...variables,
projectPath: this.projectPath,
},
update: this.updateIntegrations,
})
.then(({ data: { httpIntegrationCreate, prometheusIntegrationCreate } = {} } = {}) => {
const error = httpIntegrationCreate?.errors[0] || prometheusIntegrationCreate?.errors[0];
if (error) {
return createFlash({ message: error });
}
return createFlash({
message: this.$options.i18n.changesSaved,
type: FLASH_TYPES.SUCCESS,
});
})
.catch(err => {
this.errored = true;
createFlash({ message: err });
})
.finally(() => {
this.isUpdating = false;
});
},
updateIntegrations(
store,
{
data: { httpIntegrationCreate, prometheusIntegrationCreate },
},
) {
const integration =
httpIntegrationCreate?.integration || prometheusIntegrationCreate?.integration;
if (!integration) {
return;
}
const sourceData = store.readQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
});
const data = produce(sourceData, draftData => {
// eslint-disable-next-line no-param-reassign
draftData.project.alertManagementIntegrations.nodes = [
integration,
...draftData.project.alertManagementIntegrations.nodes,
];
});
store.writeQuery({
query: getIntegrationsQuery,
variables: {
projectPath: this.projectPath,
},
data,
});
},
},
}; };
</script> </script>
...@@ -80,7 +161,11 @@ export default { ...@@ -80,7 +161,11 @@ export default {
:integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld" :integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld"
:loading="loading" :loading="loading"
/> />
<settings-form-new v-if="glFeatures.httpIntegrationsList" /> <settings-form-new
v-if="glFeatures.httpIntegrationsList"
:loading="loading"
@on-create-new-integration="onCreateNewIntegration"
/>
<settings-form-old v-else /> <settings-form-old v-else />
</div> </div>
</template> </template>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
// TODO: Remove this as part of the form old removal
export const i18n = { export const i18n = {
usageSection: s__( usageSection: s__(
'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.', 'AlertSettings|You must provide this URL and authorization key to authorize an external service to send alerts to GitLab. You can provide this URL and key to multiple services. After configuring an external service, alerts from your service will display on the GitLab %{linkStart}Alerts%{linkEnd} page.',
...@@ -39,13 +40,23 @@ export const i18n = { ...@@ -39,13 +40,23 @@ export const i18n = {
integration: s__('AlertSettings|Integration'), integration: s__('AlertSettings|Integration'),
}; };
// TODO: Delete as part of old form removal in 13.6
export const integrationTypes = [ export const integrationTypes = [
{ value: 'HTTP', text: s__('AlertSettings|HTTP Endpoint') },
{ value: 'PROMETHEUS', text: s__('AlertSettings|External Prometheus') },
{ value: 'OPSGENIE', text: s__('AlertSettings|Opsgenie') },
];
export const integrationTypesNew = [
{ value: '', text: s__('AlertSettings|Select integration type') }, { value: '', text: s__('AlertSettings|Select integration type') },
{ value: 'generic', text: s__('AlertSettings|HTTP Endpoint') }, ...integrationTypes,
{ value: 'prometheus', text: s__('AlertSettings|External Prometheus') },
{ value: 'opsgenie', text: s__('AlertSettings|Opsgenie') },
]; ];
export const typeSet = {
http: 'HTTP',
prometheus: 'PROMETHEUS',
};
export const JSON_VALIDATE_DELAY = 250; export const JSON_VALIDATE_DELAY = 250;
export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/'; export const targetPrometheusUrlPlaceholder = 'http://prometheus.example.com/';
......
#import "../fragments/integration_item.fragment.graphql"
mutation createHttpIntegration($projectPath: ID!, $name: String!, $active: Boolean!) {
httpIntegrationCreate(input: { projectPath: $projectPath, name: $name, active: $active }) {
errors
integration {
...IntegrationItem
}
}
}
#import "../fragments/integration_item.fragment.graphql"
mutation createPrometheusIntegration($projectPath: ID!, $apiUrl: String!, $active: Boolean!) {
prometheusIntegrationCreate(
input: { projectPath: $projectPath, apiUrl: $apiUrl, active: $active }
) {
errors
integration {
...IntegrationItem
}
}
}
...@@ -48,9 +48,9 @@ export default el => { ...@@ -48,9 +48,9 @@ export default el => {
el, el,
provide: { provide: {
prometheus: { prometheus: {
activated: parseBoolean(prometheusActivated), active: parseBoolean(prometheusActivated),
prometheusUrl, url: prometheusUrl,
authorizationKey: prometheusAuthorizationKey, authKey: prometheusAuthorizationKey,
prometheusFormPath, prometheusFormPath,
prometheusResetKeyPath, prometheusResetKeyPath,
prometheusApiUrl, prometheusApiUrl,
...@@ -58,14 +58,14 @@ export default el => { ...@@ -58,14 +58,14 @@ export default el => {
generic: { generic: {
alertsSetupUrl, alertsSetupUrl,
alertsUsageUrl, alertsUsageUrl,
activated: parseBoolean(activatedStr), active: parseBoolean(activatedStr),
formPath, formPath,
authorizationKey, authKey: authorizationKey,
url, url,
}, },
opsgenie: { opsgenie: {
formPath: opsgenieMvcFormPath, formPath: opsgenieMvcFormPath,
activated: parseBoolean(opsgenieMvcEnabled), active: parseBoolean(opsgenieMvcEnabled),
opsgenieMvcTargetUrl, opsgenieMvcTargetUrl,
opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable), opsgenieMvcIsAvailable: parseBoolean(opsgenieMvcAvailable),
}, },
......
...@@ -70,6 +70,7 @@ const Api = { ...@@ -70,6 +70,7 @@ const Api = {
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members', billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
group(groupId, callback = () => {}) { group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -106,6 +107,11 @@ const Api = { ...@@ -106,6 +107,11 @@ const Api = {
return axios.delete(url); return axios.delete(url);
}, },
containerRegistryDetails(registryId, options = {}) {
const url = Api.buildUrl(this.containerRegistryDetailsPath).replace(':id', registryId);
return axios.get(url, options);
},
groupMembers(id, options) { groupMembers(id, options) {
const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id)); const url = Api.buildUrl(this.groupMembersPath).replace(':id', encodeURIComponent(id));
......
...@@ -168,9 +168,6 @@ export default class CreateMergeRequestDropdown { ...@@ -168,9 +168,6 @@ export default class CreateMergeRequestDropdown {
disable() { disable() {
this.disableCreateAction(); this.disableCreateAction();
this.dropdownToggle.classList.add('disabled');
this.dropdownToggle.setAttribute('disabled', 'disabled');
} }
disableCreateAction() { disableCreateAction() {
...@@ -189,9 +186,6 @@ export default class CreateMergeRequestDropdown { ...@@ -189,9 +186,6 @@ export default class CreateMergeRequestDropdown {
this.createTargetButton.classList.remove('disabled'); this.createTargetButton.classList.remove('disabled');
this.createTargetButton.removeAttribute('disabled'); this.createTargetButton.removeAttribute('disabled');
this.dropdownToggle.classList.remove('disabled');
this.dropdownToggle.removeAttribute('disabled');
} }
static findByValue(objects, ref, returnFirstMatch = false) { static findByValue(objects, ref, returnFirstMatch = false) {
......
...@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d ...@@ -626,7 +626,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
.then(({ data }) => { .then(({ data }) => {
const lines = data.map((line, index) => const lines = data.map((line, index) =>
prepareLineForRenamedFile({ prepareLineForRenamedFile({
diffViewType: state.diffViewType, diffViewType: window.gon?.features?.unifiedDiffLines ? 'inline' : state.diffViewType,
line, line,
diffFile, diffFile,
index, index,
...@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d ...@@ -638,6 +638,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
viewer: { viewer: {
...diffFile.alternate_viewer, ...diffFile.alternate_viewer,
automaticallyCollapsed: false, automaticallyCollapsed: false,
manuallyCollapsed: false,
}, },
}); });
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines }); commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
......
...@@ -378,8 +378,13 @@ export default { ...@@ -378,8 +378,13 @@ export default {
}, },
[types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) { [types.SET_CURRENT_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
const file = state.diffFiles.find(f => f.file_path === filePath); const file = state.diffFiles.find(f => f.file_path === filePath);
const currentDiffLinesKey = let currentDiffLinesKey;
state.diffViewType === 'inline' ? 'highlighted_diff_lines' : 'parallel_diff_lines';
if (window.gon?.features?.unifiedDiffLines || state.diffViewType === 'inline') {
currentDiffLinesKey = 'highlighted_diff_lines';
} else {
currentDiffLinesKey = 'parallel_diff_lines';
}
file[currentDiffLinesKey] = lines; file[currentDiffLinesKey] = lines;
}, },
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
rolloutUserListLabel: s__('FeatureFlag|User List'), rolloutUserListLabel: s__('FeatureFlag|User List'),
rolloutUserListDescription: s__('FeatureFlag|Select a user list'), rolloutUserListDescription: s__('FeatureFlag|Select a user list'),
rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'), rolloutUserListNoListError: s__('FeatureFlag|There are no configured user lists'),
defaultDropdownText: s__('FeatureFlags|Select a user list'), defaultDropdownText: s__('FeatureFlags|No user list selected'),
}, },
computed: { computed: {
...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']), ...mapGetters(['hasUserLists', 'isLoading', 'hasError', 'userListOptions']),
...@@ -36,7 +36,7 @@ export default { ...@@ -36,7 +36,7 @@ export default {
return this.strategy?.userList?.id ?? ''; return this.strategy?.userList?.id ?? '';
}, },
dropdownText() { dropdownText() {
return this.strategy?.userList?.name ?? this.$options.defaultDropdownText; return this.strategy?.userList?.name ?? this.$options.translations.defaultDropdownText;
}, },
}, },
mounted() { mounted() {
......
...@@ -21,35 +21,32 @@ function mountRemoveMemberModal() { ...@@ -21,35 +21,32 @@ function mountRemoveMemberModal() {
}); });
} }
document.addEventListener('DOMContentLoaded', () => { const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
groupsSelect(); initGroupMembersApp(
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
const SHARED_FIELDS = ['account', 'expires', 'maxRole', 'expiration', 'actions'];
initGroupMembersApp(
document.querySelector('.js-group-members-list'), document.querySelector('.js-group-members-list'),
SHARED_FIELDS.concat(['source', 'granted']), SHARED_FIELDS.concat(['source', 'granted']),
memberRequestFormatter, memberRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-linked-list'), document.querySelector('.js-group-linked-list'),
SHARED_FIELDS.concat('granted'), SHARED_FIELDS.concat('granted'),
groupLinkRequestFormatter, groupLinkRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-invited-members-list'), document.querySelector('.js-group-invited-members-list'),
SHARED_FIELDS.concat('invited'), SHARED_FIELDS.concat('invited'),
memberRequestFormatter, memberRequestFormatter,
); );
initGroupMembersApp( initGroupMembersApp(
document.querySelector('.js-group-access-requests-list'), document.querySelector('.js-group-access-requests-list'),
SHARED_FIELDS.concat('requested'), SHARED_FIELDS.concat('requested'),
memberRequestFormatter, memberRequestFormatter,
); );
groupsSelect();
memberExpirationDate();
memberExpirationDate('.js-access-expiration-date-groups');
mountRemoveMemberModal();
new Members(); // eslint-disable-line no-new new Members(); // eslint-disable-line no-new
new UsersSelect(); // eslint-disable-line no-new new UsersSelect(); // eslint-disable-line no-new
});
...@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__( ...@@ -15,6 +15,10 @@ export const DELETE_TAGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Tags successfully marked for deletion.', 'ContainerRegistry|Tags successfully marked for deletion.',
); );
export const FETCH_IMAGE_DETAILS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the image details.',
);
export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags'); export const TAGS_LIST_TITLE = s__('ContainerRegistry|Image tags');
export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}'); export const DIGEST_LABEL = s__('ContainerRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}'); export const CREATED_AT_LABEL = s__('ContainerRegistry|Published %{timeInfo}');
......
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { import {
FETCH_IMAGES_LIST_ERROR_MESSAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE,
DEFAULT_PAGE, DEFAULT_PAGE,
DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE,
FETCH_TAGS_LIST_ERROR_MESSAGE, FETCH_TAGS_LIST_ERROR_MESSAGE,
FETCH_IMAGE_DETAILS_ERROR_MESSAGE,
} from '../constants/index'; } from '../constants/index';
import { decodeAndParse } from '../utils'; import { decodeAndParse } from '../utils';
...@@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params ...@@ -61,6 +63,19 @@ export const requestTagsList = ({ commit, dispatch }, { pagination = {}, params
}); });
}; };
export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => {
commit(types.SET_MAIN_LOADING, true);
return Api.containerRegistryDetails(id)
.then(({ data }) => {
commit(types.SET_IMAGE_DETAILS, data);
dispatch('requestTagsList');
})
.catch(() => {
createFlash(FETCH_IMAGE_DETAILS_ERROR_MESSAGE);
commit(types.SET_MAIN_LOADING, false);
});
};
export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => { export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
return axios return axios
......
...@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; ...@@ -7,3 +7,4 @@ export const SET_MAIN_LOADING = 'SET_MAIN_LOADING';
export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION'; export const SET_TAGS_PAGINATION = 'SET_TAGS_PAGINATION';
export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS'; export const SET_TAGS_LIST_SUCCESS = 'SET_TAGS_LIST_SUCCESS';
export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP'; export const SET_SHOW_GARBAGE_COLLECTION_TIP = 'SET_SHOW_GARBAGE_COLLECTION_TIP';
export const SET_IMAGE_DETAILS = 'SET_IMAGE_DETAILS';
...@@ -47,4 +47,8 @@ export default { ...@@ -47,4 +47,8 @@ export default {
const normalizedHeaders = normalizeHeaders(headers); const normalizedHeaders = normalizeHeaders(headers);
state.tagsPagination = parseIntPagination(normalizedHeaders); state.tagsPagination = parseIntPagination(normalizedHeaders);
}, },
[types.SET_IMAGE_DETAILS](state, details) {
state.imageDetails = details;
},
}; };
...@@ -3,6 +3,7 @@ export default () => ({ ...@@ -3,6 +3,7 @@ export default () => ({
showGarbageCollectionTip: false, showGarbageCollectionTip: false,
config: {}, config: {},
images: [], images: [],
imageDetails: {},
tags: [], tags: [],
pagination: {}, pagination: {},
tagsPagination: {}, tagsPagination: {},
......
export const decodeAndParse = param => JSON.parse(window.atob(param)); export const decodeAndParse = param => JSON.parse(window.atob(param));
// eslint-disable-next-line @gitlab/require-i18n-strings
export const pathGenerator = (imageDetails, ending = 'tags?format=json') => {
// this method is a temporary workaround, to be removed with graphql implementation
// https://gitlab.com/gitlab-org/gitlab/-/issues/276432
const basePath = imageDetails.path.replace(`/${imageDetails.name}`, '');
return `/${basePath}/registry/repository/${imageDetails.id}/${ending}`;
};
...@@ -137,8 +137,8 @@ export default { ...@@ -137,8 +137,8 @@ export default {
:href="commit.author.webPath" :href="commit.author.webPath"
class="commit-author-link js-user-link" class="commit-author-link js-user-link"
> >
{{ commit.author.name }} {{ commit.author.name }}</gl-link
</gl-link> >
<template v-else> <template v-else>
{{ commit.authorName }} {{ commit.authorName }}
</template> </template>
......
...@@ -188,6 +188,12 @@ ul.related-merge-requests > li { ...@@ -188,6 +188,12 @@ ul.related-merge-requests > li {
border-width: 1px; border-width: 1px;
line-height: $line-height-base; line-height: $line-height-base;
width: auto; width: auto;
&.disabled {
background-color: $gray-light;
border-color: $gray-100;
color: $gl-text-color-disabled;
}
} }
} }
......
...@@ -132,13 +132,23 @@ class GroupsController < Groups::ApplicationController ...@@ -132,13 +132,23 @@ class GroupsController < Groups::ApplicationController
def update def update
if Groups::UpdateService.new(@group, current_user, group_params).execute if Groups::UpdateService.new(@group, current_user, group_params).execute
redirect_to edit_group_path(@group, anchor: params[:update_section]), notice: "Group '#{@group.name}' was successfully updated." notice = "Group '#{@group.name}' was successfully updated."
redirect_to edit_group_origin_location, notice: notice
else else
@group.reset @group.reset
render action: "edit" render action: "edit"
end end
end end
def edit_group_origin_location
if params.dig(:group, :redirect_target) == 'repository_settings'
group_settings_repository_path(@group, anchor: 'js-default-branch-name')
else
edit_group_path(@group, anchor: params[:update_section])
end
end
def destroy def destroy
Groups::DestroyService.new(@group, current_user).async_execute Groups::DestroyService.new(@group, current_user).async_execute
......
...@@ -30,7 +30,7 @@ module OperationsHelper ...@@ -30,7 +30,7 @@ module OperationsHelper
'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'), 'alerts_setup_url' => help_page_path('operations/incident_management/alert_integrations.md', anchor: 'generic-http-endpoint'),
'alerts_usage_url' => project_alert_management_index_path(@project), 'alerts_usage_url' => project_alert_management_index_path(@project),
'disabled' => disabled.to_s, 'disabled' => disabled.to_s,
'project_path' => project_path(@project) 'project_path' => @project.full_path
} }
end end
......
...@@ -48,6 +48,8 @@ class ApplicationRecord < ActiveRecord::Base ...@@ -48,6 +48,8 @@ class ApplicationRecord < ActiveRecord::Base
def self.safe_find_or_create_by!(*args, &block) def self.safe_find_or_create_by!(*args, &block)
safe_find_or_create_by(*args, &block).tap do |record| safe_find_or_create_by(*args, &block).tap do |record|
raise ActiveRecord::RecordNotFound unless record.present?
record.validate! unless record.persisted? record.validate! unless record.persisted?
end end
end end
......
...@@ -109,6 +109,8 @@ class Group < Namespace ...@@ -109,6 +109,8 @@ class Group < Namespace
.where("project_authorizations.user_id IN (?)", user_ids) .where("project_authorizations.user_id IN (?)", user_ids)
end end
delegate :default_branch_name, to: :namespace_settings
class << self class << self
def sort_by_attribute(method) def sort_by_attribute(method)
if method == 'storage_size_desc' if method == 'storage_size_desc'
...@@ -587,7 +589,7 @@ class Group < Namespace ...@@ -587,7 +589,7 @@ class Group < Namespace
def update_two_factor_requirement def update_two_factor_requirement
return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period? return unless saved_change_to_require_two_factor_authentication? || saved_change_to_two_factor_grace_period?
members_with_descendants.find_each(&:update_two_factor_requirement) direct_and_indirect_members.find_each(&:update_two_factor_requirement)
end end
def path_changed_hook def path_changed_hook
......
...@@ -6,10 +6,18 @@ class NamespaceSetting < ApplicationRecord ...@@ -6,10 +6,18 @@ class NamespaceSetting < ApplicationRecord
validate :default_branch_name_content validate :default_branch_name_content
validate :allow_mfa_for_group validate :allow_mfa_for_group
before_validation :normalize_default_branch_name
NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze NAMESPACE_SETTINGS_PARAMS = [:default_branch_name].freeze
self.primary_key = :namespace_id self.primary_key = :namespace_id
private
def normalize_default_branch_name
self.default_branch_name = nil if default_branch_name.blank?
end
def default_branch_name_content def default_branch_name_content
return if default_branch_name.nil? return if default_branch_name.nil?
......
...@@ -163,16 +163,18 @@ module Ci ...@@ -163,16 +163,18 @@ module Ci
end end
def ensure_pending_state def ensure_pending_state
Ci::BuildPendingState.create_or_find_by!( build_state = Ci::BuildPendingState.safe_find_or_create_by(
build_id: build.id, build_id: build.id,
state: params.fetch(:state), state: params.fetch(:state),
trace_checksum: params.fetch(:checksum), trace_checksum: params.fetch(:checksum),
failure_reason: params.dig(:failure_reason) failure_reason: params.dig(:failure_reason)
) )
rescue ActiveRecord::RecordNotFound
unless build_state.present?
metrics.increment_trace_operation(operation: :conflict) metrics.increment_trace_operation(operation: :conflict)
end
build.pending_state build_state || build.pending_state
end end
## ##
......
- breadcrumb_title _("Dashboard") - breadcrumb_title _("Dashboard")
- page_title _("Dashboard") - page_title _("Dashboard")
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
- if @notices - if @notices
- @notices.each do |notice| - @notices.each do |notice|
...@@ -22,10 +24,20 @@ ...@@ -22,10 +24,20 @@
= link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full") = link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-success gl-w-full")
.col-sm-4 .col-sm-4
.info-well.dark-well .info-well.dark-well
.well-segment.well-centered .well-segment.well-centered.gl-text-center
= link_to admin_users_path do = link_to admin_users_path do
%h3.text-center %h3.gl-display-inline-block.gl-mb-0
= s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) } = s_('AdminArea|Users: %{number_of_users}') % { number_of_users: approximate_count_with_delimiters(@counts, User) }
%span.gl-outline-0.gl-ml-2{ href: "#", tabindex: "0", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}.").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe },
} }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700 gl-mb-1')
%hr %hr
.btn-group.d-flex{ role: 'group' } .btn-group.d-flex{ role: 'group' }
= link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full" = link_to s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-success gl-w-full"
......
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Default initial branch name')
%button.gl-button.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Set the default name of the initial branch when creating new repositories through the user interface.')
.settings-content
= form_for @group, url: group_path(@group, anchor: 'js-default-branch-name'), html: { class: 'fieldset-form' } do |f|
= form_errors(@group)
- fallback_branch_name = '<code>master</code>'
%fieldset
.form-group
= f.label :default_branch_name, _('Default initial branch name'), class: 'label-light'
= f.text_field :default_branch_name, value: group.namespace_settings&.default_branch_name, placeholder: 'master', class: 'form-control'
%span.form-text.text-muted
= (_("Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used.") % { branch_name_default: fallback_branch_name }).html_safe
= f.hidden_field :redirect_target, value: "repository_settings"
= f.submit _('Save changes'), class: 'gl-button btn-success'
...@@ -4,3 +4,4 @@ ...@@ -4,3 +4,4 @@
- deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.') - deploy_token_description = s_('DeployTokens|Group deploy tokens allow access to the packages, repositories, and registry images within the group.')
= render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description = render "shared/deploy_tokens/index", group_or_project: @group, description: deploy_token_description
= render "initial_branch_name", group: @group
---
title: Resolve User stuck in 2FA setup page even if group disable 2FA enforce
merge_request: 46432
author:
type: fixed
---
title: Remove feedback alert from on-demand scans form
merge_request: 45217
author:
type: changed
---
title: Fix logging handling for API integer params
merge_request: 46551
author:
type: fixed
---
title: Assign new incoming diff lines for renamed files to the correct view type
merge_request: 46823
author:
type: fixed
---
title: Fix linebreak issue in last commit anchor
merge_request: 46643
author:
type: fixed
---
title: Add Default Initial Branch Name for Repositories Group Setting
merge_request: 43290
author:
type: added
---
title: Fix example responses for Group Issue Board creation API in the docs
merge_request: 46760
author: Takuya Noguchi
type: fixed
---
title: Show "No user list selected" in feature flags
merge_request: 46790
author:
type: fixed
---
title: Fix 'File name too long' error happening during Project Export when exporting
project uploads
merge_request: 46674
author:
type: fixed
---
title: Fixed create merge request dropdown not re-opening after typing invalid source
branch
merge_request: 46802
author:
type: fixed
---
name: incident_sla
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43648
rollout_issue_url:
group: group::health
type: licensed
default_enabled: true
...@@ -14,10 +14,8 @@ if Gitlab.ee? && Gitlab.dev_or_test_env? ...@@ -14,10 +14,8 @@ if Gitlab.ee? && Gitlab.dev_or_test_env?
# being unique to licensed names. These feature flags should be reworked to # being unique to licensed names. These feature flags should be reworked to
# be "development" with explicit check # be "development" with explicit check
IGNORED_FEATURE_FLAGS = %i[ IGNORED_FEATURE_FLAGS = %i[
ci_secrets_management
feature_flags_related_issues feature_flags_related_issues
group_wikis group_wikis
incident_sla
swimlanes swimlanes
minimal_access_role minimal_access_role
].to_set ].to_set
......
...@@ -118,7 +118,7 @@ To disable NFS server delegation, do the following: ...@@ -118,7 +118,7 @@ To disable NFS server delegation, do the following:
1. Restart the NFS server process. For example, on CentOS run `service nfs restart`. 1. Restart the NFS server process. For example, on CentOS run `service nfs restart`.
NOTE: **Important note:** NOTE: **Note:**
The kernel bug may be fixed in The kernel bug may be fixed in
[more recent kernels with this commit](https://github.com/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140). [more recent kernels with this commit](https://github.com/torvalds/linux/commit/95da1b3a5aded124dd1bda1e3cdb876184813140).
Red Hat Enterprise 7 [shipped a kernel update](https://access.redhat.com/errata/RHSA-2019:2029) Red Hat Enterprise 7 [shipped a kernel update](https://access.redhat.com/errata/RHSA-2019:2029)
......
...@@ -95,7 +95,6 @@ you want using steps 1 and 2 from the GitLab downloads page. ...@@ -95,7 +95,6 @@ you want using steps 1 and 2 from the GitLab downloads page.
1. Run `gitlab-ctl reconfigure`. 1. Run `gitlab-ctl reconfigure`.
NOTE: **Note:**
You will need to restart the Sidekiq nodes after an update has occurred and database You will need to restart the Sidekiq nodes after an update has occurred and database
migrations performed. migrations performed.
......
...@@ -39,16 +39,15 @@ Feature.disable(:upload_middleware_jwt_params_handler) ...@@ -39,16 +39,15 @@ Feature.disable(:upload_middleware_jwt_params_handler)
## Using local storage ## Using local storage
NOTE: **Note:** This is the default configuration. To change the location where the uploads are
This is the default configuration stored locally, use the steps in this section based on your installation method:
To change the location where the uploads are stored locally, follow the steps
below.
**In Omnibus installations:** **In Omnibus GitLab installations:**
NOTE: **Note:** NOTE: **Note:**
For historical reasons, uploads are stored into a base directory, which by default is `uploads/-/system`. It is strongly discouraged to change this configuration option on an existing GitLab installation. For historical reasons, uploads are stored into a base directory, which by
default is `uploads/-/system`. It's strongly discouraged to change this
configuration option for an existing GitLab installation.
_The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads`._ _The uploads are stored by default in `/var/opt/gitlab/gitlab-rails/uploads`._
...@@ -92,7 +91,6 @@ This configuration relies on valid AWS credentials to be configured already. ...@@ -92,7 +91,6 @@ This configuration relies on valid AWS credentials to be configured already.
[Read more about using object storage with GitLab](object_storage.md). [Read more about using object storage with GitLab](object_storage.md).
NOTE: **Note:**
We recommend using the [consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration). The following instructions apply to the original configuration format. We recommend using the [consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration). The following instructions apply to the original configuration format.
## Object Storage Settings ## Object Storage Settings
...@@ -131,7 +129,6 @@ _The uploads are stored by default in ...@@ -131,7 +129,6 @@ _The uploads are stored by default in
} }
``` ```
NOTE: **Note:**
If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs. If you are using AWS IAM profiles, be sure to omit the AWS access key and secret access key/value pairs.
```ruby ```ruby
......
...@@ -265,44 +265,17 @@ Example response: ...@@ -265,44 +265,17 @@ Example response:
{ {
"id": 1, "id": 1,
"name": "newboard", "name": "newboard",
"project": null,
"lists" : [],
"group": { "group": {
"id": 5, "id": 5,
"name": "Documentcloud", "name": "Documentcloud",
"web_url": "http://example.com/groups/documentcloud" "web_url": "http://example.com/groups/documentcloud"
}, },
"milestone": { "milestone": null,
"id": 12 "assignee" : null,
"title": "10.0" "labels" : [],
}, "weight" : null
"lists" : [
{
"id" : 1,
"label" : {
"name" : "Testing",
"color" : "#F0AD4E",
"description" : null
},
"position" : 1
},
{
"id" : 2,
"label" : {
"name" : "Ready",
"color" : "#FF0000",
"description" : null
},
"position" : 2
},
{
"id" : 3,
"label" : {
"name" : "Production",
"color" : "#FF5F00",
"description" : null
},
"position" : 3
}
]
} }
``` ```
......
...@@ -132,8 +132,23 @@ Non-nullable fields should only be used when a field is required, very unlikely ...@@ -132,8 +132,23 @@ Non-nullable fields should only be used when a field is required, very unlikely
to become optional in the future, and very easy to calculate. An example would to become optional in the future, and very easy to calculate. An example would
be `id` fields. be `id` fields.
A non-nullable GraphQL schema field is an object type followed by the exclamation point (bang) `!`. Here's an example from the `gitlab_schema.graphql` file:
```graphql
id: ProjectID!
```
Here's an example of a non-nullable GraphQL array:
```graphql
errors: [String!]!
```
Further reading: Further reading:
- [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability).
- GraphQL documentation on [Object types and fields](https://graphql.org/learn/schema/#object-types-and-fields).
- [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability) - [GraphQL Best Practices Guide](https://graphql.org/learn/best-practices/#nullability)
- [Using nullability in GraphQL](https://www.apollographql.com/blog/using-nullability-in-graphql-2254f84c4ed7) - [Using nullability in GraphQL](https://www.apollographql.com/blog/using-nullability-in-graphql-2254f84c4ed7)
......
...@@ -19,7 +19,6 @@ cd /home/git/gitlab ...@@ -19,7 +19,6 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:list_repos RAILS_ENV=production
``` ```
NOTE: **Note:**
The results use the default ordering of the GitLab Rails application. The results use the default ordering of the GitLab Rails application.
## Limit search results ## Limit search results
......
...@@ -32,7 +32,6 @@ sudo gitlab-rake gitlab:import:all_users_to_all_projects ...@@ -32,7 +32,6 @@ sudo gitlab-rake gitlab:import:all_users_to_all_projects
bundle exec rake gitlab:import:all_users_to_all_projects RAILS_ENV=production bundle exec rake gitlab:import:all_users_to_all_projects RAILS_ENV=production
``` ```
NOTE: **Note:**
Admin users are added as maintainers. Admin users are added as maintainers.
## Add user as a developer to all groups ## Add user as a developer to all groups
...@@ -59,7 +58,6 @@ sudo gitlab-rake gitlab:import:all_users_to_all_groups ...@@ -59,7 +58,6 @@ sudo gitlab-rake gitlab:import:all_users_to_all_groups
bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production bundle exec rake gitlab:import:all_users_to_all_groups RAILS_ENV=production
``` ```
NOTE: **Note:**
Admin users are added as owners so they can add additional users to the group. Admin users are added as owners so they can add additional users to the group.
## Control the number of active users ## Control the number of active users
......
...@@ -74,7 +74,6 @@ Docker image based on based on the `ruby:alpine` instead of the default `ruby:la ...@@ -74,7 +74,6 @@ Docker image based on based on the `ruby:alpine` instead of the default `ruby:la
# ... put your stuff here # ... put your stuff here
``` ```
NOTE: **Note:**
Use Base64 encoding if you need to pass complex values, such as newlines and Use Base64 encoding if you need to pass complex values, such as newlines and
spaces. Left unencoded, complex values like these can cause escaping issues spaces. Left unencoded, complex values like these can cause escaping issues
due to how Auto DevOps uses the arguments. due to how Auto DevOps uses the arguments.
...@@ -123,7 +122,6 @@ to `CI_COMMIT_SHA,CI_ENVIRONMENT_NAME`. ...@@ -123,7 +122,6 @@ to `CI_COMMIT_SHA,CI_ENVIRONMENT_NAME`.
RUN --mount=type=secret,id=auto-devops-build-secrets . /run/secrets/auto-devops-build-secrets && $COMMAND RUN --mount=type=secret,id=auto-devops-build-secrets . /run/secrets/auto-devops-build-secrets && $COMMAND
``` ```
NOTE: **Note:**
When `AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES` is set, Auto DevOps When `AUTO_DEVOPS_BUILD_IMAGE_FORWARDED_CI_VARIABLES` is set, Auto DevOps
enables the experimental [Docker BuildKit](https://docs.docker.com/develop/develop-images/build_enhancements/) enables the experimental [Docker BuildKit](https://docs.docker.com/develop/develop-images/build_enhancements/)
feature to use the `--secret` flag. feature to use the `--secret` flag.
...@@ -453,7 +451,6 @@ the updated secrets. To update the secrets, either: ...@@ -453,7 +451,6 @@ the updated secrets. To update the secrets, either:
- Manually delete running pods to cause Kubernetes to create new pods with updated - Manually delete running pods to cause Kubernetes to create new pods with updated
secrets. secrets.
NOTE: **Note:**
Variables with multi-line values are not currently supported due to Variables with multi-line values are not currently supported due to
limitations with the current Auto DevOps scripting environment. limitations with the current Auto DevOps scripting environment.
......
...@@ -114,7 +114,6 @@ In this guide, we will install Ingress and Prometheus: ...@@ -114,7 +114,6 @@ In this guide, we will install Ingress and Prometheus:
- Prometheus - An open-source monitoring and alerting system used to supervise the - Prometheus - An open-source monitoring and alerting system used to supervise the
deployed application. deployed application.
NOTE: **Note:**
We won't install GitLab Runner in this quick start guide, as this guide uses the We won't install GitLab Runner in this quick start guide, as this guide uses the
shared runners provided by GitLab.com. shared runners provided by GitLab.com.
...@@ -161,7 +160,8 @@ The jobs are separated into stages: ...@@ -161,7 +160,8 @@ The jobs are separated into stages:
- **Build** - The application builds a Docker image and uploads it to your project's - **Build** - The application builds a Docker image and uploads it to your project's
[Container Registry](../../user/packages/container_registry/index.md) ([Auto Build](stages.md#auto-build)). [Container Registry](../../user/packages/container_registry/index.md) ([Auto Build](stages.md#auto-build)).
- **Test** - GitLab runs various checks on the application: - **Test** - GitLab runs various checks on the application, but all jobs except `test`
are allowed to fail in the test stage:
- The `test` job runs unit and integration tests by detecting the language and - The `test` job runs unit and integration tests by detecting the language and
framework ([Auto Test](stages.md#auto-test)) framework ([Auto Test](stages.md#auto-test))
...@@ -179,9 +179,6 @@ The jobs are separated into stages: ...@@ -179,9 +179,6 @@ The jobs are separated into stages:
licenses and is allowed to fail licenses and is allowed to fail
([Auto License Compliance](stages.md#auto-license-compliance)) **(ULTIMATE)** ([Auto License Compliance](stages.md#auto-license-compliance)) **(ULTIMATE)**
NOTE: **Note:**
All jobs except `test` are allowed to fail in the test stage.
- **Review** - Pipelines on `master` include this stage with a `dast_environment_deploy` job. - **Review** - Pipelines on `master` include this stage with a `dast_environment_deploy` job.
To learn more, see [Dynamic Application Security Testing (DAST)](../../user/application_security/dast/index.md). To learn more, see [Dynamic Application Security Testing (DAST)](../../user/application_security/dast/index.md).
......
...@@ -126,11 +126,9 @@ When you trigger a pipeline, if you have Auto DevOps enabled and if you have cor ...@@ -126,11 +126,9 @@ When you trigger a pipeline, if you have Auto DevOps enabled and if you have cor
[entered AWS credentials as environment variables](../../ci/cloud_deployment/index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs), [entered AWS credentials as environment variables](../../ci/cloud_deployment/index.md#deploy-your-application-to-the-aws-elastic-container-service-ecs),
your application will be deployed to AWS ECS. your application will be deployed to AWS ECS.
NOTE: **Note:**
[GitLab Managed Apps](../../user/clusters/applications.md) are not available when deploying to AWS ECS. [GitLab Managed Apps](../../user/clusters/applications.md) are not available when deploying to AWS ECS.
You must manually configure your application (such as Ingress or Help) on AWS ECS. You must manually configure your application (such as Ingress or Help) on AWS ECS.
NOTE: **Note:**
If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project, If you have both a valid `AUTO_DEVOPS_PLATFORM_TARGET` variable and a Kubernetes cluster tied to your project,
only the deployment to Kubernetes will run. only the deployment to Kubernetes will run.
......
...@@ -114,8 +114,11 @@ See the documentation on [File Locking](../../../user/project/file_lock.md). ...@@ -114,8 +114,11 @@ See the documentation on [File Locking](../../../user/project/file_lock.md).
## LFS objects in project archives ## LFS objects in project archives
> - Support for including Git LFS blobs inside [project source downloads](../../../user/project/repository/index.md) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5. > - Support for including Git LFS blobs inside [project source downloads](../../../user/project/repository/index.md) was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/15079) in GitLab 13.5.
> - It's [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. > - It was [deployed behind a feature flag](../../../user/feature_flags.md), disabled by default.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-lfs-objects-in-project-archives). **(CORE ONLY)** > - [Became enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/268409) on GitLab 13.6.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-lfs-objects-in-project-archives).
CAUTION: **Warning:** CAUTION: **Warning:**
This feature might not be available to you. Check the **version history** note above for details. This feature might not be available to you. Check the **version history** note above for details.
...@@ -139,10 +142,10 @@ Technical details about how this works can be found in the [development document ...@@ -139,10 +142,10 @@ Technical details about how this works can be found in the [development document
### Enable or disable LFS objects in project archives ### Enable or disable LFS objects in project archives
_LFS objects in project archives_ is under development and not ready for production use. It is _LFS objects in project archives_ is under development but ready for production use.
deployed behind a feature flag that is **disabled by default**. It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it. can opt to disable it.
To enable it: To enable it:
......
...@@ -7,7 +7,7 @@ type: reference, howto ...@@ -7,7 +7,7 @@ type: reference, howto
# Migration guide from Git Annex to Git LFS # Migration guide from Git Annex to Git LFS
NOTE: **Note:** DANGER: **Deprecated:**
Git Annex support [has been removed](https://gitlab.com/gitlab-org/gitlab/-/issues/1648) in GitLab Enterprise Git Annex support [has been removed](https://gitlab.com/gitlab-org/gitlab/-/issues/1648) in GitLab Enterprise
Edition 9.0 (2017/03/22). Edition 9.0 (2017/03/22).
...@@ -37,7 +37,6 @@ ones that GitLab developed. ...@@ -37,7 +37,6 @@ ones that GitLab developed.
## Migration steps ## Migration steps
NOTE: **Note:**
Since Git Annex files are stored in a sub-directory of the normal repositories Since Git Annex files are stored in a sub-directory of the normal repositories
(`.git/annex/objects`) and LFS files are stored outside of the repositories, (`.git/annex/objects`) and LFS files are stored outside of the repositories,
they are not compatible as they are using a different scheme. Therefore, the they are not compatible as they are using a different scheme. Therefore, the
......
...@@ -13,9 +13,7 @@ instance entirely offline. ...@@ -13,9 +13,7 @@ instance entirely offline.
NOTE: **Note:** NOTE: **Note:**
This guide assumes the server is Ubuntu 18.04. Instructions for other servers may vary. This guide assumes the server is Ubuntu 18.04. Instructions for other servers may vary.
This guide also assumes the server host resolves as `my-host`, which you should replace with your
NOTE: **Note:**
This guide assumes the server host resolves as `my-host`, which you should replace with your
server's name. server's name.
Follow the installation instructions [as outlined in the omnibus install Follow the installation instructions [as outlined in the omnibus install
......
...@@ -518,6 +518,23 @@ If you want to retain ownership over the original namespace and ...@@ -518,6 +518,23 @@ If you want to retain ownership over the original namespace and
protect the URL redirects, then instead of changing a group's path or renaming a protect the URL redirects, then instead of changing a group's path or renaming a
username, you can create a new group and transfer projects to it. username, you can create a new group and transfer projects to it.
### Group repository settings
You can change settings that are specific to repositories in your group.
#### Custom initial branch name **(CORE ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/43290) in GitLab 13.6.
By default, when you create a new project in GitLab, the initial branch is called `master`.
For groups, a group administrator can customize the initial branch name to something
else. This way, every new project created under that group from then on will start from the custom branch name rather than `master`. To do so:
1. Go to the **Group page > Settings > Repository** and expand **Default initial
branch name**.
1. Change the default initial branch to a custom name of your choice.
1. **Save Changes**.
### Remove a group ### Remove a group
To remove a group and its contents: To remove a group and its contents:
...@@ -530,7 +547,10 @@ To remove a group and its contents: ...@@ -530,7 +547,10 @@ To remove a group and its contents:
This action either: This action either:
- Removes the group, and also queues a background job to delete all projects in that group. - Removes the group, and also queues a background job to delete all projects in that group.
- Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, marks a group for deletion. The deletion will happen 7 days later by default, but this can be changed in the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay). - Since [GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/-/issues/33257), on [Premium or Silver](https://about.gitlab.com/pricing/premium/) or higher tiers, this action adds a background job to mark a group for deletion. By default, the job schedules the deletion 7 days in the future. You can modify this waiting period through the [instance settings](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay).
Since [GitLab 13.6](https://gitlab.com/gitlab-org/gitlab/-/issues/39504), if the user who sets up the deletion leaves or is otherwise removed from the group before the
actual deletion happens, the job is cancelled, and the group is no longer scheduled for deletion.
### Restore a group **(PREMIUM)** ### Restore a group **(PREMIUM)**
......
<script> <script>
import Vue from 'vue'; import Vue from 'vue';
import { GlCard, GlEmptyState, GlSkeletonLoader, GlTable } from '@gitlab/ui'; import { GlCard, GlEmptyState, GlLink, GlSkeletonLoader, GlTable } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { __, s__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format'; import { SUPPORTED_FORMATS, getFormatter } from '~/lib/utils/unit_format';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SelectProjectsDropdown from './select_projects_dropdown.vue'; import SelectProjectsDropdown from './select_projects_dropdown.vue';
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
components: { components: {
GlCard, GlCard,
GlEmptyState, GlEmptyState,
GlLink,
GlSkeletonLoader, GlSkeletonLoader,
GlTable, GlTable,
SelectProjectsDropdown, SelectProjectsDropdown,
...@@ -31,11 +33,15 @@ export default { ...@@ -31,11 +33,15 @@ export default {
// fetch the same data more than once // fetch the same data more than once
this.allCoverageData = [ this.allCoverageData = [
...this.allCoverageData, ...this.allCoverageData,
...data.projects.nodes.map(project => ({ // Remove the projects that don't have any code coverage
...data.projects.nodes
.filter(({ codeCoverageSummary }) => Boolean(codeCoverageSummary))
.map(project => ({
...project, ...project,
// if a project has no code coverage, set to default values codeCoveragePath: joinPaths(
codeCoverageSummary: gon.relative_url_root || '',
project.codeCoverageSummary || this.$options.noCoverageDefaultSummary, `/${project.fullPath}/-/graphs/${project.repository.rootRef}/charts`,
),
})), })),
]; ];
}, },
...@@ -76,6 +82,17 @@ export default { ...@@ -76,6 +82,17 @@ export default {
selectedCoverageData() { selectedCoverageData() {
return this.allCoverageData.filter(({ id }) => this.projectIds[id]); return this.allCoverageData.filter(({ id }) => this.projectIds[id]);
}, },
sortedCoverageData() {
// Sort the table by most recently updated coverage report
return [...this.selectedCoverageData].sort((a, b) => {
if (a.codeCoverageSummary.lastUpdatedAt > b.codeCoverageSummary.lastUpdatedAt) {
return -1;
} else if (a.codeCoverageSummary.lastUpdatedAt < b.codeCoverageSummary.lastUpdatedAt) {
return 1;
}
return 0;
});
},
}, },
methods: { methods: {
handleError() { handleError() {
...@@ -127,11 +144,6 @@ export default { ...@@ -127,11 +144,6 @@ export default {
'RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data.', 'RepositoriesAnalytics|Please select a project or multiple projects to display their most recent test coverage data.',
), ),
}, },
noCoverageDefaultSummary: {
averageCoverage: 0,
coverageCount: 0,
lastUpdatedAt: '', // empty string will default to "just now" in table
},
LOADING_STATE: { LOADING_STATE: {
rows: 4, rows: 4,
height: 10, height: 10,
...@@ -183,7 +195,7 @@ export default { ...@@ -183,7 +195,7 @@ export default {
data-testid="test-coverage-data-table" data-testid="test-coverage-data-table"
thead-class="thead-white" thead-class="thead-white"
:fields="$options.tableFields" :fields="$options.tableFields"
:items="selectedCoverageData" :items="sortedCoverageData"
> >
<template #head(project)="data"> <template #head(project)="data">
<div>{{ data.label }}</div> <div>{{ data.label }}</div>
...@@ -199,7 +211,9 @@ export default { ...@@ -199,7 +211,9 @@ export default {
</template> </template>
<template #cell(project)="{ item }"> <template #cell(project)="{ item }">
<div :data-testid="`${item.id}-name`">{{ item.name }}</div> <gl-link target="_blank" :href="item.codeCoveragePath" :data-testid="`${item.id}-name`">
{{ item.name }}
</gl-link>
</template> </template>
<template #cell(averageCoverage)="{ item }"> <template #cell(averageCoverage)="{ item }">
<div :data-testid="`${item.id}-average`"> <div :data-testid="`${item.id}-average`">
......
query getProjectsTestCoverage($projectIds: [ID!]) { query getProjectsTestCoverage($projectIds: [ID!]) {
projects(ids: $projectIds) { projects(ids: $projectIds) {
nodes { nodes {
fullPath
id id
name name
repository {
rootRef
}
codeCoverageSummary { codeCoverageSummary {
averageCoverage averageCoverage
coverageCount coverageCount
......
...@@ -21,7 +21,6 @@ import { ...@@ -21,7 +21,6 @@ import {
SITE_PROFILES_QUERY, SITE_PROFILES_QUERY,
} from '../settings'; } from '../settings';
import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql'; import dastOnDemandScanCreateMutation from '../graphql/dast_on_demand_scan_create.mutation.graphql';
import DismissibleFeedbackAlert from '~/vue_shared/components/dismissible_feedback_alert.vue';
import OnDemandScansScannerProfileSelector from './profile_selector/scanner_profile_selector.vue'; import OnDemandScansScannerProfileSelector from './profile_selector/scanner_profile_selector.vue';
import OnDemandScansSiteProfileSelector from './profile_selector/site_profile_selector.vue'; import OnDemandScansSiteProfileSelector from './profile_selector/site_profile_selector.vue';
...@@ -53,7 +52,6 @@ export default { ...@@ -53,7 +52,6 @@ export default {
GlLink, GlLink,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
DismissibleFeedbackAlert,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -162,15 +160,6 @@ export default { ...@@ -162,15 +160,6 @@ export default {
<template> <template>
<gl-form @submit.prevent="onSubmit"> <gl-form @submit.prevent="onSubmit">
<!--
This is a temporary change to solicit feedback from users
and shall be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/255889
-->
<dismissible-feedback-alert
feature-name="on-demand DAST scans"
feedback-link="https://gitlab.com/gitlab-org/gitlab/-/issues/249684"
/>
<header class="gl-mb-6"> <header class="gl-mb-6">
<h2>{{ s__('OnDemandScans|New on-demand DAST scan') }}</h2> <h2>{{ s__('OnDemandScans|New on-demand DAST scan') }}</h2>
<p> <p>
......
...@@ -13,6 +13,7 @@ module EE ...@@ -13,6 +13,7 @@ module EE
end end
cleanup_group_identity(member) cleanup_group_identity(member)
cleanup_group_deletion_schedule(member) if member.source&.is_a?(Group)
end end
private private
...@@ -40,6 +41,14 @@ module EE ...@@ -40,6 +41,14 @@ module EE
saml_provider.identities.for_user(member.user).delete_all saml_provider.identities.for_user(member.user).delete_all
end end
def cleanup_group_deletion_schedule(member)
deletion_schedule = member.source&.deletion_schedule
return unless deletion_schedule
deletion_schedule.destroy if deletion_schedule.deleting_user == member.user
end
end end
end end
end end
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
= s_('AdminArea|Billable users') = s_('AdminArea|Billable users')
%span.gl-outline-0.gl-ml-2{ href: "#", tabindex: "0", role: "button", data: { container: "body",
toggle: "popover",
placement: "top",
html: "true",
trigger: "focus",
content: s_("AdminArea|%{billable_users_link_start}Learn more%{billable_users_link_end} about what defines a billable user").html_safe % { billable_users_link_start: billable_users_link_start, billable_users_link_end: '</a>'.html_safe } } }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-700')
...@@ -13,6 +13,8 @@ ...@@ -13,6 +13,8 @@
- true_up_url = 'https://about.gitlab.com/license-faq/' - true_up_url = 'https://about.gitlab.com/license-faq/'
- true_up_link_start = '<a href="%{url}">'.html_safe % { url: true_up_url } - true_up_link_start = '<a href="%{url}">'.html_safe % { url: true_up_url }
- billable_users_url = help_page_path('subscriptions/self_managed/index', anchor: 'billable-users')
- billable_users_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer nofollow">'.html_safe % { url: billable_users_url }
- link_end = '</a>'.html_safe - link_end = '</a>'.html_safe
.license-panel.gl-mt-5 .license-panel.gl-mt-5
...@@ -57,7 +59,7 @@ ...@@ -57,7 +59,7 @@
= number_with_delimiter current_active_user_count = number_with_delimiter current_active_user_count
%hr %hr
%p %p
= _('This is the number of currently active users on your installation, and this is the minimum number you need to purchase when you renew your license.') = _('This is the number of %{billable_users_link_start}billable users%{link_end} on your installation, and this is the minimum number you need to purchase when you renew your license.').html_safe % { billable_users_link_start: billable_users_link_start, link_end: link_end }
.d-flex.gl-pb-5 .d-flex.gl-pb-5
.col-sm-6.d-flex.pl-0 .col-sm-6.d-flex.pl-0
.info-well.dark-well.flex-fill.gl-mb-0 .info-well.dark-well.flex-fill.gl-mb-0
......
---
title: Add text/tips to clarify various user counts in self-managed admin panels
merge_request: 44986
author:
type: other
---
title: Unschedule group deletion on deleting member removal
merge_request: 45715
author:
type: changed
---
name: ci_secrets_management
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42055
rollout_issue_url:
group: group::release management
type: licensed
default_enabled: true
...@@ -17,6 +17,7 @@ describe('Test coverage table component', () => { ...@@ -17,6 +17,7 @@ describe('Test coverage table component', () => {
const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]'); const findEmptyState = () => wrapper.find('[data-testid="test-coverage-table-empty-state"]');
const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"'); const findLoadingState = () => wrapper.find('[data-testid="test-coverage-loading-state"');
const findTable = () => wrapper.find('[data-testid="test-coverage-data-table"'); const findTable = () => wrapper.find('[data-testid="test-coverage-data-table"');
const findTableRows = () => findTable().findAll('tbody tr');
const findProjectNameById = id => wrapper.find(`[data-testid="${id}-name"`); const findProjectNameById = id => wrapper.find(`[data-testid="${id}-name"`);
const findProjectAverageById = id => wrapper.find(`[data-testid="${id}-average"`); const findProjectAverageById = id => wrapper.find(`[data-testid="${id}-average"`);
const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`); const findProjectCountById = id => wrapper.find(`[data-testid="${id}-count"`);
...@@ -96,8 +97,10 @@ describe('Test coverage table component', () => { ...@@ -96,8 +97,10 @@ describe('Test coverage table component', () => {
describe('when code coverage is available', () => { describe('when code coverage is available', () => {
it('renders coverage table', () => { it('renders coverage table', () => {
const fullPath = 'gitlab-org/gitlab';
const id = 'gid://gitlab/Project/1'; const id = 'gid://gitlab/Project/1';
const name = 'GitLab'; const name = 'GitLab';
const rootRef = 'master';
const averageCoverage = '74.35'; const averageCoverage = '74.35';
const coverageCount = '5'; const coverageCount = '5';
const yesterday = new Date(); const yesterday = new Date();
...@@ -107,8 +110,13 @@ describe('Test coverage table component', () => { ...@@ -107,8 +110,13 @@ describe('Test coverage table component', () => {
data: { data: {
allCoverageData: [ allCoverageData: [
{ {
fullPath,
id, id,
name, name,
repository: {
rootRef,
},
codeCoveragePath: '#',
codeCoverageSummary: { codeCoverageSummary: {
averageCoverage, averageCoverage,
coverageCount, coverageCount,
...@@ -129,11 +137,103 @@ describe('Test coverage table component', () => { ...@@ -129,11 +137,103 @@ describe('Test coverage table component', () => {
expect(findProjectCountById(id).text()).toBe(coverageCount); expect(findProjectCountById(id).text()).toBe(coverageCount);
expect(findProjectDateById(id).text()).toBe('1 day ago'); expect(findProjectDateById(id).text()).toBe('1 day ago');
}); });
it('sorts the table by the most recently updated report', () => {
const today = new Date();
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const allCoverageData = [
{
fullPath: '-',
id: 1,
name: 'should be last',
repository: { rootRef: 'master' },
codeCoveragePath: '#',
codeCoverageSummary: {
averageCoverage: '1.45',
coverageCount: '1',
lastUpdatedAt: yesterday.toISOString(),
},
},
{
fullPath: '-',
id: 2,
name: 'should be first',
repository: { rootRef: 'master' },
codeCoveragePath: '#',
codeCoverageSummary: {
averageCoverage: '1.45',
coverageCount: '1',
lastUpdatedAt: today.toISOString(),
},
},
];
createComponent({
data: {
allCoverageData,
projectIds: {
1: true,
2: true,
},
},
mountFn: mount,
});
expect(findTable().exists()).toBe(true);
expect(
findTableRows()
.at(0)
.text(),
).toContain('should be first');
expect(
findTableRows()
.at(1)
.text(),
).toContain('should be last');
});
it('renders the correct link', async () => {
const id = 1;
const fullPath = 'test/test';
const rootRef = 'master';
const expectedPath = `/${fullPath}/-/graphs/${rootRef}/charts`;
createComponentWithApollo({
data: {
projectIds: { [id]: true },
},
queryData: {
data: {
projects: {
nodes: [
{
fullPath,
name: 'test',
id,
repository: {
rootRef,
},
codeCoverageSummary: {
averageCoverage: '1.45',
coverageCount: '1',
lastUpdatedAt: new Date().toISOString(),
},
},
],
},
},
},
mountFn: mount,
});
jest.runOnlyPendingTimers();
await waitForPromises();
expect(findTable().exists()).toBe(true);
expect(findProjectNameById(id).attributes('href')).toBe(expectedPath);
});
}); });
describe('when selected project has no coverage', () => { describe('when selected project has no coverage', () => {
it('sets coverage to default values', async () => { it('does not render the table', async () => {
const name = 'test';
const id = 1; const id = 1;
createComponentWithApollo({ createComponentWithApollo({
data: { data: {
...@@ -144,8 +244,12 @@ describe('Test coverage table component', () => { ...@@ -144,8 +244,12 @@ describe('Test coverage table component', () => {
projects: { projects: {
nodes: [ nodes: [
{ {
name, fullPath: 'test/test',
name: 'test',
id, id,
repository: {
rootRef: 'master',
},
codeCoverageSummary: null, codeCoverageSummary: null,
}, },
], ],
...@@ -157,11 +261,7 @@ describe('Test coverage table component', () => { ...@@ -157,11 +261,7 @@ describe('Test coverage table component', () => {
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
await waitForPromises(); await waitForPromises();
expect(findTable().exists()).toBe(true); expect(findTable().exists()).toBe(false);
expect(findProjectNameById(id).text()).toBe(name);
expect(findProjectAverageById(id).text()).toBe('0.00%');
expect(findProjectCountById(id).text()).toBe('0');
expect(findProjectDateById(id).text()).toBe('just now');
}); });
}); });
}); });
...@@ -57,6 +57,30 @@ RSpec.describe Members::DestroyService do ...@@ -57,6 +57,30 @@ RSpec.describe Members::DestroyService do
expect(details[:reason]).to be_nil expect(details[:reason]).to be_nil
end end
end end
context 'group deletion schedule' do
context 'when member user has a scheduled deletion for the group' do
let!(:group_deletion_schedule) { create(:group_deletion_schedule, group: group, user_id: member_user.id, marked_for_deletion_on: 2.days.ago) }
it 'deletes the group deletion schedule' do
expect(group.reload.deletion_schedule).to eq(group_deletion_schedule)
subject.execute(member)
expect(group.reload.deletion_schedule).to be nil
end
end
context 'when scheduled deletion for the group belongs to different user' do
let!(:group_deletion_schedule) { create(:group_deletion_schedule, group: group, user_id: current_user.id, marked_for_deletion_on: 2.days.ago) }
it 'does not delete the group deletion schedule' do
subject.execute(member)
expect(group.reload.deletion_schedule).to eq(group_deletion_schedule)
end
end
end
end end
context 'when current user is not present' do # ie, when the system initiates the destroy context 'when current user is not present' do # ie, when the system initiates the destroy
......
...@@ -4,6 +4,7 @@ stages: ...@@ -4,6 +4,7 @@ stages:
- review - review
- deploy - deploy
- production - production
- cleanup
variables: variables:
AUTO_DEVOPS_PLATFORM_TARGET: ECS AUTO_DEVOPS_PLATFORM_TARGET: ECS
......
...@@ -41,6 +41,8 @@ module Gitlab ...@@ -41,6 +41,8 @@ module Gitlab
data.map! { |v| utf8_encode_values(v) } data.map! { |v| utf8_encode_values(v) }
when String when String
encode_utf8(data) encode_utf8(data)
when Integer
data
end end
end end
end end
......
...@@ -86,6 +86,10 @@ module Gitlab ...@@ -86,6 +86,10 @@ module Gitlab
mkdir_p(File.join(uploads_export_path, secret)) mkdir_p(File.join(uploads_export_path, secret))
download_or_copy_upload(upload, upload_path) download_or_copy_upload(upload, upload_path)
rescue Errno::ENAMETOOLONG => e
# Do not fail entire project export if downloaded file has filename that exceeds 255 characters.
# Ignore raised exception, skip such upload, log the error and keep going with the export instead.
Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
end end
end end
end end
......
...@@ -1856,9 +1856,15 @@ msgstr "" ...@@ -1856,9 +1856,15 @@ msgstr ""
msgid "Admin notes" msgid "Admin notes"
msgstr "" msgstr ""
msgid "AdminArea|%{billable_users_link_start}Learn more%{billable_users_link_end} about what defines a billable user"
msgstr ""
msgid "AdminArea|Active users" msgid "AdminArea|Active users"
msgstr "" msgstr ""
msgid "AdminArea|All users created in the instance, including users who are not %{billable_users_link_start}billable users%{billable_users_link_end}."
msgstr ""
msgid "AdminArea|Billable users" msgid "AdminArea|Billable users"
msgstr "" msgstr ""
...@@ -2494,6 +2500,12 @@ msgstr "" ...@@ -2494,6 +2500,12 @@ msgstr ""
msgid "AlertSettings|2. Name integration" msgid "AlertSettings|2. Name integration"
msgstr "" msgstr ""
msgid "AlertSettings|3. Set up webhook"
msgstr ""
msgid "AlertSettings|4. Test integration(optional)"
msgstr ""
msgid "AlertSettings|5. Map fields (optional)" msgid "AlertSettings|5. Map fields (optional)"
msgstr "" msgstr ""
...@@ -2548,6 +2560,15 @@ msgstr "" ...@@ -2548,6 +2560,15 @@ msgstr ""
msgid "AlertSettings|Opsgenie" msgid "AlertSettings|Opsgenie"
msgstr "" msgstr ""
msgid "AlertSettings|Prometheus API base URL"
msgstr ""
msgid "AlertSettings|Provide an example payload from the monitoring tool you intend to integrate with to send a test alert to the %{linkStart}alerts page%{linkEnd}. This payload can be used to test the integration using the \"save and test payload\" button."
msgstr ""
msgid "AlertSettings|Reset Key"
msgstr ""
msgid "AlertSettings|Reset key" msgid "AlertSettings|Reset key"
msgstr "" msgstr ""
...@@ -2557,6 +2578,9 @@ msgstr "" ...@@ -2557,6 +2578,9 @@ msgstr ""
msgid "AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint." msgid "AlertSettings|Review your external service's documentation to learn where to provide this information to your external service, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr "" msgstr ""
msgid "AlertSettings|Save and test payload"
msgstr ""
msgid "AlertSettings|Save integration" msgid "AlertSettings|Save integration"
msgstr "" msgstr ""
...@@ -2584,6 +2608,9 @@ msgstr "" ...@@ -2584,6 +2608,9 @@ msgstr ""
msgid "AlertSettings|URL cannot be blank and must start with http or https" msgid "AlertSettings|URL cannot be blank and must start with http or https"
msgstr "" msgstr ""
msgid "AlertSettings|Utilize the URL and authorization key below to authorize an external service to send Alerts to GitLab. Review your chosen services documentation to learn where to add these details, and the %{linkStart}GitLab documentation%{linkEnd} to learn more about configuring your endpoint."
msgstr ""
msgid "AlertSettings|Webhook URL" msgid "AlertSettings|Webhook URL"
msgstr "" msgstr ""
...@@ -2623,6 +2650,9 @@ msgstr "" ...@@ -2623,6 +2650,9 @@ msgstr ""
msgid "AlertsIntegrations|Prometheus" msgid "AlertsIntegrations|Prometheus"
msgstr "" msgstr ""
msgid "AlertsIntegrations|The integration has been successfully saved. Alerts from this new integration should now appear on your alerts list."
msgstr ""
msgid "Algorithm" msgid "Algorithm"
msgstr "" msgstr ""
...@@ -4929,6 +4959,9 @@ msgstr "" ...@@ -4929,6 +4959,9 @@ msgstr ""
msgid "Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used." msgid "Changes affect new repositories only. If not specified, Git's default name %{branch_name_default} will be used."
msgstr "" msgstr ""
msgid "Changes affect new repositories only. If not specified, either the configured application-wide default or Git's default name %{branch_name_default} will be used."
msgstr ""
msgid "Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision." msgid "Changes are shown as if the %{b_open}source%{b_close} revision was being merged into the %{b_open}target%{b_close} revision."
msgstr "" msgstr ""
...@@ -7214,6 +7247,9 @@ msgstr "" ...@@ -7214,6 +7247,9 @@ msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy." msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the image details."
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the repository list." msgid "ContainerRegistry|Something went wrong while fetching the repository list."
msgstr "" msgstr ""
...@@ -11473,6 +11509,9 @@ msgstr "" ...@@ -11473,6 +11509,9 @@ msgstr ""
msgid "FeatureFlags|New user list" msgid "FeatureFlags|New user list"
msgstr "" msgstr ""
msgid "FeatureFlags|No user list selected"
msgstr ""
msgid "FeatureFlags|Percent of users" msgid "FeatureFlags|Percent of users"
msgstr "" msgstr ""
...@@ -11497,9 +11536,6 @@ msgstr "" ...@@ -11497,9 +11536,6 @@ msgstr ""
msgid "FeatureFlags|Rollout Strategy" msgid "FeatureFlags|Rollout Strategy"
msgstr "" msgstr ""
msgid "FeatureFlags|Select a user list"
msgstr ""
msgid "FeatureFlags|Set the Unleash client application name to the name of the environment your application runs in. This value is used to match environment scopes. See the %{linkStart}example client configuration%{linkEnd}." msgid "FeatureFlags|Set the Unleash client application name to the name of the environment your application runs in. This value is used to match environment scopes. See the %{linkStart}example client configuration%{linkEnd}."
msgstr "" msgstr ""
...@@ -23249,9 +23285,6 @@ msgstr "" ...@@ -23249,9 +23285,6 @@ msgstr ""
msgid "Save Push Rules" msgid "Save Push Rules"
msgstr "" msgstr ""
msgid "Save and test payload"
msgstr ""
msgid "Save anyway" msgid "Save anyway"
msgstr "" msgstr ""
...@@ -27325,7 +27358,7 @@ msgstr "" ...@@ -27325,7 +27358,7 @@ msgstr ""
msgid "This is the highest peak of users on your installation since the license started." msgid "This is the highest peak of users on your installation since the license started."
msgstr "" msgstr ""
msgid "This is the number of currently active users on your installation, and this is the minimum number you need to purchase when you renew your license." msgid "This is the number of %{billable_users_link_start}billable users%{link_end} on your installation, and this is the minimum number you need to purchase when you renew your license."
msgstr "" msgstr ""
msgid "This is your current session" msgid "This is your current session"
......
...@@ -25,4 +25,21 @@ RSpec.describe 'Group Repository settings' do ...@@ -25,4 +25,21 @@ RSpec.describe 'Group Repository settings' do
let(:entity_type) { 'group' } let(:entity_type) { 'group' }
end end
end end
context 'Default initial branch name' do
before do
visit group_settings_repository_path(group)
end
it 'has the setting section' do
expect(page).to have_css("#js-default-branch-name")
end
it 'renders the correct setting section content' do
within("#js-default-branch-name") do
expect(page).to have_content("Default initial branch name")
expect(page).to have_content("Set the default name of the initial branch when creating new repositories through the user interface.")
end
end
end
end end
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = ` exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = `
"<gl-form-stub class=\\"gl-mt-6\\"> "<form class=\\"gl-mt-6\\">
<h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5> <h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5>
<gl-form-group-stub id=\\"integration-type\\" label=\\"1. Select integration type\\" label-for=\\"integration-type\\"> <div id=\\"integration-type\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-type__BV_label_\\" for=\\"integration-type\\" class=\\"d-block col-form-label\\">1. Select integration type</label>
<gl-form-select-stub options=\\"[object Object],[object Object],[object Object],[object Object]\\" value=\\"\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> <div class=\\"bv-no-focus-ring\\"><select class=\\"gl-form-select custom-select\\" id=\\"__BVID__8\\">
</gl-form-group-stub> <option value=\\"\\">Select integration type</option>
<b-collapse-stub tag=\\"div\\" class=\\"gl-mt-3\\"> <option value=\\"HTTP\\">HTTP Endpoint</option>
<gl-form-group-stub id=\\"name-integration\\" label=\\"2. Name integration\\" label-for=\\"name-integration\\"> <option value=\\"PROMETHEUS\\">External Prometheus</option>
<b-form-input-stub value=\\"\\" placeholder=\\"Enter integration name\\" debounce=\\"0\\" type=\\"text\\" class=\\"gl-form-input\\"></b-form-input-stub> <option value=\\"OPSGENIE\\">Opsgenie</option>
</gl-form-group-stub> </select> <span class=\\"gl-text-gray-500\\">Learn more about our upcoming <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://gitlab.com/groups/gitlab-org/-/epics/4390\\" class=\\"gl-link gl-display-inline-block\\">integrations</a></span>
<!----> <!---->
<div class=\\"gl-display-flex gl-justify-content-end\\"> <!---->
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"reset\\" class=\\"gl-mr-3 js-no-auto-disable\\">Cancel</gl-button-stub> <!---->
<gl-button-stub category=\\"secondary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"gl-mr-1 js-no-auto-disable\\">Save and test payload</gl-button-stub> </div>
<gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" type=\\"submit\\" class=\\"js-no-auto-disable\\">Save integration</gl-button-stub> </div>
</div> <div class=\\"gl-mt-3 collapse\\" style=\\"display: none;\\" id=\\"__BVID__13\\">
</b-collapse-stub> <div id=\\"name-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"name-integration__BV_label_\\" for=\\"name-integration\\" class=\\"d-block col-form-label\\">2. Name integration</label>
</gl-form-stub>" <div class=\\"bv-no-focus-ring\\"><input type=\\"text\\" placeholder=\\"Enter integration name\\" class=\\"gl-form-input form-control\\" id=\\"__BVID__18\\">
<!---->
<!---->
<!---->
</div>
</div>
<div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-label\\">3. Set up webhook</label>
<div class=\\"bv-no-focus-ring\\"><span class=\\"gl-text-gray-500\\">Utilize the URL and authorization key below to authorize an external service to send Alerts to GitLab. Review your chosen services documentation to learn where to add these details, and the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">GitLab documentation</a> to learn more about configuring your endpoint.</span> <label class=\\"gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal\\">
<div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span>
<!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div>
<!---->
</label>
<!---->
<div class=\\"gl-my-4\\"><span>
Webhook URL
</span>
<div id=\\"url\\" readonly=\\"readonly\\">
<div role=\\"group\\" class=\\"input-group\\">
<!---->
<!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
<div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
<!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\">
<use href=\\"#copy-to-clipboard\\"></use>
</svg>
<!----></button></div>
<!---->
</div>
</div>
</div>
<div class=\\"gl-my-4\\"><span>
Authorization key
</span>
<div id=\\"authorization-key\\" readonly=\\"readonly\\" class=\\"gl-mb-2\\">
<div role=\\"group\\" class=\\"input-group\\">
<!---->
<!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
<div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
<!----> <svg data-testid=\\"copy-to-clipboard-icon\\" class=\\"gl-button-icon gl-icon s16\\">
<use href=\\"#copy-to-clipboard\\"></use>
</svg>
<!----></button></div>
<!---->
</div>
</div> <button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mt-3 btn-default btn-md disabled gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Reset Key</span></button>
<!---->
</div>
<!---->
<!---->
<!---->
</div>
</div>
<div id=\\"test-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"test-integration__BV_label_\\" for=\\"test-integration\\" class=\\"d-block col-form-label\\">4. Test integration(optional)</label>
<div class=\\"bv-no-focus-ring\\"><span class=\\"gl-text-gray-500\\">Provide an example payload from the monitoring tool you intend to integrate with to send a test alert to the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"http://invalid\\" class=\\"gl-link gl-display-inline-block\\">alerts page</a>. This payload can be used to test the integration using the \\"save and test payload\\" button.</span> <textarea id=\\"test-integration\\" disabled=\\"disabled\\" placeholder=\\"Enter test alert JSON....\\" wrap=\\"soft\\" class=\\"gl-form-input gl-form-textarea gl-my-4 form-control is-valid\\" style=\\"resize: none; overflow-y: scroll;\\"></textarea>
<!---->
<!---->
<!---->
</div>
</div>
<!---->
<div class=\\"gl-display-flex gl-justify-content-end\\"><button type=\\"reset\\" class=\\"btn gl-mr-3 js-no-auto-disable btn-default btn-md gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Cancel</span></button> <button type=\\"button\\" class=\\"btn gl-mr-1 js-no-auto-disable btn-success btn-md gl-button btn-success-secondary\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Save integration</span></button></div>
</div>
</form>"
`; `;
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertsSettingsForm with default values renders the initial template 1`] = ` exports[`AlertsSettingsFormOld with default values renders the initial template 1`] = `
"<gl-form-stub> "<gl-form-stub>
<h5 class=\\"gl-font-lg gl-my-5\\"></h5> <h5 class=\\"gl-font-lg gl-my-5\\"></h5>
<!----> <!---->
...@@ -13,10 +13,10 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] ...@@ -13,10 +13,10 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</p> </p>
</div> </div>
<gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\"> <gl-form-group-stub label-for=\\"integration-type\\" label=\\"Integration\\">
<gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span> <gl-form-select-stub id=\\"integration-type\\" options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"HTTP\\"></gl-form-select-stub> <span class=\\"gl-text-gray-500\\"><gl-sprintf-stub message=\\"Learn more about our our upcoming %{linkStart}integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub> </gl-form-group-stub>
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\"> <gl-form-group-stub label=\\"Active\\" label-for=\\"active\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub> <toggle-button-stub id=\\"active\\"></toggle-button-stub>
</gl-form-group-stub> </gl-form-group-stub>
<!----> <!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\"> <gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\">
...@@ -25,7 +25,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`] ...@@ -25,7 +25,7 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</span> </span>
</gl-form-group-stub> </gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\"> <gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\">
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub> <gl-form-input-group-stub value=\\"\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub> <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" buttontextclasses=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\"> <gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in. Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
......
import { shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlForm, GlFormSelect, GlCollapse, GlFormInput } from '@gitlab/ui'; import { GlForm, GlFormSelect, GlCollapse, GlFormInput, GlToggle } from '@gitlab/ui';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue'; import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form_new.vue';
import { defaultAlertSettingsConfig } from './util'; import { defaultAlertSettingsConfig } from './util';
import { typeSet } from '~/alerts_settings/constants';
describe('AlertsSettingsFormNew', () => { describe('AlertsSettingsFormNew', () => {
let wrapper; let wrapper;
const createComponent = ( const createComponent = ({
{ methods } = {}, data = {},
data, props = { loading: false },
multipleHttpIntegrationsCustomMapping = false, multipleHttpIntegrationsCustomMapping = false,
) => { } = {}) => {
wrapper = shallowMount(AlertsSettingsForm, { wrapper = mount(AlertsSettingsForm, {
data() { data() {
return { ...data }; return { ...data };
}, },
propsData: {
...props,
},
provide: { provide: {
glFeatures: { multipleHttpIntegrationsCustomMapping }, glFeatures: { multipleHttpIntegrationsCustomMapping },
...defaultAlertSettingsConfig, ...defaultAlertSettingsConfig,
}, },
methods,
stubs: { GlCollapse, GlFormInput },
}); });
}; };
const findForm = () => wrapper.find(GlForm); const findForm = () => wrapper.find(GlForm);
const findSelect = () => wrapper.find(GlFormSelect); const findSelect = () => wrapper.find(GlFormSelect);
const findFormSteps = () => wrapper.find(GlCollapse); const findFormSteps = () => wrapper.find(GlCollapse);
const findFormName = () => wrapper.find(GlFormInput); const findFormFields = () => wrapper.findAll(GlFormInput);
const findFormToggle = () => wrapper.find(GlToggle);
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`); const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
afterEach(() => { afterEach(() => {
if (wrapper) { if (wrapper) {
...@@ -53,17 +57,83 @@ describe('AlertsSettingsFormNew', () => { ...@@ -53,17 +57,83 @@ describe('AlertsSettingsFormNew', () => {
}); });
it('shows the rest of the form when the dropdown is used', async () => { it('shows the rest of the form when the dropdown is used', async () => {
findSelect().vm.$emit('change', 'prometheus'); const options = findSelect().findAll('option');
await options.at(1).setSelected();
await wrapper.vm.$nextTick();
expect(
findFormFields()
.at(0)
.isVisible(),
).toBe(true);
});
});
describe('when form is invalid', () => {
// TODO, implement specs for when form is invalid
});
describe('when form is valid', () => {
beforeEach(() => {
createComponent({});
});
it('allows for on-create-new-integration with the correct form values for HTTP', async () => {
const options = findSelect().findAll('option');
await options.at(1).setSelected();
await findFormFields()
.at(0)
.setValue('Test integration');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(findSubmitButton().exists()).toBe(true);
expect(findSubmitButton().text()).toBe('Save integration');
findForm().trigger('submit');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('on-create-new-integration')).toBeTruthy();
expect(wrapper.emitted('on-create-new-integration')[0]).toEqual([
{ type: typeSet.http, variables: { name: 'Test integration', active: true } },
]);
});
it('allows for on-create-new-integration with the correct form values for PROMETHEUS', async () => {
const options = findSelect().findAll('option');
await options.at(2).setSelected();
await findFormFields()
.at(0)
.setValue('Test integration');
await findFormFields()
.at(1)
.setValue('https://test.com');
await findFormToggle().trigger('click');
await wrapper.vm.$nextTick();
expect(findSubmitButton().exists()).toBe(true);
expect(findSubmitButton().text()).toBe('Save integration');
findForm().trigger('submit');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findFormName().isVisible()).toBe(true); expect(wrapper.emitted('on-create-new-integration')).toBeTruthy();
expect(wrapper.emitted('on-create-new-integration')[0]).toEqual([
{ type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
]);
}); });
}); });
describe('Mapping builder section', () => { describe('Mapping builder section', () => {
beforeEach(() => { beforeEach(() => {
createComponent({}, {}); createComponent({});
}); });
it('should NOT render when feature flag disabled', () => { it('should NOT render when feature flag disabled', () => {
...@@ -71,7 +141,7 @@ describe('AlertsSettingsFormNew', () => { ...@@ -71,7 +141,7 @@ describe('AlertsSettingsFormNew', () => {
}); });
it('should render when feature flag is enabled', () => { it('should render when feature flag is enabled', () => {
createComponent({}, {}, true); createComponent({ multipleHttpIntegrationsCustomMapping: true });
expect(findMappingBuilderSection().exists()).toBe(true); expect(findMappingBuilderSection().exists()).toBe(true);
}); });
}); });
......
...@@ -8,7 +8,7 @@ import { defaultAlertSettingsConfig } from './util'; ...@@ -8,7 +8,7 @@ import { defaultAlertSettingsConfig } from './util';
jest.mock('~/alerts_settings/services'); jest.mock('~/alerts_settings/services');
describe('AlertsSettingsForm', () => { describe('AlertsSettingsFormOld', () => {
let wrapper; let wrapper;
const createComponent = ({ methods } = {}, data) => { const createComponent = ({ methods } = {}, data) => {
...@@ -113,7 +113,7 @@ describe('AlertsSettingsForm', () => { ...@@ -113,7 +113,7 @@ describe('AlertsSettingsForm', () => {
createComponent( createComponent(
{}, {},
{ {
selectedIntegration: 'prometheus', selectedIntegration: 'PROMETHEUS',
}, },
); );
}); });
...@@ -127,9 +127,7 @@ describe('AlertsSettingsForm', () => { ...@@ -127,9 +127,7 @@ describe('AlertsSettingsForm', () => {
}); });
it('shows the correct default API URL', () => { it('shows the correct default API URL', () => {
expect(findUrl().attributes('value')).toBe( expect(findUrl().attributes('value')).toBe(defaultAlertSettingsConfig.prometheus.url);
defaultAlertSettingsConfig.prometheus.prometheusUrl,
);
}); });
}); });
...@@ -138,7 +136,7 @@ describe('AlertsSettingsForm', () => { ...@@ -138,7 +136,7 @@ describe('AlertsSettingsForm', () => {
createComponent( createComponent(
{}, {},
{ {
selectedIntegration: 'opsgenie', selectedIntegration: 'OPSGENIE',
}, },
); );
}); });
......
...@@ -4,9 +4,16 @@ import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_ ...@@ -4,9 +4,16 @@ import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_
import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue'; import AlertsSettingsFormOld from '~/alerts_settings/components/alerts_settings_form_old.vue';
import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue'; import AlertsSettingsFormNew from '~/alerts_settings/components/alerts_settings_form_new.vue';
import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue'; import IntegrationsList from '~/alerts_settings/components/alerts_integrations_list.vue';
import createHttpIntegrationMutation from '~/alerts_settings/graphql/mutations/create_http_integration.mutation.graphql';
import createPrometheusIntegrationMutation from '~/alerts_settings/graphql/mutations/create_prometheus_integration.mutation.graphql';
import createFlash from '~/flash';
import { defaultAlertSettingsConfig } from './util'; import { defaultAlertSettingsConfig } from './util';
import mockIntegrations from './mocks/integrations.json'; import mockIntegrations from './mocks/integrations.json';
jest.mock('~/flash');
const projectPath = '';
describe('AlertsSettingsWrapper', () => { describe('AlertsSettingsWrapper', () => {
let wrapper; let wrapper;
...@@ -25,6 +32,7 @@ describe('AlertsSettingsWrapper', () => { ...@@ -25,6 +32,7 @@ describe('AlertsSettingsWrapper', () => {
}, },
mocks: { mocks: {
$apollo: { $apollo: {
mutate: jest.fn(),
query: jest.fn(), query: jest.fn(),
queries: { queries: {
integrations: { integrations: {
...@@ -79,5 +87,85 @@ describe('AlertsSettingsWrapper', () => { ...@@ -79,5 +87,85 @@ describe('AlertsSettingsWrapper', () => {
expect(findLoader().exists()).toBe(false); expect(findLoader().exists()).toBe(false);
expect(findIntegrations()).toHaveLength(mockIntegrations.length); expect(findIntegrations()).toHaveLength(mockIntegrations.length);
}); });
it('shows an error message when a user cannot create a new integration', () => {
createComponent({
data: { integrations: { list: mockIntegrations } },
provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
expect(findLoader().exists()).toBe(false);
expect(findIntegrations()).toHaveLength(mockIntegrations.length);
});
it('calls `$apollo.mutate` with `createHttpIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations } },
provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createHttpIntegrationMutation: { integration: { id: '1' } } },
});
wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', {
type: 'HTTP',
variables: { name: 'Test 1', active: true },
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createHttpIntegrationMutation,
update: expect.anything(),
variables: {
name: 'Test 1',
active: true,
projectPath,
},
});
});
it('calls `$apollo.mutate` with `createPrometheusIntegrationMutation`', () => {
createComponent({
data: { integrations: { list: mockIntegrations } },
provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
data: { createPrometheusIntegrationMutation: { integration: { id: '2' } } },
});
wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', {
type: 'PROMETHEUS',
variables: { apiUrl: 'https://test.com', active: true },
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createPrometheusIntegrationMutation,
update: expect.anything(),
variables: {
apiUrl: 'https://test.com',
active: true,
projectPath,
},
});
});
it('shows error alert when integration creation fails ', () => {
const errorMsg = 'Something went wrong';
createComponent({
data: { integrations: { list: mockIntegrations } },
provide: { glFeatures: { httpIntegrationsList: true } },
loading: false,
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
wrapper.find(AlertsSettingsFormNew).vm.$emit('on-create-new-integration', {});
setImmediate(() => {
expect(createFlash).toHaveBeenCalledWith({ message: errorMsg });
});
});
}); });
}); });
...@@ -2,7 +2,7 @@ const PROMETHEUS_URL = '/prometheus/alerts/notify.json'; ...@@ -2,7 +2,7 @@ const PROMETHEUS_URL = '/prometheus/alerts/notify.json';
const GENERIC_URL = '/alerts/notify.json'; const GENERIC_URL = '/alerts/notify.json';
const KEY = 'abcedfg123'; const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid'; const INVALID_URL = 'http://invalid';
const ACTIVATED = false; const ACTIVE = false;
export const defaultAlertSettingsConfig = { export const defaultAlertSettingsConfig = {
generic: { generic: {
...@@ -11,18 +11,18 @@ export const defaultAlertSettingsConfig = { ...@@ -11,18 +11,18 @@ export const defaultAlertSettingsConfig = {
url: GENERIC_URL, url: GENERIC_URL,
alertsSetupUrl: INVALID_URL, alertsSetupUrl: INVALID_URL,
alertsUsageUrl: INVALID_URL, alertsUsageUrl: INVALID_URL,
activated: ACTIVATED, active: ACTIVE,
}, },
prometheus: { prometheus: {
authorizationKey: KEY, authorizationKey: KEY,
prometheusFormPath: INVALID_URL, prometheusFormPath: INVALID_URL,
prometheusUrl: PROMETHEUS_URL, url: PROMETHEUS_URL,
activated: ACTIVATED, active: ACTIVE,
}, },
opsgenie: { opsgenie: {
opsgenieMvcIsAvailable: true, opsgenieMvcIsAvailable: true,
formPath: INVALID_URL, formPath: INVALID_URL,
activated: ACTIVATED, active: ACTIVE,
opsgenieMvcTargetUrl: GENERIC_URL, opsgenieMvcTargetUrl: GENERIC_URL,
}, },
}; };
...@@ -118,6 +118,24 @@ describe('Api', () => { ...@@ -118,6 +118,24 @@ describe('Api', () => {
}); });
}); });
describe('container registry', () => {
describe('containerRegistryDetails', () => {
it('fetch container registry details', async () => {
const expectedUrl = `foo`;
const apiResponse = {};
jest.spyOn(axios, 'get');
jest.spyOn(Api, 'buildUrl').mockReturnValueOnce(expectedUrl);
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
const { data } = await Api.containerRegistryDetails(1);
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl, {});
});
});
});
describe('group', () => { describe('group', () => {
it('fetches a group', done => { it('fetches a group', done => {
const groupId = '123456'; const groupId = '123456';
......
...@@ -1149,7 +1149,11 @@ describe('DiffsStoreActions', () => { ...@@ -1149,7 +1149,11 @@ describe('DiffsStoreActions', () => {
file_hash: 'testhash', file_hash: 'testhash',
alternate_viewer: { name: updatedViewerName }, alternate_viewer: { name: updatedViewerName },
}; };
const updatedViewer = { name: updatedViewerName, automaticallyCollapsed: false }; const updatedViewer = {
name: updatedViewerName,
automaticallyCollapsed: false,
manuallyCollapsed: false,
};
const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }]; const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }];
let renamedFile; let renamedFile;
let mock; let mock;
......
...@@ -25,6 +25,8 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { ...@@ -25,6 +25,8 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
propsData: { ...DEFAULT_PROPS, ...props }, propsData: { ...DEFAULT_PROPS, ...props },
}); });
const findDropdown = () => wrapper.find(GlDropdown);
describe('with user lists', () => { describe('with user lists', () => {
const findDropdownItem = () => wrapper.find(GlDropdownItem); const findDropdownItem = () => wrapper.find(GlDropdownItem);
...@@ -34,7 +36,7 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { ...@@ -34,7 +36,7 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
}); });
it('should show the input for userListId with the correct value', () => { it('should show the input for userListId with the correct value', () => {
const dropdownWrapper = wrapper.find(GlDropdown); const dropdownWrapper = findDropdown();
expect(dropdownWrapper.exists()).toBe(true); expect(dropdownWrapper.exists()).toBe(true);
expect(dropdownWrapper.props('text')).toBe(userList.name); expect(dropdownWrapper.props('text')).toBe(userList.name);
}); });
...@@ -87,11 +89,15 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => { ...@@ -87,11 +89,15 @@ describe('~/feature_flags/components/strategies/gitlab_user_list.vue', () => {
describe('without user lists', () => { describe('without user lists', () => {
beforeEach(() => { beforeEach(() => {
Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [] }); Api.searchFeatureFlagUserLists.mockResolvedValue({ data: [] });
wrapper = factory(); wrapper = factory({ strategy: { ...userListStrategy, userList: null } });
}); });
it('should display a message that there are no user lists', () => { it('should display a message that there are no user lists', () => {
expect(wrapper.text()).toContain('There are no configured user lists'); expect(wrapper.text()).toContain('There are no configured user lists');
}); });
it('should dispaly a message that no list has been selected', () => {
expect(findDropdown().text()).toContain('No user list selected');
});
}); });
}); });
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import * as actions from '~/registry/explorer/stores/actions'; import * as actions from '~/registry/explorer/stores/actions';
import * as types from '~/registry/explorer/stores/mutation_types'; import * as types from '~/registry/explorer/stores/mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { reposServerResponse, registryServerResponse } from '../mock_data'; import { reposServerResponse, registryServerResponse } from '../mock_data';
jest.mock('~/flash.js'); jest.mock('~/flash.js');
...@@ -227,6 +228,47 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -227,6 +228,47 @@ describe('Actions RegistryExplorer Store', () => {
}); });
}); });
describe('requestImageDetailsAndTagsList', () => {
it('sets the imageDetails and dispatch requestTagsList', done => {
const resolvedValue = { foo: 'bar' };
jest.spyOn(Api, 'containerRegistryDetails').mockResolvedValue({ data: resolvedValue });
testAction(
actions.requestImageDetailsAndTagsList,
1,
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_IMAGE_DETAILS, payload: resolvedValue },
],
[
{
type: 'requestTagsList',
},
],
done,
);
});
it('should create flash on error', done => {
jest.spyOn(Api, 'containerRegistryDetails').mockRejectedValue();
testAction(
actions.requestImageDetailsAndTagsList,
1,
{},
[
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
describe('request delete multiple tags', () => { describe('request delete multiple tags', () => {
const url = `project-path/registry/repository/foo/tags`; const url = `project-path/registry/repository/foo/tags`;
const params = window.btoa(JSON.stringify({ tags_path: `${url}?format=json` })); const params = window.btoa(JSON.stringify({ tags_path: `${url}?format=json` }));
......
...@@ -121,4 +121,13 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -121,4 +121,13 @@ describe('Mutations Registry Explorer Store', () => {
expect(mockState).toEqual(expectedState); expect(mockState).toEqual(expectedState);
}); });
}); });
describe('SET_IMAGE_DETAILS', () => {
it('should set imageDetails', () => {
const expectedState = { ...mockState, imageDetails: { foo: 'bar' } };
mutations[types.SET_IMAGE_DETAILS](mockState, { foo: 'bar' });
expect(mockState).toEqual(expectedState);
});
});
}); });
import { pathGenerator } from '~/registry/explorer/utils';
describe('Utils', () => {
describe('pathGenerator', () => {
const imageDetails = {
path: 'foo/bar/baz',
name: 'baz',
id: 1,
};
it('returns the fetch url when no ending is passed', () => {
expect(pathGenerator(imageDetails)).toBe('/foo/bar/registry/repository/1/tags?format=json');
});
it('returns the url with an ending when is passed', () => {
expect(pathGenerator(imageDetails, 'foo')).toBe('/foo/bar/registry/repository/1/foo');
});
});
});
...@@ -40,7 +40,6 @@ exports[`Repository last commit component renders commit widget 1`] = ` ...@@ -40,7 +40,6 @@ exports[`Repository last commit component renders commit widget 1`] = `
> >
Test Test
</gl-link-stub> </gl-link-stub>
authored authored
...@@ -147,7 +146,6 @@ exports[`Repository last commit component renders the signature HTML as returned ...@@ -147,7 +146,6 @@ exports[`Repository last commit component renders the signature HTML as returned
> >
Test Test
</gl-link-stub> </gl-link-stub>
authored authored
......
...@@ -44,7 +44,7 @@ RSpec.describe OperationsHelper do ...@@ -44,7 +44,7 @@ RSpec.describe OperationsHelper do
'prometheus_activated' => 'false', 'prometheus_activated' => 'false',
'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json), 'prometheus_url' => notify_project_prometheus_alerts_url(project, format: :json),
'disabled' => 'false', 'disabled' => 'false',
'project_path' => project_path(project) 'project_path' => project.full_path
) )
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do
subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('AWS/Deploy-ECS') }
describe 'the created pipeline' do
let_it_be(:user) { create(:admin) }
let(:default_branch) { 'master' }
let(:pipeline_branch) { default_branch }
let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) }
let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) }
let(:pipeline) { service.execute!(:push) }
let(:build_names) { pipeline.builds.pluck(:name) }
let(:platform_target) { 'ECS' }
before do
create(:ci_variable, project: project, key: 'AUTO_DEVOPS_PLATFORM_TARGET', value: platform_target)
stub_ci_pipeline_yaml_file(template.content)
allow_any_instance_of(Ci::BuildScheduleWorker).to receive(:perform).and_return(true)
allow(project).to receive(:default_branch).and_return(default_branch)
end
shared_examples 'no pipeline yaml error' do
it 'does not have any error' do
expect(pipeline.has_yaml_errors?).to be_falsey
end
end
it_behaves_like 'no pipeline yaml error'
it 'creates the expected jobs' do
expect(build_names).to include('production_ecs')
end
context 'when running a pipeline for a branch' do
let(:pipeline_branch) { 'test_branch' }
before do
project.repository.create_branch(pipeline_branch)
end
it_behaves_like 'no pipeline yaml error'
it 'creates the expected jobs' do
expect(build_names).to include('review_ecs', 'stop_review_ecs')
end
context 'when deploying to ECS Fargate' do
let(:platform_target) { 'FARGATE' }
it 'creates the expected jobs' do
expect(build_names).to include('review_fargate', 'stop_review_fargate')
end
end
end
end
end
...@@ -15,7 +15,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do ...@@ -15,7 +15,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do
path: '/api/v4/projects/1', path: '/api/v4/projects/1',
params: { params: {
'description': '[FILTERED]', 'description': '[FILTERED]',
'name': 'gitlab test' 'name': 'gitlab test',
'int': 42
}, },
host: 'localhost', host: 'localhost',
remote_ip: '127.0.0.1', remote_ip: '127.0.0.1',
...@@ -44,7 +45,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do ...@@ -44,7 +45,8 @@ RSpec.describe Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp do
expect(params).to eq([ expect(params).to eq([
{ 'key' => 'description', 'value' => '[FILTERED]' }, { 'key' => 'description', 'value' => '[FILTERED]' },
{ 'key' => 'name', 'value' => 'gitlab test' } { 'key' => 'name', 'value' => 'gitlab test' },
{ 'key' => 'int', 'value' => 42 }
]) ])
end end
end end
...@@ -23,13 +23,13 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do ...@@ -23,13 +23,13 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
end end
describe '#save' do describe '#save' do
context 'when the project has uploads locally stored' do
let(:upload) { create(:upload, :issuable_upload, :with_file, model: project) }
before do before do
project.uploads << upload project.uploads << upload
end end
context 'when the project has uploads locally stored' do
let(:upload) { create(:upload, :issuable_upload, :with_file, model: project) }
it 'does not cause errors' do it 'does not cause errors' do
manager.save manager.save
...@@ -74,6 +74,22 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do ...@@ -74,6 +74,22 @@ RSpec.describe Gitlab::ImportExport::UploadsManager do
end end
end end
end end
context 'when upload is in object storage' do
before do
stub_uploads_object_storage(FileUploader)
allow(manager).to receive(:download_or_copy_upload).and_raise(Errno::ENAMETOOLONG)
end
it 'ignores problematic upload and logs exception' do
expect(Gitlab::ErrorTracking).to receive(:log_exception).with(instance_of(Errno::ENAMETOOLONG), project_id: project.id)
manager.save
expect(shared.errors).to be_empty
expect(File).not_to exist(exported_file_path)
end
end
end end
describe '#restore' do describe '#restore' do
......
...@@ -67,7 +67,8 @@ RSpec.describe ApplicationRecord do ...@@ -67,7 +67,8 @@ RSpec.describe ApplicationRecord do
end end
it 'raises a validation error if the record was not persisted' do it 'raises a validation error if the record was not persisted' do
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid) expect { Suggestion.safe_find_or_create_by!(note: nil) }
.to raise_error(ActiveRecord::RecordInvalid)
end end
it 'passes a block to find_or_create_by' do it 'passes a block to find_or_create_by' do
...@@ -75,6 +76,14 @@ RSpec.describe ApplicationRecord do ...@@ -75,6 +76,14 @@ RSpec.describe ApplicationRecord do
Suggestion.safe_find_or_create_by!(suggestion_attributes, &block) Suggestion.safe_find_or_create_by!(suggestion_attributes, &block)
end.to yield_with_args(an_object_having_attributes(suggestion_attributes)) end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
end end
it 'raises a record not found error in case of attributes mismatch' do
suggestion = Suggestion.safe_find_or_create_by!(suggestion_attributes)
attributes = suggestion_attributes.merge(outdated: !suggestion.outdated)
expect { Suggestion.safe_find_or_create_by!(attributes) }
.to raise_error(ActiveRecord::RecordNotFound)
end
end end
end end
......
...@@ -967,6 +967,8 @@ RSpec.describe Group do ...@@ -967,6 +967,8 @@ RSpec.describe Group do
context 'expanded group members' do context 'expanded group members' do
let(:indirect_user) { create(:user) } let(:indirect_user) { create(:user) }
context 'two_factor_requirement is enabled' do
context 'two_factor_requirement is also enabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group) subgroup = create(:group, :nested, parent: group)
subgroup.add_user(indirect_user, GroupMember::OWNER) subgroup.add_user(indirect_user, GroupMember::OWNER)
...@@ -975,16 +977,63 @@ RSpec.describe Group do ...@@ -975,16 +977,63 @@ RSpec.describe Group do
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end end
end
context 'two_factor_requirement is disabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group, require_two_factor_authentication: true)
subgroup.add_user(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
it 'does not enable two_factor_requirement for ancestor group member' do expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
it 'enable two_factor_requirement for ancestor group member' do
ancestor_group = create(:group) ancestor_group = create(:group)
ancestor_group.add_user(indirect_user, GroupMember::OWNER) ancestor_group.add_user(indirect_user, GroupMember::OWNER)
group.update!(parent: ancestor_group) group.update!(parent: ancestor_group)
group.update!(require_two_factor_authentication: true) group.update!(require_two_factor_authentication: true)
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
end
context 'two_factor_requirement is disabled' do
context 'two_factor_requirement is enabled for ancestor group' do
it 'enables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_user(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: true)
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_truthy
end
end
context 'two_factor_requirement is also disabled for ancestor group' do
it 'disables two_factor_requirement for subgroup member' do
subgroup = create(:group, :nested, parent: group)
subgroup.add_user(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end end
it 'disables two_factor_requirement for ancestor group member' do
ancestor_group = create(:group, require_two_factor_authentication: false)
indirect_user.update!(require_two_factor_authentication_from_group: true)
ancestor_group.add_user(indirect_user, GroupMember::OWNER)
group.update!(require_two_factor_authentication: false)
expect(indirect_user.reload.require_two_factor_authentication_from_group).to be_falsey
end
end
end
end end
context 'project members' do context 'project members' do
......
...@@ -36,13 +36,10 @@ RSpec.describe NamespaceSetting, type: :model do ...@@ -36,13 +36,10 @@ RSpec.describe NamespaceSetting, type: :model do
context "when an empty string" do context "when an empty string" do
before do before do
namespace_settings.default_branch_name = '' namespace_settings.default_branch_name = ""
end end
it "returns an error" do it_behaves_like "doesn't return an error"
expect(namespace_settings.valid?).to be_falsey
expect(namespace_settings.errors.full_messages).not_to be_empty
end
end end
end end
......
...@@ -866,10 +866,10 @@ ...@@ -866,10 +866,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.175.0.tgz#734f341784af1cd1d62d160a17bcdfb61ff7b04d" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.175.0.tgz#734f341784af1cd1d62d160a17bcdfb61ff7b04d"
integrity sha512-gXpc87TGSXIzfAr4QER1Qw1v3P47pBO6BXkma52blgwXVmcFNe3nhQzqsqt66wKNzrIrk3lAcB4GUyPHbPVXpg== integrity sha512-gXpc87TGSXIzfAr4QER1Qw1v3P47pBO6BXkma52blgwXVmcFNe3nhQzqsqt66wKNzrIrk3lAcB4GUyPHbPVXpg==
"@gitlab/ui@21.44.0": "@gitlab/ui@22.0.3":
version "21.44.0" version "22.0.3"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-21.44.0.tgz#a8bf21434aa269eb198bcf88091eedb9d021e58f" resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-22.0.3.tgz#5c2cf8fa6cb95dea63b7ad390a310eb4b1dfc793"
integrity sha512-WvHodwtJX2Xxj0D4QBG8KwQ21zpvDjL6rA1lRGbh5TO90p2NMYL+COi2WRqX+5Hj0bQhKcMEmpCQLqdO7Y2u4g== integrity sha512-mTVNQTZwWtHJW03EpJEhdP+ZHYHFuIzamrskH7Sa1VdLce86zIeagi79tu2xZvf70CaQ7QhVZfVZIl5kJYMtfg==
dependencies: dependencies:
"@babel/standalone" "^7.0.0" "@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0" "@gitlab/vue-toasted" "^1.3.0"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment