Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-14 09:09:14 +00:00
parent 90fa047c0d
commit 4ab67d6529
51 changed files with 1638 additions and 297 deletions

View File

@ -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
}

View File

@ -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: {

View File

@ -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>

View File

@ -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,
},

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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');
},
});
};

View File

@ -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();
}

View File

@ -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>

View File

@ -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>

View File

@ -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();
})();

View File

@ -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,
},
});
},
});
}

View File

@ -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),
});
}

View File

@ -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;
};

View File

@ -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();

View File

@ -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;
}
}

View File

@ -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

View File

@ -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) })

View File

@ -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] } }

View File

@ -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') } }

View File

@ -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")

View File

@ -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

View File

@ -3,7 +3,7 @@
class UpdateHighestRoleWorker
include ApplicationWorker
feature_category :authentication_and_authorization
feature_category :utilization
urgency :high
weight 2

View File

@ -0,0 +1,6 @@
---
title: Improve runners status icon usability and accessibility in the project settings
view
merge_request: 58781
author:
type: changed

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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);
});
});
});
});

View File

@ -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);
});
});
});
});
});

View File

@ -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!',
};

View File

@ -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;
}
});
};

View File

@ -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);
});
});
});

View File

@ -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

View File

@ -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`;

View File

@ -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`, () => {

View File

@ -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"

View File

@ -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');
});
});

View File

@ -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);
});
});
});

View File

@ -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');
});
});

View File

@ -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();
});
});

View File

@ -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,
});
});
}
});
});

View File

@ -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

View File

@ -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,

View File

@ -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']

View File

@ -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

View File

@ -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"