Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-11-16 15:09:23 +00:00
parent 127e6d9610
commit 01a8b31afe
66 changed files with 1698 additions and 68 deletions

View file

@ -3,6 +3,7 @@ extends:
- plugin:@gitlab/i18n
- plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4
- ./tooling/eslint-config/conditionally_ignore_ee.js
globals:
__webpack_public_path__: true
gl: false

View file

@ -8,6 +8,7 @@ scss_files:
exclude:
- 'app/assets/stylesheets/pages/emojis.scss'
- 'app/assets/stylesheets/startup/startup-*.scss'
- 'app/assets/stylesheets/lazy_bundles/select2.scss'
linters:
# Reports when you use improper spacing around ! (the "bang") in !default,

View file

@ -1 +1 @@
506c44cc07dcb804ce970ec1c02bb6e0d52320d8
40d58655a42f71b6180a3cbaf369cc20b60e695a

View file

@ -467,6 +467,17 @@ export default {
notebooks to a class of students, a corporate data science group,
or a scientific research group.`)
}}
<gl-sprintf
:message="
s__(
'ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed.',
)
"
>
<template #bold="{ content }">
<b>{{ content }}</b>
</template>
</gl-sprintf>
</p>
<template v-if="ingressExternalEndpoint">

View file

@ -20,17 +20,7 @@ const handleStartupEvents = () => {
}
};
/* Wait for.... The methods can be used:
- with a callback (preferred),
waitFor(action)
- with then (discouraged),
await waitFor().then(action);
- with await,
await waitFor;
action();
-*/
/* For `waitForCSSLoaded` methods, see docs.gitlab.com/ee/development/fe_guide/performance.html#important-considerations */
export const waitForCSSLoaded = (action = () => {}) => {
if (!gon?.features?.startupCss || allLinksLoaded()) {
return new Promise(resolve => {

View file

@ -8,14 +8,14 @@ export default {
GlModal,
},
computed: {
...mapGetters(['isSavingOrTesting']),
...mapGetters(['isDisabled']),
primaryProps() {
return {
text: __('Save'),
attributes: [
{ variant: 'success' },
{ category: 'primary' },
{ disabled: this.isSavingOrTesting },
{ disabled: this.isDisabled },
],
};
},

View file

@ -12,6 +12,7 @@ import JiraIssuesFields from './jira_issues_fields.vue';
import TriggerFields from './trigger_fields.vue';
import DynamicField from './dynamic_field.vue';
import ConfirmationModal from './confirmation_modal.vue';
import ResetConfirmationModal from './reset_confirmation_modal.vue';
export default {
name: 'IntegrationForm',
@ -23,6 +24,7 @@ export default {
TriggerFields,
DynamicField,
ConfirmationModal,
ResetConfirmationModal,
GlButton,
},
directives: {
@ -30,8 +32,8 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentKey', 'propsSource', 'isSavingOrTesting']),
...mapState(['defaultState', 'override', 'isSaving', 'isTesting']),
...mapGetters(['currentKey', 'propsSource', 'isDisabled']),
...mapState(['defaultState', 'override', 'isSaving', 'isTesting', 'isResetting']),
isEditable() {
return this.propsSource.editable;
},
@ -47,9 +49,12 @@ export default {
showJiraIssuesFields() {
return this.isJira && this.glFeatures.jiraIssuesIntegration;
},
showReset() {
return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
},
},
methods: {
...mapActions(['setOverride', 'setIsSaving', 'setIsTesting']),
...mapActions(['setOverride', 'setIsSaving', 'setIsTesting', 'setIsResetting']),
onSaveClick() {
this.setIsSaving(true);
eventHub.$emit('saveIntegration');
@ -58,6 +63,7 @@ export default {
this.setIsTesting(true);
eventHub.$emit('testIntegration');
},
onResetClick() {},
},
};
</script>
@ -100,7 +106,7 @@ export default {
category="primary"
variant="success"
:loading="isSaving"
:disabled="isSavingOrTesting"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
>
{{ __('Save changes') }}
@ -113,7 +119,7 @@ export default {
variant="success"
type="submit"
:loading="isSaving"
:disabled="isSavingOrTesting"
:disabled="isDisabled"
data-qa-selector="save_changes_button"
@click.prevent="onSaveClick"
>
@ -123,13 +129,27 @@ export default {
<gl-button
v-if="propsSource.canTest"
:loading="isTesting"
:disabled="isSavingOrTesting"
:disabled="isDisabled"
:href="propsSource.testPath"
@click.prevent="onTestClick"
>
{{ __('Test settings') }}
</gl-button>
<template v-if="showReset">
<gl-button
v-gl-modal.confirmResetIntegration
category="secondary"
variant="default"
:loading="isResetting"
:disabled="isDisabled"
data-testid="reset-button"
>
{{ __('Reset') }}
</gl-button>
<reset-confirmation-modal @reset="onResetClick" />
</template>
<gl-button class="btn-cancel" :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
</div>
</div>

View file

@ -0,0 +1,61 @@
<script>
import { mapGetters } from 'vuex';
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlModal,
},
computed: {
...mapGetters(['isDisabled']),
primaryProps() {
return {
text: __('Reset'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
{ disabled: this.isDisabled },
],
};
},
cancelProps() {
return {
text: __('Cancel'),
};
},
},
methods: {
onReset() {
this.$emit('reset');
},
},
};
</script>
<template>
<gl-modal
modal-id="confirmResetIntegration"
size="sm"
:title="s__('Integrations|Reset integration?')"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="onReset"
>
<p>
{{
s__(
'Integrations|Resetting this integration will clear the settings and deactivate this integration.',
)
}}
</p>
<p>
{{ s__('Integrations|All projects inheriting these settings will also be reset.') }}
</p>
<p class="gl-mb-0">
{{ s__('Integrations|Projects using custom settings will not be affected.') }}
</p>
</gl-modal>
</template>

View file

@ -26,6 +26,7 @@ function parseDatasetToProps(data) {
integrationLevel,
cancelPath,
testPath,
resetPath,
...booleanAttributes
} = data;
const {
@ -49,6 +50,7 @@ function parseDatasetToProps(data) {
editable,
canTest,
testPath,
resetPath,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,

View file

@ -3,3 +3,5 @@ import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
export const setIsSaving = ({ commit }, isSaving) => commit(types.SET_IS_SAVING, isSaving);
export const setIsTesting = ({ commit }, isTesting) => commit(types.SET_IS_TESTING, isTesting);
export const setIsResetting = ({ commit }, isResetting) =>
commit(types.SET_IS_RESETTING, isResetting);

View file

@ -1,6 +1,6 @@
export const isInheriting = state => (state.defaultState === null ? false : !state.override);
export const isSavingOrTesting = state => state.isSaving || state.isTesting;
export const isDisabled = state => state.isSaving || state.isTesting || state.isResetting;
export const propsSource = (state, getters) =>
getters.isInheriting ? state.defaultState : state.customState;

View file

@ -1,3 +1,4 @@
export const SET_OVERRIDE = 'SET_OVERRIDE';
export const SET_IS_SAVING = 'SET_IS_SAVING';
export const SET_IS_TESTING = 'SET_IS_TESTING';
export const SET_IS_RESETTING = 'SET_IS_RESETTING';

View file

@ -10,4 +10,7 @@ export default {
[types.SET_IS_TESTING](state, isTesting) {
state.isTesting = isTesting;
},
[types.SET_IS_RESETTING](state, isResetting) {
state.isResetting = isResetting;
},
};

View file

@ -7,5 +7,6 @@ export default ({ defaultState = null, customState = {} } = {}) => {
customState,
isSaving: false,
isTesting: false,
isResetting: false,
};
};

View file

@ -1,5 +1,7 @@
export function loadCSSFile(path) {
return new Promise(resolve => {
if (!path) resolve();
if (document.querySelector(`link[href="${path}"]`)) {
resolve();
} else {

View file

@ -399,3 +399,15 @@ export const truncateNamespace = (string = '') => {
* @returns {Boolean}
*/
export const hasContent = obj => isString(obj) && obj.trim() !== '';
/**
* A utility function that validates if a
* string is valid SHA1 hash format.
*
* @param {String} hash to validate
*
* @return {Boolean} true if valid
*/
export const isValidSha1Hash = str => {
return /^[0-9a-f]{5,40}$/.test(str);
};

View file

@ -0,0 +1,654 @@
/*
Version: 3.5.2 Timestamp: Sat Nov 1 14:43:36 EDT 2014
Updated 2020-10-05 by TimZ
*/
.select2-container {
margin: 0;
position: relative;
display: inline-block;
}
.select2-container,
.select2-drop,
.select2-search,
.select2-search input {
box-sizing: border-box;
}
.select2-container .select2-choice {
display: block;
height: 26px;
padding: 0 0 0 8px;
overflow: hidden;
position: relative;
border: 1px solid #aaa;
white-space: nowrap;
line-height: 26px;
color: #444;
text-decoration: none;
border-radius: 4px;
background-clip: padding-box;
user-select: none;
background-color: #fff;
background-image: linear-gradient(to top, #eee 0%, #fff 50%);
}
html[dir='rtl'] .select2-container .select2-choice {
padding: 0 8px 0 0;
}
.select2-container.select2-drop-above .select2-choice {
border-bottom-color: #aaa;
border-radius: 0 0 4px 4px;
background-image: linear-gradient(to bottom, #eee 0%, #fff 90%);
}
.select2-container.select2-allowclear .select2-choice .select2-chosen {
margin-right: 42px;
}
.select2-container .select2-choice > .select2-chosen {
margin-right: 26px;
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
float: none;
width: auto;
}
html[dir='rtl'] .select2-container .select2-choice > .select2-chosen {
margin-left: 26px;
margin-right: 0;
}
.select2-container .select2-choice abbr {
display: none;
width: 12px;
height: 12px;
position: absolute;
right: 24px;
top: 8px;
font-size: 1px;
text-decoration: none;
border: 0;
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path('select2.png')) right top no-repeat;
cursor: pointer;
outline: 0;
}
.select2-container.select2-allowclear .select2-choice abbr {
display: inline-block;
}
.select2-container .select2-choice abbr:hover {
background-position: right -11px;
cursor: pointer;
}
.select2-drop-mask {
border: 0;
margin: 0;
padding: 0;
position: fixed;
left: 0;
top: 0;
min-height: 100%;
min-width: 100%;
height: auto;
width: auto;
opacity: 0;
z-index: 9998;
/* styles required for IE to work */
background-color: #fff;
filter: alpha(opacity=0);
}
.select2-drop {
width: 100%;
margin-top: -1px;
position: absolute;
z-index: 9999;
top: 100%;
background: #fff;
color: #000;
border: 1px solid #aaa;
border-top: 0;
border-radius: 0 0 4px 4px;
box-shadow: 0 4px 5px rgba(0, 0, 0, 0.15);
}
.select2-drop.select2-drop-above {
margin-top: 1px;
border-top: 1px solid #aaa;
border-bottom: 0;
border-radius: 4px 4px 0 0;
box-shadow: 0 -4px 5px rgba(0, 0, 0, 0.15);
}
.select2-drop-active {
border: 1px solid #5897fb;
border-top: 0;
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid #5897fb;
}
.select2-drop-auto-width {
border-top: 1px solid #aaa;
width: auto;
}
.select2-drop-auto-width .select2-search {
padding-top: 4px;
}
.select2-container .select2-choice .select2-arrow {
display: inline-block;
width: 18px;
height: 100%;
position: absolute;
right: 0;
top: 0;
border-left: 1px solid #aaa;
border-radius: 0 4px 4px 0;
background-clip: padding-box;
background: #ccc;
background-image: linear-gradient(to top, #ccc 0%, #eee 60%);
}
html[dir='rtl'] .select2-container .select2-choice .select2-arrow {
left: 0;
right: auto;
border-left: 0;
border-right: 1px solid #aaa;
border-radius: 4px 0 0 4px;
}
.select2-container .select2-choice .select2-arrow b {
display: block;
width: 100%;
height: 100%;
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path("select2.png")) no-repeat 0 1px;
}
html[dir='rtl'] .select2-container .select2-choice .select2-arrow b {
background-position: 2px 1px;
}
.select2-search {
display: inline-block;
width: 100%;
min-height: 26px;
margin: 0;
padding-left: 4px;
padding-right: 4px;
position: relative;
z-index: 10000;
white-space: nowrap;
}
.select2-search input {
width: 100%;
height: auto !important;
min-height: 26px;
padding: 4px 20px 4px 5px;
margin: 0;
outline: 0;
font-family: sans-serif;
font-size: 1em;
border: 1px solid #aaa;
border-radius: 0;
box-shadow: none;
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path('select2.png')) no-repeat 100% -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
html[dir='rtl'] .select2-search input {
padding: 4px 5px 4px 20px;
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path('select2.png')) no-repeat -37px -22px, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
.select2-drop.select2-drop-above .select2-search input {
margin-top: 4px;
}
.select2-search input.select2-active {
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path('select2-spinner.gif')) no-repeat 100%, linear-gradient(to bottom, #fff 85%, #eee 99%) 0 0;
}
.select2-container-active .select2-choice,
.select2-container-active .select2-choices {
border: 1px solid #5897fb;
outline: none;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.select2-dropdown-open .select2-choice {
border-bottom-color: transparent;
box-shadow: 0 1px 0 #fff inset;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
background-color: #eee;
background-image: linear-gradient(to top, #fff 0%, #eee 50%);
}
.select2-dropdown-open.select2-drop-above .select2-choice,
.select2-dropdown-open.select2-drop-above .select2-choices {
border: 1px solid #5897fb;
border-top-color: transparent;
background-image: linear-gradient(to bottom, #fff 0%, #eee 50%);
}
.select2-dropdown-open .select2-choice .select2-arrow {
background: transparent;
border-left: 0;
filter: none;
}
html[dir='rtl'] .select2-dropdown-open .select2-choice .select2-arrow {
border-right: 0;
}
.select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -18px 1px;
}
html[dir='rtl'] .select2-dropdown-open .select2-choice .select2-arrow b {
background-position: -16px 1px;
}
.select2-hidden-accessible {
border: 0;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
/* results */
.select2-results {
max-height: 200px;
padding: 0 0 0 4px;
margin: 4px 4px 4px 0;
position: relative;
overflow-x: hidden;
overflow-y: auto;
}
html[dir='rtl'] .select2-results {
padding: 0 4px 0 0;
margin: 4px 0 4px 4px;
}
.select2-results ul.select2-result-sub {
margin: 0;
padding-left: 0;
}
.select2-results li {
list-style: none;
display: list-item;
background-image: none;
}
.select2-results li.select2-result-with-children > .select2-result-label {
font-weight: bold;
}
.select2-results .select2-result-label {
padding: 3px 7px 4px;
margin: 0;
cursor: pointer;
min-height: 1em;
user-select: none;
}
.select2-results-dept-1 .select2-result-label { padding-left: 20px; }
.select2-results-dept-2 .select2-result-label { padding-left: 40px; }
.select2-results-dept-3 .select2-result-label { padding-left: 60px; }
.select2-results-dept-4 .select2-result-label { padding-left: 80px; }
.select2-results-dept-5 .select2-result-label { padding-left: 100px; }
.select2-results-dept-6 .select2-result-label { padding-left: 110px; }
.select2-results-dept-7 .select2-result-label { padding-left: 120px; }
.select2-results .select2-highlighted {
background: #3875d7;
color: #fff;
}
.select2-results li em {
background: #feffde;
font-style: normal;
}
.select2-results .select2-highlighted em {
background: transparent;
}
.select2-results .select2-highlighted ul {
background: #fff;
color: #000;
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: #f4f4f4;
display: list-item;
padding-left: 5px;
}
/*
disabled look for disabled choices in the results dropdown
*/
.select2-results .select2-disabled.select2-highlighted {
color: #666;
background: #f4f4f4;
display: list-item;
cursor: default;
}
.select2-results .select2-disabled {
background: #f4f4f4;
display: list-item;
cursor: default;
}
.select2-results .select2-selected {
display: none;
}
.select2-more-results.select2-active {
/* stylelint-disable-next-line function-url-quotes */
background: #f4f4f4 url(image-path('select2-spinner.gif')) no-repeat 100%;
}
.select2-results .select2-ajax-error {
background: rgba(255, 50, 50, 0.2);
}
.select2-more-results {
background: #f4f4f4;
display: list-item;
}
/* disabled styles */
.select2-container.select2-container-disabled .select2-choice {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
cursor: default;
}
.select2-container.select2-container-disabled .select2-choice .select2-arrow {
background-color: #f4f4f4;
background-image: none;
border-left: 0;
}
.select2-container.select2-container-disabled .select2-choice abbr {
display: none;
}
/* multiselect */
.select2-container-multi .select2-choices {
height: auto !important;
height: 1%;
margin: 0;
padding: 0 5px 0 0;
position: relative;
border: 1px solid #aaa;
cursor: text;
overflow: hidden;
background-color: #fff;
background-image: linear-gradient(to bottom, #eee 1%, #fff 15%);
}
html[dir='rtl'] .select2-container-multi .select2-choices {
padding: 0 0 0 5px;
}
.select2-locked {
padding: 3px 5px !important;
}
.select2-container-multi .select2-choices {
min-height: 26px;
}
.select2-container-multi.select2-container-active .select2-choices {
border: 1px solid #5897fb;
outline: none;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.3);
}
.select2-container-multi .select2-choices li {
float: left;
list-style: none;
}
html[dir='rtl'] .select2-container-multi .select2-choices li {
float: right;
}
.select2-container-multi .select2-choices .select2-search-field {
margin: 0;
padding: 0;
white-space: nowrap;
}
.select2-container-multi .select2-choices .select2-search-field input {
padding: 5px;
margin: 1px 0;
font-family: sans-serif;
font-size: 100%;
color: #666;
outline: 0;
border: 0;
box-shadow: none;
background: transparent !important;
}
.select2-container-multi .select2-choices .select2-search-field input.select2-active {
/* stylelint-disable-next-line function-url-quotes */
background: #fff url(image-path('select2-spinner.gif')) no-repeat 100% !important;
}
.select2-default {
color: #999 !important;
}
.select2-container-multi .select2-choices .select2-search-choice {
padding: 3px 5px 3px 18px;
margin: 3px 0 3px 5px;
position: relative;
line-height: 13px;
color: #333;
cursor: default;
border: 1px solid #aaa;
border-radius: 3px;
box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05);
background-clip: padding-box;
user-select: none;
background-color: #e4e4e4;
background-image: linear-gradient(to bottom, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%);
}
html[dir='rtl'] .select2-container-multi .select2-choices .select2-search-choice {
margin: 3px 5px 3px 0;
padding: 3px 18px 3px 5px;
}
.select2-container-multi .select2-choices .select2-search-choice .select2-chosen {
cursor: default;
}
.select2-container-multi .select2-choices .select2-search-choice-focus {
background: #d4d4d4;
}
.select2-search-choice-close {
display: block;
width: 12px;
height: 13px;
position: absolute;
right: 3px;
top: 4px;
font-size: 1px;
outline: none;
/* stylelint-disable-next-line function-url-quotes */
background: url(image-path('select2.png')) right top no-repeat;
}
html[dir='rtl'] .select2-search-choice-close {
right: auto;
left: 3px;
}
.select2-container-multi .select2-search-choice-close {
left: 3px;
}
html[dir='rtl'] .select2-container-multi .select2-search-choice-close {
left: auto;
right: 2px;
}
.select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover {
background-position: right -11px;
}
.select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close {
background-position: right -11px;
}
/* disabled styles */
.select2-container-multi.select2-container-disabled .select2-choices {
background-color: #f4f4f4;
background-image: none;
border: 1px solid #ddd;
cursor: default;
}
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice {
padding: 3px 5px;
border: 1px solid #ddd;
background-image: none;
background-color: #f4f4f4;
}
.select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close {
display: none;
background: none;
}
/* end multiselect */
.select2-result-selectable .select2-match,
.select2-result-unselectable .select2-match {
text-decoration: underline;
}
.select2-offscreen,
.select2-offscreen:focus {
clip: rect(0 0 0 0) !important;
width: 1px !important;
height: 1px !important;
border: 0 !important;
margin: 0 !important;
padding: 0 !important;
overflow: hidden !important;
position: absolute !important;
outline: 0 !important;
left: 0 !important;
top: 0 !important;
}
.select2-display-none {
display: none;
}
.select2-measure-scrollbar {
position: absolute;
top: -10000px;
left: -10000px;
width: 100px;
height: 100px;
overflow: scroll;
}
@media only screen and (min-resolution: 120dpi) {
.select2-search input,
.select2-search-choice-close,
.select2-container .select2-choice abbr,
.select2-container .select2-choice .select2-arrow b {
/* stylelint-disable-next-line function-url-quotes */
background-image: url(image-path("select2x2.png")) !important;
background-repeat: no-repeat !important;
background-size: 60px 40px !important;
}
.select2-search input {
background-position: 100% -21px !important;
}
}
/* End of select2.css */
@import './select2_overrides';

View file

@ -0,0 +1,359 @@
@import 'page_bundles/mixins_and_variables_and_functions';
/** Select2 selectbox style override **/
.select2-container {
width: 100% !important;
&.input-md,
&.input-lg {
display: block;
}
}
.select2-container,
.select2-container.select2-drop-above {
.select2-choice {
background: $white;
color: $gl-text-color;
border-color: $border-color;
height: 34px;
padding: $gl-vert-padding $gl-input-padding;
font-size: $gl-font-size;
line-height: 1.42857143;
border-radius: $gl-border-radius-base;
.select2-arrow {
background-image: none;
background-color: transparent;
border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
b {
display: none;
}
&::after {
content: '\f078';
position: absolute;
z-index: 1;
text-align: center;
pointer-events: none;
box-sizing: border-box;
color: $gray-darkest;
display: inline-block;
font: normal normal normal 14px/1 FontAwesome;
font-size: inherit;
text-rendering: auto;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
.select2-chosen {
margin-right: 15px;
}
&:hover {
border-color: $gray-darkest;
color: $gl-text-color;
}
}
// Essentially were doing @include form-control-focus here (from
// bootstrap/scss/mixins/_forms.scss), except that the bootstrap mixin adds a
// `&:focus` selector and were never actually focusing the .select2-choice
// link nor the .select2-container, the Select2 library focuses an off-screen
// .select2-focusser element instead.
&.select2-container-active:not(.select2-dropdown-open) {
.select2-choice {
color: $input-focus-color;
background-color: $input-focus-bg;
border-color: $input-focus-border-color;
outline: 0;
}
// Reusable focus glow box-shadow
@mixin form-control-focus-glow {
@if $enable-shadows {
box-shadow: $input-box-shadow, $input-focus-box-shadow;
} @else {
box-shadow: $input-focus-box-shadow;
}
}
// Apply the focus glow shadow to the .select2-container if it also has
// the .block-truncated class as that applies an overflow: hidden, thereby
// hiding the glow of the nested .select2-choice element.
&.block-truncated {
@include form-control-focus-glow;
}
// Apply the glow directly to the .select2-choice link if were not
// block-truncating the container.
&:not(.block-truncated) .select2-choice {
@include form-control-focus-glow;
}
}
&.is-invalid {
~ .invalid-feedback {
display: block;
}
.select2-choices,
.select2-choice {
border-color: $red-500;
}
}
}
.select2-drop,
.select2-drop.select2-drop-above {
background: $white;
box-shadow: 0 2px 4px $dropdown-shadow-color;
border-radius: $gl-border-radius-base;
border: 1px solid $border-color;
min-width: 175px;
color: $gl-text-color;
z-index: 999;
.modal-open & {
z-index: $zindex-modal + 200;
}
}
.select2-drop-mask {
z-index: 998;
.modal-open & {
z-index: $zindex-modal + 100;
}
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $border-color;
margin-top: -6px;
}
.select2-container-active {
.select2-choice,
.select2-choices {
box-shadow: none;
}
}
.select2-dropdown-open,
.select2-dropdown-open.select2-drop-above {
.select2-choice {
border-color: $gray-darkest;
outline: 0;
}
}
.select2-container-multi {
.select2-choices {
border-radius: $border-radius-default;
border-color: $border-color;
background: none;
.select2-search-field input {
padding: 5px $gl-input-padding;
height: auto;
font-family: inherit;
font-size: inherit;
}
.select2-search-choice {
margin: 5px 0 0 8px;
box-shadow: none;
border-color: $border-color;
color: $gl-text-color;
line-height: 15px;
background-color: $gray-light;
background-image: none;
padding: 3px 18px 3px 5px;
.select2-search-choice-close {
top: 5px;
left: initial;
right: 3px;
}
&.select2-search-choice-focus {
border-color: $gl-text-color;
}
}
}
}
.select2-drop-active {
margin-top: $dropdown-vertical-offset;
font-size: 14px;
.select2-results {
max-height: 350px;
}
}
.select2-search {
padding: $grid-size;
.select2-drop-auto-width & {
padding: $grid-size;
}
input {
padding: $grid-size;
background: transparent image-url('select2.png');
color: $gl-text-color;
background-clip: content-box;
background-origin: content-box;
background-repeat: no-repeat;
background-position: right 0 bottom 0 !important;
border: 1px solid $border-color;
border-radius: $border-radius-default;
line-height: 16px;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
border-color: $blue-300;
}
&.select2-active {
background-color: $white;
background-image: image-url('select2-spinner.gif') !important;
background-origin: content-box;
background-repeat: no-repeat;
background-position: right 6px center !important;
background-size: 16px 16px !important;
}
}
+ .select2-results {
padding-top: 0;
}
}
.select2-results {
margin: 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $gray-darker;
}
}
.select2-result {
padding: 0 1px;
}
li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold;
color: $gl-text-color;
}
}
.select2-highlighted {
.group-result {
.group-path {
color: $gray-700;
}
}
}
.select2-result-selectable,
.select2-result-unselectable {
.select2-match {
font-weight: $gl-font-weight-bold;
text-decoration: none;
}
}
.input-group {
.select2-container {
display: table-cell;
max-width: 180px;
}
}
.file-editor {
.select2 {
float: right;
}
}
.import-namespace-select {
> .select2-choice {
border-radius: $border-radius-default 0 0 $border-radius-default;
position: relative;
left: 1px;
}
}
.issue-form {
.select2-container {
width: 250px !important;
}
}
.new_project,
.edit-project,
.import-project {
.input-group {
.select2-container {
display: unset;
max-width: unset;
flex-grow: 1;
}
}
.input-group-prepend,
.input-group-append {
+ .select2 a {
border-radius: 0 $gl-border-radius-base $gl-border-radius-base 0;
}
}
}
.project-path {
.select2-choice {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
.transfer-project .select2-container {
min-width: 200px;
}
.right-sidebar {
.block {
.select2-container span {
margin-top: 0;
}
}
}
.block-truncated {
> div:not(.block):not(.select2-display-none) {
display: inline;
}
}

View file

@ -208,9 +208,9 @@
display: inline-flex;
.label,
.btn {
.btn:not(.gl-button) {
padding: $gl-vert-padding $gl-btn-padding;
border: 1px $border-color solid;
border: 1px $gray-200 solid;
font-size: $gl-font-size;
line-height: $line-height-base;
border-radius: 0;

View file

@ -93,7 +93,8 @@ module ServicesHelper
editable: integration.editable?.to_s,
cancel_path: scoped_integrations_path,
can_test: integration.can_test?.to_s,
test_path: scoped_test_integration_path(integration)
test_path: scoped_test_integration_path(integration),
reset_path: ''
}
end

View file

@ -120,7 +120,7 @@ module ApplicationSettingImplementation
repository_checks_enabled: true,
repository_storages_weighted: { default: 100 },
repository_storages: ['default'],
require_admin_approval_after_user_signup: false,
require_admin_approval_after_user_signup: true,
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: 0,

View file

@ -419,6 +419,10 @@ class Member < ApplicationRecord
invite? && user_id.nil?
end
def created_by_name
created_by&.name
end
private
def send_invite

View file

@ -21,7 +21,7 @@
.form-actions
= button_tag class: 'btn gl-button btn-success btn-upload-file', id: 'submit-all', type: 'button' do
= icon('spin spinner', class: 'js-loading-icon hidden' )
.spinner.spinner-sm.gl-mr-2.js-loading-icon.hidden
= button_title
= link_to _("Cancel"), '#', class: "btn gl-button btn-cancel", "data-dismiss" => "modal"

View file

@ -57,7 +57,7 @@
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
.commit-sha-group.d-none.d-sm-flex
.commit-sha-group.btn-group.d-none.d-sm-flex
.label.label-monospace.monospace
= commit.short_id
= clipboard_button(text: commit.id, title: _("Copy commit SHA"), class: "gl-button btn btn-default", container: "body")

View file

@ -26,7 +26,7 @@
- apply_is_default_styles = (selected.nil? || selected.empty?) && !no_default_styles
%span.dropdown-toggle-text{ class: ("is-default" if apply_is_default_styles) }
= multi_label_name(selected, label_name)
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable.dropdown-extended-height
= render partial: "shared/issuable/label_page_default", locals: { title: dropdown_title, show_footer: show_footer, show_create: show_create }
- if show_create && project && can?(current_user, :admin_label, project)

View file

@ -96,7 +96,7 @@
%button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable_type}[due_date]", ability_name: issuable_type, issue_update: issuable_sidebar[:issuable_json_path], display: 'static' } }
%span.dropdown-toggle-text
= _('Due date')
= icon('chevron-down', 'aria-hidden': 'true')
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-menu-due-date
= dropdown_title(_('Due date'))
= dropdown_content do

View file

@ -7,7 +7,7 @@
.btn-group{ role: 'group' }
%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' }, class: 'btn btn-default' }
= sort_title
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable.dropdown-menu-sort
%li
= sortable_item(sort_title_priority, page_filter_path(sort: sort_value_priority), sort_title)

View file

@ -8,7 +8,7 @@
%button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-toggle-text.is-default
= issuable.issue_type.capitalize || _("Select type")
= icon('chevron-down')
= sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
.dropdown-menu.dropdown-menu-selectable.dropdown-select
.dropdown-title.gl-display-flex
%span.gl-ml-auto

View file

@ -0,0 +1,5 @@
---
title: Update icons to svg for issuable pages
merge_request: 47596
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Admin approval required on user registration by default
merge_request: 46937
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add API get /invitations for project and group
merge_request: 46046
author:
type: added

View file

@ -202,6 +202,7 @@ module Gitlab
config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/alert_management_settings.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "lazy_bundles/select2.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "disable_animations.css"
config.assets.precompile << "snippets.css"

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddDefaultTrueRequireAdminApprovalAfterUserSignupToApplicationSettings < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
change_column_default :application_settings, :require_admin_approval_after_user_signup, from: false, to: true
end
end

View file

@ -0,0 +1 @@
07160ee3c92e68273042df979640c3927abbb187f79e1a4645471e28061e1c2c

View file

@ -9324,7 +9324,7 @@ CREATE TABLE application_settings (
gitpod_enabled boolean DEFAULT false NOT NULL,
gitpod_url text DEFAULT 'https://gitpod.io/'::text,
abuse_notification_email character varying,
require_admin_approval_after_user_signup boolean DEFAULT false NOT NULL,
require_admin_approval_after_user_signup boolean DEFAULT true NOT NULL,
help_page_documentation_base_url text,
automatic_purchased_storage_allocation boolean DEFAULT false NOT NULL,
encrypted_ci_jwt_signing_key text,

View file

@ -113,7 +113,6 @@ prometheus['enable'] = false
gitlab_rails['auto_migrate'] = false
alertmanager['enable'] = false
gitaly['enable'] = false
gitlab_monitor['enable'] = false
gitlab_workhorse['enable'] = false
nginx['enable'] = false
postgres_exporter['enable'] = false

View file

@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Invitations API
Use the Invitations API to send email to users you want to join a group or project.
Use the Invitations API to send email to users you want to join a group or project, and to list pending
invitations.
## Valid access levels
@ -64,3 +65,43 @@ When there was any error sending the email:
}
}
```
## List all invitations pending for a group or project
Gets a list of invited group or project members viewable by the authenticated user.
Returns invitations to direct members only, and not through inherited ancestors' groups.
This function takes pagination parameters `page` and `per_page` to restrict the list of users.
```plaintext
GET /groups/:id/invitations
GET /projects/:id/invitations
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project or group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `page` | integer | no | Page to retrieve |
| `per_page`| integer | no | Number of member invitations to return per page |
| `query` | string | no | A query string to search for invited members by invite email. Query text must match email address exactly. When empty, returns all invitations. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/:id/invitations?query=member@example.org"
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/:id/invitations?query=member@example.org"
```
Example response:
```json
[
{
"id": 1,
"invite_email": "member@example.org",
"invited_at": "2020-10-22T14:13:35Z",
"access_level": 30,
"expires_at": "2020-11-22T14:13:35Z",
"user_name": "Raymond Smith",
"created_by_name": "Administrator"
},
]
```

View file

@ -38,12 +38,16 @@ The following can be used as a template to get started:
````markdown
## Descriptive title
> Version history note.
One or two sentence description of what endpoint does.
```plaintext
METHOD /endpoint
```
Supported attributes:
| Attribute | Type | Required | Description |
|:------------|:---------|:---------|:----------------------|
| `attribute` | datatype | yes/no | Detailed description. |
@ -65,6 +69,9 @@ Example response:
```
````
Adjust the [version history note accordingly](styleguide/index.md#version-text-in-the-version-history)
to describe the GitLab release that introduced the API call.
## Method description
Use the following table headers to describe the methods. Attributes should

View file

@ -115,8 +115,36 @@ browser's developer console while on any page within GitLab.
import initMyWidget from './my_widget';
import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
waitForCSSLoaded(initMyWidget);
```
Note that `waitForCSSLoaded()` methods supports receiving the action in different ways:
- With a callback:
```javascript
waitForCSSLoaded(action)
```
- With `then()`:
```javascript
waitForCSSLoaded().then(action);
```
- With `await` followed by `action`:
```javascript
await waitForCSSLoaded;
action();
```
For example, see how we use this in [app/assets/javascripts/pages/projects/graphs/charts/index.js](https://gitlab.com/gitlab-org/gitlab/-/commit/5e90885d6afd4497002df55bf015b338efcfc3c5#02e81de37f5b1716a3ef3222fa7f7edf22c40969_9_8):
```javascript
waitForCSSLoaded(() => {
initMyWidget();
const languagesContainer = document.getElementById('js-languages-chart');
//...
});
```

View file

@ -188,6 +188,30 @@ Check this [page](vuex.md) for more details.
- It is acceptable for Vue to listen to existing jQuery events using jQuery event listeners.
- It is not recommended to add new jQuery events for Vue to interact with jQuery.
### Mixing Vue and JavaScript classes (in the data function)
In the [Vue documentation](https://vuejs.org/v2/api/#Options-Data) the Data function/object is defined as follows:
> The data object for the Vue instance. Vue will recursively convert its properties into getter/setters to make it “reactive”. The object must be plain: native objects such as browser API objects and prototype properties are ignored. A rule of thumb is that data should just be data - it is not recommended to observe objects with their own stateful behavior.
Based on the Vue guidance:
- **Do not** use or create a JavaScript class in your [data function](https://vuejs.org/v2/api/#data), such as `user: new User()`.
- **Do not** add new JavaScript class implementations.
- **Do** use [GraphQL](../api_graphql_styleguide.md), [Vuex](vuex.md) or a set of components if cannot use simple primitives or objects.
- **Do** maintain existing implementations using such approaches.
- **Do** Migrate components to a pure object model when there are substantial changes to it.
- **Do** add business logic to helpers or utils, so you can test them separately from your component.
#### Why
There are additional reasons why having a JavaScript class presents maintainability issues on a huge codebase:
- Once a class is created, it is easy to extend it in a way that can infringe Vue reactivity and best practices.
- A class adds a layer of abstraction, which makes the component API and its inner workings less clear.
- It makes it harder to test. Since the class is instantiated by the component data function, it is harder to 'manage' component and class separately.
- Adding OOP to a functional codebase adds yet another way of writing code, reducing consistency and clarity.
## Style guide
Please refer to the Vue section of our [style guide](style/vue.md)

View file

@ -27,9 +27,10 @@ To disable sign ups:
## Require administrator approval for new sign ups
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4491) in GitLab 13.5.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4491) in GitLab 13.5.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/267568) in GitLab 13.6.
When this setting is enabled, any user visiting your GitLab domain and signing up for a new account must be explicitly [approved](../approving_users.md#approving-a-user) by an administrator before they can start using their account. This setting is only applicable if sign ups are enabled.
When this setting is enabled, any user visiting your GitLab domain and signing up for a new account must be explicitly [approved](../approving_users.md#approving-a-user) by an administrator before they can start using their account. This setting is enabled by default for newly created instances. This setting is only applicable if sign ups are enabled.
To require administrator approval for new sign ups:

View file

@ -8,7 +8,8 @@ module API
expose :expires_at
expose :invite_email
expose :invite_token
expose :user_id
expose :user_name, if: -> (member, _) { member.user.present? }
expose :created_by_name
end
end
end

View file

@ -27,6 +27,13 @@ module API
members
end
def retrieve_member_invitations(source, query = nil)
members = source_members(source).where.not(invite_token: nil)
members = members.includes(:user)
members = members.where(invite_email: query) if query.present?
members
end
def source_members(source)
source.members
end
@ -52,6 +59,10 @@ module API
def present_members(members)
present members, with: Entities::Member, current_user: current_user, show_seat_info: params[:show_seat_info]
end
def present_member_invitations(invitations)
present invitations, with: Entities::Invitation, current_user: current_user
end
end
end
end

View file

@ -2,6 +2,8 @@
module API
class Invitations < ::API::Base
include PaginationParams
feature_category :users
before { authenticate! }
@ -29,6 +31,23 @@ module API
::Members::InviteService.new(current_user, params).execute(source)
end
desc 'Get a list of group or project invitations viewable by the authenticated user' do
detail 'This feature was introduced in GitLab 13.6'
success Entities::Invitation
end
params do
optional :query, type: String, desc: 'A query string to search for members'
use :pagination
end
get ":id/invitations" do
source = find_source(source_type, params[:id])
query = params[:query]
invitations = paginate(retrieve_member_invitations(source, query))
present_member_invitations invitations
end
end
end
end

View file

@ -16,8 +16,8 @@ module Gitlab
@in_memory_application_settings = nil
end
def method_missing(name, *args, &block)
current_application_settings.send(name, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
def method_missing(name, *args, **kwargs, &block)
current_application_settings.send(name, *args, **kwargs, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(name, include_private = false)

View file

@ -28,6 +28,7 @@ module Gitlab
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
gon.emoji_sprites_css_path = ActionController::Base.helpers.stylesheet_path('emoji_sprites')
gon.select2_css_path = ActionController::Base.helpers.stylesheet_path('lazy_bundles/select2.css')
gon.test_env = Rails.env.test?
gon.disable_animations = Gitlab.config.gitlab['disable_animations']
gon.suggested_label_colors = LabelsHelper.suggested_colors

View file

@ -5725,6 +5725,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed."
msgstr ""
msgid "ClusterIntegration|%{externalIp}.nip.io"
msgstr ""
@ -11172,6 +11175,9 @@ msgstr ""
msgid "Export as CSV"
msgstr ""
msgid "Export commit custody report"
msgstr ""
msgid "Export group"
msgstr ""
@ -14621,6 +14627,9 @@ msgstr ""
msgid "Integrations|All details"
msgstr ""
msgid "Integrations|All projects inheriting these settings will also be reset."
msgstr ""
msgid "Integrations|Comment detail:"
msgstr ""
@ -14654,9 +14663,18 @@ msgstr ""
msgid "Integrations|Issues created in Jira are shown here once you have created the issues in project setup in Jira."
msgstr ""
msgid "Integrations|Projects using custom settings will not be affected."
msgstr ""
msgid "Integrations|Projects using custom settings will not be impacted unless the project owner chooses to use parent level defaults."
msgstr ""
msgid "Integrations|Reset integration?"
msgstr ""
msgid "Integrations|Resetting this integration will clear the settings and deactivate this integration."
msgstr ""
msgid "Integrations|Return to GitLab for Jira"
msgstr ""
@ -14768,6 +14786,9 @@ msgstr ""
msgid "Invalid file."
msgstr ""
msgid "Invalid hash"
msgstr ""
msgid "Invalid import params"
msgstr ""
@ -16842,6 +16863,9 @@ msgstr ""
msgid "Merge automatically (%{strategy})"
msgstr ""
msgid "Merge commit SHA"
msgstr ""
msgid "Merge commit message"
msgstr ""
@ -23062,6 +23086,9 @@ msgstr ""
msgid "Resend it"
msgstr ""
msgid "Reset"
msgstr ""
msgid "Reset authorization key"
msgstr ""

View file

@ -18,15 +18,13 @@ module QA
end
def sign_up!(user)
fill_element :new_user_first_name_field, user.first_name
fill_element :new_user_last_name_field, user.last_name
fill_element :new_user_username_field, user.username
fill_element :new_user_email_field, user.email
fill_element :new_user_password_field, user.password
signed_in = retry_until do
signed_in = retry_until(raise_on_failure: false) do
fill_element :new_user_first_name_field, user.first_name
fill_element :new_user_last_name_field, user.last_name
fill_element :new_user_username_field, user.username
fill_element :new_user_email_field, user.email
fill_element :new_user_password_field, user.password
click_element :new_user_register_button if has_element?(:new_user_register_button)
click_element :get_started_button if has_element?(:get_started_button)
Page::Main::Menu.perform(&:has_personal_area?)

View file

@ -33,7 +33,7 @@ module QA
def api_client
@api_client ||= Runtime::API::Client.as_admin
rescue AuthorizationError => e
rescue API::Client::AuthorizationError => e
raise "Administrator access is required to set application settings. #{e.message}"
end
end

View file

@ -8,7 +8,7 @@ module QA
module Env
extend self
attr_writer :personal_access_token, :ldap_username, :ldap_password
attr_writer :personal_access_token
ENV_VARIABLES = Gitlab::QA::Runtime::Env::ENV_VARIABLES
@ -293,6 +293,11 @@ module QA
@ldap_username ||= ENV['GITLAB_LDAP_USERNAME']
end
def ldap_username=(ldap_username)
@ldap_username = ldap_username # rubocop:disable Gitlab/ModuleWithInstanceVariables
ENV['GITLAB_LDAP_USERNAME'] = ldap_username
end
def ldap_password
@ldap_password ||= ENV['GITLAB_LDAP_PASSWORD']
end

View file

@ -13,11 +13,39 @@ module QA
end
end
RSpec.describe 'Manage', :skip_signup_disabled do
describe 'standard', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/936' do
RSpec.describe 'Manage', :skip_signup_disabled, :requires_admin do
describe 'while LDAP is enabled', :orchestrated, :ldap_no_tls, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/935' do
before do
# When LDAP is enabled, a previous test might have created a token for the LDAP 'tanuki' user who is not an admin
# So we need to set it to nil in order to create a new token for admin user so that we are able to set_application_settings
# Also, when GITLAB_LDAP_USERNAME is provided, it is used to create a token. This also needs to be set to nil temporarily
# for the same reason as above.
@personal_access_token = Runtime::Env.personal_access_token
Runtime::Env.personal_access_token = nil
ldap_username = Runtime::Env.ldap_username
Runtime::Env.ldap_username = nil
disable_require_admin_approval_after_user_signup
Runtime::Env.ldap_username = ldap_username
end
it_behaves_like 'registration and login'
context 'when user account is deleted', :requires_admin do
after do
Runtime::Env.personal_access_token = @personal_access_token
end
end
describe 'standard', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/936' do
before(:all) do
disable_require_admin_approval_after_user_signup
end
it_behaves_like 'registration and login'
context 'when user account is deleted' do
let(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = admin_api_client
@ -61,11 +89,10 @@ module QA
end
end
end
end
RSpec.describe 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do
describe 'while LDAP is enabled', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/935' do
it_behaves_like 'registration and login'
def disable_require_admin_approval_after_user_signup
Runtime::ApplicationSettings.set_application_settings(require_admin_approval_after_user_signup: false)
sleep 10 # It takes a moment for the setting to come into effect
end
end
end

View file

@ -7,6 +7,7 @@ RSpec.describe RegistrationsController do
before do
stub_feature_flags(invisible_captcha: false)
stub_application_setting(require_admin_approval_after_user_signup: false)
end
describe '#new' do
@ -76,10 +77,6 @@ RSpec.describe RegistrationsController do
end
context 'when the `require_admin_approval_after_user_signup` setting is turned off' do
before do
stub_application_setting(require_admin_approval_after_user_signup: false)
end
it 'signs up the user in `active` state' do
subject
created_user = User.find_by(email: 'new@user.com')

View file

@ -10,6 +10,7 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
let(:group_invite) { group.group_members.invite.last }
before do
stub_application_setting(require_admin_approval_after_user_signup: false)
project.add_maintainer(owner)
group.add_owner(owner)
group.add_developer('user@example.com', owner)
@ -97,6 +98,21 @@ RSpec.describe 'Group or Project invitations', :aggregate_failures do
click_link 'Register now'
end
context 'with admin appoval required enabled' do
before do
stub_application_setting(require_admin_approval_after_user_signup: true)
end
let(:send_email_confirmation) { true }
it 'does not sign the user in' do
fill_in_sign_up_form(new_user)
expect(current_path).to eq(new_user_session_path)
expect(page).to have_content('You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator')
end
end
context 'email confirmation disabled' do
let(:send_email_confirmation) { false }

View file

@ -62,7 +62,7 @@ RSpec.describe 'Projects > Snippets > Create Snippet', :js do
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
link = find('a.no-attachment-icon img.js-lazy-loaded[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end

View file

@ -78,7 +78,7 @@ RSpec.describe 'User uploads file to note' do
click_button 'Comment'
wait_for_requests
expect(find('a.no-attachment-icon img[alt="dk"]')['src'])
expect(find('a.no-attachment-icon img.js-lazy-loaded[alt="dk"]')['src'])
.to match(%r{/#{project.full_path}/uploads/\h{32}/dk\.png$})
end
end

View file

@ -43,6 +43,10 @@ end
RSpec.describe 'Signup' do
include TermsHelper
before do
stub_application_setting(require_admin_approval_after_user_signup: false)
end
let(:new_user) { build_stubbed(:user) }
def fill_in_signup_form
@ -228,6 +232,22 @@ RSpec.describe 'Signup' do
expect(current_path).to eq users_sign_up_welcome_path
end
end
context 'with required admin approval enabled' do
before do
stub_application_setting(require_admin_approval_after_user_signup: true)
end
it 'creates the user but does not sign them in' do
visit new_user_registration_path
fill_in_signup_form
expect { click_button 'Register' }.to change { User.count }.by(1)
expect(current_path).to eq new_user_session_path
expect(page).to have_content("You have signed up successfully. However, we could not sign you in because your account is awaiting approval from your GitLab administrator")
end
end
end
context 'with errors' do

View file

@ -5,6 +5,7 @@ import IntegrationForm from '~/integrations/edit/components/integration_form.vue
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ActiveCheckbox from '~/integrations/edit/components/active_checkbox.vue';
import ConfirmationModal from '~/integrations/edit/components/confirmation_modal.vue';
import ResetConfirmationModal from '~/integrations/edit/components/reset_confirmation_modal.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
@ -44,6 +45,8 @@ describe('IntegrationForm', () => {
const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
const findActiveCheckbox = () => wrapper.find(ActiveCheckbox);
const findConfirmationModal = () => wrapper.find(ConfirmationModal);
const findResetConfirmationModal = () => wrapper.find(ResetConfirmationModal);
const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
const findTriggerFields = () => wrapper.find(TriggerFields);
@ -75,6 +78,29 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(true);
});
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
integrationLevel: integrationLevels.INSTANCE,
});
expect(findResetButton().exists()).toBe(false);
expect(findResetConfirmationModal().exists()).toBe(false);
});
});
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
integrationLevel: integrationLevels.INSTANCE,
resetPath: 'resetPath',
});
expect(findResetButton().exists()).toBe(true);
expect(findResetConfirmationModal().exists()).toBe(true);
});
});
});
describe('integrationLevel is group', () => {
@ -85,6 +111,29 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(true);
});
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
integrationLevel: integrationLevels.GROUP,
});
expect(findResetButton().exists()).toBe(false);
expect(findResetConfirmationModal().exists()).toBe(false);
});
});
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
integrationLevel: integrationLevels.GROUP,
resetPath: 'resetPath',
});
expect(findResetButton().exists()).toBe(true);
expect(findResetConfirmationModal().exists()).toBe(true);
});
});
});
describe('integrationLevel is project', () => {
@ -95,6 +144,16 @@ describe('IntegrationForm', () => {
expect(findConfirmationModal().exists()).toBe(false);
});
it('does not render ResetConfirmationModal and button', () => {
createComponent({
integrationLevel: 'project',
resetPath: 'resetPath',
});
expect(findResetButton().exists()).toBe(false);
expect(findResetConfirmationModal().exists()).toBe(false);
});
});
describe('type is "slack"', () => {

View file

@ -1,6 +1,11 @@
import testAction from 'helpers/vuex_action_helper';
import createState from '~/integrations/edit/store/state';
import { setOverride } from '~/integrations/edit/store/actions';
import {
setOverride,
setIsSaving,
setIsTesting,
setIsResetting,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
describe('Integration form store actions', () => {
@ -15,4 +20,24 @@ describe('Integration form store actions', () => {
return testAction(setOverride, true, state, [{ type: types.SET_OVERRIDE, payload: true }]);
});
});
describe('setIsSaving', () => {
it('should commit isSaving mutation', () => {
return testAction(setIsSaving, true, state, [{ type: types.SET_IS_SAVING, payload: true }]);
});
});
describe('setIsTesting', () => {
it('should commit isTesting mutation', () => {
return testAction(setIsTesting, true, state, [{ type: types.SET_IS_TESTING, payload: true }]);
});
});
describe('setIsResetting', () => {
it('should commit isResetting mutation', () => {
return testAction(setIsResetting, true, state, [
{ type: types.SET_IS_RESETTING, payload: true },
]);
});
});
});

View file

@ -1,5 +1,12 @@
import { currentKey, isInheriting, propsSource } from '~/integrations/edit/store/getters';
import {
currentKey,
isInheriting,
isDisabled,
propsSource,
} from '~/integrations/edit/store/getters';
import createState from '~/integrations/edit/store/state';
import mutations from '~/integrations/edit/store/mutations';
import * as types from '~/integrations/edit/store/mutation_types';
import { mockIntegrationProps } from '../mock_data';
describe('Integration form store getters', () => {
@ -45,6 +52,29 @@ describe('Integration form store getters', () => {
});
});
describe('isDisabled', () => {
it.each`
isSaving | isTesting | isResetting | expected
${false} | ${false} | ${false} | ${false}
${true} | ${false} | ${false} | ${true}
${false} | ${true} | ${false} | ${true}
${false} | ${false} | ${true} | ${true}
${false} | ${true} | ${true} | ${true}
${true} | ${false} | ${true} | ${true}
${true} | ${true} | ${false} | ${true}
${true} | ${true} | ${true} | ${true}
`(
'when isSaving = $isSaving, isTesting = $isTesting, isResetting = $isResetting then isDisabled = $expected',
({ isSaving, isTesting, isResetting, expected }) => {
mutations[types.SET_IS_SAVING](state, isSaving);
mutations[types.SET_IS_TESTING](state, isTesting);
mutations[types.SET_IS_RESETTING](state, isResetting);
expect(isDisabled(state)).toBe(expected);
},
);
});
describe('propsSource', () => {
beforeEach(() => {
state.defaultState = defaultState;

View file

@ -16,4 +16,28 @@ describe('Integration form store mutations', () => {
expect(state.override).toBe(true);
});
});
describe(`${types.SET_IS_SAVING}`, () => {
it('sets isSaving', () => {
mutations[types.SET_IS_SAVING](state, true);
expect(state.isSaving).toBe(true);
});
});
describe(`${types.SET_IS_TESTING}`, () => {
it('sets isTesting', () => {
mutations[types.SET_IS_TESTING](state, true);
expect(state.isTesting).toBe(true);
});
});
describe(`${types.SET_IS_RESETTING}`, () => {
it('sets isResetting', () => {
mutations[types.SET_IS_RESETTING](state, true);
expect(state.isResetting).toBe(true);
});
});
});

View file

@ -7,6 +7,7 @@ describe('Integration form state factory', () => {
customState: {},
isSaving: false,
isTesting: false,
isResetting: false,
override: false,
});
});

