Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
edb317e9fe
commit
7bbc731c75
|
@ -152,41 +152,3 @@ feature-flags-usage:
|
||||||
when: always
|
when: always
|
||||||
paths:
|
paths:
|
||||||
- tmp/feature_flags/
|
- tmp/feature_flags/
|
||||||
|
|
||||||
semgrep-appsec-custom-rules:
|
|
||||||
stage: lint
|
|
||||||
extends:
|
|
||||||
- .static-analysis:rules:ee
|
|
||||||
image: returntocorp/semgrep
|
|
||||||
needs: []
|
|
||||||
script:
|
|
||||||
# Required to avoid a timeout https://github.com/returntocorp/semgrep/issues/5395
|
|
||||||
- git fetch origin master
|
|
||||||
# Include/exclude list isn't ideal https://github.com/returntocorp/semgrep/issues/5399
|
|
||||||
- |
|
|
||||||
semgrep ci --gitlab-sast --metrics off --config $CUSTOM_RULES_URL \
|
|
||||||
--include app --include lib --include workhorse \
|
|
||||||
--exclude '*_test.go' --exclude spec --exclude qa > gl-sast-report.json || true
|
|
||||||
variables:
|
|
||||||
CUSTOM_RULES_URL: https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/-/raw/main/appsec-pings/rules.yml
|
|
||||||
artifacts:
|
|
||||||
paths:
|
|
||||||
- gl-sast-report.json
|
|
||||||
reports:
|
|
||||||
sast: gl-sast-report.json
|
|
||||||
|
|
||||||
ping-appsec-for-sast-findings:
|
|
||||||
stage: lint
|
|
||||||
image: alpine:latest
|
|
||||||
variables:
|
|
||||||
# Project Access Token bot ID for /gitlab-com/gl-security/appsec/sast-custom-rules
|
|
||||||
BOT_USER_ID: 11727358
|
|
||||||
needs:
|
|
||||||
- semgrep-appsec-custom-rules
|
|
||||||
rules:
|
|
||||||
# Requiring $CUSTOM_SAST_RULES_BOT_PAT prevents the bot from running on forks or CE
|
|
||||||
# Without it the script would fail too.
|
|
||||||
- if: "$CI_MERGE_REQUEST_IID && $CUSTOM_SAST_RULES_BOT_PAT"
|
|
||||||
script:
|
|
||||||
- apk add jq curl
|
|
||||||
- scripts/process_custom_semgrep_results.sh
|
|
||||||
|
|
107
.semgrepignore
107
.semgrepignore
|
@ -1,107 +0,0 @@
|
||||||
*.log
|
|
||||||
*.swp
|
|
||||||
*.mo
|
|
||||||
*.edit.po
|
|
||||||
*.rej
|
|
||||||
.dir-locals.el
|
|
||||||
.DS_Store
|
|
||||||
.bundle
|
|
||||||
.chef
|
|
||||||
.directory
|
|
||||||
.eslintcache
|
|
||||||
/.envrc
|
|
||||||
eslint-report.html
|
|
||||||
/.gitlab_shell_secret
|
|
||||||
.idea
|
|
||||||
.nova
|
|
||||||
/.vscode/*
|
|
||||||
/.rbenv-version
|
|
||||||
.rbx/
|
|
||||||
/.ruby-gemset
|
|
||||||
/.ruby-version
|
|
||||||
/.tool-versions
|
|
||||||
/.rvmrc
|
|
||||||
/.secret
|
|
||||||
.sass-cache/
|
|
||||||
/.vagrant
|
|
||||||
/.yarn-cache
|
|
||||||
/.byebug_history
|
|
||||||
/Vagrantfile
|
|
||||||
/app/assets/images/icons.json
|
|
||||||
/app/assets/images/icons.svg
|
|
||||||
/app/assets/images/illustrations/
|
|
||||||
/app/assets/javascripts/locale/**/app.js
|
|
||||||
/backups/*
|
|
||||||
/config/aws.yml
|
|
||||||
/config/cable.yml
|
|
||||||
/config/database*.yml
|
|
||||||
/config/gitlab.yml
|
|
||||||
/config/gitlab_ci.yml
|
|
||||||
/config/Gitlab.gitlab-license
|
|
||||||
/config/initializers/smtp_settings.rb
|
|
||||||
/config/initializers/relative_url.rb
|
|
||||||
/config/resque.yml
|
|
||||||
/config/redis.*.yml
|
|
||||||
/config/unicorn.rb
|
|
||||||
/config/puma.rb
|
|
||||||
/config/secrets.yml
|
|
||||||
/config/sidekiq.yml
|
|
||||||
/config/registry.key
|
|
||||||
/coverage/*
|
|
||||||
/db/*.sqlite3
|
|
||||||
/db/*.sqlite3-journal
|
|
||||||
/db/data.yml
|
|
||||||
/doc/code/*
|
|
||||||
/dump.rdb
|
|
||||||
/jsconfig.json
|
|
||||||
/lefthook-local.yml
|
|
||||||
/log/*.log*
|
|
||||||
/node_modules
|
|
||||||
/nohup.out
|
|
||||||
/public/assets/
|
|
||||||
/public/uploads.*
|
|
||||||
/public/uploads/
|
|
||||||
/public/sitemap.xml
|
|
||||||
/public/sitemap.xml.gz
|
|
||||||
/shared/artifacts/
|
|
||||||
/spec/examples.txt
|
|
||||||
/rails_best_practices_output.html
|
|
||||||
/tags
|
|
||||||
/vendor/bundle/*
|
|
||||||
/vendor/gitaly-ruby
|
|
||||||
/builds*
|
|
||||||
/.gitlab_workhorse_secret
|
|
||||||
/.gitlab_pages_secret
|
|
||||||
/.gitlab_kas_secret
|
|
||||||
/webpack-report/
|
|
||||||
/crystalball/
|
|
||||||
/test_results/
|
|
||||||
/deprecations/
|
|
||||||
/knapsack/
|
|
||||||
/rspec_flaky/
|
|
||||||
/rspec/
|
|
||||||
/locale/**/LC_MESSAGES
|
|
||||||
/locale/**/*.time_stamp
|
|
||||||
/.rspec
|
|
||||||
/.gitlab_smime_key
|
|
||||||
/.gitlab_smime_cert
|
|
||||||
package-lock.json
|
|
||||||
/junit_*.xml
|
|
||||||
/coverage-frontend/
|
|
||||||
jsdoc/
|
|
||||||
**/tmp/rubocop_cache/**
|
|
||||||
.overcommit.yml
|
|
||||||
.overcommit.yml.backup
|
|
||||||
.projections.json
|
|
||||||
/qa/.rakeTasks
|
|
||||||
webpack-dev-server.json
|
|
||||||
/.nvimrc
|
|
||||||
.solargraph.yml
|
|
||||||
/tmp/matching_foss_tests.txt
|
|
||||||
/tmp/matching_tests.txt
|
|
||||||
ee/changelogs/unreleased-ee
|
|
||||||
/sitespeed-result
|
|
||||||
tags.lock
|
|
||||||
tags.temp
|
|
||||||
.stylelintcache
|
|
||||||
.solargraph.yml
|
|
|
@ -1 +1 @@
|
||||||
f099614e635d05483055ba6fbebc74d961bf2ce5
|
70d6aa021ebfc05d9d727a7eb4c9ff4782db4c30
|
||||||
|
|
|
@ -21,6 +21,7 @@ export default {
|
||||||
description: __("Make sure you save it - you won't be able to access it again."),
|
description: __("Make sure you save it - you won't be able to access it again."),
|
||||||
label: __('Your new %{accessTokenType}'),
|
label: __('Your new %{accessTokenType}'),
|
||||||
},
|
},
|
||||||
|
tokenInputId: 'new-access-token',
|
||||||
inject: ['accessTokenType'],
|
inject: ['accessTokenType'],
|
||||||
data() {
|
data() {
|
||||||
return { errors: null, infoAlert: null, newToken: null };
|
return { errors: null, infoAlert: null, newToken: null };
|
||||||
|
@ -41,6 +42,14 @@ export default {
|
||||||
copyButtonTitle() {
|
copyButtonTitle() {
|
||||||
return sprintf(this.$options.i18n.copyButtonTitle, { accessTokenType: this.accessTokenType });
|
return sprintf(this.$options.i18n.copyButtonTitle, { accessTokenType: this.accessTokenType });
|
||||||
},
|
},
|
||||||
|
formInputGroupProps() {
|
||||||
|
return {
|
||||||
|
id: this.$options.tokenInputId,
|
||||||
|
class: 'qa-created-access-token',
|
||||||
|
'data-qa-selector': 'created_access_token_field',
|
||||||
|
name: this.$options.tokenInputId,
|
||||||
|
};
|
||||||
|
},
|
||||||
label() {
|
label() {
|
||||||
return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType });
|
return sprintf(this.$options.i18n.label, { accessTokenType: this.accessTokenType });
|
||||||
},
|
},
|
||||||
|
@ -92,16 +101,15 @@ export default {
|
||||||
<template v-if="newToken">
|
<template v-if="newToken">
|
||||||
<!--
|
<!--
|
||||||
After issue https://gitlab.com/gitlab-org/gitlab/-/issues/360921 is
|
After issue https://gitlab.com/gitlab-org/gitlab/-/issues/360921 is
|
||||||
closed remove the `initial-visibility` and `input-class` props.
|
closed remove the `initial-visibility`.
|
||||||
-->
|
-->
|
||||||
<input-copy-toggle-visibility
|
<input-copy-toggle-visibility
|
||||||
data-testid="new-access-token"
|
|
||||||
:copy-button-title="copyButtonTitle"
|
:copy-button-title="copyButtonTitle"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:label-for="$options.tokenInputId"
|
||||||
:value="newToken"
|
:value="newToken"
|
||||||
initial-visibility
|
initial-visibility
|
||||||
input-class="qa-created-access-token"
|
:form-input-group-props="formInputGroupProps"
|
||||||
qa-selector="created_access_token_field"
|
|
||||||
>
|
>
|
||||||
<template #description>
|
<template #description>
|
||||||
{{ $options.i18n.description }}
|
{{ $options.i18n.description }}
|
||||||
|
|
|
@ -17,7 +17,7 @@ export default {
|
||||||
text: __('Syntax is incorrect.'),
|
text: __('Syntax is incorrect.'),
|
||||||
},
|
},
|
||||||
includesText: __(
|
includesText: __(
|
||||||
'CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}',
|
'CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}',
|
||||||
),
|
),
|
||||||
warningTitle: __('The form contains the following warning:'),
|
warningTitle: __('The form contains the following warning:'),
|
||||||
fields: [
|
fields: [
|
||||||
|
|
|
@ -231,7 +231,7 @@ export default {
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="linkedPipeline"
|
ref="linkedPipeline"
|
||||||
class="gl-h-full gl-display-flex!"
|
class="gl-h-full gl-display-flex! gl-px-2"
|
||||||
:class="flexDirection"
|
:class="flexDirection"
|
||||||
data-qa-selector="linked_pipeline_container"
|
data-qa-selector="linked_pipeline_container"
|
||||||
@mouseover="onDownstreamHovered"
|
@mouseover="onDownstreamHovered"
|
||||||
|
|
|
@ -45,7 +45,6 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
currentRegistrationToken: this.registrationToken,
|
currentRegistrationToken: this.registrationToken,
|
||||||
instructionsModalOpened: false,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
@ -64,15 +63,7 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onShowInstructionsClick() {
|
onShowInstructionsClick() {
|
||||||
// Rendering the modal on demand, to avoid
|
this.$refs.runnerInstructionsModal.show();
|
||||||
// loading instructions prematurely from API.
|
|
||||||
this.instructionsModalOpened = true;
|
|
||||||
|
|
||||||
this.$nextTick(() => {
|
|
||||||
// $refs.runnerInstructionsModal is defined in
|
|
||||||
// the tick after the modal is rendered
|
|
||||||
this.$refs.runnerInstructionsModal.show();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
onTokenReset(token) {
|
onTokenReset(token) {
|
||||||
this.currentRegistrationToken = token;
|
this.currentRegistrationToken = token;
|
||||||
|
@ -94,7 +85,6 @@ export default {
|
||||||
<gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
|
<gl-dropdown-item @click.capture.native.stop="onShowInstructionsClick">
|
||||||
{{ $options.i18n.showInstallationInstructions }}
|
{{ $options.i18n.showInstallationInstructions }}
|
||||||
<runner-instructions-modal
|
<runner-instructions-modal
|
||||||
v-if="instructionsModalOpened"
|
|
||||||
ref="runnerInstructionsModal"
|
ref="runnerInstructionsModal"
|
||||||
:registration-token="currentRegistrationToken"
|
:registration-token="currentRegistrationToken"
|
||||||
data-testid="runner-instructions-modal"
|
data-testid="runner-instructions-modal"
|
||||||
|
|
|
@ -13,6 +13,13 @@ export default {
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
formInputGroupProps() {
|
||||||
|
return {
|
||||||
|
name: 'token-value',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onCopy() {
|
onCopy() {
|
||||||
// value already in the clipboard, simply notify the user
|
// value already in the clipboard, simply notify the user
|
||||||
|
@ -26,8 +33,8 @@ export default {
|
||||||
<input-copy-toggle-visibility
|
<input-copy-toggle-visibility
|
||||||
class="gl-m-0"
|
class="gl-m-0"
|
||||||
:value="value"
|
:value="value"
|
||||||
data-testid="token-value"
|
|
||||||
:copy-button-title="$options.I18N_COPY_BUTTON_TITLE"
|
:copy-button-title="$options.I18N_COPY_BUTTON_TITLE"
|
||||||
|
:form-input-group-props="formInputGroupProps"
|
||||||
@copy="onCopy"
|
@copy="onCopy"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,7 +1,16 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlAlert, GlBadge, GlLink, GlLoadingIcon, GlSprintf, GlTable, GlTooltip } from '@gitlab/ui';
|
import {
|
||||||
|
GlAlert,
|
||||||
|
GlBadge,
|
||||||
|
GlLink,
|
||||||
|
GlLoadingIcon,
|
||||||
|
GlSprintf,
|
||||||
|
GlTable,
|
||||||
|
GlTooltip,
|
||||||
|
GlTooltipDirective,
|
||||||
|
} from '@gitlab/ui';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import { s__ } from '~/locale';
|
import { s__, sprintf } from '~/locale';
|
||||||
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
|
@ -20,6 +29,9 @@ export default {
|
||||||
StateActions,
|
StateActions,
|
||||||
TimeAgoTooltip,
|
TimeAgoTooltip,
|
||||||
},
|
},
|
||||||
|
directives: {
|
||||||
|
GlTooltip: GlTooltipDirective,
|
||||||
|
},
|
||||||
mixins: [timeagoMixin],
|
mixins: [timeagoMixin],
|
||||||
props: {
|
props: {
|
||||||
states: {
|
states: {
|
||||||
|
@ -68,6 +80,8 @@ export default {
|
||||||
locked: s__('Terraform|Locked'),
|
locked: s__('Terraform|Locked'),
|
||||||
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
|
lockedByUser: s__('Terraform|Locked by %{user} %{timeAgo}'),
|
||||||
lockingState: s__('Terraform|Locking state'),
|
lockingState: s__('Terraform|Locking state'),
|
||||||
|
deleting: s__('Terraform|Removed'),
|
||||||
|
deletionInProgress: s__('Terraform|Deletion in progress'),
|
||||||
name: s__('Terraform|Name'),
|
name: s__('Terraform|Name'),
|
||||||
pipeline: s__('Terraform|Pipeline'),
|
pipeline: s__('Terraform|Pipeline'),
|
||||||
removing: s__('Terraform|Removing'),
|
removing: s__('Terraform|Removing'),
|
||||||
|
@ -85,6 +99,12 @@ export default {
|
||||||
lockedByUserName(item) {
|
lockedByUserName(item) {
|
||||||
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
|
return item.lockedByUser?.name || this.$options.i18n.unknownUser;
|
||||||
},
|
},
|
||||||
|
lockedByUserText(item) {
|
||||||
|
return sprintf(this.$options.i18n.lockedByUser, {
|
||||||
|
user: this.lockedByUserName(item),
|
||||||
|
timeAgo: this.timeFormatted(item.lockedAt),
|
||||||
|
});
|
||||||
|
},
|
||||||
pipelineDetailedStatus(item) {
|
pipelineDetailedStatus(item) {
|
||||||
return item.latestVersion?.job?.detailedStatus;
|
return item.latestVersion?.job?.detailedStatus;
|
||||||
},
|
},
|
||||||
|
@ -142,29 +162,27 @@ export default {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="item.lockedAt"
|
v-else-if="item.deletedAt"
|
||||||
:id="`terraformLockedBadgeContainer${item.name}`"
|
v-gl-tooltip.right
|
||||||
class="gl-mx-3"
|
class="gl-mx-3"
|
||||||
|
:title="$options.i18n.deletionInProgress"
|
||||||
|
:data-testid="`state-badge-${item.name}`"
|
||||||
>
|
>
|
||||||
<gl-badge :id="`terraformLockedBadge${item.name}`" icon="lock">
|
<gl-badge icon="remove">
|
||||||
|
{{ $options.i18n.deleting }}
|
||||||
|
</gl-badge>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-else-if="item.lockedAt"
|
||||||
|
v-gl-tooltip.right
|
||||||
|
class="gl-mx-3"
|
||||||
|
:title="lockedByUserText(item)"
|
||||||
|
:data-testid="`state-badge-${item.name}`"
|
||||||
|
>
|
||||||
|
<gl-badge icon="lock">
|
||||||
{{ $options.i18n.locked }}
|
{{ $options.i18n.locked }}
|
||||||
</gl-badge>
|
</gl-badge>
|
||||||
|
|
||||||
<gl-tooltip
|
|
||||||
:container="`terraformLockedBadgeContainer${item.name}`"
|
|
||||||
:target="`terraformLockedBadge${item.name}`"
|
|
||||||
placement="right"
|
|
||||||
>
|
|
||||||
<gl-sprintf :message="$options.i18n.lockedByUser">
|
|
||||||
<template #user>
|
|
||||||
{{ lockedByUserName(item) }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #timeAgo>
|
|
||||||
{{ timeFormatted(item.lockedAt) }}
|
|
||||||
</template>
|
|
||||||
</gl-sprintf>
|
|
||||||
</gl-tooltip>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -11,6 +11,7 @@ fragment State on TerraformState {
|
||||||
name
|
name
|
||||||
lockedAt
|
lockedAt
|
||||||
updatedAt
|
updatedAt
|
||||||
|
deletedAt
|
||||||
|
|
||||||
lockedByUser {
|
lockedByUser {
|
||||||
...User
|
...User
|
||||||
|
|
|
@ -63,13 +63,13 @@ export default {
|
||||||
<div class="gl-w-full">
|
<div class="gl-w-full">
|
||||||
<div class="gl-display-flex gl-flex-nowrap">
|
<div class="gl-display-flex gl-flex-nowrap">
|
||||||
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
<div class="gl-flex-wrap gl-display-flex gl-w-full">
|
||||||
<div class="gl-mr-4 gl-display-flex gl-align-items-center">
|
<div class="gl-display-flex gl-align-items-center">
|
||||||
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
|
<p v-safe-html="generateText(data.text)" class="gl-m-0"></p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data.link">
|
<div v-if="data.link" class="gl-pr-2">
|
||||||
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
<gl-link :href="data.link.href">{{ data.link.text }}</gl-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="data.modal">
|
<div v-if="data.modal" class="gl-pr-2">
|
||||||
<gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
|
<gl-link v-gl-modal="modalId" data-testid="modal-link" @click="data.modal.onClick">
|
||||||
{{ data.modal.text }}
|
{{ data.modal.text }}
|
||||||
</gl-link>
|
</gl-link>
|
||||||
|
@ -81,7 +81,11 @@ export default {
|
||||||
{{ data.badge.text }}
|
{{ data.badge.text }}
|
||||||
</gl-badge>
|
</gl-badge>
|
||||||
</div>
|
</div>
|
||||||
<actions :widget="widgetLabel" :tertiary-buttons="data.actions" class="gl-ml-auto" />
|
<actions
|
||||||
|
:widget="widgetLabel"
|
||||||
|
:tertiary-buttons="data.actions"
|
||||||
|
class="gl-ml-auto gl-pl-3"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p
|
<p
|
||||||
v-if="data.subtext"
|
v-if="data.subtext"
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { GlFormInputGroup, GlFormGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
|
import {
|
||||||
|
GlFormInputGroup,
|
||||||
|
GlFormInput,
|
||||||
|
GlFormGroup,
|
||||||
|
GlButton,
|
||||||
|
GlTooltipDirective,
|
||||||
|
} from '@gitlab/ui';
|
||||||
|
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
|
||||||
|
@ -12,6 +18,7 @@ export default {
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
GlFormInputGroup,
|
GlFormInputGroup,
|
||||||
|
GlFormInput,
|
||||||
GlFormGroup,
|
GlFormGroup,
|
||||||
GlButton,
|
GlButton,
|
||||||
ClipboardButton,
|
ClipboardButton,
|
||||||
|
@ -52,20 +59,6 @@ export default {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/*
|
|
||||||
`inputClass` prop should be removed after https://gitlab.com/gitlab-org/gitlab/-/issues/357848
|
|
||||||
is implemented.
|
|
||||||
*/
|
|
||||||
inputClass: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
qaSelector: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: undefined,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -87,9 +80,6 @@ export default {
|
||||||
displayedValue() {
|
displayedValue() {
|
||||||
return this.computedValueIsVisible ? this.value : '*'.repeat(this.value.length || 20);
|
return this.computedValueIsVisible ? this.value : '*'.repeat(this.value.length || 20);
|
||||||
},
|
},
|
||||||
classInput() {
|
|
||||||
return `gl-font-monospace! gl-cursor-default! ${this.inputClass}`.trimEnd();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleToggleVisibilityButtonClick() {
|
handleToggleVisibilityButtonClick() {
|
||||||
|
@ -97,6 +87,9 @@ export default {
|
||||||
|
|
||||||
this.$emit('visibility-change', this.valueIsVisible);
|
this.$emit('visibility-change', this.valueIsVisible);
|
||||||
},
|
},
|
||||||
|
handleClick() {
|
||||||
|
this.$refs.input.$el.select();
|
||||||
|
},
|
||||||
handleCopyButtonClick() {
|
handleCopyButtonClick() {
|
||||||
this.$emit('copy');
|
this.$emit('copy');
|
||||||
},
|
},
|
||||||
|
@ -115,15 +108,21 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<gl-form-group v-bind="$attrs">
|
<gl-form-group v-bind="$attrs">
|
||||||
<gl-form-input-group
|
<gl-form-input-group>
|
||||||
:value="displayedValue"
|
<gl-form-input
|
||||||
:input-class="classInput"
|
ref="input"
|
||||||
:data-qa-selector="qaSelector"
|
readonly
|
||||||
select-on-click
|
class="gl-font-monospace! gl-cursor-default!"
|
||||||
readonly
|
v-bind="formInputGroupProps"
|
||||||
v-bind="formInputGroupProps"
|
:value="displayedValue"
|
||||||
@copy="handleFormInputCopy"
|
@copy="handleFormInputCopy"
|
||||||
>
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
This v-if is necessary to avoid an issue with border radius.
|
||||||
|
See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88059#note_969812649
|
||||||
|
-->
|
||||||
<template v-if="showToggleVisibilityButton || showCopyButton" #append>
|
<template v-if="showToggleVisibilityButton || showCopyButton" #append>
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="showToggleVisibilityButton"
|
v-if="showToggleVisibilityButton"
|
||||||
|
|
|
@ -9,35 +9,19 @@ export default {
|
||||||
RunnerInstructionsModal,
|
RunnerInstructionsModal,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlModalDirective,
|
GlModal: GlModalDirective,
|
||||||
},
|
},
|
||||||
modalId: 'runner-instructions-modal',
|
modalId: 'runner-instructions-modal',
|
||||||
i18n: {
|
i18n: {
|
||||||
buttonText: s__('Runners|Show runner installation instructions'),
|
buttonText: s__('Runners|Show runner installation instructions'),
|
||||||
},
|
},
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
opened: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
// lazily mount modal to prevent premature instructions requests
|
|
||||||
this.opened = true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-button
|
<gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button">
|
||||||
v-gl-modal-directive="$options.modalId"
|
|
||||||
class="gl-mt-4"
|
|
||||||
data-testid="show-modal-button"
|
|
||||||
@click="onClick"
|
|
||||||
>
|
|
||||||
{{ $options.i18n.buttonText }}
|
{{ $options.i18n.buttonText }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<runner-instructions-modal v-if="opened" :modal-id="$options.modalId" />
|
<runner-instructions-modal :modal-id="$options.modalId" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -58,6 +58,10 @@ export default {
|
||||||
apollo: {
|
apollo: {
|
||||||
platforms: {
|
platforms: {
|
||||||
query: getRunnerPlatformsQuery,
|
query: getRunnerPlatformsQuery,
|
||||||
|
skip() {
|
||||||
|
// Only load instructions once the modal is shown
|
||||||
|
return !this.shown;
|
||||||
|
},
|
||||||
update(data) {
|
update(data) {
|
||||||
return (
|
return (
|
||||||
data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
|
data?.runnerPlatforms?.nodes.map(({ name, humanReadableName, architectures }) => {
|
||||||
|
@ -81,7 +85,7 @@ export default {
|
||||||
instructions: {
|
instructions: {
|
||||||
query: getRunnerSetupInstructionsQuery,
|
query: getRunnerSetupInstructionsQuery,
|
||||||
skip() {
|
skip() {
|
||||||
return !this.selectedPlatform;
|
return !this.shown || !this.selectedPlatform;
|
||||||
},
|
},
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
|
@ -99,6 +103,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
shown: false,
|
||||||
platforms: [],
|
platforms: [],
|
||||||
selectedPlatform: null,
|
selectedPlatform: null,
|
||||||
selectedArchitecture: null,
|
selectedArchitecture: null,
|
||||||
|
@ -136,13 +141,24 @@ export default {
|
||||||
return registerInstructions;
|
return registerInstructions;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
updated() {
|
||||||
|
// Refocus on dom changes, after loading data
|
||||||
|
this.refocusSelectedPlatformButton();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
show() {
|
show() {
|
||||||
this.$refs.modal.show();
|
this.$refs.modal.show();
|
||||||
},
|
},
|
||||||
focusSelected() {
|
onShown() {
|
||||||
// By default the first platform always gets the focus, but when the `defaultPlatformName`
|
this.shown = true;
|
||||||
// property is present, any other platform might actually be selected.
|
this.refocusSelectedPlatformButton();
|
||||||
|
},
|
||||||
|
refocusSelectedPlatformButton() {
|
||||||
|
// On modal opening, the first focusable element is auto-focused by bootstrap-vue
|
||||||
|
// This can be confusing for users, because the wrong platform button can
|
||||||
|
// get focused when setting a `defaultPlatformName`.
|
||||||
|
// This method refocuses the expected button.
|
||||||
|
// See more about this auto-focus: https://bootstrap-vue.org/docs/components/modal#auto-focus-on-open
|
||||||
this.$refs[this.selectedPlatform]?.[0].$el.focus();
|
this.$refs[this.selectedPlatform]?.[0].$el.focus();
|
||||||
},
|
},
|
||||||
selectPlatform(platformName) {
|
selectPlatform(platformName) {
|
||||||
|
@ -171,6 +187,7 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
|
environment: __('Environment'),
|
||||||
installARunner: s__('Runners|Install a runner'),
|
installARunner: s__('Runners|Install a runner'),
|
||||||
architecture: s__('Runners|Architecture'),
|
architecture: s__('Runners|Architecture'),
|
||||||
downloadInstallBinary: s__('Runners|Download and install binary'),
|
downloadInstallBinary: s__('Runners|Download and install binary'),
|
||||||
|
@ -178,6 +195,7 @@ export default {
|
||||||
registerRunnerCommand: s__('Runners|Command to register runner'),
|
registerRunnerCommand: s__('Runners|Command to register runner'),
|
||||||
fetchError: s__('Runners|An error has occurred fetching instructions'),
|
fetchError: s__('Runners|An error has occurred fetching instructions'),
|
||||||
copyInstructions: s__('Runners|Copy instructions'),
|
copyInstructions: s__('Runners|Copy instructions'),
|
||||||
|
viewInstallationInstructions: s__('Runners|View installation instructions'),
|
||||||
},
|
},
|
||||||
closeButton: {
|
closeButton: {
|
||||||
text: __('Close'),
|
text: __('Close'),
|
||||||
|
@ -193,7 +211,7 @@ export default {
|
||||||
:action-secondary="$options.closeButton"
|
:action-secondary="$options.closeButton"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
@shown="focusSelected"
|
@shown="onShown"
|
||||||
>
|
>
|
||||||
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
|
<gl-alert v-if="showAlert" variant="danger" @dismiss="toggleAlert(false)">
|
||||||
{{ $options.i18n.fetchError }}
|
{{ $options.i18n.fetchError }}
|
||||||
|
@ -203,7 +221,7 @@ export default {
|
||||||
|
|
||||||
<template v-if="platforms.length">
|
<template v-if="platforms.length">
|
||||||
<h5>
|
<h5>
|
||||||
{{ __('Environment') }}
|
{{ $options.i18n.environment }}
|
||||||
</h5>
|
</h5>
|
||||||
<div v-gl-resize-observer="onPlatformsButtonResize">
|
<div v-gl-resize-observer="onPlatformsButtonResize">
|
||||||
<gl-button-group
|
<gl-button-group
|
||||||
|
@ -295,7 +313,7 @@ export default {
|
||||||
<p>{{ instructionsWithoutArchitecture }}</p>
|
<p>{{ instructionsWithoutArchitecture }}</p>
|
||||||
<gl-button :href="runnerInstallationLink">
|
<gl-button :href="runnerInstallationLink">
|
||||||
<gl-icon name="external-link" />
|
<gl-icon name="external-link" />
|
||||||
{{ s__('Runners|View installation instructions') }}
|
{{ $options.i18n.viewInstallationInstructions }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -169,6 +169,9 @@ export default {
|
||||||
setFocus() {
|
setFocus() {
|
||||||
this.$refs.header.focusInput();
|
this.$refs.header.focusInput();
|
||||||
},
|
},
|
||||||
|
hideDropdown() {
|
||||||
|
this.$refs.dropdown.hide();
|
||||||
|
},
|
||||||
showDropdown() {
|
showDropdown() {
|
||||||
this.$refs.dropdown.show();
|
this.$refs.dropdown.show();
|
||||||
},
|
},
|
||||||
|
@ -205,7 +208,7 @@ export default {
|
||||||
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
|
:show-dropdown-contents-create-view="showDropdownContentsCreateView"
|
||||||
:is-standalone="isStandalone"
|
:is-standalone="isStandalone"
|
||||||
@toggleDropdownContentsCreateView="toggleDropdownContent"
|
@toggleDropdownContentsCreateView="toggleDropdownContent"
|
||||||
@closeDropdown="$emit('closeDropdown')"
|
@closeDropdown="hideDropdown"
|
||||||
@input="debouncedSearchKeyUpdate"
|
@input="debouncedSearchKeyUpdate"
|
||||||
@searchEnter="selectFirstItem"
|
@searchEnter="selectFirstItem"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Mutations
|
||||||
|
module Packages
|
||||||
|
module Cleanup
|
||||||
|
module Policy
|
||||||
|
class Update < Mutations::BaseMutation
|
||||||
|
graphql_name 'UpdatePackagesCleanupPolicy'
|
||||||
|
|
||||||
|
include FindsProject
|
||||||
|
|
||||||
|
authorize :admin_package
|
||||||
|
|
||||||
|
argument :project_path,
|
||||||
|
GraphQL::Types::ID,
|
||||||
|
required: true,
|
||||||
|
description: 'Project path where the packages cleanup policy is located.'
|
||||||
|
|
||||||
|
argument :keep_n_duplicated_package_files,
|
||||||
|
Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
|
||||||
|
required: false,
|
||||||
|
description: copy_field_description(
|
||||||
|
Types::Packages::Cleanup::PolicyType,
|
||||||
|
:keep_n_duplicated_package_files
|
||||||
|
)
|
||||||
|
|
||||||
|
field :packages_cleanup_policy,
|
||||||
|
Types::Packages::Cleanup::PolicyType,
|
||||||
|
null: true,
|
||||||
|
description: 'Packages cleanup policy after mutation.'
|
||||||
|
|
||||||
|
def resolve(project_path:, **args)
|
||||||
|
project = authorized_find!(project_path)
|
||||||
|
|
||||||
|
result = ::Packages::Cleanup::UpdatePolicyService
|
||||||
|
.new(project: project, current_user: current_user, params: args)
|
||||||
|
.execute
|
||||||
|
|
||||||
|
{
|
||||||
|
packages_cleanup_policy: result.payload[:packages_cleanup_policy],
|
||||||
|
errors: result.errors
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -136,6 +136,7 @@ module Types
|
||||||
mount_mutation Mutations::UserPreferences::Update
|
mount_mutation Mutations::UserPreferences::Update
|
||||||
mount_mutation Mutations::Packages::Destroy
|
mount_mutation Mutations::Packages::Destroy
|
||||||
mount_mutation Mutations::Packages::DestroyFile
|
mount_mutation Mutations::Packages::DestroyFile
|
||||||
|
mount_mutation Mutations::Packages::Cleanup::Policy::Update
|
||||||
mount_mutation Mutations::Echo
|
mount_mutation Mutations::Echo
|
||||||
mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha }
|
mount_mutation Mutations::WorkItems::Create, deprecated: { milestone: '15.1', reason: :alpha }
|
||||||
mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha }
|
mount_mutation Mutations::WorkItems::CreateFromTask, deprecated: { milestone: '15.1', reason: :alpha }
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module Packages
|
||||||
|
module Cleanup
|
||||||
|
class KeepDuplicatedPackageFilesEnum < BaseEnum
|
||||||
|
graphql_name 'PackagesCleanupKeepDuplicatedPackageFilesEnum'
|
||||||
|
|
||||||
|
OPTIONS_MAPPING = {
|
||||||
|
'all' => 'ALL_PACKAGE_FILES',
|
||||||
|
'1' => 'ONE_PACKAGE_FILE',
|
||||||
|
'10' => 'TEN_PACKAGE_FILES',
|
||||||
|
'20' => 'TWENTY_PACKAGE_FILES',
|
||||||
|
'30' => 'THIRTY_PACKAGE_FILES',
|
||||||
|
'40' => 'FORTY_PACKAGE_FILES',
|
||||||
|
'50' => 'FIFTY_PACKAGE_FILES'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
::Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES.each do |keep_value|
|
||||||
|
value OPTIONS_MAPPING[keep_value], value: keep_value, description: "Value to keep #{keep_value} package files"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,23 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
module Packages
|
||||||
|
module Cleanup
|
||||||
|
class PolicyType < ::Types::BaseObject
|
||||||
|
graphql_name 'PackagesCleanupPolicy'
|
||||||
|
description 'A packages cleanup policy designed to keep only packages and packages assets that matter most'
|
||||||
|
|
||||||
|
authorize :admin_package
|
||||||
|
|
||||||
|
field :keep_n_duplicated_package_files,
|
||||||
|
Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum,
|
||||||
|
null: false,
|
||||||
|
description: 'Number of duplicated package files to retain.'
|
||||||
|
field :next_run_at,
|
||||||
|
Types::TimeType,
|
||||||
|
null: true,
|
||||||
|
description: 'Next time that this packages cleanup policy will be executed.'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -181,6 +181,11 @@ module Types
|
||||||
description: 'Packages of the project.',
|
description: 'Packages of the project.',
|
||||||
resolver: Resolvers::ProjectPackagesResolver
|
resolver: Resolvers::ProjectPackagesResolver
|
||||||
|
|
||||||
|
field :packages_cleanup_policy,
|
||||||
|
Types::Packages::Cleanup::PolicyType,
|
||||||
|
null: true,
|
||||||
|
description: 'Packages cleanup policy for the project.'
|
||||||
|
|
||||||
field :jobs,
|
field :jobs,
|
||||||
type: Types::Ci::JobType.connection_type,
|
type: Types::Ci::JobType.connection_type,
|
||||||
null: true,
|
null: true,
|
||||||
|
|
|
@ -38,6 +38,10 @@ module Types
|
||||||
null: false,
|
null: false,
|
||||||
description: 'Timestamp the Terraform state was updated.'
|
description: 'Timestamp the Terraform state was updated.'
|
||||||
|
|
||||||
|
field :deleted_at, Types::TimeType,
|
||||||
|
null: true,
|
||||||
|
description: 'Timestamp the Terraform state was deleted.'
|
||||||
|
|
||||||
def locked_by_user
|
def locked_by_user
|
||||||
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find
|
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.locked_by_user_id).find
|
||||||
end
|
end
|
||||||
|
|
|
@ -15,7 +15,7 @@ module Packages
|
||||||
validates :keep_n_duplicated_package_files,
|
validates :keep_n_duplicated_package_files,
|
||||||
inclusion: {
|
inclusion: {
|
||||||
in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES,
|
in: KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES,
|
||||||
message: 'keep_n_duplicated_package_files is invalid'
|
message: 'is invalid'
|
||||||
}
|
}
|
||||||
|
|
||||||
# used by Schedulable
|
# used by Schedulable
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Packages
|
||||||
|
module Cleanup
|
||||||
|
class PolicyPolicy < BasePolicy
|
||||||
|
delegate { @subject.project }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -433,6 +433,7 @@ class ProjectPolicy < BasePolicy
|
||||||
|
|
||||||
rule { can?(:maintainer_access) }.policy do
|
rule { can?(:maintainer_access) }.policy do
|
||||||
enable :destroy_package
|
enable :destroy_package
|
||||||
|
enable :admin_package
|
||||||
enable :admin_issue_board
|
enable :admin_issue_board
|
||||||
enable :push_to_delete_protected_branch
|
enable :push_to_delete_protected_branch
|
||||||
enable :update_snippet
|
enable :update_snippet
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Packages
|
||||||
|
module Cleanup
|
||||||
|
class UpdatePolicyService < BaseProjectService
|
||||||
|
ALLOWED_ATTRIBUTES = %i[keep_n_duplicated_package_files].freeze
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return ServiceResponse.error(message: 'Access denied') unless allowed?
|
||||||
|
|
||||||
|
if policy.update(policy_params)
|
||||||
|
ServiceResponse.success(payload: { packages_cleanup_policy: policy })
|
||||||
|
else
|
||||||
|
ServiceResponse.error(message: policy.errors.full_messages.to_sentence || 'Bad request')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def policy
|
||||||
|
strong_memoize(:policy) do
|
||||||
|
project.packages_cleanup_policy
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def allowed?
|
||||||
|
can?(current_user, :admin_package, project)
|
||||||
|
end
|
||||||
|
|
||||||
|
def policy_params
|
||||||
|
params.slice(*ALLOWED_ATTRIBUTES)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: standard_context_type_check
|
||||||
|
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/88540
|
||||||
|
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364265
|
||||||
|
milestone: '15.1'
|
||||||
|
type: development
|
||||||
|
group: group::product intelligence
|
||||||
|
default_enabled: false
|
|
@ -145,6 +145,8 @@
|
||||||
- 1
|
- 1
|
||||||
- - elastic_namespace_rollout
|
- - elastic_namespace_rollout
|
||||||
- 1
|
- 1
|
||||||
|
- - elastic_project_transfer
|
||||||
|
- 1
|
||||||
- - email_receiver
|
- - email_receiver
|
||||||
- 2
|
- 2
|
||||||
- - emails_on_push
|
- - emails_on_push
|
||||||
|
|
|
@ -5112,6 +5112,26 @@ Input type: `UpdateNoteInput`
|
||||||
| <a id="mutationupdatenoteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
| <a id="mutationupdatenoteerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||||
| <a id="mutationupdatenotenote"></a>`note` | [`Note`](#note) | Note after mutation. |
|
| <a id="mutationupdatenotenote"></a>`note` | [`Note`](#note) | Note after mutation. |
|
||||||
|
|
||||||
|
### `Mutation.updatePackagesCleanupPolicy`
|
||||||
|
|
||||||
|
Input type: `UpdatePackagesCleanupPolicyInput`
|
||||||
|
|
||||||
|
#### Arguments
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicykeepnduplicatedpackagefiles"></a>`keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicyprojectpath"></a>`projectPath` | [`ID!`](#id) | Project path where the packages cleanup policy is located. |
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicyclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
|
||||||
|
| <a id="mutationupdatepackagescleanuppolicypackagescleanuppolicy"></a>`packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy after mutation. |
|
||||||
|
|
||||||
### `Mutation.updateRequirement`
|
### `Mutation.updateRequirement`
|
||||||
|
|
||||||
Input type: `UpdateRequirementInput`
|
Input type: `UpdateRequirementInput`
|
||||||
|
@ -14382,6 +14402,17 @@ Represents a package tag.
|
||||||
| <a id="packagetagname"></a>`name` | [`String!`](#string) | Name of the tag. |
|
| <a id="packagetagname"></a>`name` | [`String!`](#string) | Name of the tag. |
|
||||||
| <a id="packagetagupdatedat"></a>`updatedAt` | [`Time!`](#time) | Updated date. |
|
| <a id="packagetagupdatedat"></a>`updatedAt` | [`Time!`](#time) | Updated date. |
|
||||||
|
|
||||||
|
### `PackagesCleanupPolicy`
|
||||||
|
|
||||||
|
A packages cleanup policy designed to keep only packages and packages assets that matter most.
|
||||||
|
|
||||||
|
#### Fields
|
||||||
|
|
||||||
|
| Name | Type | Description |
|
||||||
|
| ---- | ---- | ----------- |
|
||||||
|
| <a id="packagescleanuppolicykeepnduplicatedpackagefiles"></a>`keepNDuplicatedPackageFiles` | [`PackagesCleanupKeepDuplicatedPackageFilesEnum!`](#packagescleanupkeepduplicatedpackagefilesenum) | Number of duplicated package files to retain. |
|
||||||
|
| <a id="packagescleanuppolicynextrunat"></a>`nextRunAt` | [`Time`](#time) | Next time that this packages cleanup policy will be executed. |
|
||||||
|
|
||||||
### `PageInfo`
|
### `PageInfo`
|
||||||
|
|
||||||
Information about pagination in a connection.
|
Information about pagination in a connection.
|
||||||
|
@ -14690,6 +14721,7 @@ Represents vulnerability finding of a security report on the pipeline.
|
||||||
| <a id="projectonlyallowmergeifalldiscussionsareresolved"></a>`onlyAllowMergeIfAllDiscussionsAreResolved` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged when all the discussions are resolved. |
|
| <a id="projectonlyallowmergeifalldiscussionsareresolved"></a>`onlyAllowMergeIfAllDiscussionsAreResolved` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged when all the discussions are resolved. |
|
||||||
| <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. |
|
| <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. |
|
||||||
| <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. |
|
| <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. |
|
||||||
|
| <a id="projectpackagescleanuppolicy"></a>`packagesCleanupPolicy` | [`PackagesCleanupPolicy`](#packagescleanuppolicy) | Packages cleanup policy for the project. |
|
||||||
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
|
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
|
||||||
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
|
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
|
||||||
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
|
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
|
||||||
|
@ -16765,6 +16797,7 @@ Completion status of tasks.
|
||||||
| Name | Type | Description |
|
| Name | Type | Description |
|
||||||
| ---- | ---- | ----------- |
|
| ---- | ---- | ----------- |
|
||||||
| <a id="terraformstatecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the Terraform state was created. |
|
| <a id="terraformstatecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp the Terraform state was created. |
|
||||||
|
| <a id="terraformstatedeletedat"></a>`deletedAt` | [`Time`](#time) | Timestamp the Terraform state was deleted. |
|
||||||
| <a id="terraformstateid"></a>`id` | [`ID!`](#id) | ID of the Terraform state. |
|
| <a id="terraformstateid"></a>`id` | [`ID!`](#id) | ID of the Terraform state. |
|
||||||
| <a id="terraformstatelatestversion"></a>`latestVersion` | [`TerraformStateVersion`](#terraformstateversion) | Latest version of the Terraform state. |
|
| <a id="terraformstatelatestversion"></a>`latestVersion` | [`TerraformStateVersion`](#terraformstateversion) | Latest version of the Terraform state. |
|
||||||
| <a id="terraformstatelockedat"></a>`lockedAt` | [`Time`](#time) | Timestamp the Terraform state was locked. |
|
| <a id="terraformstatelockedat"></a>`lockedAt` | [`Time`](#time) | Timestamp the Terraform state was locked. |
|
||||||
|
@ -19177,6 +19210,18 @@ Values for sorting package.
|
||||||
| <a id="packagetypeenumrubygems"></a>`RUBYGEMS` | Packages from the Rubygems package manager. |
|
| <a id="packagetypeenumrubygems"></a>`RUBYGEMS` | Packages from the Rubygems package manager. |
|
||||||
| <a id="packagetypeenumterraform_module"></a>`TERRAFORM_MODULE` | Packages from the Terraform Module package manager. |
|
| <a id="packagetypeenumterraform_module"></a>`TERRAFORM_MODULE` | Packages from the Terraform Module package manager. |
|
||||||
|
|
||||||
|
### `PackagesCleanupKeepDuplicatedPackageFilesEnum`
|
||||||
|
|
||||||
|
| Value | Description |
|
||||||
|
| ----- | ----------- |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumall_package_files"></a>`ALL_PACKAGE_FILES` | Value to keep all package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumfifty_package_files"></a>`FIFTY_PACKAGE_FILES` | Value to keep 50 package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumforty_package_files"></a>`FORTY_PACKAGE_FILES` | Value to keep 40 package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumone_package_file"></a>`ONE_PACKAGE_FILE` | Value to keep 1 package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumten_package_files"></a>`TEN_PACKAGE_FILES` | Value to keep 10 package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumthirty_package_files"></a>`THIRTY_PACKAGE_FILES` | Value to keep 30 package files. |
|
||||||
|
| <a id="packagescleanupkeepduplicatedpackagefilesenumtwenty_package_files"></a>`TWENTY_PACKAGE_FILES` | Value to keep 20 package files. |
|
||||||
|
|
||||||
### `PipelineConfigSourceEnum`
|
### `PipelineConfigSourceEnum`
|
||||||
|
|
||||||
| Value | Description |
|
| Value | Description |
|
||||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
||||||
|
|
||||||
# Free user limit **(FREE SAAS)**
|
# Free user limit **(FREE SAAS)**
|
||||||
|
|
||||||
In GitLab 15.1 (June 22, 2022) and later, namespaces in GitLab.com on the Free tier
|
From September 15, 2022, namespaces in GitLab.com on the Free tier
|
||||||
will be limited to five (5) members per [namespace](group/index.md#namespaces).
|
will be limited to five (5) members per [namespace](group/index.md#namespaces).
|
||||||
This limit applies to top-level groups and personal namespaces.
|
This limit applies to top-level groups and personal namespaces.
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,12 @@ module Gitlab
|
||||||
GITLAB_RAILS_SOURCE = 'gitlab-rails'
|
GITLAB_RAILS_SOURCE = 'gitlab-rails'
|
||||||
|
|
||||||
def initialize(namespace: nil, project: nil, user: nil, **extra)
|
def initialize(namespace: nil, project: nil, user: nil, **extra)
|
||||||
|
if Feature.enabled?(:standard_context_type_check)
|
||||||
|
check_argument_type(:namespace, namespace, [Namespace])
|
||||||
|
check_argument_type(:project, project, [Project, Integer])
|
||||||
|
check_argument_type(:user, user, [User, DeployToken])
|
||||||
|
end
|
||||||
|
|
||||||
@namespace = namespace
|
@namespace = namespace
|
||||||
@plan = namespace&.actual_plan_name
|
@plan = namespace&.actual_plan_name
|
||||||
@project = project
|
@project = project
|
||||||
|
@ -54,6 +60,14 @@ module Gitlab
|
||||||
def project_id
|
def project_id
|
||||||
project.is_a?(Integer) ? project : project&.id
|
project.is_a?(Integer) ? project : project&.id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_argument_type(argument_name, argument_value, allowed_classes)
|
||||||
|
return if argument_value.nil? || allowed_classes.any? { |allowed_class| argument_value.is_a?(allowed_class) }
|
||||||
|
|
||||||
|
exception = "Invalid argument type passed for #{argument_name}." \
|
||||||
|
" Should be one of #{allowed_classes.map(&:to_s)}"
|
||||||
|
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(ArgumentError.new(exception))
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -6829,7 +6829,7 @@ msgstr ""
|
||||||
msgid "CI Lint"
|
msgid "CI Lint"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "CI configuration validated, including all configuration added with the %{codeStart}includes%{codeEnd} keyword. %{link}"
|
msgid "CI configuration validated, including all configuration added with the %{codeStart}include%{codeEnd} keyword. %{link}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "CI settings"
|
msgid "CI settings"
|
||||||
|
@ -37451,6 +37451,9 @@ msgstr ""
|
||||||
msgid "Terraform|Copy Terraform init command"
|
msgid "Terraform|Copy Terraform init command"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Terraform|Deletion in progress"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Terraform|Details"
|
msgid "Terraform|Details"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -37496,6 +37499,9 @@ msgstr ""
|
||||||
msgid "Terraform|Remove state file and versions"
|
msgid "Terraform|Remove state file and versions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Terraform|Removed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Terraform|Removing"
|
msgid "Terraform|Removing"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -2,10 +2,7 @@
|
||||||
|
|
||||||
module QA
|
module QA
|
||||||
RSpec.describe 'Manage' do
|
RSpec.describe 'Manage' do
|
||||||
describe 'Project transfer between groups', :reliable, quarantine: {
|
describe 'Project transfer between groups', :reliable do
|
||||||
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/359043',
|
|
||||||
type: :investigating
|
|
||||||
} do
|
|
||||||
let(:source_group) do
|
let(:source_group) do
|
||||||
Resource::Group.fabricate_via_api! do |group|
|
Resource::Group.fabricate_via_api! do |group|
|
||||||
group.path = "source-group-#{SecureRandom.hex(8)}"
|
group.path = "source-group-#{SecureRandom.hex(8)}"
|
||||||
|
|
|
@ -1,49 +1,70 @@
|
||||||
# Make sure to update the docs if this file moves. Docs URL: https://docs.gitlab.com/ee/development/migration_style_guide.html#when-to-use-the-helper-method
|
# Make sure to update the docs if this file moves. Docs URL: https://docs.gitlab.com/ee/development/migration_style_guide.html#when-to-use-the-helper-method
|
||||||
Migration/UpdateLargeTable:
|
Migration/UpdateLargeTable:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
HighTrafficTables: &high_traffic_tables # size in GB (>= 10 GB on GitLab.com as of 02/2020) and/or number of records
|
HighTrafficTables: &high_traffic_tables # size in GB (>= 10 GB on GitLab.com as of 06/2022) and/or number of records
|
||||||
|
- :alert_management_alerts
|
||||||
|
- :approval_merge_request_rules_users
|
||||||
- :audit_events
|
- :audit_events
|
||||||
- :ci_build_trace_sections
|
- :authentication_events
|
||||||
|
- :ci_build_needs
|
||||||
|
- :ci_build_report_results
|
||||||
- :ci_builds
|
- :ci_builds
|
||||||
- :ci_builds_metadata
|
- :ci_builds_metadata
|
||||||
|
- :ci_build_trace_metadata
|
||||||
- :ci_job_artifacts
|
- :ci_job_artifacts
|
||||||
- :ci_pipeline_variables
|
- :ci_pipeline_messages
|
||||||
- :ci_pipelines
|
- :ci_pipelines
|
||||||
|
- :ci_pipelines_config
|
||||||
|
- :ci_pipeline_variables
|
||||||
- :ci_stages
|
- :ci_stages
|
||||||
- :deployments
|
- :deployments
|
||||||
|
- :description_versions
|
||||||
|
- :error_tracking_error_events
|
||||||
- :events
|
- :events
|
||||||
- :gitlab_subscriptions
|
- :gitlab_subscriptions
|
||||||
|
- :gpg_signatures
|
||||||
- :issues
|
- :issues
|
||||||
|
- :label_links
|
||||||
|
- :lfs_objects
|
||||||
|
- :lfs_objects_projects
|
||||||
- :members
|
- :members
|
||||||
|
- :merge_request_cleanup_schedules
|
||||||
- :merge_request_diff_commits
|
- :merge_request_diff_commits
|
||||||
- :merge_request_diff_files
|
- :merge_request_diff_files
|
||||||
- :merge_request_diffs
|
- :merge_request_diffs
|
||||||
- :merge_request_metrics
|
- :merge_request_metrics
|
||||||
- :merge_requests
|
- :merge_requests
|
||||||
- :namespace_settings
|
|
||||||
- :namespaces
|
- :namespaces
|
||||||
|
- :namespace_settings
|
||||||
- :note_diff_files
|
- :note_diff_files
|
||||||
- :notes
|
- :notes
|
||||||
|
- :packages_package_files
|
||||||
- :project_authorizations
|
- :project_authorizations
|
||||||
- :projects
|
|
||||||
- :project_ci_cd_settings
|
- :project_ci_cd_settings
|
||||||
- :project_settings
|
- :project_daily_statistics
|
||||||
- :project_features
|
- :project_features
|
||||||
|
- :projects
|
||||||
|
- :project_settings
|
||||||
- :protected_branches
|
- :protected_branches
|
||||||
- :push_event_payloads
|
- :push_event_payloads
|
||||||
- :resource_label_events
|
- :resource_label_events
|
||||||
|
- :resource_state_events
|
||||||
- :routes
|
- :routes
|
||||||
- :security_findings
|
- :security_findings
|
||||||
- :sent_notifications
|
- :sent_notifications
|
||||||
- :system_note_metadata
|
- :system_note_metadata
|
||||||
- :taggings
|
- :taggings
|
||||||
- :todos
|
- :todos
|
||||||
- :users
|
- :uploads
|
||||||
- :user_preferences
|
|
||||||
- :user_details
|
- :user_details
|
||||||
- :vulnerability_occurrences
|
- :user_preferences
|
||||||
- :web_hook_logs
|
- :users
|
||||||
- :vulnerabilities
|
- :vulnerabilities
|
||||||
|
- :vulnerability_occurrence_identifiers
|
||||||
|
- :vulnerability_occurrence_pipelines
|
||||||
|
- :vulnerability_occurrences
|
||||||
|
- :vulnerability_reads
|
||||||
|
- :web_hook_logs
|
||||||
DeniedMethods:
|
DeniedMethods:
|
||||||
- :change_column_type_concurrently
|
- :change_column_type_concurrently
|
||||||
- :rename_column_concurrently
|
- :rename_column_concurrently
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
# This script requires BOT_USER_ID, CUSTOM_SAST_RULES_BOT_PAT and CI_MERGE_REQUEST_IID variables to be set
|
|
||||||
|
|
||||||
echo "Processing vuln report"
|
|
||||||
|
|
||||||
# Preparing the message for the comment that will be posted by the bot
|
|
||||||
# Empty string if there are no findings
|
|
||||||
jq -crM '.vulnerabilities |
|
|
||||||
map( select( .identifiers[0].name | test( "glappsec_" ) ) |
|
|
||||||
"- `" + .location.file + "` line " + ( .location.start_line | tostring ) +
|
|
||||||
(
|
|
||||||
if .location.start_line = .location.end_line then ""
|
|
||||||
else ( " to " + ( .location.end_line | tostring ) ) end
|
|
||||||
) + ": " + .message
|
|
||||||
) |
|
|
||||||
sort |
|
|
||||||
if length > 0 then
|
|
||||||
{ body: ("The findings below have been detected based on the [AppSec custom Semgrep rules](https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/) and need attention:\n\n" + join("\n") + "\n\n/cc @gitlab-com/gl-security/appsec") }
|
|
||||||
else
|
|
||||||
empty
|
|
||||||
end' gl-sast-report.json >findings.txt
|
|
||||||
|
|
||||||
echo "Resulting file:"
|
|
||||||
cat findings.txt
|
|
||||||
|
|
||||||
EXISTING_COMMENT_ID=$(curl "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
|
|
||||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" |
|
|
||||||
jq -crM 'map( select( .author.id == (env.BOT_USER_ID | tonumber) ) | .id ) | first')
|
|
||||||
|
|
||||||
echo "EXISTING_COMMENT_ID: $EXISTING_COMMENT_ID"
|
|
||||||
|
|
||||||
if [ "$EXISTING_COMMENT_ID" == "null" ]; then
|
|
||||||
if [ -s findings.txt ]; then
|
|
||||||
echo "No existing comment and there are findings: a new comment will be posted"
|
|
||||||
curl "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes" \
|
|
||||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '@findings.txt'
|
|
||||||
else
|
|
||||||
echo "No existing comment and no findings: nothing to do"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
if [ -s findings.txt ]; then
|
|
||||||
echo "There is an existing comment and there are findings: the existing comment will be updated"
|
|
||||||
curl --request PUT "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$EXISTING_COMMENT_ID" \
|
|
||||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '@findings.txt'
|
|
||||||
else
|
|
||||||
echo "There is an existing comment but no findings: the existing comment will be updated to mention everything is resolved"
|
|
||||||
curl --request PUT "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes/$EXISTING_COMMENT_ID" \
|
|
||||||
--header "Private-Token: $CUSTOM_SAST_RULES_BOT_PAT" \
|
|
||||||
--header 'Content-Type: application/json' \
|
|
||||||
--data '{"body":"All findings based on the [AppSec custom Semgrep rules](https://gitlab.com/gitlab-com/gl-security/appsec/sast-custom-rules/) have been resolved! :tada:"}'
|
|
||||||
fi
|
|
||||||
fi
|
|
|
@ -11,7 +11,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
|
||||||
end
|
end
|
||||||
|
|
||||||
def created_personal_access_token
|
def created_personal_access_token
|
||||||
find("[data-testid='new-access-token'] input").value
|
find_field('new-access-token').value
|
||||||
end
|
end
|
||||||
|
|
||||||
def feed_token_description
|
def feed_token_description
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { GlAlert } from '@gitlab/ui';
|
import { GlAlert } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
|
||||||
|
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
|
import NewAccessTokenApp from '~/access_tokens/components/new_access_token_app.vue';
|
||||||
import { createAlert, VARIANT_INFO } from '~/flash';
|
import { createAlert, VARIANT_INFO } from '~/flash';
|
||||||
import { __, sprintf } from '~/locale';
|
import { __, sprintf } from '~/locale';
|
||||||
|
@ -16,7 +16,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
|
||||||
const accessTokenType = 'personal access token';
|
const accessTokenType = 'personal access token';
|
||||||
|
|
||||||
const createComponent = (provide = { accessTokenType }) => {
|
const createComponent = (provide = { accessTokenType }) => {
|
||||||
wrapper = shallowMount(NewAccessTokenApp, {
|
wrapper = mountExtended(NewAccessTokenApp, {
|
||||||
provide,
|
provide,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -64,17 +64,26 @@ describe('~/access_tokens/components/new_access_token_app', () => {
|
||||||
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
|
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
|
||||||
);
|
);
|
||||||
expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
|
expect(InputCopyToggleVisibilityComponent.props('initialVisibility')).toBe(true);
|
||||||
expect(InputCopyToggleVisibilityComponent.props('inputClass')).toBe(
|
|
||||||
'qa-created-access-token',
|
|
||||||
);
|
|
||||||
expect(InputCopyToggleVisibilityComponent.props('qaSelector')).toBe(
|
|
||||||
'created_access_token_field',
|
|
||||||
);
|
|
||||||
expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
|
expect(InputCopyToggleVisibilityComponent.attributes('label')).toBe(
|
||||||
sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
|
sprintf(__('Your new %{accessTokenType}'), { accessTokenType }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('input field should contain QA-related selectors', async () => {
|
||||||
|
const newToken = '12345';
|
||||||
|
await triggerSuccess(newToken);
|
||||||
|
|
||||||
|
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
|
||||||
|
|
||||||
|
const inputAttributes = wrapper
|
||||||
|
.findByLabelText(sprintf(__('Your new %{accessTokenType}'), { accessTokenType }))
|
||||||
|
.attributes();
|
||||||
|
expect(inputAttributes).toMatchObject({
|
||||||
|
class: expect.stringContaining('qa-created-access-token'),
|
||||||
|
'data-qa-selector': 'created_access_token_field',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should render an info alert', async () => {
|
it('should render an info alert', async () => {
|
||||||
await triggerSuccess();
|
await triggerSuccess();
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
|
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
|
||||||
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
|
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
|
||||||
import Vue, { nextTick } from 'vue';
|
import Vue, { nextTick } from 'vue';
|
||||||
|
|
||||||
|
@ -24,6 +24,8 @@ import {
|
||||||
const mockToken = '0123456789';
|
const mockToken = '0123456789';
|
||||||
const maskToken = '**********';
|
const maskToken = '**********';
|
||||||
|
|
||||||
|
Vue.use(VueApollo);
|
||||||
|
|
||||||
describe('RegistrationDropdown', () => {
|
describe('RegistrationDropdown', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
|
@ -32,9 +34,10 @@ describe('RegistrationDropdown', () => {
|
||||||
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
|
const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
|
||||||
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
|
||||||
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
|
||||||
const findRegistrationTokenInput = () => wrapper.findByTestId('token-value').find('input');
|
const findRegistrationTokenInput = () => wrapper.find('[name=token-value]');
|
||||||
const findTokenResetDropdownItem = () =>
|
const findTokenResetDropdownItem = () =>
|
||||||
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
wrapper.findComponent(RegistrationTokenResetDropdownItem);
|
||||||
|
const findModal = () => wrapper.findComponent(GlModal);
|
||||||
const findModalContent = () =>
|
const findModalContent = () =>
|
||||||
createWrapper(document.body)
|
createWrapper(document.body)
|
||||||
.find('[data-testid="runner-instructions-modal"]')
|
.find('[data-testid="runner-instructions-modal"]')
|
||||||
|
@ -43,6 +46,8 @@ describe('RegistrationDropdown', () => {
|
||||||
|
|
||||||
const openModal = async () => {
|
const openModal = async () => {
|
||||||
await findRegistrationInstructionsDropdownItem().trigger('click');
|
await findRegistrationInstructionsDropdownItem().trigger('click');
|
||||||
|
findModal().vm.$emit('shown');
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -60,8 +65,6 @@ describe('RegistrationDropdown', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponentWithModal = () => {
|
const createComponentWithModal = () => {
|
||||||
Vue.use(VueApollo);
|
|
||||||
|
|
||||||
const requestHandlers = [
|
const requestHandlers = [
|
||||||
[getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
|
[getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
|
||||||
[getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
|
[getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { GlBadge, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import { nextTick } from 'vue';
|
import { nextTick } from 'vue';
|
||||||
import { useFakeDate } from 'helpers/fake_date';
|
import { useFakeDate } from 'helpers/fake_date';
|
||||||
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import StatesTable from '~/terraform/components/states_table.vue';
|
import StatesTable from '~/terraform/components/states_table.vue';
|
||||||
import StateActions from '~/terraform/components/states_table_actions.vue';
|
import StateActions from '~/terraform/components/states_table_actions.vue';
|
||||||
|
|
||||||
|
@ -104,11 +106,30 @@ describe('StatesTable', () => {
|
||||||
updatedAt: '2020-10-10T00:00:00Z',
|
updatedAt: '2020-10-10T00:00:00Z',
|
||||||
latestVersion: null,
|
latestVersion: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
_showDetails: false,
|
||||||
|
errorMessages: [],
|
||||||
|
name: 'state-6',
|
||||||
|
loadingLock: false,
|
||||||
|
loadingRemove: false,
|
||||||
|
lockedAt: null,
|
||||||
|
lockedByUser: null,
|
||||||
|
updatedAt: '2020-10-10T00:00:00Z',
|
||||||
|
deletedAt: '2022-02-02T00:00:00Z',
|
||||||
|
latestVersion: null,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const createComponent = async (propsData = defaultProps) => {
|
const createComponent = async (propsData = defaultProps) => {
|
||||||
wrapper = mount(StatesTable, { propsData });
|
wrapper = extendedWrapper(
|
||||||
|
mount(StatesTable, {
|
||||||
|
propsData,
|
||||||
|
directives: {
|
||||||
|
GlTooltip: createMockDirective(),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -124,27 +145,28 @@ describe('StatesTable', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each`
|
it.each`
|
||||||
name | toolTipText | locked | loading | lineNumber
|
name | toolTipText | hasBadge | loading | lineNumber
|
||||||
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
|
${'state-1'} | ${'Locked by user-1 2 days ago'} | ${true} | ${false} | ${0}
|
||||||
${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1}
|
${'state-2'} | ${'Locking state'} | ${false} | ${true} | ${1}
|
||||||
${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2}
|
${'state-3'} | ${'Unlocking state'} | ${false} | ${true} | ${2}
|
||||||
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3}
|
${'state-4'} | ${'Locked by Unknown User 5 days ago'} | ${true} | ${false} | ${3}
|
||||||
${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4}
|
${'state-5'} | ${'Removing'} | ${false} | ${true} | ${4}
|
||||||
|
${'state-6'} | ${'Deletion in progress'} | ${true} | ${false} | ${5}
|
||||||
`(
|
`(
|
||||||
'displays the name and locked information "$name" for line "$lineNumber"',
|
'displays the name and locked information "$name" for line "$lineNumber"',
|
||||||
({ name, toolTipText, locked, loading, lineNumber }) => {
|
({ name, toolTipText, hasBadge, loading, lineNumber }) => {
|
||||||
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
|
const states = wrapper.findAll('[data-testid="terraform-states-table-name"]');
|
||||||
|
|
||||||
const state = states.at(lineNumber);
|
const state = states.at(lineNumber);
|
||||||
const toolTip = state.find(GlTooltip);
|
|
||||||
|
|
||||||
expect(state.text()).toContain(name);
|
expect(state.text()).toContain(name);
|
||||||
expect(state.find(GlBadge).exists()).toBe(locked);
|
expect(state.find(GlBadge).exists()).toBe(hasBadge);
|
||||||
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
|
expect(state.find(GlLoadingIcon).exists()).toBe(loading);
|
||||||
expect(toolTip.exists()).toBe(locked);
|
|
||||||
|
|
||||||
if (locked) {
|
if (hasBadge) {
|
||||||
expect(toolTip.text()).toMatchInterpolatedText(toolTipText);
|
const badge = wrapper.findByTestId(`state-badge-${name}`);
|
||||||
|
|
||||||
|
expect(getBinding(badge.element, 'gl-tooltip')).toBeDefined();
|
||||||
|
expect(badge.attributes('title')).toMatchInterpolatedText(toolTipText);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,7 +66,7 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays value as hidden', () => {
|
it('displays value as hidden', () => {
|
||||||
expect(findFormInputGroup().props('value')).toBe('********************');
|
expect(findFormInput().element.value).toBe('********************');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('saves actual value to clipboard when manually copied', () => {
|
it('saves actual value to clipboard when manually copied', () => {
|
||||||
|
@ -107,7 +107,7 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays value', () => {
|
it('displays value', () => {
|
||||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
expect(findFormInput().element.value).toBe(valueProp);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders a hide button', () => {
|
it('renders a hide button', () => {
|
||||||
|
@ -159,25 +159,52 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays value as hidden with 20 asterisks', () => {
|
it('displays value as hidden with 20 asterisks', () => {
|
||||||
expect(findFormInputGroup().props('value')).toBe('********************');
|
expect(findFormInput().element.value).toBe('********************');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when `initialVisibility` prop is `true`', () => {
|
describe('when `initialVisibility` prop is `true`', () => {
|
||||||
|
const label = 'My label';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
value: valueProp,
|
value: valueProp,
|
||||||
initialVisibility: true,
|
initialVisibility: true,
|
||||||
|
label,
|
||||||
|
'label-for': 'my-input',
|
||||||
|
formInputGroupProps: {
|
||||||
|
id: 'my-input',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays value', () => {
|
it('displays value', () => {
|
||||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
expect(findFormInput().element.value).toBe(valueProp);
|
||||||
});
|
});
|
||||||
|
|
||||||
itDoesNotModifyCopyEvent();
|
itDoesNotModifyCopyEvent();
|
||||||
|
|
||||||
|
describe('when input is clicked', () => {
|
||||||
|
it('selects input value', async () => {
|
||||||
|
const mockSelect = jest.fn();
|
||||||
|
wrapper.vm.$refs.input.$el.select = mockSelect;
|
||||||
|
await wrapper.findByLabelText(label).trigger('click');
|
||||||
|
|
||||||
|
expect(mockSelect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when label is clicked', () => {
|
||||||
|
it('selects input value', async () => {
|
||||||
|
const mockSelect = jest.fn();
|
||||||
|
wrapper.vm.$refs.input.$el.select = mockSelect;
|
||||||
|
await wrapper.find('label').trigger('click');
|
||||||
|
|
||||||
|
expect(mockSelect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when `showToggleVisibilityButton` is `false`', () => {
|
describe('when `showToggleVisibilityButton` is `false`', () => {
|
||||||
|
@ -196,7 +223,7 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays value', () => {
|
it('displays value', () => {
|
||||||
expect(findFormInputGroup().props('value')).toBe(valueProp);
|
expect(findFormInput().element.value).toBe(valueProp);
|
||||||
});
|
});
|
||||||
|
|
||||||
itDoesNotModifyCopyEvent();
|
itDoesNotModifyCopyEvent();
|
||||||
|
@ -216,16 +243,30 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes `formInputGroupProps` prop to `GlFormInputGroup`', () => {
|
it('passes `formInputGroupProps` prop only to the input', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
propsData: {
|
propsData: {
|
||||||
formInputGroupProps: {
|
formInputGroupProps: {
|
||||||
label: 'Foo bar',
|
name: 'Foo bar',
|
||||||
|
'data-qa-selector': 'Foo bar',
|
||||||
|
class: 'Foo bar',
|
||||||
|
id: 'Foo bar',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(findFormInputGroup().props('label')).toBe('Foo bar');
|
expect(findFormInput().attributes()).toMatchObject({
|
||||||
|
name: 'Foo bar',
|
||||||
|
'data-qa-selector': 'Foo bar',
|
||||||
|
class: expect.stringContaining('Foo bar'),
|
||||||
|
id: 'Foo bar',
|
||||||
|
});
|
||||||
|
|
||||||
|
const attributesInputGroup = findFormInputGroup().attributes();
|
||||||
|
expect(attributesInputGroup.name).toBeUndefined();
|
||||||
|
expect(attributesInputGroup['data-qa-selector']).toBeUndefined();
|
||||||
|
expect(attributesInputGroup.class).not.toContain('Foo bar');
|
||||||
|
expect(attributesInputGroup.id).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes `copyButtonTitle` prop to `ClipboardButton`', () => {
|
it('passes `copyButtonTitle` prop to `ClipboardButton`', () => {
|
||||||
|
@ -248,32 +289,4 @@ describe('InputCopyToggleVisibility', () => {
|
||||||
|
|
||||||
expect(wrapper.findByText(description).exists()).toBe(true);
|
expect(wrapper.findByText(description).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes `inputClass` prop to `GlFormInputGroup`', () => {
|
|
||||||
createComponent();
|
|
||||||
expect(findFormInputGroup().props('inputClass')).toBe('gl-font-monospace! gl-cursor-default!');
|
|
||||||
wrapper.destroy();
|
|
||||||
|
|
||||||
createComponent({
|
|
||||||
propsData: {
|
|
||||||
inputClass: 'Foo bar',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(findFormInputGroup().props('inputClass')).toBe(
|
|
||||||
'gl-font-monospace! gl-cursor-default! Foo bar',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('passes `qaSelector` prop as an `data-qa-selector` attribute to `GlFormInputGroup`', () => {
|
|
||||||
createComponent();
|
|
||||||
expect(findFormInputGroup().attributes('data-qa-selector')).toBeUndefined();
|
|
||||||
wrapper.destroy();
|
|
||||||
|
|
||||||
createComponent({
|
|
||||||
propsData: {
|
|
||||||
qaSelector: 'Foo bar',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(findFormInputGroup().attributes('data-qa-selector')).toBe('Foo bar');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -48,13 +48,12 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
const findModal = () => wrapper.findComponent(GlModal);
|
const findModal = () => wrapper.findComponent(GlModal);
|
||||||
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
|
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
|
||||||
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
|
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
|
||||||
const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
|
|
||||||
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
|
const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
|
||||||
const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
|
const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
|
||||||
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
|
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
|
||||||
const findRegisterCommand = () => wrapper.findByTestId('register-command');
|
const findRegisterCommand = () => wrapper.findByTestId('register-command');
|
||||||
|
|
||||||
const createComponent = ({ props, ...options } = {}) => {
|
const createComponent = ({ props, shown = true, ...options } = {}) => {
|
||||||
const requestHandlers = [
|
const requestHandlers = [
|
||||||
[getRunnerPlatformsQuery, runnerPlatformsHandler],
|
[getRunnerPlatformsQuery, runnerPlatformsHandler],
|
||||||
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
|
[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
|
||||||
|
@ -73,184 +72,202 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
...options,
|
...options,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// trigger open modal
|
||||||
|
if (shown) {
|
||||||
|
findModal().vm.$emit('shown');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
|
runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
|
||||||
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
|
runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
|
||||||
|
|
||||||
createComponent();
|
|
||||||
await waitForPromises();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show alert', () => {
|
describe('when the modal is shown', () => {
|
||||||
expect(findAlert().exists()).toBe(false);
|
beforeEach(async () => {
|
||||||
});
|
createComponent();
|
||||||
|
await waitForPromises();
|
||||||
it('should contain a number of platforms buttons', () => {
|
|
||||||
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
|
|
||||||
|
|
||||||
const buttons = findPlatformButtons();
|
|
||||||
|
|
||||||
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a number of dropdown items for the architecture options', () => {
|
|
||||||
expect(findArchitectureDropdownItems()).toHaveLength(
|
|
||||||
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should display default instructions', () => {
|
|
||||||
const { installInstructions, registerInstructions } = mockGraphqlInstructions.data.runnerSetup;
|
|
||||||
|
|
||||||
it('runner instructions are requested', () => {
|
|
||||||
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
|
|
||||||
platform: 'linux',
|
|
||||||
architecture: 'amd64',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('binary instructions are shown', async () => {
|
it('should not show alert', async () => {
|
||||||
await waitForPromises();
|
expect(findAlert().exists()).toBe(false);
|
||||||
const instructions = findBinaryInstructions().text();
|
|
||||||
|
|
||||||
expect(instructions).toBe(installInstructions);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('register command is shown with a replaced token', async () => {
|
it('should contain a number of platforms buttons', () => {
|
||||||
await waitForPromises();
|
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
|
||||||
const instructions = findRegisterCommand().text();
|
|
||||||
|
|
||||||
expect(instructions).toBe(
|
const buttons = findPlatformButtons();
|
||||||
'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
|
|
||||||
|
expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a number of dropdown items for the architecture options', () => {
|
||||||
|
expect(findArchitectureDropdownItems()).toHaveLength(
|
||||||
|
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when a register token is not shown', () => {
|
describe('should display default instructions', () => {
|
||||||
beforeEach(async () => {
|
const {
|
||||||
createComponent({ props: { registrationToken: undefined } });
|
installInstructions,
|
||||||
await waitForPromises();
|
registerInstructions,
|
||||||
});
|
} = mockGraphqlInstructions.data.runnerSetup;
|
||||||
|
|
||||||
it('register command is shown without a defined registration token', () => {
|
it('runner instructions are requested', () => {
|
||||||
const instructions = findRegisterCommand().text();
|
|
||||||
|
|
||||||
expect(instructions).toBe(registerInstructions);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the modal is shown', () => {
|
|
||||||
it('sets the focus on the selected platform', () => {
|
|
||||||
findPlatformButtons().at(0).element.focus = jest.fn();
|
|
||||||
|
|
||||||
findModal().vm.$emit('shown');
|
|
||||||
|
|
||||||
expect(findPlatformButtons().at(0).element.focus).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when providing a defaultPlatformName', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
createComponent({ props: { defaultPlatformName: 'osx' } });
|
|
||||||
await waitForPromises();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runner instructions for the default selected platform are requested', () => {
|
|
||||||
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
|
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
|
||||||
platform: 'osx',
|
platform: 'linux',
|
||||||
architecture: 'amd64',
|
architecture: 'amd64',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets the focus on the default selected platform', () => {
|
it('binary instructions are shown', async () => {
|
||||||
findOsxPlatformButton().element.focus = jest.fn();
|
const instructions = findBinaryInstructions().text();
|
||||||
|
|
||||||
findModal().vm.$emit('shown');
|
expect(instructions).toBe(installInstructions);
|
||||||
|
});
|
||||||
|
|
||||||
expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
|
it('register command is shown with a replaced token', async () => {
|
||||||
|
const command = findRegisterCommand().text();
|
||||||
|
|
||||||
|
expect(command).toBe(
|
||||||
|
'sudo gitlab-runner register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a register token is not shown', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createComponent({ props: { registrationToken: undefined } });
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register command is shown without a defined registration token', () => {
|
||||||
|
const instructions = findRegisterCommand().text();
|
||||||
|
|
||||||
|
expect(instructions).toBe(registerInstructions);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when providing a defaultPlatformName', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
createComponent({ props: { defaultPlatformName: 'osx' } });
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runner instructions for the default selected platform are requested', () => {
|
||||||
|
expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
|
||||||
|
platform: 'osx',
|
||||||
|
architecture: 'amd64',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the focus on the default selected platform', () => {
|
||||||
|
const findOsxPlatformButton = () => wrapper.find({ ref: 'osx' });
|
||||||
|
|
||||||
|
findOsxPlatformButton().element.focus = jest.fn();
|
||||||
|
|
||||||
|
findModal().vm.$emit('shown');
|
||||||
|
|
||||||
|
expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after a platform and architecture are selected', () => {
|
||||||
|
const windowsIndex = 2;
|
||||||
|
const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
|
||||||
|
|
||||||
|
findPlatformButtons().at(windowsIndex).vm.$emit('click');
|
||||||
|
await waitForPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runner instructions are requested', () => {
|
||||||
|
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
|
||||||
|
platform: 'windows',
|
||||||
|
architecture: 'amd64',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('architecture download link is updated', () => {
|
||||||
|
const architectures =
|
||||||
|
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
|
||||||
|
|
||||||
|
expect(findBinaryDownloadButton().attributes('href')).toBe(
|
||||||
|
architectures[0].downloadLocation,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('other binary instructions are shown', () => {
|
||||||
|
const instructions = findBinaryInstructions().text();
|
||||||
|
|
||||||
|
expect(instructions).toBe(installInstructions);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('register command is shown', () => {
|
||||||
|
const command = findRegisterCommand().text();
|
||||||
|
|
||||||
|
expect(command).toBe(
|
||||||
|
'./gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('runner instructions are requested with another architecture', async () => {
|
||||||
|
findArchitectureDropdownItems().at(1).vm.$emit('click');
|
||||||
|
await waitForPromises();
|
||||||
|
|
||||||
|
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
|
||||||
|
platform: 'windows',
|
||||||
|
architecture: '386',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the modal resizes', () => {
|
||||||
|
it('to an xs viewport', async () => {
|
||||||
|
MockResizeObserver.mockResize('xs');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to a non-xs viewport', async () => {
|
||||||
|
MockResizeObserver.mockResize('sm');
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('after a platform and architecture are selected', () => {
|
describe('when the modal is not shown', () => {
|
||||||
const windowsIndex = 2;
|
|
||||||
const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
|
createComponent({ shown: false });
|
||||||
|
|
||||||
findPlatformButtons().at(windowsIndex).vm.$emit('click');
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('runner instructions are requested', () => {
|
it('does not fetch instructions', () => {
|
||||||
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
|
expect(runnerPlatformsHandler).not.toHaveBeenCalled();
|
||||||
platform: 'windows',
|
expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled();
|
||||||
architecture: 'amd64',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('architecture download link is updated', () => {
|
|
||||||
const architectures =
|
|
||||||
mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
|
|
||||||
|
|
||||||
expect(findBinaryDownloadButton().attributes('href')).toBe(architectures[0].downloadLocation);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('other binary instructions are shown', () => {
|
|
||||||
const instructions = findBinaryInstructions().text();
|
|
||||||
|
|
||||||
expect(instructions).toBe(installInstructions);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('register command is shown', () => {
|
|
||||||
const command = findRegisterCommand().text();
|
|
||||||
|
|
||||||
expect(command).toBe(
|
|
||||||
'./gitlab-runner.exe register --url http://gdk.test:3000/ --registration-token MY_TOKEN',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('runner instructions are requested with another architecture', async () => {
|
|
||||||
findArchitectureDropdownItems().at(1).vm.$emit('click');
|
|
||||||
await waitForPromises();
|
|
||||||
|
|
||||||
expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
|
|
||||||
platform: 'windows',
|
|
||||||
architecture: '386',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the modal resizes', () => {
|
|
||||||
it('to an xs viewport', async () => {
|
|
||||||
MockResizeObserver.mockResize('xs');
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(findPlatformButtonGroup().attributes('vertical')).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('to a non-xs viewport', async () => {
|
|
||||||
MockResizeObserver.mockResize('sm');
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(findPlatformButtonGroup().props('vertical')).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when apollo is loading', () => {
|
describe('when apollo is loading', () => {
|
||||||
it('should show a skeleton loader', async () => {
|
beforeEach(() => {
|
||||||
createComponent();
|
createComponent();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a skeleton loader', async () => {
|
||||||
expect(findSkeletonLoader().exists()).toBe(true);
|
expect(findSkeletonLoader().exists()).toBe(true);
|
||||||
expect(findGlLoadingIcon().exists()).toBe(false);
|
expect(findGlLoadingIcon().exists()).toBe(false);
|
||||||
|
|
||||||
await nextTick();
|
// wait on fetch of both `platforms` and `instructions`
|
||||||
jest.runOnlyPendingTimers();
|
|
||||||
await nextTick();
|
await nextTick();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
|
||||||
|
@ -258,7 +275,6 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('once loaded, should not show a loading state', async () => {
|
it('once loaded, should not show a loading state', async () => {
|
||||||
createComponent();
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
|
|
||||||
expect(findSkeletonLoader().exists()).toBe(false);
|
expect(findSkeletonLoader().exists()).toBe(false);
|
||||||
|
@ -271,7 +287,6 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
runnerSetupInstructionsHandler.mockRejectedValue();
|
runnerSetupInstructionsHandler.mockRejectedValue();
|
||||||
|
|
||||||
createComponent();
|
createComponent();
|
||||||
|
|
||||||
await waitForPromises();
|
await waitForPromises();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -303,6 +318,7 @@ describe('RunnerInstructionsModal component', () => {
|
||||||
mockShow = jest.fn();
|
mockShow = jest.fn();
|
||||||
|
|
||||||
createComponent({
|
createComponent({
|
||||||
|
shown: false,
|
||||||
stubs: {
|
stubs: {
|
||||||
GlModal: getGlModalStub({ show: mockShow }),
|
GlModal: getGlModalStub({ show: mockShow }),
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
||||||
import { nextTick } from 'vue';
|
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
|
||||||
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
|
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
|
||||||
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
|
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
|
||||||
|
|
||||||
|
@ -11,7 +10,11 @@ describe('RunnerInstructions component', () => {
|
||||||
const findModal = () => wrapper.findComponent(RunnerInstructionsModal);
|
const findModal = () => wrapper.findComponent(RunnerInstructionsModal);
|
||||||
|
|
||||||
const createComponent = () => {
|
const createComponent = () => {
|
||||||
wrapper = extendedWrapper(shallowMount(RunnerInstructions));
|
wrapper = shallowMountExtended(RunnerInstructions, {
|
||||||
|
directives: {
|
||||||
|
GlModal: createMockDirective(),
|
||||||
|
},
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -23,19 +26,12 @@ describe('RunnerInstructions component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show the "Show runner installation instructions" button', () => {
|
it('should show the "Show runner installation instructions" button', () => {
|
||||||
expect(findModalButton().exists()).toBe(true);
|
|
||||||
expect(findModalButton().text()).toBe('Show runner installation instructions');
|
expect(findModalButton().text()).toBe('Show runner installation instructions');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render the modal once mounted', () => {
|
it('should render the modal', () => {
|
||||||
expect(findModal().exists()).toBe(false);
|
const modalId = getBinding(findModal().element, 'gl-modal');
|
||||||
});
|
|
||||||
|
|
||||||
it('should render the modal once clicked', async () => {
|
expect(findModalButton().attributes('modal-id')).toBe(modalId);
|
||||||
findModalButton().vm.$emit('click');
|
|
||||||
|
|
||||||
await nextTick();
|
|
||||||
|
|
||||||
expect(findModal().exists()).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe GitlabSchema.types['PackagesCleanupKeepDuplicatedPackageFilesEnum'] do
|
||||||
|
it 'exposes all options' do
|
||||||
|
expect(described_class.values.keys)
|
||||||
|
.to contain_exactly(*Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum::OPTIONS_MAPPING.values)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'uses all possible options from model' do
|
||||||
|
all_options = Packages::Cleanup::Policy::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES
|
||||||
|
expect(described_class::OPTIONS_MAPPING.keys).to contain_exactly(*all_options)
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,26 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe GitlabSchema.types['PackagesCleanupPolicy'] do
|
||||||
|
specify { expect(described_class.graphql_name).to eq('PackagesCleanupPolicy') }
|
||||||
|
|
||||||
|
specify do
|
||||||
|
expect(described_class.description)
|
||||||
|
.to eq('A packages cleanup policy designed to keep only packages and packages assets that matter most')
|
||||||
|
end
|
||||||
|
|
||||||
|
specify { expect(described_class).to require_graphql_authorizations(:admin_package) }
|
||||||
|
|
||||||
|
describe 'keep_n_duplicated_package_files' do
|
||||||
|
subject { described_class.fields['keepNDuplicatedPackageFiles'] }
|
||||||
|
|
||||||
|
it { is_expected.to have_non_null_graphql_type(Types::Packages::Cleanup::KeepDuplicatedPackageFilesEnum) }
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'next_run_at' do
|
||||||
|
subject { described_class.fields['nextRunAt'] }
|
||||||
|
|
||||||
|
it { is_expected.to have_nullable_graphql_type(Types::TimeType) }
|
||||||
|
end
|
||||||
|
end
|
|
@ -36,7 +36,7 @@ RSpec.describe GitlabSchema.types['Project'] do
|
||||||
pipeline_analytics squash_read_only sast_ci_configuration
|
pipeline_analytics squash_read_only sast_ci_configuration
|
||||||
cluster_agent cluster_agents agent_configurations
|
cluster_agent cluster_agents agent_configurations
|
||||||
ci_template timelogs merge_commit_template squash_commit_template work_item_types
|
ci_template timelogs merge_commit_template squash_commit_template work_item_types
|
||||||
recent_issue_boards ci_config_path_or_default
|
recent_issue_boards ci_config_path_or_default packages_cleanup_policy
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||||
|
@ -421,6 +421,12 @@ RSpec.describe GitlabSchema.types['Project'] do
|
||||||
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
|
it { is_expected.to have_graphql_type(Types::ContainerExpirationPolicyType) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'packages cleanup policy field' do
|
||||||
|
subject { described_class.fields['packagesCleanupPolicy'] }
|
||||||
|
|
||||||
|
it { is_expected.to have_graphql_type(Types::Packages::Cleanup::PolicyType) }
|
||||||
|
end
|
||||||
|
|
||||||
describe 'terraform state field' do
|
describe 'terraform state field' do
|
||||||
subject { described_class.fields['terraformState'] }
|
subject { described_class.fields['terraformState'] }
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do
|
||||||
it { expect(described_class).to require_graphql_authorizations(:read_terraform_state) }
|
it { expect(described_class).to require_graphql_authorizations(:read_terraform_state) }
|
||||||
|
|
||||||
describe 'fields' do
|
describe 'fields' do
|
||||||
let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at] }
|
let(:fields) { %i[id name locked_by_user locked_at latest_version created_at updated_at deleted_at] }
|
||||||
|
|
||||||
it { expect(described_class).to have_graphql_fields(fields) }
|
it { expect(described_class).to have_graphql_fields(fields) }
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ RSpec.describe GitlabSchema.types['TerraformState'] do
|
||||||
it { expect(described_class.fields['lockedAt'].type).not_to be_non_null }
|
it { expect(described_class.fields['lockedAt'].type).not_to be_non_null }
|
||||||
it { expect(described_class.fields['createdAt'].type).to be_non_null }
|
it { expect(described_class.fields['createdAt'].type).to be_non_null }
|
||||||
it { expect(described_class.fields['updatedAt'].type).to be_non_null }
|
it { expect(described_class.fields['updatedAt'].type).to be_non_null }
|
||||||
|
it { expect(described_class.fields['deletedAt'].type).not_to be_non_null }
|
||||||
|
|
||||||
it { expect(described_class.fields['latestVersion'].type).not_to be_non_null }
|
it { expect(described_class.fields['latestVersion'].type).not_to be_non_null }
|
||||||
it { expect(described_class.fields['latestVersion'].complexity).to eq(3) }
|
it { expect(described_class.fields['latestVersion'].complexity).to eq(3) }
|
||||||
|
|
|
@ -92,6 +92,34 @@ RSpec.describe Gitlab::Tracking::StandardContext do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with incorrect argument type' do
|
||||||
|
context 'when standard_context_type_check FF is disabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(standard_context_type_check: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(project: create(:group)) }
|
||||||
|
|
||||||
|
it 'does not call `track_and_raise_for_dev_exception`' do
|
||||||
|
expect(Gitlab::ErrorTracking).not_to receive(:track_and_raise_for_dev_exception)
|
||||||
|
snowplow_context
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when standard_context_type_check FF is enabled' do
|
||||||
|
before do
|
||||||
|
stub_feature_flags(standard_context_type_check: true)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(project: create(:group)) }
|
||||||
|
|
||||||
|
it 'does call `track_and_raise_for_dev_exception`' do
|
||||||
|
expect(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception)
|
||||||
|
snowplow_context
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it 'contains user id' do
|
it 'contains user id' do
|
||||||
expect(snowplow_context.to_json[:data].keys).to include(:user_id)
|
expect(snowplow_context.to_json[:data].keys).to include(:user_id)
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ RSpec.describe Packages::Cleanup::Policy, type: :model do
|
||||||
is_expected
|
is_expected
|
||||||
.to validate_inclusion_of(:keep_n_duplicated_package_files)
|
.to validate_inclusion_of(:keep_n_duplicated_package_files)
|
||||||
.in_array(described_class::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES)
|
.in_array(described_class::KEEP_N_DUPLICATED_PACKAGE_FILES_VALUES)
|
||||||
.with_message('keep_n_duplicated_package_files is invalid')
|
.with_message('is invalid')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1356,6 +1356,36 @@ RSpec.describe ProjectPolicy do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'admin_package' do
|
||||||
|
context 'with admin' do
|
||||||
|
let(:current_user) { admin }
|
||||||
|
|
||||||
|
context 'when admin mode enabled', :enable_admin_mode do
|
||||||
|
it { is_expected.to be_allowed(:admin_package) }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when admin mode disabled' do
|
||||||
|
it { is_expected.to be_disallowed(:admin_package) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[owner maintainer].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_allowed(:admin_package) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
%i[developer reporter guest non_member anonymous].each do |role|
|
||||||
|
context "with #{role}" do
|
||||||
|
let(:current_user) { public_send(role) }
|
||||||
|
|
||||||
|
it { is_expected.to be_disallowed(:admin_package) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'read_feature_flag' do
|
describe 'read_feature_flag' do
|
||||||
subject { described_class.new(current_user, project) }
|
subject { described_class.new(current_user, project) }
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,109 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Updating the packages cleanup policy' do
|
||||||
|
include GraphqlHelpers
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let_it_be(:project, reload: true) { create(:project) }
|
||||||
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
project_path: project.full_path,
|
||||||
|
keep_n_duplicated_package_files: 'TWENTY_PACKAGE_FILES'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:mutation) do
|
||||||
|
graphql_mutation(:update_packages_cleanup_policy, params,
|
||||||
|
<<~QUERY
|
||||||
|
packagesCleanupPolicy {
|
||||||
|
keepNDuplicatedPackageFiles
|
||||||
|
nextRunAt
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
QUERY
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:mutation_response) { graphql_mutation_response(:update_packages_cleanup_policy) }
|
||||||
|
let(:packages_cleanup_policy_response) { mutation_response['packagesCleanupPolicy'] }
|
||||||
|
|
||||||
|
shared_examples 'accepting the mutation request and updates the existing policy' do
|
||||||
|
it 'returns the updated packages cleanup policy' do
|
||||||
|
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||||
|
|
||||||
|
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20')
|
||||||
|
expect_graphql_errors_to_be_empty
|
||||||
|
expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles'])
|
||||||
|
.to eq(params[:keep_n_duplicated_package_files])
|
||||||
|
expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'accepting the mutation request and creates a policy' do
|
||||||
|
it 'returns the created packages cleanup policy' do
|
||||||
|
expect { subject }.to change { ::Packages::Cleanup::Policy.count }.by(1)
|
||||||
|
|
||||||
|
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('20')
|
||||||
|
expect_graphql_errors_to_be_empty
|
||||||
|
expect(packages_cleanup_policy_response['keepNDuplicatedPackageFiles'])
|
||||||
|
.to eq(params[:keep_n_duplicated_package_files])
|
||||||
|
expect(packages_cleanup_policy_response['nextRunAt']).not_to eq(nil)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'denying the mutation request' do
|
||||||
|
it 'returns an error' do
|
||||||
|
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||||
|
|
||||||
|
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).not_to eq('20')
|
||||||
|
expect(mutation_response).to be_nil
|
||||||
|
expect_graphql_errors_to_include(/you don't have permission to perform this action/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'post graphql mutation' do
|
||||||
|
subject { post_graphql_mutation(mutation, current_user: user) }
|
||||||
|
|
||||||
|
context 'with existing packages cleanup policy' do
|
||||||
|
let_it_be(:project_packages_cleanup_policy) { create(:packages_cleanup_policy, project: project) }
|
||||||
|
|
||||||
|
where(:user_role, :shared_examples_name) do
|
||||||
|
:maintainer | 'accepting the mutation request and updates the existing policy'
|
||||||
|
:developer | 'denying the mutation request'
|
||||||
|
:reporter | 'denying the mutation request'
|
||||||
|
:guest | 'denying the mutation request'
|
||||||
|
:anonymous | 'denying the mutation request'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
project.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like params[:shared_examples_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without existing packages cleanup policy' do
|
||||||
|
where(:user_role, :shared_examples_name) do
|
||||||
|
:maintainer | 'accepting the mutation request and creates a policy'
|
||||||
|
:developer | 'denying the mutation request'
|
||||||
|
:reporter | 'denying the mutation request'
|
||||||
|
:guest | 'denying the mutation request'
|
||||||
|
:anonymous | 'denying the mutation request'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
project.send("add_#{user_role}", user) unless user_role == :anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like params[:shared_examples_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,79 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'getting the packages cleanup policy linked to a project' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
include GraphqlHelpers
|
||||||
|
|
||||||
|
let_it_be_with_reload(:project) { create(:project) }
|
||||||
|
let_it_be(:current_user) { project.first_owner }
|
||||||
|
|
||||||
|
let(:fields) do
|
||||||
|
<<~QUERY
|
||||||
|
#{all_graphql_fields_for('packages_cleanup_policy'.classify)}
|
||||||
|
QUERY
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:query) do
|
||||||
|
graphql_query_for(
|
||||||
|
'project',
|
||||||
|
{ 'fullPath' => project.full_path },
|
||||||
|
query_graphql_field('packagesCleanupPolicy', {}, fields)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { post_graphql(query, current_user: current_user) }
|
||||||
|
|
||||||
|
it_behaves_like 'a working graphql query' do
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an existing policy' do
|
||||||
|
let_it_be(:policy) { create(:packages_cleanup_policy, project: project) }
|
||||||
|
|
||||||
|
it_behaves_like 'a working graphql query' do
|
||||||
|
before do
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with different permissions' do
|
||||||
|
let_it_be(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
let(:packages_cleanup_policy_response) { graphql_data_at('project', 'packagesCleanupPolicy') }
|
||||||
|
|
||||||
|
where(:visibility, :role, :policy_visible) do
|
||||||
|
:private | :maintainer | true
|
||||||
|
:private | :developer | false
|
||||||
|
:private | :reporter | false
|
||||||
|
:private | :guest | false
|
||||||
|
:private | :anonymous | false
|
||||||
|
:public | :maintainer | true
|
||||||
|
:public | :developer | false
|
||||||
|
:public | :reporter | false
|
||||||
|
:public | :guest | false
|
||||||
|
:public | :anonymous | false
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
project.update!(visibility: visibility.to_s)
|
||||||
|
project.add_user(current_user, role) unless role == :anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'return the proper response' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
if policy_visible
|
||||||
|
expect(packages_cleanup_policy_response)
|
||||||
|
.to eq('keepNDuplicatedPackageFiles' => 'ALL_PACKAGE_FILES', 'nextRunAt' => nil)
|
||||||
|
else
|
||||||
|
expect(packages_cleanup_policy_response).to be_blank
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1,105 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe Packages::Cleanup::UpdatePolicyService do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
|
let_it_be_with_reload(:project) { create(:project) }
|
||||||
|
let_it_be(:current_user) { create(:user) }
|
||||||
|
|
||||||
|
let(:params) { { keep_n_duplicated_package_files: 50 } }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
subject { described_class.new(project: project, current_user: current_user, params: params).execute }
|
||||||
|
|
||||||
|
shared_examples 'creating the policy' do
|
||||||
|
it 'creates a new one' do
|
||||||
|
expect { subject }.to change { ::Packages::Cleanup::Policy.count }.from(0).to(1)
|
||||||
|
|
||||||
|
expect(subject.payload[:packages_cleanup_policy]).to be_present
|
||||||
|
expect(subject.success?).to be_truthy
|
||||||
|
expect(project.packages_cleanup_policy).to be_persisted
|
||||||
|
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid parameters' do
|
||||||
|
let(:params) { { keep_n_duplicated_package_files: 100 } }
|
||||||
|
|
||||||
|
it 'does not create one' do
|
||||||
|
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||||
|
|
||||||
|
expect(subject.status).to eq(:error)
|
||||||
|
expect(subject.message).to eq('Keep n duplicated package files is invalid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'updating the policy' do
|
||||||
|
it 'updates the existing one' do
|
||||||
|
expect { subject }.not_to change { ::Packages::Cleanup::Policy.count }
|
||||||
|
|
||||||
|
expect(subject.payload[:packages_cleanup_policy]).to be_present
|
||||||
|
expect(subject.success?).to be_truthy
|
||||||
|
expect(project.packages_cleanup_policy.keep_n_duplicated_package_files).to eq('50')
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with invalid parameters' do
|
||||||
|
let(:params) { { keep_n_duplicated_package_files: 100 } }
|
||||||
|
|
||||||
|
it 'does not update one' do
|
||||||
|
expect { subject }.not_to change { policy.keep_n_duplicated_package_files }
|
||||||
|
|
||||||
|
expect(subject.status).to eq(:error)
|
||||||
|
expect(subject.message).to eq('Keep n duplicated package files is invalid')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'denying access' do
|
||||||
|
it 'returns an error' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(subject.message).to eq('Access denied')
|
||||||
|
expect(subject.status).to eq(:error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with existing container expiration policy' do
|
||||||
|
let_it_be(:policy) { create(:packages_cleanup_policy, project: project) }
|
||||||
|
|
||||||
|
where(:user_role, :shared_examples_name) do
|
||||||
|
:maintainer | 'updating the policy'
|
||||||
|
:developer | 'denying access'
|
||||||
|
:reporter | 'denying access'
|
||||||
|
:guest | 'denying access'
|
||||||
|
:anonymous | 'denying access'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
project.send("add_#{user_role}", current_user) unless user_role == :anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like params[:shared_examples_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without existing container expiration policy' do
|
||||||
|
where(:user_role, :shared_examples_name) do
|
||||||
|
:maintainer | 'creating the policy'
|
||||||
|
:developer | 'denying access'
|
||||||
|
:reporter | 'denying access'
|
||||||
|
:guest | 'denying access'
|
||||||
|
:anonymous | 'denying access'
|
||||||
|
end
|
||||||
|
|
||||||
|
with_them do
|
||||||
|
before do
|
||||||
|
project.send("add_#{user_role}", current_user) unless user_role == :anonymous
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like params[:shared_examples_name]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -22,7 +22,7 @@ module NextInstanceOf
|
||||||
def stub_new(target, number, ordered = false, *new_args, &blk)
|
def stub_new(target, number, ordered = false, *new_args, &blk)
|
||||||
receive_new = receive(:new)
|
receive_new = receive(:new)
|
||||||
receive_new.ordered if ordered
|
receive_new.ordered if ordered
|
||||||
receive_new.with(*new_args) if new_args.any?
|
receive_new.with(*new_args) if new_args.present?
|
||||||
|
|
||||||
if number.is_a?(Range)
|
if number.is_a?(Range)
|
||||||
receive_new.at_least(number.begin).times if number.begin
|
receive_new.at_least(number.begin).times if number.begin
|
||||||
|
|
|
@ -35,11 +35,11 @@ RSpec.shared_examples 'shows and resets runner registration token' do
|
||||||
|
|
||||||
it 'has a registration token' do
|
it 'has a registration token' do
|
||||||
click_on 'Click to reveal'
|
click_on 'Click to reveal'
|
||||||
expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token)
|
expect(page.find_field('token-value').value).to have_content(registration_token)
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'reset registration token' do
|
describe 'reset registration token' do
|
||||||
let!(:old_registration_token) { find('[data-testid="token-value"] input').value }
|
let!(:old_registration_token) { find_field('token-value').value }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
click_on 'Reset registration token'
|
click_on 'Reset registration token'
|
||||||
|
|
|
@ -25,7 +25,7 @@ RSpec.describe Ci::ResourceGroups::AssignResourceFromResourceGroupWorker do
|
||||||
|
|
||||||
context 'when resource group exists' do
|
context 'when resource group exists' do
|
||||||
it 'executes AssignResourceFromResourceGroupService' do
|
it 'executes AssignResourceFromResourceGroupService' do
|
||||||
expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, resource_group.project, nil) do |service|
|
expect_next_instances_of(Ci::ResourceGroups::AssignResourceFromResourceGroupService, 2, false, resource_group.project, nil) do |service|
|
||||||
expect(service).to receive(:execute).with(resource_group)
|
expect(service).to receive(:execute).with(resource_group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue