Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
90fa047c0d
commit
4ab67d6529
|
@ -11,10 +11,4 @@ export function resetServiceWorkersPublicPath() {
|
|||
const relativeRootPath = (gon && gon.relative_url_root) || '';
|
||||
const webpackAssetPath = joinPaths(relativeRootPath, '/assets/webpack/');
|
||||
__webpack_public_path__ = webpackAssetPath; // eslint-disable-line babel/camelcase
|
||||
|
||||
// monaco-editor-webpack-plugin currently (incorrectly) references the
|
||||
// public path as a property of `window`. Once this is fixed upstream we
|
||||
// can remove this line
|
||||
// see: https://github.com/Microsoft/monaco-editor-webpack-plugin/pull/63
|
||||
window.__webpack_public_path__ = webpackAssetPath; // eslint-disable-line
|
||||
}
|
||||
|
|
|
@ -11,25 +11,25 @@ export default {
|
|||
MetadataItem,
|
||||
},
|
||||
props: {
|
||||
packagesCount: {
|
||||
count: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
packageHelpUrl: {
|
||||
helpUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showPackageCount() {
|
||||
return Number.isInteger(this.packagesCount);
|
||||
return Number.isInteger(this.count);
|
||||
},
|
||||
packageAmountText() {
|
||||
return n__(`%d Package`, `%d Packages`, this.packagesCount);
|
||||
return n__(`%d Package`, `%d Packages`, this.count);
|
||||
},
|
||||
infoMessages() {
|
||||
return [{ text: LIST_INTRO_TEXT, link: this.packageHelpUrl }];
|
||||
return [{ text: LIST_INTRO_TEXT, link: this.helpUrl }];
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
|
|
|
@ -8,8 +8,6 @@ import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
|||
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
|
||||
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
|
||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '../constants';
|
||||
import PackageSearch from './package_search.vue';
|
||||
import PackageTitle from './package_title.vue';
|
||||
import PackageList from './packages_list.vue';
|
||||
|
||||
export default {
|
||||
|
@ -18,8 +16,38 @@ export default {
|
|||
GlLink,
|
||||
GlSprintf,
|
||||
PackageList,
|
||||
PackageTitle,
|
||||
PackageSearch,
|
||||
PackageTitle: () =>
|
||||
import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'),
|
||||
PackageSearch: () =>
|
||||
import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'),
|
||||
InfrastructureTitle: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue'
|
||||
),
|
||||
InfrastructureSearch: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue'
|
||||
),
|
||||
},
|
||||
inject: {
|
||||
titleComponent: {
|
||||
from: 'titleComponent',
|
||||
default: 'PackageTitle',
|
||||
},
|
||||
searchComponent: {
|
||||
from: 'searchComponent',
|
||||
default: 'PackageSearch',
|
||||
},
|
||||
emptyPageTitle: {
|
||||
from: 'emptyPageTitle',
|
||||
default: s__('PackageRegistry|There are no packages yet'),
|
||||
},
|
||||
noResultsText: {
|
||||
from: 'noResultsText',
|
||||
default: s__(
|
||||
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
|
||||
),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
|
@ -38,7 +66,7 @@ export default {
|
|||
|
||||
emptyStateTitle() {
|
||||
return this.emptySearch
|
||||
? s__('PackageRegistry|There are no packages yet')
|
||||
? this.emptyPageTitle
|
||||
: s__('PackageRegistry|Sorry, your filter produced no results');
|
||||
},
|
||||
},
|
||||
|
@ -77,24 +105,21 @@ export default {
|
|||
},
|
||||
i18n: {
|
||||
widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'),
|
||||
noResults: s__(
|
||||
'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<package-title :package-help-url="packageHelpUrl" :packages-count="packagesCount" />
|
||||
<package-search @update="requestPackagesList" />
|
||||
<component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" />
|
||||
<component :is="searchComponent" @update="requestPackagesList" />
|
||||
|
||||
<package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest">
|
||||
<template #empty-state>
|
||||
<gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration">
|
||||
<template #description>
|
||||
<gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" />
|
||||
<gl-sprintf v-else :message="$options.i18n.noResults">
|
||||
<gl-sprintf v-else :message="noResultsText">
|
||||
<template #noPackagesLink="{ content }">
|
||||
<gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import createDefaultClient from '~/lib/graphql';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import PackagesListApp from './components/packages_list_app.vue';
|
||||
import { createStore } from './stores';
|
||||
|
||||
Vue.use(VueApollo);
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
|
@ -13,14 +10,9 @@ export default () => {
|
|||
const store = createStore();
|
||||
store.dispatch('setInitialState', el.dataset);
|
||||
|
||||
const apolloProvider = new VueApollo({
|
||||
defaultClient: createDefaultClient(),
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
apolloProvider,
|
||||
components: {
|
||||
PackagesListApp,
|
||||
},
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'PackageIconAndName',
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<gl-icon name="package" class="gl-ml-3 gl-mr-2" />
|
||||
<span><slot></slot></span>
|
||||
</div>
|
||||
</template>
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton, GlIcon, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
|
||||
import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import { getPackageTypeLabel } from '../utils';
|
||||
|
@ -11,7 +11,6 @@ export default {
|
|||
name: 'PackageListRow',
|
||||
components: {
|
||||
GlButton,
|
||||
GlIcon,
|
||||
GlLink,
|
||||
GlSprintf,
|
||||
GlTruncate,
|
||||
|
@ -19,11 +18,23 @@ export default {
|
|||
PackagePath,
|
||||
PublishMethod,
|
||||
ListItem,
|
||||
PackageIconAndName: () =>
|
||||
import(/* webpackChunkName: 'package_registry_components' */ './package_icon_and_name.vue'),
|
||||
InfrastructureIconAndName: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue'
|
||||
),
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
mixins: [timeagoMixin],
|
||||
inject: {
|
||||
iconComponent: {
|
||||
from: 'iconComponent',
|
||||
default: 'PackageIconAndName',
|
||||
},
|
||||
},
|
||||
props: {
|
||||
packageEntity: {
|
||||
type: Object,
|
||||
|
@ -94,10 +105,9 @@ export default {
|
|||
</gl-sprintf>
|
||||
</div>
|
||||
|
||||
<div v-if="showPackageType" class="d-flex align-items-center" data-testid="package-type">
|
||||
<gl-icon name="package" class="gl-ml-3 gl-mr-2" />
|
||||
<span>{{ packageType }}</span>
|
||||
</div>
|
||||
<component :is="iconComponent" v-if="showPackageType">
|
||||
{{ packageType }}
|
||||
</component>
|
||||
|
||||
<package-path v-if="hasProjectLink" :path="packageEntity.project_path" />
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<script>
|
||||
import { GlIcon } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
name: 'InfrastructureIconAndName',
|
||||
components: {
|
||||
GlIcon,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<gl-icon name="infrastructure-registry" class="gl-ml-3 gl-mr-2" />
|
||||
<span>{{ s__('InfrastructureRegistry|Terraform') }}</span>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { LIST_KEY_PACKAGE_TYPE } from '~/packages/list/constants';
|
||||
import getTableHeaders from '~/packages/list/utils';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||
|
||||
export default {
|
||||
components: { RegistrySearch, UrlSync },
|
||||
computed: {
|
||||
...mapState({
|
||||
isGroupPage: (state) => state.config.isGroupPage,
|
||||
sorting: (state) => state.sorting,
|
||||
filter: (state) => state.filter,
|
||||
}),
|
||||
sortableFields() {
|
||||
return getTableHeaders(this.isGroupPage).filter((h) => h.orderBy !== LIST_KEY_PACKAGE_TYPE);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setSorting', 'setFilter']),
|
||||
updateSorting(newValue) {
|
||||
this.setSorting(newValue);
|
||||
this.$emit('update');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<url-sync>
|
||||
<template #default="{ updateQuery }">
|
||||
<registry-search
|
||||
:filter="filter"
|
||||
:sorting="sorting"
|
||||
:tokens="[]"
|
||||
:sortable-fields="sortableFields"
|
||||
@sorting:changed="updateSorting"
|
||||
@filter:changed="setFilter"
|
||||
@filter:submit="$emit('update')"
|
||||
@query:changed="updateQuery"
|
||||
/>
|
||||
</template>
|
||||
</url-sync>
|
||||
</template>
|
|
@ -0,0 +1,53 @@
|
|||
<script>
|
||||
import { s__, n__ } from '~/locale';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
|
||||
export default {
|
||||
name: 'InfrastructureTitle',
|
||||
components: {
|
||||
TitleArea,
|
||||
MetadataItem,
|
||||
},
|
||||
props: {
|
||||
count: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
helpUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
showModuleCount() {
|
||||
return Number.isInteger(this.count);
|
||||
},
|
||||
moduleAmountText() {
|
||||
return n__(`%d Module`, `%d Modules`, this.count);
|
||||
},
|
||||
infoMessages() {
|
||||
return [{ text: this.$options.i18n.LIST_INTRO_TEXT, link: this.helpUrl }];
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
LIST_TITLE_TEXT: s__('InfrastructureRegistry|Infrastructure Registry'),
|
||||
LIST_INTRO_TEXT: s__(
|
||||
'InfrastructureRegistry|Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<title-area :title="$options.i18n.LIST_TITLE_TEXT" :info-messages="infoMessages">
|
||||
<template #metadata-amount>
|
||||
<metadata-item
|
||||
v-if="showModuleCount"
|
||||
icon="infrastructure-registry"
|
||||
:text="moduleAmountText"
|
||||
/>
|
||||
</template>
|
||||
</title-area>
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
import Vue from 'vue';
|
||||
import { s__ } from '~/locale';
|
||||
import PackagesListApp from '~/packages/list/components/packages_list_app.vue';
|
||||
import { createStore } from '~/packages/list/stores';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
|
||||
Vue.use(Translate);
|
||||
|
||||
export default () => {
|
||||
const el = document.getElementById('js-vue-packages-list');
|
||||
const store = createStore();
|
||||
store.dispatch('setInitialState', el.dataset);
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
store,
|
||||
components: {
|
||||
PackagesListApp,
|
||||
},
|
||||
provide: {
|
||||
titleComponent: 'InfrastructureTitle',
|
||||
searchComponent: 'InfrastructureSearch',
|
||||
iconComponent: 'InfrastructureIconAndName',
|
||||
emptyPageTitle: s__('InfrastructureRegistry|You have no Terraform modules in your project'),
|
||||
noResultsText: s__(
|
||||
'InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab.',
|
||||
),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('packages-list-app');
|
||||
},
|
||||
});
|
||||
};
|
|
@ -1,16 +1,6 @@
|
|||
import $ from 'jquery';
|
||||
import { refreshCurrentPage } from '../../lib/utils/url_utility';
|
||||
|
||||
function showDenylistType() {
|
||||
if ($('input[name="denylist_type"]:checked').val() === 'file') {
|
||||
$('.js-denylist-file').show();
|
||||
$('.js-denylist-raw').hide();
|
||||
} else {
|
||||
$('.js-denylist-file').hide();
|
||||
$('.js-denylist-raw').show();
|
||||
}
|
||||
}
|
||||
|
||||
export default function adminInit() {
|
||||
$('input#user_force_random_password').on('change', function randomPasswordClick() {
|
||||
const $elems = $('#user_password, #user_password_confirmation');
|
||||
|
@ -27,7 +17,4 @@ export default function adminInit() {
|
|||
});
|
||||
|
||||
$('li.project_member, li.group_member').on('ajax:success', refreshCurrentPage);
|
||||
|
||||
$("input[name='denylist_type']").on('click', showDenylistType);
|
||||
showDenylistType();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<script>
|
||||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlFormCheckbox,
|
||||
},
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
helpText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
dataQaSelector: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<input :name="name" type="hidden" :value="value ? '1' : '0'" data-testid="input" />
|
||||
|
||||
<gl-form-checkbox
|
||||
:checked="value"
|
||||
:data-qa-selector="dataQaSelector"
|
||||
@input="$emit('input', $event)"
|
||||
>
|
||||
<span data-testid="label">{{ label }}</span>
|
||||
<template v-if="helpText" #help>
|
||||
<span data-testid="helpText">{{ helpText }}</span>
|
||||
</template>
|
||||
</gl-form-checkbox>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,331 @@
|
|||
<script>
|
||||
import {
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
} from '@gitlab/ui';
|
||||
import csrf from '~/lib/utils/csrf';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import SignupCheckbox from './signup_checkbox.vue';
|
||||
|
||||
const DENYLIST_TYPE_RAW = 'raw';
|
||||
const DENYLIST_TYPE_FILE = 'file';
|
||||
|
||||
export default {
|
||||
csrf,
|
||||
DENYLIST_TYPE_RAW,
|
||||
DENYLIST_TYPE_FILE,
|
||||
components: {
|
||||
GlButton,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormRadio,
|
||||
GlFormRadioGroup,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
SignupCheckbox,
|
||||
},
|
||||
inject: [
|
||||
'host',
|
||||
'settingsPath',
|
||||
'signupEnabled',
|
||||
'requireAdminApprovalAfterUserSignup',
|
||||
'sendUserConfirmationEmail',
|
||||
'minimumPasswordLength',
|
||||
'minimumPasswordLengthMin',
|
||||
'minimumPasswordLengthMax',
|
||||
'minimumPasswordLengthHelpLink',
|
||||
'domainAllowlistRaw',
|
||||
'newUserSignupsCap',
|
||||
'domainDenylistEnabled',
|
||||
'denylistTypeRawSelected',
|
||||
'domainDenylistRaw',
|
||||
'emailRestrictionsEnabled',
|
||||
'supportedSyntaxLinkUrl',
|
||||
'emailRestrictions',
|
||||
'afterSignUpText',
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
signupEnabled: this.signupEnabled,
|
||||
requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
|
||||
sendConfirmationEmail: this.sendUserConfirmationEmail,
|
||||
minimumPasswordLength: this.minimumPasswordLength,
|
||||
minimumPasswordLengthMin: this.minimumPasswordLengthMin,
|
||||
minimumPasswordLengthMax: this.minimumPasswordLengthMax,
|
||||
minimumPasswordLengthHelpLink: this.minimumPasswordLengthHelpLink,
|
||||
domainAllowlistRaw: this.domainAllowlistRaw,
|
||||
userCap: this.newUserSignupsCap,
|
||||
domainDenylistEnabled: this.domainDenylistEnabled,
|
||||
denylistType: this.denylistTypeRawSelected
|
||||
? this.$options.DENYLIST_TYPE_RAW
|
||||
: this.$options.DENYLIST_TYPE_FILE,
|
||||
domainDenylistRaw: this.domainDenylistRaw,
|
||||
emailRestrictionsEnabled: this.emailRestrictionsEnabled,
|
||||
supportedSyntaxLinkUrl: this.supportedSyntaxLinkUrl,
|
||||
emailRestrictions: this.emailRestrictions,
|
||||
afterSignUpText: this.afterSignUpText,
|
||||
},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
signupEnabledHelpText() {
|
||||
const text = sprintf(
|
||||
s__(
|
||||
'ApplicationSettings|When enabled, any user visiting %{host} will be able to create an account.',
|
||||
),
|
||||
{
|
||||
host: this.host,
|
||||
},
|
||||
);
|
||||
|
||||
return text;
|
||||
},
|
||||
requireAdminApprovalHelpText() {
|
||||
const text = sprintf(
|
||||
s__(
|
||||
'ApplicationSettings|When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled.',
|
||||
),
|
||||
{
|
||||
host: this.host,
|
||||
},
|
||||
);
|
||||
|
||||
return text;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
submitButtonHandler() {
|
||||
this.$refs.form.submit();
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
buttonText: s__('ApplicationSettings|Save changes'),
|
||||
signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
|
||||
requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
|
||||
sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
|
||||
minimumPasswordLengthLabel: s__(
|
||||
'ApplicationSettings|Minimum password length (number of characters)',
|
||||
),
|
||||
domainAllowListLabel: s__('ApplicationSettings|Allowed domains for sign-ups'),
|
||||
domainAllowListDescription: s__(
|
||||
'ApplicationSettings|ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
|
||||
),
|
||||
userCapLabel: s__('ApplicationSettings|User cap'),
|
||||
userCapDescription: s__(
|
||||
'ApplicationSettings|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited.',
|
||||
),
|
||||
domainDenyListGroupLabel: s__('ApplicationSettings|Domain denylist'),
|
||||
domainDenyListLabel: s__('ApplicationSettings|Enable domain denylist for sign ups'),
|
||||
domainDenyListTypeFileLabel: s__('ApplicationSettings|Upload denylist file'),
|
||||
domainDenyListTypeRawLabel: s__('ApplicationSettings|Enter denylist manually'),
|
||||
domainDenyListFileLabel: s__('ApplicationSettings|Denylist file'),
|
||||
domainDenyListFileDescription: s__(
|
||||
'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.',
|
||||
),
|
||||
domainDenyListListLabel: s__('ApplicationSettings|Denied domains for sign-ups'),
|
||||
domainDenyListListDescription: s__(
|
||||
'ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com',
|
||||
),
|
||||
domainPlaceholder: s__('ApplicationSettings|domain.com'),
|
||||
emailRestrictionsEnabledGroupLabel: s__('ApplicationSettings|Email restrictions'),
|
||||
emailRestrictionsEnabledLabel: s__(
|
||||
'ApplicationSettings|Enable email restrictions for sign ups',
|
||||
),
|
||||
emailRestrictionsGroupLabel: s__('ApplicationSettings|Email restrictions for sign-ups'),
|
||||
afterSignUpTextGroupLabel: s__('ApplicationSettings|After sign up text'),
|
||||
afterSignUpTextGroupDescription: s__('ApplicationSettings|Markdown enabled'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
ref="form"
|
||||
accept-charset="UTF-8"
|
||||
data-testid="form"
|
||||
method="post"
|
||||
:action="settingsPath"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<input type="hidden" name="utf8" value="✓" />
|
||||
<input type="hidden" name="_method" value="patch" />
|
||||
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
|
||||
|
||||
<section class="gl-mb-8">
|
||||
<signup-checkbox
|
||||
v-model="form.signupEnabled"
|
||||
class="gl-mb-5"
|
||||
name="application_setting[signup_enabled]"
|
||||
:help-text="signupEnabledHelpText"
|
||||
:label="$options.i18n.signupEnabledLabel"
|
||||
data-qa-selector="signup_enabled_checkbox"
|
||||
/>
|
||||
|
||||
<signup-checkbox
|
||||
v-model="form.requireAdminApproval"
|
||||
class="gl-mb-5"
|
||||
name="application_setting[require_admin_approval_after_user_signup]"
|
||||
:help-text="requireAdminApprovalHelpText"
|
||||
:label="$options.i18n.requireAdminApprovalLabel"
|
||||
data-qa-selector="require_admin_approval_after_user_signup_checkbox"
|
||||
/>
|
||||
|
||||
<signup-checkbox
|
||||
v-model="form.sendConfirmationEmail"
|
||||
class="gl-mb-5"
|
||||
name="application_setting[send_user_confirmation_email]"
|
||||
:label="$options.i18n.sendConfirmationEmailLabel"
|
||||
/>
|
||||
|
||||
<gl-form-group
|
||||
:label="$options.i18n.userCapLabel"
|
||||
:description="$options.i18n.userCapDescription"
|
||||
>
|
||||
<gl-form-input
|
||||
v-model="form.userCap"
|
||||
type="text"
|
||||
name="application_setting[new_user_signups_cap]"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group :label="$options.i18n.minimumPasswordLengthLabel">
|
||||
<gl-form-input
|
||||
v-model="form.minimumPasswordLength"
|
||||
:min="form.minimumPasswordLengthMin"
|
||||
:max="form.minimumPasswordLengthMax"
|
||||
type="number"
|
||||
name="application_setting[minimum_password_length]"
|
||||
/>
|
||||
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'ApplicationSettings|See GitLab\'s %{linkStart}Password Policy Guidelines%{linkEnd}',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="form.minimumPasswordLengthHelpLink" target="_blank">{{
|
||||
content
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:description="$options.i18n.domainAllowListDescription"
|
||||
:label="$options.i18n.domainAllowListLabel"
|
||||
>
|
||||
<textarea
|
||||
v-model="form.domainAllowlistRaw"
|
||||
:placeholder="$options.i18n.domainPlaceholder"
|
||||
rows="8"
|
||||
class="form-control gl-form-input"
|
||||
name="application_setting[domain_allowlist_raw]"
|
||||
></textarea>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group :label="$options.i18n.domainDenyListGroupLabel">
|
||||
<signup-checkbox
|
||||
v-model="form.domainDenylistEnabled"
|
||||
name="application_setting[domain_denylist_enabled]"
|
||||
:label="$options.i18n.domainDenyListLabel"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-radio-group v-model="form.denylistType" name="denylist_type" class="gl-mb-5">
|
||||
<gl-form-radio :value="$options.DENYLIST_TYPE_FILE">{{
|
||||
$options.i18n.domainDenyListTypeFileLabel
|
||||
}}</gl-form-radio>
|
||||
<gl-form-radio :value="$options.DENYLIST_TYPE_RAW">{{
|
||||
$options.i18n.domainDenyListTypeRawLabel
|
||||
}}</gl-form-radio>
|
||||
</gl-form-radio-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="form.denylistType === $options.DENYLIST_TYPE_FILE"
|
||||
:description="$options.i18n.domainDenyListFileDescription"
|
||||
:label="$options.i18n.domainDenyListFileLabel"
|
||||
label-for="domain-denylist-file-input"
|
||||
data-testid="domain-denylist-file-input-group"
|
||||
>
|
||||
<input
|
||||
id="domain-denylist-file-input"
|
||||
class="form-control gl-form-input"
|
||||
type="file"
|
||||
accept=".txt,.conf"
|
||||
name="application_setting[domain_denylist_file]"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
v-if="form.denylistType !== $options.DENYLIST_TYPE_FILE"
|
||||
:description="$options.i18n.domainDenyListListDescription"
|
||||
:label="$options.i18n.domainDenyListListLabel"
|
||||
data-testid="domain-denylist-raw-input-group"
|
||||
>
|
||||
<textarea
|
||||
v-model="form.domainDenylistRaw"
|
||||
:placeholder="$options.i18n.domainPlaceholder"
|
||||
rows="8"
|
||||
class="form-control gl-form-input"
|
||||
name="application_setting[domain_denylist_raw]"
|
||||
></textarea>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group :label="$options.i18n.emailRestrictionsEnabledGroupLabel">
|
||||
<signup-checkbox
|
||||
v-model="form.emailRestrictionsEnabled"
|
||||
name="application_setting[email_restrictions_enabled]"
|
||||
:label="$options.i18n.emailRestrictionsEnabledLabel"
|
||||
/>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group :label="$options.i18n.emailRestrictionsGroupLabel">
|
||||
<textarea
|
||||
v-model="form.emailRestrictions"
|
||||
rows="4"
|
||||
class="form-control gl-form-input"
|
||||
name="application_setting[email_restrictions]"
|
||||
></textarea>
|
||||
|
||||
<gl-sprintf
|
||||
:message="
|
||||
s__(
|
||||
'ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. See the %{linkStart}supported syntax%{linkEnd} for more information.',
|
||||
)
|
||||
"
|
||||
>
|
||||
<template #link="{ content }">
|
||||
<gl-link :href="form.supportedSyntaxLinkUrl" target="_blank">{{ content }}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-form-group>
|
||||
|
||||
<gl-form-group
|
||||
:label="$options.i18n.afterSignUpTextGroupLabel"
|
||||
:description="$options.i18n.afterSignUpTextGroupDescription"
|
||||
>
|
||||
<textarea
|
||||
v-model="form.afterSignUpText"
|
||||
rows="4"
|
||||
class="form-control gl-form-input"
|
||||
name="application_setting[after_sign_up_text]"
|
||||
></textarea>
|
||||
</gl-form-group>
|
||||
</section>
|
||||
<gl-button
|
||||
data-qa-selector="save_changes_button"
|
||||
variant="confirm"
|
||||
@click="submitButtonHandler"
|
||||
>
|
||||
{{ $options.i18n.buttonText }}
|
||||
</gl-button>
|
||||
</form>
|
||||
</template>
|
|
@ -1,27 +1,9 @@
|
|||
import Vue from 'vue';
|
||||
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||
import initUserInternalRegexPlaceholder from '../account_and_limits';
|
||||
import initGitpod from '../gitpod';
|
||||
import initSignupRestrictions from '../signup_restrictions';
|
||||
|
||||
(() => {
|
||||
initUserInternalRegexPlaceholder();
|
||||
|
||||
const el = document.querySelector('#js-gitpod-settings-help-text');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { message, messageUrl } = el.dataset;
|
||||
|
||||
// eslint-disable-next-line no-new
|
||||
new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(IntegrationHelpText, {
|
||||
props: {
|
||||
message,
|
||||
messageUrl,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
initGitpod();
|
||||
initSignupRestrictions();
|
||||
})();
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import Vue from 'vue';
|
||||
import IntegrationHelpText from '~/vue_shared/components/integrations_help_text.vue';
|
||||
|
||||
export default function initGitpod() {
|
||||
const el = document.querySelector('#js-gitpod-settings-help-text');
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { message, messageUrl } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
render(createElement) {
|
||||
return createElement(IntegrationHelpText, {
|
||||
props: {
|
||||
message,
|
||||
messageUrl,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import Vue from 'vue';
|
||||
import SignupForm from './general/components/signup_form.vue';
|
||||
import { getParsedDataset } from './utils';
|
||||
|
||||
export default function initSignupRestrictions(elementSelector = '#js-signup-form') {
|
||||
const el = document.querySelector(elementSelector);
|
||||
|
||||
if (!el) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parsedDataset = getParsedDataset({
|
||||
dataset: el.dataset,
|
||||
booleanAttributes: [
|
||||
'signupEnabled',
|
||||
'requireAdminApprovalAfterUserSignup',
|
||||
'sendUserConfirmationEmail',
|
||||
'domainDenylistEnabled',
|
||||
'denylistTypeRawSelected',
|
||||
'emailRestrictionsEnabled',
|
||||
],
|
||||
});
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
...parsedDataset,
|
||||
},
|
||||
render: (createElement) => createElement(SignupForm),
|
||||
});
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { includes } from 'lodash';
|
||||
import { parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
||||
/**
|
||||
* Returns a new dataset that has all the values of keys indicated in
|
||||
* booleanAttributes transformed by the parseBoolean() helper function
|
||||
*
|
||||
* @param {Object}
|
||||
* @returns {Object}
|
||||
*/
|
||||
export const getParsedDataset = ({ dataset = {}, booleanAttributes = [] } = {}) => {
|
||||
const parsedDataset = {};
|
||||
|
||||
Object.keys(dataset).forEach((key) => {
|
||||
parsedDataset[key] = includes(booleanAttributes, key)
|
||||
? parseBoolean(dataset[key])
|
||||
: dataset[key];
|
||||
});
|
||||
|
||||
return parsedDataset;
|
||||
};
|
|
@ -1,3 +1,3 @@
|
|||
import initPackageList from '~/packages/list/packages_list_app_bundle';
|
||||
import initList from '~/packages_and_registries/infrastructure_registry/list_app_bundle';
|
||||
|
||||
initPackageList();
|
||||
initList();
|
||||
|
|
|
@ -11,17 +11,3 @@
|
|||
background: $blue-400;
|
||||
}
|
||||
}
|
||||
|
||||
.runner-status {
|
||||
&.runner-status-online {
|
||||
background-color: $green-600;
|
||||
}
|
||||
|
||||
&.runner-status-offline {
|
||||
background-color: $gray-darkest;
|
||||
}
|
||||
|
||||
&.runner-status-paused {
|
||||
background-color: $red-500;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,33 @@ module Ci
|
|||
module RunnersHelper
|
||||
include IconsHelper
|
||||
|
||||
def runner_status_icon(runner)
|
||||
def runner_status_icon(runner, size: 16, icon_class: '')
|
||||
status = runner.status
|
||||
|
||||
title = ''
|
||||
icon = 'warning-solid'
|
||||
span_class = ''
|
||||
|
||||
case status
|
||||
when :not_connected
|
||||
content_tag(:span, title: _("New runner. Has not connected yet")) do
|
||||
sprite_icon("warning-solid", size: 24, css_class: "gl-vertical-align-bottom!")
|
||||
end
|
||||
title = s_("Runners|New runner, has not connected yet")
|
||||
icon = 'warning-solid'
|
||||
when :online
|
||||
title = s_("Runners|Runner is online, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
|
||||
icon = 'status-active'
|
||||
span_class = 'gl-text-green-500'
|
||||
when :offline
|
||||
title = s_("Runners|Runner is offline, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
|
||||
icon = 'status-failed'
|
||||
span_class = 'gl-text-red-500'
|
||||
when :paused
|
||||
title = s_("Runners|Runner is paused, last contact was %{runner_contact} ago") % { runner_contact: time_ago_in_words(runner.contacted_at) }
|
||||
icon = 'status-paused'
|
||||
span_class = 'gl-text-gray-600'
|
||||
end
|
||||
|
||||
when :online, :offline, :paused
|
||||
content_tag :span, nil,
|
||||
class: "gl-display-inline-block gl-avatar gl-avatar-s16 gl-avatar-circle runner-status runner-status-#{status}",
|
||||
title: _("Runner is %{status}, last contact was %{runner_contact} ago") % { status: status, runner_contact: time_ago_in_words(runner.contacted_at) }
|
||||
content_tag(:span, class: span_class, title: title, data: { toggle: 'tooltip', container: 'body', testid: 'runner_status_icon', qa_selector: "runner_status_#{status}_content" }) do
|
||||
sprite_icon(icon, size: size, css_class: icon_class)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ module Namespaces
|
|||
object_hierarchy(self.class.where(id: id))
|
||||
.all_objects
|
||||
end
|
||||
alias_method :recursive_self_and_hierarchy, :self_and_hierarchy
|
||||
|
||||
# Returns all the ancestors of the current namespaces.
|
||||
def ancestors
|
||||
|
@ -30,6 +31,7 @@ module Namespaces
|
|||
object_hierarchy(self.class.where(id: parent_id))
|
||||
.base_and_ancestors
|
||||
end
|
||||
alias_method :recursive_ancestors, :ancestors
|
||||
|
||||
# returns all ancestors upto but excluding the given namespace
|
||||
# when no namespace is given, all ancestors upto the top are returned
|
||||
|
@ -44,17 +46,20 @@ module Namespaces
|
|||
object_hierarchy(self.class.where(id: id))
|
||||
.base_and_ancestors(hierarchy_order: hierarchy_order)
|
||||
end
|
||||
alias_method :recursive_self_and_ancestors, :self_and_ancestors
|
||||
|
||||
# Returns all the descendants of the current namespace.
|
||||
def descendants
|
||||
object_hierarchy(self.class.where(parent_id: id))
|
||||
.base_and_descendants
|
||||
end
|
||||
alias_method :recursive_descendants, :descendants
|
||||
|
||||
def self_and_descendants
|
||||
object_hierarchy(self.class.where(id: id))
|
||||
.base_and_descendants
|
||||
end
|
||||
alias_method :recursive_self_and_descendants, :self_and_descendants
|
||||
|
||||
def object_hierarchy(ancestors_base)
|
||||
Gitlab::ObjectHierarchy.new(ancestors_base, options: { use_distinct: Feature.enabled?(:use_distinct_in_object_hierarchy, self) })
|
||||
|
|
|
@ -1,80 +1,20 @@
|
|||
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-signup-settings'), html: { class: 'fieldset-form' } do |f|
|
||||
= form_errors(@application_setting)
|
||||
= form_errors(@application_setting)
|
||||
|
||||
%fieldset
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :signup_enabled, class: 'form-check-input', data: { qa_selector: 'signup_enabled_checkbox' }
|
||||
= f.label :signup_enabled, class: 'form-check-label' do
|
||||
Sign-up enabled
|
||||
.form-text.text-muted
|
||||
= _("When enabled, any user visiting %{host} will be able to create an account.") % { host: "#{new_user_session_url(host: Gitlab.config.gitlab.host)}" }
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :require_admin_approval_after_user_signup, class: 'form-check-input', data: { qa_selector: 'require_admin_approval_after_user_signup_checkbox' }
|
||||
= f.label :require_admin_approval_after_user_signup, class: 'form-check-label' do
|
||||
= _('Require admin approval for new sign-ups')
|
||||
.form-text.text-muted
|
||||
= _("When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled.") % { host: "#{new_user_session_url(host: Gitlab.config.gitlab.host)}" }
|
||||
.form-group
|
||||
.form-check
|
||||
= f.check_box :send_user_confirmation_email, class: 'form-check-input'
|
||||
= f.label :send_user_confirmation_email, class: 'form-check-label' do
|
||||
Send confirmation email on sign-up
|
||||
|
||||
= render_if_exists 'admin/application_settings/new_user_signups_cap', form: f
|
||||
|
||||
.form-group
|
||||
= f.label :minimum_password_length, _('Minimum password length (number of characters)'), class: 'label-bold'
|
||||
= f.number_field :minimum_password_length, class: 'form-control gl-form-input', rows: 4, min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH, max: Devise.password_length.max
|
||||
- password_policy_guidelines_link = link_to _('Password Policy Guidelines'), 'https://about.gitlab.com/handbook/security/#gitlab-password-policy-guidelines', target: '_blank', rel: 'noopener noreferrer nofollow'
|
||||
.form-text.text-muted
|
||||
= _("See GitLab's %{password_policy_guidelines}").html_safe % { password_policy_guidelines: password_policy_guidelines_link }
|
||||
.form-group
|
||||
= f.label :domain_allowlist, _('Allowed domains for sign-ups'), class: 'label-bold'
|
||||
= f.text_area :domain_allowlist_raw, placeholder: 'domain.com', class: 'form-control gl-form-input', rows: 8
|
||||
.form-text.text-muted ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
|
||||
.form-group
|
||||
= f.label :domain_denylist_enabled, _('Domain denylist'), class: 'label-bold'
|
||||
.form-check
|
||||
= f.check_box :domain_denylist_enabled, class: 'form-check-input'
|
||||
= f.label :domain_denylist_enabled, class: 'form-check-label' do
|
||||
Enable domain denylist for sign ups
|
||||
.form-group
|
||||
.form-check
|
||||
= radio_button_tag :denylist_type, :file, false, class: 'form-check-input'
|
||||
= label_tag :denylist_type_file, class: 'form-check-label' do
|
||||
.option-title
|
||||
Upload denylist file
|
||||
.form-check
|
||||
= radio_button_tag :denylist_type, :raw, @application_setting.domain_denylist.present? || @application_setting.domain_denylist.blank?, class: 'form-check-input'
|
||||
= label_tag :denylist_type_raw, class: 'form-check-label' do
|
||||
.option-title
|
||||
Enter denylist manually
|
||||
.form-group.js-denylist-file
|
||||
= f.label :domain_denylist_file, _('Denylist file'), class: 'label-bold'
|
||||
= f.file_field :domain_denylist_file, class: 'form-control gl-form-input', accept: '.txt,.conf'
|
||||
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries.
|
||||
.form-group.js-denylist-raw
|
||||
= f.label :domain_denylist, _('Denied domains for sign-ups'), class: 'label-bold'
|
||||
= f.text_area :domain_denylist_raw, placeholder: 'domain.com', class: 'form-control gl-form-input', rows: 8
|
||||
.form-text.text-muted Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com
|
||||
.form-group
|
||||
= f.label :email_restrictions_enabled, _('Email restrictions'), class: 'label-bold'
|
||||
.form-check
|
||||
= f.check_box :email_restrictions_enabled, class: 'form-check-input'
|
||||
= f.label :email_restrictions_enabled, class: 'form-check-label' do
|
||||
= _('Enable email restrictions for sign ups')
|
||||
.form-group
|
||||
= f.label :email_restrictions, _('Email restrictions for sign-ups'), class: 'label-bold'
|
||||
= f.text_area :email_restrictions, class: 'form-control gl-form-input', rows: 4
|
||||
.form-text.text-muted
|
||||
- supported_syntax_link_url = 'https://github.com/google/re2/wiki/Syntax'
|
||||
- supported_syntax_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: supported_syntax_link_url }
|
||||
= _('Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information.').html_safe % { supported_syntax_link_start: supported_syntax_link_start, supported_syntax_link_end: '</a>'.html_safe }
|
||||
|
||||
.form-group
|
||||
= f.label :after_sign_up_text, class: 'label-bold'
|
||||
= f.text_area :after_sign_up_text, class: 'form-control gl-form-input', rows: 4
|
||||
.form-text.text-muted Markdown enabled
|
||||
= f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
|
||||
#js-signup-form{ data: { host: new_user_session_url(host: Gitlab.config.gitlab.host),
|
||||
settings_path: general_admin_application_settings_path(anchor: 'js-signup-settings'),
|
||||
signup_enabled: @application_setting[:signup_enabled].to_s,
|
||||
require_admin_approval_after_user_signup: @application_setting[:require_admin_approval_after_user_signup].to_s,
|
||||
send_user_confirmation_email: @application_setting[:send_user_confirmation_email].to_s,
|
||||
minimum_password_length: @application_setting[:minimum_password_length],
|
||||
minimum_password_length_min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH,
|
||||
minimum_password_length_max: Devise.password_length.max,
|
||||
minimum_password_length_help_link: 'https://about.gitlab.com/handbook/security/#gitlab-password-policy-guidelines',
|
||||
domain_allowlist_raw: @application_setting.domain_allowlist_raw,
|
||||
new_user_signups_cap: @application_setting[:new_user_signups_cap].to_s,
|
||||
domain_denylist_enabled: @application_setting[:domain_denylist_enabled].to_s,
|
||||
denylist_type_raw_selected: (@application_setting.domain_denylist.present? || @application_setting.domain_denylist.blank?).to_s,
|
||||
domain_denylist_raw: @application_setting.domain_denylist_raw,
|
||||
email_restrictions_enabled: @application_setting[:email_restrictions_enabled].to_s,
|
||||
supported_syntax_link_url: 'https://github.com/google/re2/wiki/Syntax',
|
||||
email_restrictions: @application_setting.email_restrictions,
|
||||
after_sign_up_text: @application_setting[:after_sign_up_text] } }
|
||||
|
|
|
@ -5,6 +5,6 @@
|
|||
.col-12
|
||||
#js-vue-packages-list{ data: { resource_id: @project.id,
|
||||
page_type: 'project',
|
||||
empty_list_help_url: help_page_path('user/packages/package_registry/index'),
|
||||
empty_list_illustration: image_path('illustrations/no-packages.svg'),
|
||||
package_help_url: help_page_path('user/packages/index') } }
|
||||
empty_list_help_url: help_page_path('user/infrastructure/index'),
|
||||
empty_list_illustration: image_path('illustrations/empty-state/empty-terraform-register-lg.svg'),
|
||||
package_help_url: help_page_path('user/infrastructure/index') } }
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
%li.runner{ id: dom_id(runner) }
|
||||
%h4.gl-font-weight-normal
|
||||
= runner_status_icon(runner)
|
||||
= runner_status_icon(runner, size: 16, icon_class: "gl-vertical-align-middle!")
|
||||
|
||||
- if @project_runners.include?(runner)
|
||||
= link_to _("%{token}...") % { token: runner.short_sha }, project_runner_path(@project, runner), class: 'commit-sha has-tooltip', title: _("Partial token for reference only")
|
||||
|
|
|
@ -2377,7 +2377,7 @@
|
|||
:idempotent:
|
||||
:tags: []
|
||||
- :name: update_highest_role
|
||||
:feature_category: :authentication_and_authorization
|
||||
:feature_category: :utilization
|
||||
:has_external_dependencies:
|
||||
:urgency: :high
|
||||
:resource_boundary: :unknown
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
class UpdateHighestRoleWorker
|
||||
include ApplicationWorker
|
||||
|
||||
feature_category :authentication_and_authorization
|
||||
feature_category :utilization
|
||||
urgency :high
|
||||
weight 2
|
||||
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Improve runners status icon usability and accessibility in the project settings
|
||||
view
|
||||
merge_request: 58781
|
||||
author:
|
||||
type: changed
|
|
@ -91,6 +91,11 @@ msgid_plural "%d Approvals"
|
|||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d Module"
|
||||
msgid_plural "%d Modules"
|
||||
msgstr[0] ""
|
||||
msgstr[1] ""
|
||||
|
||||
msgid "%d Other"
|
||||
msgid_plural "%d Others"
|
||||
msgstr[0] ""
|
||||
|
@ -2220,9 +2225,6 @@ msgstr ""
|
|||
msgid "AdminArea|New user"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited."
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Owner"
|
||||
msgstr ""
|
||||
|
||||
|
@ -2247,9 +2249,6 @@ msgstr ""
|
|||
msgid "AdminArea|Total users"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|User cap"
|
||||
msgstr ""
|
||||
|
||||
msgid "AdminArea|Users"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3242,9 +3241,6 @@ msgstr ""
|
|||
msgid "Allowed Geo IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Allowed domains for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Allowed email domain restriction only permitted for top-level groups"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3854,6 +3850,87 @@ msgstr ""
|
|||
msgid "Application: %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|After sign up text"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Allowed domains for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Denied domains for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Denylist file"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Domain denylist"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Email restrictions"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Email restrictions for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Enable domain denylist for sign ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Enable email restrictions for sign ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Enter denylist manually"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Markdown enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Minimum password length (number of characters)"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|ONLY users with e-mail addresses that match these domain(s) will be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Once the instance reaches the user cap, any user who is added or requests access will have to be approved by an admin. Leave the field empty for unlimited."
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Require admin approval for new sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Restricts sign-ups for email addresses that match the given regex. See the %{linkStart}supported syntax%{linkEnd} for more information."
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Save changes"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|See GitLab's %{linkStart}Password Policy Guidelines%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Send confirmation email on sign-up"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Sign-up enabled"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Upload denylist file"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|User cap"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines for multiple entries. Ex: domain.com, *.domain.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|Users with e-mail addresses that match these domain(s) will NOT be able to sign-up. Wildcards allowed. Use separate lines or commas for multiple entries."
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|When enabled, any user visiting %{host} will be able to create an account."
|
||||
msgstr ""
|
||||
|
||||
msgid "ApplicationSettings|domain.com"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applications"
|
||||
msgstr ""
|
||||
|
||||
|
@ -10409,18 +10486,12 @@ msgstr ""
|
|||
msgid "Denied authorization of chat nickname %{user_name}."
|
||||
msgstr ""
|
||||
|
||||
msgid "Denied domains for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deny"
|
||||
msgstr ""
|
||||
|
||||
msgid "Deny access request"
|
||||
msgstr ""
|
||||
|
||||
msgid "Denylist file"
|
||||
msgstr ""
|
||||
|
||||
msgid "Dependencies"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11310,9 +11381,6 @@ msgstr ""
|
|||
msgid "Domain cannot be deleted while associated to one or more clusters."
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain denylist"
|
||||
msgstr ""
|
||||
|
||||
msgid "Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11640,12 +11708,6 @@ msgstr ""
|
|||
msgid "Email patch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email restrictions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email restrictions for sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Email sent"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11811,9 +11873,6 @@ msgstr ""
|
|||
msgid "Enable container expiration and retention policies for projects created earlier than GitLab 12.7."
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable email restrictions for sign ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Enable error tracking"
|
||||
msgstr ""
|
||||
|
||||
|
@ -16851,6 +16910,21 @@ msgstr ""
|
|||
msgid "Infrastructure Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "InfrastructureRegistry|Infrastructure Registry"
|
||||
msgstr ""
|
||||
|
||||
msgid "InfrastructureRegistry|Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "InfrastructureRegistry|Terraform"
|
||||
msgstr ""
|
||||
|
||||
msgid "InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab."
|
||||
msgstr ""
|
||||
|
||||
msgid "InfrastructureRegistry|You have no Terraform modules in your project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Inherited"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20386,9 +20460,6 @@ msgstr ""
|
|||
msgid "Minimum interval in days"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minimum password length (number of characters)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Minutes"
|
||||
msgstr ""
|
||||
|
||||
|
@ -21134,9 +21205,6 @@ msgstr ""
|
|||
msgid "New response for issue #%{issue_iid}:"
|
||||
msgstr ""
|
||||
|
||||
msgid "New runner. Has not connected yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "New runners registration token has been generated!"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22725,9 +22793,6 @@ msgstr ""
|
|||
msgid "Password (optional)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password Policy Guidelines"
|
||||
msgstr ""
|
||||
|
||||
msgid "Password authentication is unavailable."
|
||||
msgstr ""
|
||||
|
||||
|
@ -26661,9 +26726,6 @@ msgstr ""
|
|||
msgid "Require additional authentication for administrative tasks"
|
||||
msgstr ""
|
||||
|
||||
msgid "Require admin approval for new sign-ups"
|
||||
msgstr ""
|
||||
|
||||
msgid "Require all users in this group to setup Two-factor authentication"
|
||||
msgstr ""
|
||||
|
||||
|
@ -26863,9 +26925,6 @@ msgstr ""
|
|||
msgid "Restricted shift times are not available for hourly shifts"
|
||||
msgstr ""
|
||||
|
||||
msgid "Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information."
|
||||
msgstr ""
|
||||
|
||||
msgid "Resume"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27032,9 +27091,6 @@ msgstr ""
|
|||
msgid "Runner API"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runner is %{status}, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runner token"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27113,6 +27169,9 @@ msgstr ""
|
|||
msgid "Runners|Name"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|New runner, has not connected yet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Platform"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27128,6 +27187,15 @@ msgstr ""
|
|||
msgid "Runners|Runner #%{runner_id}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner is offline, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner is online, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Runner is paused, last contact was %{runner_contact} ago"
|
||||
msgstr ""
|
||||
|
||||
msgid "Runners|Shared"
|
||||
msgstr ""
|
||||
|
||||
|
@ -27972,9 +28040,6 @@ msgstr ""
|
|||
msgid "SecurityReports|Your feedback is important to us! We will ask again in a week."
|
||||
msgstr ""
|
||||
|
||||
msgid "See GitLab's %{password_policy_guidelines}"
|
||||
msgstr ""
|
||||
|
||||
msgid "See metrics"
|
||||
msgstr ""
|
||||
|
||||
|
@ -35087,12 +35152,6 @@ msgstr ""
|
|||
msgid "When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong."
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, any user visiting %{host} and creating an account will have to be explicitly approved by an admin before they can sign in. This setting is effective only if sign-ups are enabled."
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, any user visiting %{host} will be able to create an account."
|
||||
msgstr ""
|
||||
|
||||
msgid "When enabled, if an npm package isn't found in the GitLab Registry, we will attempt to pull from the global npm registry."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
"mermaid": "^8.9.2",
|
||||
"minimatch": "^3.0.4",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.0",
|
||||
"monaco-editor-webpack-plugin": "^1.9.1",
|
||||
"monaco-yaml": "^2.5.1",
|
||||
"mousetrap": "1.6.5",
|
||||
"pdfjs-dist": "^2.0.943",
|
||||
|
|
|
@ -6,19 +6,19 @@ module QA
|
|||
module Settings
|
||||
module Component
|
||||
class SignUpRestrictions < Page::Base
|
||||
view 'app/views/admin/application_settings/_signup.html.haml' do
|
||||
view 'app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue' do
|
||||
element :require_admin_approval_after_user_signup_checkbox
|
||||
element :signup_enabled_checkbox
|
||||
element :save_changes_button
|
||||
end
|
||||
|
||||
def require_admin_approval_after_user_signup
|
||||
check_element(:require_admin_approval_after_user_signup_checkbox)
|
||||
click_element_coordinates(:require_admin_approval_after_user_signup_checkbox, visible: false)
|
||||
click_element(:save_changes_button)
|
||||
end
|
||||
|
||||
def disable_signups
|
||||
uncheck_element(:signup_enabled_checkbox)
|
||||
click_element_coordinates(:signup_enabled_checkbox, visible: false)
|
||||
click_element(:save_changes_button)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,12 +10,9 @@ module QA
|
|||
element :coordinator_address, '%code#coordinator_address' # rubocop:disable QA/ElementWithPattern
|
||||
end
|
||||
|
||||
##
|
||||
# TODO, phase-out CSS classes added in Ruby helpers.
|
||||
#
|
||||
view 'app/helpers/ci/runners_helper.rb' do
|
||||
# rubocop:disable Lint/InterpolationCheck
|
||||
element :runner_status, 'runner-status-#{status}' # rubocop:disable QA/ElementWithPattern
|
||||
element :runner_status_icon, 'qa_selector: "runner_status_#{status}_content"' # rubocop:disable QA/ElementWithPattern
|
||||
# rubocop:enable Lint/InterpolationCheck
|
||||
end
|
||||
|
||||
|
@ -28,7 +25,7 @@ module QA
|
|||
end
|
||||
|
||||
def has_online_runner?
|
||||
page.has_css?('.runner-status-online')
|
||||
has_element?(:runner_status_online_content)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -129,7 +129,7 @@ RSpec.describe 'Admin updates settings' do
|
|||
|
||||
context 'Change Sign-up restrictions' do
|
||||
context 'Require Admin approval for new signup setting' do
|
||||
it 'changes the setting' do
|
||||
it 'changes the setting', :js do
|
||||
page.within('.as-signup') do
|
||||
check 'Require admin approval for new sign-ups'
|
||||
click_button 'Save changes'
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { GlFormCheckbox } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import SignupCheckbox from '~/pages/admin/application_settings/general/components/signup_checkbox.vue';
|
||||
|
||||
describe('Signup Form', () => {
|
||||
let wrapper;
|
||||
|
||||
const props = {
|
||||
name: 'name',
|
||||
helpText: 'some help text',
|
||||
label: 'a label',
|
||||
value: true,
|
||||
dataQaSelector: 'qa_selector',
|
||||
};
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMount(SignupCheckbox, {
|
||||
propsData: props,
|
||||
stubs: {
|
||||
GlFormCheckbox,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
|
||||
const findHiddenInput = () => findByTestId('input');
|
||||
const findCheckbox = () => wrapper.find(GlFormCheckbox);
|
||||
const findCheckboxLabel = () => findByTestId('label');
|
||||
const findHelpText = () => findByTestId('helpText');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('Signup Checkbox', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
describe('hidden input element', () => {
|
||||
it('gets passed correct values from props', () => {
|
||||
expect(findHiddenInput().attributes('name')).toBe(props.name);
|
||||
|
||||
expect(findHiddenInput().attributes('value')).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkbox', () => {
|
||||
it('gets passed correct checked value', () => {
|
||||
expect(findCheckbox().attributes('checked')).toBe('true');
|
||||
});
|
||||
|
||||
it('gets passed correct label', () => {
|
||||
expect(findCheckboxLabel().text()).toBe(props.label);
|
||||
});
|
||||
|
||||
it('gets passed correct help text', () => {
|
||||
expect(findHelpText().text()).toBe(props.helpText);
|
||||
});
|
||||
|
||||
it('gets passed data qa selector', () => {
|
||||
expect(findCheckbox().attributes('data-qa-selector')).toBe(props.dataQaSelector);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,198 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { within, fireEvent } from '@testing-library/dom';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||
import SignupForm from '~/pages/admin/application_settings/general/components/signup_form.vue';
|
||||
import { mockData } from '../mock_data';
|
||||
|
||||
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
|
||||
|
||||
describe('Signup Form', () => {
|
||||
let wrapper;
|
||||
let formSubmitSpy;
|
||||
|
||||
const mountComponent = ({ injectedProps = {}, mountFn = shallowMount, stubs = {} } = {}) => {
|
||||
wrapper = extendedWrapper(
|
||||
mountFn(SignupForm, {
|
||||
provide: {
|
||||
...mockData,
|
||||
...injectedProps,
|
||||
},
|
||||
stubs,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const queryByLabelText = (text) => within(wrapper.element).queryByLabelText(text);
|
||||
|
||||
const findForm = () => wrapper.findByTestId('form');
|
||||
const findInputCsrf = () => findForm().find('[name="authenticity_token"]');
|
||||
const findFormSubmitButton = () => findForm().find(GlButton);
|
||||
|
||||
const findDenyListRawRadio = () => queryByLabelText('Enter denylist manually');
|
||||
const findDenyListFileRadio = () => queryByLabelText('Upload denylist file');
|
||||
|
||||
const findDenyListRawInputGroup = () => wrapper.findByTestId('domain-denylist-raw-input-group');
|
||||
const findDenyListFileInputGroup = () => wrapper.findByTestId('domain-denylist-file-input-group');
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('form data', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
});
|
||||
|
||||
it.each`
|
||||
prop | propValue | elementSelector | formElementPassedDataType | formElementKey | expected
|
||||
${'signupEnabled'} | ${mockData.signupEnabled} | ${'[name="application_setting[signup_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.signupEnabled}
|
||||
${'requireAdminApprovalAfterUserSignup'} | ${mockData.requireAdminApprovalAfterUserSignup} | ${'[name="application_setting[require_admin_approval_after_user_signup]"]'} | ${'prop'} | ${'value'} | ${mockData.requireAdminApprovalAfterUserSignup}
|
||||
${'sendUserConfirmationEmail'} | ${mockData.sendUserConfirmationEmail} | ${'[name="application_setting[send_user_confirmation_email]"]'} | ${'prop'} | ${'value'} | ${mockData.sendUserConfirmationEmail}
|
||||
${'newUserSignupsCap'} | ${mockData.newUserSignupsCap} | ${'[name="application_setting[new_user_signups_cap]"]'} | ${'attribute'} | ${'value'} | ${mockData.newUserSignupsCap}
|
||||
${'minimumPasswordLength'} | ${mockData.minimumPasswordLength} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'value'} | ${mockData.minimumPasswordLength}
|
||||
${'minimumPasswordLengthMin'} | ${mockData.minimumPasswordLengthMin} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'min'} | ${mockData.minimumPasswordLengthMin}
|
||||
${'minimumPasswordLengthMax'} | ${mockData.minimumPasswordLengthMax} | ${'[name="application_setting[minimum_password_length]"]'} | ${'attribute'} | ${'max'} | ${mockData.minimumPasswordLengthMax}
|
||||
${'domainAllowlistRaw'} | ${mockData.domainAllowlistRaw} | ${'[name="application_setting[domain_allowlist_raw]"]'} | ${'value'} | ${'value'} | ${mockData.domainAllowlistRaw}
|
||||
${'domainDenylistEnabled'} | ${mockData.domainDenylistEnabled} | ${'[name="application_setting[domain_denylist_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.domainDenylistEnabled}
|
||||
${'denylistTypeRawSelected'} | ${mockData.denylistTypeRawSelected} | ${'[name="denylist_type"]'} | ${'attribute'} | ${'checked'} | ${'raw'}
|
||||
${'domainDenylistRaw'} | ${mockData.domainDenylistRaw} | ${'[name="application_setting[domain_denylist_raw]"]'} | ${'value'} | ${'value'} | ${mockData.domainDenylistRaw}
|
||||
${'emailRestrictionsEnabled'} | ${mockData.emailRestrictionsEnabled} | ${'[name="application_setting[email_restrictions_enabled]"]'} | ${'prop'} | ${'value'} | ${mockData.emailRestrictionsEnabled}
|
||||
${'emailRestrictions'} | ${mockData.emailRestrictions} | ${'[name="application_setting[email_restrictions]"]'} | ${'value'} | ${'value'} | ${mockData.emailRestrictions}
|
||||
${'afterSignUpText'} | ${mockData.afterSignUpText} | ${'[name="application_setting[after_sign_up_text]"]'} | ${'value'} | ${'value'} | ${mockData.afterSignUpText}
|
||||
`(
|
||||
'form element $elementSelector gets $expected value for $formElementKey $formElementPassedDataType when prop $prop is set to $propValue',
|
||||
({ elementSelector, expected, formElementKey, formElementPassedDataType }) => {
|
||||
const formElement = wrapper.find(elementSelector);
|
||||
|
||||
switch (formElementPassedDataType) {
|
||||
case 'attribute':
|
||||
expect(formElement.attributes(formElementKey)).toBe(expected);
|
||||
break;
|
||||
case 'prop':
|
||||
expect(formElement.props(formElementKey)).toBe(expected);
|
||||
break;
|
||||
case 'value':
|
||||
expect(formElement.element.value).toBe(expected);
|
||||
break;
|
||||
default:
|
||||
expect(formElement.props(formElementKey)).toBe(expected);
|
||||
break;
|
||||
}
|
||||
},
|
||||
);
|
||||
it('gets passed the path for action attribute', () => {
|
||||
expect(findForm().attributes('action')).toBe(mockData.settingsPath);
|
||||
});
|
||||
|
||||
it('gets passed the csrf token as a hidden input value', () => {
|
||||
expect(findInputCsrf().attributes('type')).toBe('hidden');
|
||||
|
||||
expect(findInputCsrf().attributes('value')).toBe('mock-csrf-token');
|
||||
});
|
||||
});
|
||||
|
||||
describe('form submit', () => {
|
||||
beforeEach(() => {
|
||||
formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
|
||||
|
||||
mountComponent({ stubs: { GlButton } });
|
||||
});
|
||||
|
||||
it('submits the form when the primary action is clicked', () => {
|
||||
findFormSubmitButton().trigger('click');
|
||||
|
||||
expect(formSubmitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('domain deny list', () => {
|
||||
describe('when it is set to raw from props', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ mountFn: mount });
|
||||
});
|
||||
|
||||
it('has raw list selected', () => {
|
||||
expect(findDenyListRawRadio().checked).toBe(true);
|
||||
});
|
||||
|
||||
it('has file not selected', () => {
|
||||
expect(findDenyListFileRadio().checked).toBe(false);
|
||||
});
|
||||
|
||||
it('raw list input is displayed', () => {
|
||||
expect(findDenyListRawInputGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('file input is not displayed', () => {
|
||||
expect(findDenyListFileInputGroup().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when user clicks on file radio', () => {
|
||||
beforeEach(() => {
|
||||
fireEvent.click(findDenyListFileRadio());
|
||||
});
|
||||
|
||||
it('has raw list not selected', () => {
|
||||
expect(findDenyListRawRadio().checked).toBe(false);
|
||||
});
|
||||
|
||||
it('has file selected', () => {
|
||||
expect(findDenyListFileRadio().checked).toBe(true);
|
||||
});
|
||||
|
||||
it('raw list input is not displayed', () => {
|
||||
expect(findDenyListRawInputGroup().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('file input is displayed', () => {
|
||||
expect(findDenyListFileInputGroup().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is set to file from injected props', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ mountFn: mount, injectedProps: { denylistTypeRawSelected: false } });
|
||||
});
|
||||
|
||||
it('has raw list not selected', () => {
|
||||
expect(findDenyListRawRadio().checked).toBe(false);
|
||||
});
|
||||
|
||||
it('has file selected', () => {
|
||||
expect(findDenyListFileRadio().checked).toBe(true);
|
||||
});
|
||||
|
||||
it('raw list input is not displayed', () => {
|
||||
expect(findDenyListRawInputGroup().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('file input is displayed', () => {
|
||||
expect(findDenyListFileInputGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
describe('when user clicks on raw list radio', () => {
|
||||
beforeEach(() => {
|
||||
fireEvent.click(findDenyListRawRadio());
|
||||
});
|
||||
|
||||
it('has raw list selected', () => {
|
||||
expect(findDenyListRawRadio().checked).toBe(true);
|
||||
});
|
||||
|
||||
it('has file not selected', () => {
|
||||
expect(findDenyListFileRadio().checked).toBe(false);
|
||||
});
|
||||
|
||||
it('raw list input is displayed', () => {
|
||||
expect(findDenyListRawInputGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('file input is not displayed', () => {
|
||||
expect(findDenyListFileInputGroup().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
export const rawMockData = {
|
||||
host: 'path/to/host',
|
||||
settingsPath: 'path/to/settings',
|
||||
signupEnabled: 'true',
|
||||
requireAdminApprovalAfterUserSignup: 'true',
|
||||
sendUserConfirmationEmail: 'true',
|
||||
minimumPasswordLength: '8',
|
||||
minimumPasswordLengthMin: '3',
|
||||
minimumPasswordLengthMax: '10',
|
||||
minimumPasswordLengthHelpLink: 'help/link',
|
||||
domainAllowlistRaw: 'domain1.com, domain2.com',
|
||||
newUserSignupsCap: '8',
|
||||
domainDenylistEnabled: 'true',
|
||||
denylistTypeRawSelected: 'true',
|
||||
domainDenylistRaw: 'domain2.com, domain3.com',
|
||||
emailRestrictionsEnabled: 'true',
|
||||
supportedSyntaxLinkUrl: '/supported/syntax/link',
|
||||
emailRestrictions: 'user1@domain.com, user2@domain.com',
|
||||
afterSignUpText: 'Congratulations on your successful sign-up!',
|
||||
};
|
||||
|
||||
export const mockData = {
|
||||
host: 'path/to/host',
|
||||
settingsPath: 'path/to/settings',
|
||||
signupEnabled: true,
|
||||
requireAdminApprovalAfterUserSignup: true,
|
||||
sendUserConfirmationEmail: true,
|
||||
minimumPasswordLength: '8',
|
||||
minimumPasswordLengthMin: '3',
|
||||
minimumPasswordLengthMax: '10',
|
||||
minimumPasswordLengthHelpLink: 'help/link',
|
||||
domainAllowlistRaw: 'domain1.com, domain2.com',
|
||||
newUserSignupsCap: '8',
|
||||
domainDenylistEnabled: true,
|
||||
denylistTypeRawSelected: true,
|
||||
domainDenylistRaw: 'domain2.com, domain3.com',
|
||||
emailRestrictionsEnabled: true,
|
||||
supportedSyntaxLinkUrl: '/supported/syntax/link',
|
||||
emailRestrictions: 'user1@domain.com, user2@domain.com',
|
||||
afterSignUpText: 'Congratulations on your successful sign-up!',
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
export const setDataAttributes = (data, element) => {
|
||||
Object.keys(data).forEach((key) => {
|
||||
const value = data[key];
|
||||
|
||||
// attribute should be:
|
||||
// - valueless if value is 'true'
|
||||
// - absent if value is 'false'
|
||||
switch (value) {
|
||||
case false:
|
||||
break;
|
||||
case true:
|
||||
element.dataset[`${key}`] = '';
|
||||
break;
|
||||
default:
|
||||
element.dataset[`${key}`] = value;
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
import { getParsedDataset } from '~/pages/admin/application_settings/utils';
|
||||
import { rawMockData, mockData } from './mock_data';
|
||||
|
||||
describe('utils', () => {
|
||||
describe('getParsedDataset', () => {
|
||||
it('returns correct results', () => {
|
||||
expect(
|
||||
getParsedDataset({
|
||||
dataset: rawMockData,
|
||||
booleanAttributes: [
|
||||
'signupEnabled',
|
||||
'requireAdminApprovalAfterUserSignup',
|
||||
'sendUserConfirmationEmail',
|
||||
'domainDenylistEnabled',
|
||||
'denylistTypeRawSelected',
|
||||
'emailRestrictionsEnabled',
|
||||
],
|
||||
}),
|
||||
).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
exports[`packages_list_app renders 1`] = `
|
||||
<div>
|
||||
<package-title-stub
|
||||
packagehelpurl="foo"
|
||||
<div
|
||||
help-url="foo"
|
||||
/>
|
||||
|
||||
<package-search-stub />
|
||||
<div />
|
||||
|
||||
<div>
|
||||
<section
|
||||
|
|
|
@ -3,7 +3,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import createFlash from '~/flash';
|
||||
import * as commonUtils from '~/lib/utils/common_utils';
|
||||
import PackageSearch from '~/packages/list/components/package_search.vue';
|
||||
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
|
||||
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
|
||||
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
|
||||
|
@ -26,10 +25,19 @@ describe('packages_list_app', () => {
|
|||
};
|
||||
const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
|
||||
|
||||
// we need to manually stub dynamic imported components because shallowMount is not able to stub them automatically. See: https://github.com/vuejs/vue-test-utils/issues/1279
|
||||
const PackageSearch = { name: 'PackageSearch', template: '<div></div>' };
|
||||
const PackageTitle = { name: 'PackageTitle', template: '<div></div>' };
|
||||
const InfrastructureTitle = { name: 'InfrastructureTitle', template: '<div></div>' };
|
||||
const InfrastructureSearch = { name: 'InfrastructureSearch', template: '<div></div>' };
|
||||
|
||||
const emptyListHelpUrl = 'helpUrl';
|
||||
const findEmptyState = () => wrapper.find(GlEmptyState);
|
||||
const findListComponent = () => wrapper.find(PackageList);
|
||||
const findPackageSearch = () => wrapper.find(PackageSearch);
|
||||
const findPackageTitle = () => wrapper.find(PackageTitle);
|
||||
const findInfrastructureTitle = () => wrapper.find(InfrastructureTitle);
|
||||
const findInfrastructureSearch = () => wrapper.find(InfrastructureSearch);
|
||||
|
||||
const createStore = (filter = []) => {
|
||||
store = new Vuex.Store({
|
||||
|
@ -47,7 +55,7 @@ describe('packages_list_app', () => {
|
|||
store.dispatch = jest.fn();
|
||||
};
|
||||
|
||||
const mountComponent = () => {
|
||||
const mountComponent = (provide) => {
|
||||
wrapper = shallowMount(PackageListApp, {
|
||||
localVue,
|
||||
store,
|
||||
|
@ -57,7 +65,12 @@ describe('packages_list_app', () => {
|
|||
PackageList,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
PackageSearch,
|
||||
PackageTitle,
|
||||
InfrastructureTitle,
|
||||
InfrastructureSearch,
|
||||
},
|
||||
provide,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -194,6 +207,31 @@ describe('packages_list_app', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Infrastructure config', () => {
|
||||
it('defaults to package registry components', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findPackageSearch().exists()).toBe(true);
|
||||
expect(findPackageTitle().exists()).toBe(true);
|
||||
|
||||
expect(findInfrastructureTitle().exists()).toBe(false);
|
||||
expect(findInfrastructureSearch().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('mount different component based on the provided values', () => {
|
||||
mountComponent({
|
||||
titleComponent: 'InfrastructureTitle',
|
||||
searchComponent: 'InfrastructureSearch',
|
||||
});
|
||||
|
||||
expect(findPackageSearch().exists()).toBe(false);
|
||||
expect(findPackageTitle().exists()).toBe(false);
|
||||
|
||||
expect(findInfrastructureTitle().exists()).toBe(true);
|
||||
expect(findInfrastructureSearch().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete alert handling', () => {
|
||||
const { location } = window.location;
|
||||
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
|
||||
|
|
|
@ -11,7 +11,7 @@ describe('PackageTitle', () => {
|
|||
const findTitleArea = () => wrapper.find(TitleArea);
|
||||
const findMetadataItem = () => wrapper.find(MetadataItem);
|
||||
|
||||
const mountComponent = (propsData = { packageHelpUrl: 'foo' }) => {
|
||||
const mountComponent = (propsData = { helpUrl: 'foo' }) => {
|
||||
wrapper = shallowMount(PackageTitle, {
|
||||
store,
|
||||
propsData,
|
||||
|
@ -44,15 +44,15 @@ describe('PackageTitle', () => {
|
|||
});
|
||||
|
||||
describe.each`
|
||||
packagesCount | exist | text
|
||||
${null} | ${false} | ${''}
|
||||
${undefined} | ${false} | ${''}
|
||||
${0} | ${true} | ${'0 Packages'}
|
||||
${1} | ${true} | ${'1 Package'}
|
||||
${2} | ${true} | ${'2 Packages'}
|
||||
`('when packagesCount is $packagesCount metadata item', ({ packagesCount, exist, text }) => {
|
||||
count | exist | text
|
||||
${null} | ${false} | ${''}
|
||||
${undefined} | ${false} | ${''}
|
||||
${0} | ${true} | ${'0 Packages'}
|
||||
${1} | ${true} | ${'1 Package'}
|
||||
${2} | ${true} | ${'2 Packages'}
|
||||
`('when count is $count metadata item', ({ count, exist, text }) => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ packagesCount, packageHelpUrl: 'foo' });
|
||||
mountComponent({ count, helpUrl: 'foo' });
|
||||
});
|
||||
|
||||
it(`is ${exist} that it exists`, () => {
|
||||
|
|
|
@ -51,20 +51,7 @@ exports[`packages_list_row renders 1`] = `
|
|||
|
||||
<!---->
|
||||
|
||||
<div
|
||||
class="d-flex align-items-center"
|
||||
data-testid="package-type"
|
||||
>
|
||||
<gl-icon-stub
|
||||
class="gl-ml-3 gl-mr-2"
|
||||
name="package"
|
||||
size="16"
|
||||
/>
|
||||
|
||||
<span>
|
||||
Maven
|
||||
</span>
|
||||
</div>
|
||||
<div />
|
||||
|
||||
<package-path-stub
|
||||
path="foo/bar/baz"
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue';
|
||||
|
||||
describe('PackageIconAndName', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIcon = () => wrapper.find(GlIcon);
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMount(PackageIconAndName, {
|
||||
slots: {
|
||||
default: 'test',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
it('has an icon', () => {
|
||||
mountComponent();
|
||||
|
||||
const icon = findIcon();
|
||||
|
||||
expect(icon.exists()).toBe(true);
|
||||
expect(icon.props('name')).toBe('package');
|
||||
});
|
||||
|
||||
it('renders the slot content', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.text()).toBe('test');
|
||||
});
|
||||
});
|
|
@ -1,7 +1,9 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
|
||||
import PackagePath from '~/packages/shared/components/package_path.vue';
|
||||
import PackageTags from '~/packages/shared/components/package_tags.vue';
|
||||
|
||||
import ListItem from '~/vue_shared/components/registry/list_item.vue';
|
||||
import { packageList } from '../../mock_data';
|
||||
|
||||
|
@ -11,20 +13,30 @@ describe('packages_list_row', () => {
|
|||
|
||||
const [packageWithoutTags, packageWithTags] = packageList;
|
||||
|
||||
const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' };
|
||||
const PackageIconAndName = { name: 'PackageIconAndName', template: '<div></div>' };
|
||||
|
||||
const findPackageTags = () => wrapper.find(PackageTags);
|
||||
const findPackagePath = () => wrapper.find(PackagePath);
|
||||
const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]');
|
||||
const findPackageType = () => wrapper.find('[data-testid="package-type"]');
|
||||
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
|
||||
const findInfrastructureIconAndName = () => wrapper.find(InfrastructureIconAndName);
|
||||
|
||||
const mountComponent = ({
|
||||
isGroup = false,
|
||||
packageEntity = packageWithoutTags,
|
||||
showPackageType = true,
|
||||
disableDelete = false,
|
||||
provide,
|
||||
} = {}) => {
|
||||
wrapper = shallowMount(PackagesListRow, {
|
||||
store,
|
||||
stubs: { ListItem },
|
||||
provide,
|
||||
stubs: {
|
||||
ListItem,
|
||||
InfrastructureIconAndName,
|
||||
PackageIconAndName,
|
||||
},
|
||||
propsData: {
|
||||
packageLink: 'foo',
|
||||
packageEntity,
|
||||
|
@ -72,13 +84,13 @@ describe('packages_list_row', () => {
|
|||
it('shows the type when set', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findPackageType().exists()).toBe(true);
|
||||
expect(findPackageIconAndName().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not show the type when not set', () => {
|
||||
mountComponent({ showPackageType: false });
|
||||
|
||||
expect(findPackageType().exists()).toBe(false);
|
||||
expect(findPackageIconAndName().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -113,4 +125,25 @@ describe('packages_list_row', () => {
|
|||
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Infrastructure config', () => {
|
||||
it('defaults to package registry components', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findPackageIconAndName().exists()).toBe(true);
|
||||
expect(findInfrastructureIconAndName().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('mounts different component based on the provided values', () => {
|
||||
mountComponent({
|
||||
provide: {
|
||||
iconComponent: 'InfrastructureIconAndName',
|
||||
},
|
||||
});
|
||||
|
||||
expect(findPackageIconAndName().exists()).toBe(false);
|
||||
|
||||
expect(findInfrastructureIconAndName().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { GlIcon } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue';
|
||||
|
||||
describe('InfrastructureIconAndName', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIcon = () => wrapper.find(GlIcon);
|
||||
|
||||
const mountComponent = () => {
|
||||
wrapper = shallowMount(InfrastructureIconAndName, {});
|
||||
};
|
||||
|
||||
it('has an icon', () => {
|
||||
mountComponent();
|
||||
|
||||
const icon = findIcon();
|
||||
|
||||
expect(icon.exists()).toBe(true);
|
||||
expect(icon.props('name')).toBe('infrastructure-registry');
|
||||
});
|
||||
|
||||
it('has the type fixed to terraform', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.text()).toBe('Terraform');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,129 @@
|
|||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||
import Vuex from 'vuex';
|
||||
import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue';
|
||||
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
|
||||
import UrlSync from '~/vue_shared/components/url_sync.vue';
|
||||
|
||||
const localVue = createLocalVue();
|
||||
localVue.use(Vuex);
|
||||
|
||||
describe('Infrastructure Search', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const sortableFields = () => [
|
||||
{ orderBy: 'name', label: 'Name' },
|
||||
{ orderBy: 'project_path', label: 'Project' },
|
||||
{ orderBy: 'version', label: 'Version' },
|
||||
{ orderBy: 'created_at', label: 'Published' },
|
||||
];
|
||||
|
||||
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
|
||||
const findUrlSync = () => wrapper.findComponent(UrlSync);
|
||||
|
||||
const createStore = (isGroupPage) => {
|
||||
const state = {
|
||||
config: {
|
||||
isGroupPage,
|
||||
},
|
||||
sorting: {
|
||||
orderBy: 'version',
|
||||
sort: 'desc',
|
||||
},
|
||||
filter: [],
|
||||
};
|
||||
store = new Vuex.Store({
|
||||
state,
|
||||
});
|
||||
store.dispatch = jest.fn();
|
||||
};
|
||||
|
||||
const mountComponent = (isGroupPage = false) => {
|
||||
createStore(isGroupPage);
|
||||
|
||||
wrapper = shallowMount(component, {
|
||||
localVue,
|
||||
store,
|
||||
stubs: {
|
||||
UrlSync,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
it('has a registry search component', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findRegistrySearch().exists()).toBe(true);
|
||||
expect(findRegistrySearch().props()).toMatchObject({
|
||||
filter: store.state.filter,
|
||||
sorting: store.state.sorting,
|
||||
tokens: [],
|
||||
sortableFields: sortableFields(),
|
||||
});
|
||||
});
|
||||
|
||||
it.each`
|
||||
isGroupPage | page
|
||||
${false} | ${'project'}
|
||||
${true} | ${'group'}
|
||||
`('in a $page page binds the right props', ({ isGroupPage }) => {
|
||||
mountComponent(isGroupPage);
|
||||
|
||||
expect(findRegistrySearch().props()).toMatchObject({
|
||||
filter: store.state.filter,
|
||||
sorting: store.state.sorting,
|
||||
tokens: [],
|
||||
sortableFields: sortableFields(),
|
||||
});
|
||||
});
|
||||
|
||||
it('on sorting:changed emits update event and calls vuex setSorting', () => {
|
||||
const payload = { sort: 'foo' };
|
||||
|
||||
mountComponent();
|
||||
|
||||
findRegistrySearch().vm.$emit('sorting:changed', payload);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
|
||||
expect(wrapper.emitted('update')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('on filter:changed calls vuex setFilter', () => {
|
||||
const payload = ['foo'];
|
||||
|
||||
mountComponent();
|
||||
|
||||
findRegistrySearch().vm.$emit('filter:changed', payload);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
|
||||
});
|
||||
|
||||
it('on filter:submit emits update event', () => {
|
||||
mountComponent();
|
||||
|
||||
findRegistrySearch().vm.$emit('filter:submit');
|
||||
|
||||
expect(wrapper.emitted('update')).toEqual([[]]);
|
||||
});
|
||||
|
||||
it('has a UrlSync component', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findUrlSync().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('on query:changed calls updateQuery from UrlSync', () => {
|
||||
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
|
||||
|
||||
mountComponent();
|
||||
|
||||
findRegistrySearch().vm.$emit('query:changed');
|
||||
|
||||
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,75 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import component from '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue';
|
||||
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
|
||||
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
|
||||
|
||||
describe('Infrastructure Title', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findTitleArea = () => wrapper.find(TitleArea);
|
||||
const findMetadataItem = () => wrapper.find(MetadataItem);
|
||||
|
||||
const mountComponent = (propsData = { helpUrl: 'foo' }) => {
|
||||
wrapper = shallowMount(component, {
|
||||
store,
|
||||
propsData,
|
||||
stubs: {
|
||||
TitleArea,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
wrapper = null;
|
||||
});
|
||||
|
||||
describe('title area', () => {
|
||||
it('exists', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTitleArea().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has the correct props', () => {
|
||||
mountComponent();
|
||||
|
||||
expect(findTitleArea().props()).toMatchObject({
|
||||
title: 'Infrastructure Registry',
|
||||
infoMessages: [
|
||||
{
|
||||
text: 'Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}',
|
||||
link: 'foo',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe.each`
|
||||
count | exist | text
|
||||
${null} | ${false} | ${''}
|
||||
${undefined} | ${false} | ${''}
|
||||
${0} | ${true} | ${'0 Modules'}
|
||||
${1} | ${true} | ${'1 Module'}
|
||||
${2} | ${true} | ${'2 Modules'}
|
||||
`('when count is $count metadata item', ({ count, exist, text }) => {
|
||||
beforeEach(() => {
|
||||
mountComponent({ count, helpUrl: 'foo' });
|
||||
});
|
||||
|
||||
it(`is ${exist} that it exists`, () => {
|
||||
expect(findMetadataItem().exists()).toBe(exist);
|
||||
});
|
||||
|
||||
if (exist) {
|
||||
it('has the correct props', () => {
|
||||
expect(findMetadataItem().props()).toMatchObject({
|
||||
icon: 'infrastructure-registry',
|
||||
text,
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
|
@ -3,19 +3,26 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Ci::RunnersHelper do
|
||||
it "returns - not contacted yet" do
|
||||
runner = FactoryBot.build :ci_runner
|
||||
expect(runner_status_icon(runner)).to include("not connected yet")
|
||||
end
|
||||
describe '#runner_status_icon', :clean_gitlab_redis_cache do
|
||||
it "returns - not contacted yet" do
|
||||
runner = create(:ci_runner)
|
||||
expect(runner_status_icon(runner)).to include("not connected yet")
|
||||
end
|
||||
|
||||
it "returns offline text" do
|
||||
runner = FactoryBot.build(:ci_runner, contacted_at: 1.day.ago, active: true)
|
||||
expect(runner_status_icon(runner)).to include("Runner is offline")
|
||||
end
|
||||
it "returns offline text" do
|
||||
runner = create(:ci_runner, contacted_at: 1.day.ago, active: true)
|
||||
expect(runner_status_icon(runner)).to include("Runner is offline")
|
||||
end
|
||||
|
||||
it "returns online text" do
|
||||
runner = FactoryBot.build(:ci_runner, contacted_at: 1.second.ago, active: true)
|
||||
expect(runner_status_icon(runner)).to include("Runner is online")
|
||||
it "returns online text" do
|
||||
runner = create(:ci_runner, contacted_at: 1.second.ago, active: true)
|
||||
expect(runner_status_icon(runner)).to include("Runner is online")
|
||||
end
|
||||
|
||||
it "returns paused text" do
|
||||
runner = create(:ci_runner, contacted_at: 1.second.ago, active: false)
|
||||
expect(runner_status_icon(runner)).to include("Runner is paused")
|
||||
end
|
||||
end
|
||||
|
||||
describe '#runner_contacted_at' do
|
||||
|
|
|
@ -875,7 +875,7 @@ module Gitlab
|
|||
config = YAML.dump({ image: "ruby:2.7",
|
||||
services: ["mysql"],
|
||||
before_script: ["pwd"],
|
||||
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
|
||||
rspec: { image: { name: "ruby:3.0", entrypoint: ["/usr/local/bin/init", "run"] },
|
||||
services: [{ name: "postgresql", alias: "db-pg",
|
||||
entrypoint: ["/usr/local/bin/init", "run"],
|
||||
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
|
||||
|
@ -892,7 +892,7 @@ module Gitlab
|
|||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
|
||||
image: { name: "ruby:3.0", entrypoint: ["/usr/local/bin/init", "run"] },
|
||||
services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
|
||||
command: ["/usr/local/bin/init", "run"] },
|
||||
{ name: "docker:dind" }]
|
||||
|
@ -941,7 +941,7 @@ module Gitlab
|
|||
config = YAML.dump({ image: "ruby:2.7",
|
||||
services: ["mysql"],
|
||||
before_script: ["pwd"],
|
||||
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
|
||||
rspec: { image: "ruby:3.0", services: ["postgresql", "docker:dind"], script: "rspec" } })
|
||||
|
||||
config_processor = Gitlab::Ci::YamlProcessor.new(config).execute
|
||||
|
||||
|
@ -954,7 +954,7 @@ module Gitlab
|
|||
options: {
|
||||
before_script: ["pwd"],
|
||||
script: ["rspec"],
|
||||
image: { name: "ruby:2.5" },
|
||||
image: { name: "ruby:3.0" },
|
||||
services: [{ name: "postgresql" }, { name: "docker:dind" }]
|
||||
},
|
||||
allow_failure: false,
|
||||
|
|
|
@ -132,7 +132,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do
|
|||
{ before_script: %w[ls pwd],
|
||||
script: 'sleep 100',
|
||||
tags: ['webide'],
|
||||
image: 'ruby:2.5',
|
||||
image: 'ruby:3.0',
|
||||
services: ['mysql'],
|
||||
variables: { KEY: 'value' } }
|
||||
end
|
||||
|
@ -144,7 +144,7 @@ RSpec.describe Gitlab::WebIde::Config::Entry::Terminal do
|
|||
yaml_variables: [{ key: 'KEY', value: 'value', public: true }],
|
||||
job_variables: [{ key: 'KEY', value: 'value', public: true }],
|
||||
options: {
|
||||
image: { name: "ruby:2.5" },
|
||||
image: { name: "ruby:3.0" },
|
||||
services: [{ name: "mysql" }],
|
||||
before_script: %w[ls pwd],
|
||||
script: ['sleep 100']
|
||||
|
|
|
@ -1,6 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'namespace traversal' do
|
||||
shared_examples 'recursive version' do |method|
|
||||
let(:recursive_method) { "recursive_#{method}" }
|
||||
|
||||
it "is equivalent to ##{method}" do
|
||||
groups.each do |group|
|
||||
expect(group.public_send(method)).to match_array group.public_send(recursive_method)
|
||||
end
|
||||
end
|
||||
|
||||
it "makes a recursive query" do
|
||||
groups.each do |group|
|
||||
expect { group.public_send(recursive_method).load }.to make_queries_matching(/WITH RECURSIVE/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self_and_hierarchy' do
|
||||
let!(:group) { create(:group, path: 'git_lab') }
|
||||
let!(:nested_group) { create(:group, parent: group) }
|
||||
|
@ -14,6 +30,12 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
expect(nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
|
||||
expect(very_deep_nested_group.self_and_hierarchy).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
|
||||
end
|
||||
|
||||
describe '#recursive_self_and_hierarchy' do
|
||||
let(:groups) { [group, nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :self_and_hierarchy
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ancestors' do
|
||||
|
@ -28,6 +50,12 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
expect(nested_group.ancestors).to include(group)
|
||||
expect(group.ancestors).to eq([])
|
||||
end
|
||||
|
||||
describe '#recursive_ancestors' do
|
||||
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :ancestors
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self_and_ancestors' do
|
||||
|
@ -42,6 +70,12 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
expect(nested_group.self_and_ancestors).to contain_exactly(group, nested_group)
|
||||
expect(group.self_and_ancestors).to contain_exactly(group)
|
||||
end
|
||||
|
||||
describe '#recursive_self_and_ancestors' do
|
||||
let(:groups) { [nested_group, deep_nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :self_and_ancestors
|
||||
end
|
||||
end
|
||||
|
||||
describe '#descendants' do
|
||||
|
@ -58,6 +92,12 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
expect(nested_group.descendants.to_a).to include(deep_nested_group, very_deep_nested_group)
|
||||
expect(group.descendants.to_a).to include(nested_group, deep_nested_group, very_deep_nested_group)
|
||||
end
|
||||
|
||||
describe '#recursive_descendants' do
|
||||
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :descendants
|
||||
end
|
||||
end
|
||||
|
||||
describe '#self_and_descendants' do
|
||||
|
@ -74,5 +114,11 @@ RSpec.shared_examples 'namespace traversal' do
|
|||
expect(nested_group.self_and_descendants).to contain_exactly(nested_group, deep_nested_group, very_deep_nested_group)
|
||||
expect(group.self_and_descendants).to contain_exactly(group, nested_group, deep_nested_group, very_deep_nested_group)
|
||||
end
|
||||
|
||||
describe '#recursive_self_and_descendants' do
|
||||
let(:groups) { [group, nested_group, deep_nested_group, very_deep_nested_group] }
|
||||
|
||||
it_behaves_like 'recursive version', :self_and_descendants
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8462,10 +8462,10 @@ moment-mini@^2.22.1:
|
|||
resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.22.1.tgz#bc32d73e43a4505070be6b53494b17623183420d"
|
||||
integrity sha512-OUCkHOz7ehtNMYuZjNciXUfwTuz8vmF1MTbAy59ebf+ZBYZO5/tZKuChVWCX+uDo+4idJBpGltNfV8st+HwsGw==
|
||||
|
||||
monaco-editor-webpack-plugin@^1.9.0:
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.0.tgz#5b547281b9f404057dc5d8c5722390df9ac90be6"
|
||||
integrity sha512-tOiiToc94E1sb50BgZ8q8WK/bxus77SRrwCqIpAB5er3cpX78SULbEBY4YPOB8kDolOzKRt30WIHG/D6gz69Ww==
|
||||
monaco-editor-webpack-plugin@^1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.9.1.tgz#eb4bbb1c5e5bfb554541c1ae1542e74c2a9f43fd"
|
||||
integrity sha512-x7fx1w3i/uwZERIgztHAAK3VQMsL8+ku0lFXXbO81hKDg8IieACqjGEa2mqEueg0c/fX+wd0oI+75wB19KJAsA==
|
||||
dependencies:
|
||||
loader-utils "^1.2.3"
|
||||
|
||||
|
|
Loading…
Reference in New Issue