View file

@ -325,4 +325,19 @@ describe('text_utility', () => {
expect(textUtils.hasContent(txt)).toEqual(result);
});
});
describe('isValidSha1Hash', () => {
const validSha1Hash = '92d10c15';
const stringOver40 = new Array(42).join('a');
it.each`
hash | valid
${validSha1Hash} | ${true}
${'__characters'} | ${false}
${'abc'} | ${false}
${stringOver40} | ${false}
`(`returns $valid for $hash`, ({ hash, valid }) => {
expect(textUtils.isValidSha1Hash(hash)).toBe(valid);
});
});
});

View file

@ -7,7 +7,8 @@ RSpec.describe API::Invitations do
let(:developer) { create(:user) }
let(:access_requester) { create(:user) }
let(:stranger) { create(:user) }
let(:email) { 'email@example.org' }
let(:email) { 'email1@example.com' }
let(:email2) { 'email2@example.com' }
let(:project) do
create(:project, :public, creator_id: maintainer.id, namespace: maintainer.namespace) do |project|
@ -75,7 +76,7 @@ RSpec.describe API::Invitations do
it 'invites a list of new email addresses' do
expect do
email_list = 'email1@example.com,email2@example.com'
email_list = [email, email2].join(',')
post api("/#{source_type.pluralize}/#{source.id}/invitations", maintainer),
params: { email: email_list, access_level: Member::DEVELOPER }
@ -204,4 +205,97 @@ RSpec.describe API::Invitations do
let(:source) { group }
end
end
shared_examples 'GET /:source_type/:id/invitations' do |source_type|
context "with :source_type == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
let(:route) { get invitations_url(source, stranger) }
end
%i[maintainer developer access_requester stranger].each do |type|
context "when authenticated as a #{type}" do
it 'returns 200' do
user = public_send(type)
get invitations_url(source, user)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
end
end
end
it 'avoids N+1 queries' do
# Establish baseline
get invitations_url(source, maintainer)
control = ActiveRecord::QueryRecorder.new do
get invitations_url(source, maintainer)
end
invite_member_by_email(source, source_type, email, maintainer)
expect do
get invitations_url(source, maintainer)
end.not_to exceed_query_limit(control)
end
it 'does not find confirmed members' do
get invitations_url(source, developer)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.size).to eq(0)
expect(json_response.map { |u| u['id'] }).not_to match_array [maintainer.id, developer.id]
end
it 'finds all members with no query string specified' do
invite_member_by_email(source, source_type, email, developer)
invite_member_by_email(source, source_type, email2, developer)
get invitations_url(source, developer), params: { query: '' }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(2)
expect(json_response.map { |u| u['invite_email'] }).to match_array [email, email2]
end
it 'finds the invitation by invite_email with query string' do
invite_member_by_email(source, source_type, email, developer)
invite_member_by_email(source, source_type, email2, developer)
get invitations_url(source, developer), params: { query: email }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['invite_email']).to eq(email)
expect(json_response.first['created_by_name']).to eq(developer.name)
expect(json_response.first['user_name']).to eq(nil)
end
def invite_member_by_email(source, source_type, email, created_by)
create(:"#{source_type}_member", invite_token: '123', invite_email: email, source: source, user: nil, created_by: created_by)
end
end
end
describe 'GET /projects/:id/invitations' do
it_behaves_like 'GET /:source_type/:id/invitations', 'project' do
let(:source) { project }
end
end
describe 'GET /groups/:id/invitations' do
it_behaves_like 'GET /:source_type/:id/invitations', 'group' do
let(:source) { group }
end
end
end

View file

@ -41,7 +41,7 @@ RSpec.describe API::Settings, 'Settings' do
expect(json_response['spam_check_endpoint_enabled']).to be_falsey
expect(json_response['spam_check_endpoint_url']).to be_nil
expect(json_response['wiki_page_max_content_bytes']).to be_a(Integer)
expect(json_response['require_admin_approval_after_user_signup']).to eq(false)
expect(json_response['require_admin_approval_after_user_signup']).to eq(true)
end
end

4
tooling/README.md Normal file
View file

@ -0,0 +1,4 @@
# Tooling
This directory contains tools and configuration for development only.

View file

@ -0,0 +1,5 @@
/* eslint-disable import/no-commonjs */
const IS_EE = require('../../config/helpers/is_ee_env');
module.exports = IS_EE ? {} : { ignorePatterns: ['ee/**/*.*'] };