Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-07-24 03:08:37 +00:00
parent 02692fb162
commit 75ea027cb6
46 changed files with 593 additions and 155 deletions

View File

@ -0,0 +1,233 @@
<script>
import {
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormTextarea,
GlIcon,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
defaultVariableState,
ENVIRONMENT_SCOPE_LINK_TITLE,
EXPANDED_VARIABLES_NOTE,
FLAG_LINK_TITLE,
VARIABLE_ACTIONS,
variableOptions,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
const i18n = {
addVariable: s__('CiVariables|Add Variable'),
cancel: __('Cancel'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
expandedDescription: EXPANDED_VARIABLES_NOTE,
flags: __('Flags'),
flagsLinkTitle: FLAG_LINK_TITLE,
key: __('Key'),
maskedField: s__('CiVariables|Mask variable'),
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
type: __('Type'),
value: __('Value'),
};
export default {
DRAWER_Z_INDEX,
components: {
CiEnvironmentsDropdown,
GlButton,
GlDrawer,
GlFormCheckbox,
GlFormCombobox,
GlFormGroup,
GlFormSelect,
GlFormTextarea,
GlIcon,
GlLink,
GlSprintf,
},
inject: ['environmentScopeLink'],
props: {
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
environments: {
type: Array,
required: false,
default: () => [],
},
hasEnvScopeQuery: {
type: Boolean,
required: true,
},
mode: {
type: String,
required: true,
validator(val) {
return VARIABLE_ACTIONS.includes(val);
},
},
},
data() {
return {
key: defaultVariableState.key,
variableType: defaultVariableState.variableType,
};
},
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
},
methods: {
close() {
this.$emit('close-form');
},
},
awsTokenList,
flagLink: helpPagePath('ci/variables/index', {
anchor: 'define-a-cicd-variable-in-the-ui',
}),
i18n,
variableOptions,
};
</script>
<template>
<gl-drawer
open
data-testid="ci-variable-drawer"
:header-height="getDrawerHeaderHeight"
:z-index="$options.DRAWER_Z_INDEX"
@close="close"
>
<template #title>
<h2 class="gl-m-0">{{ $options.i18n.addVariable }}</h2>
</template>
<gl-form-group
:label="$options.i18n.type"
label-for="ci-variable-type"
class="gl-border-none gl-mb-n5"
>
<gl-form-select
id="ci-variable-type"
v-model="variableType"
:options="$options.variableOptions"
/>
</gl-form-group>
<gl-form-group
class="gl-border-none gl-mb-n5"
label-for="ci-variable-env"
data-testid="environment-scope"
>
<template #label>
<div class="gl-display-flex gl-align-items-center">
<span class="gl-mr-2">
{{ $options.i18n.environments }}
</span>
<gl-link
class="gl-display-flex"
:title="$options.i18n.environmentScopeLinkTitle"
:href="environmentScopeLink"
target="_blank"
data-testid="environment-scope-link"
>
<gl-icon name="question-o" :size="14" />
</gl-link>
</div>
</template>
<ci-environments-dropdown
class="gl-mb-5"
:are-environments-loading="areEnvironmentsLoading"
:environments="environments"
:has-env-scope-query="hasEnvScopeQuery"
selected-environment-scope=""
/>
</gl-form-group>
<gl-form-group class="gl-border-none gl-mb-n8">
<template #label>
<div class="gl-display-flex gl-align-items-center gl-mb-n3">
<span class="gl-mr-2">
{{ $options.i18n.flags }}
</span>
<gl-link
class="gl-display-flex"
:title="$options.i18n.flagsLinkTitle"
:href="$options.flagLink"
target="_blank"
>
<gl-icon name="question-o" :size="14" />
</gl-link>
</div>
</template>
<gl-form-checkbox data-testid="ci-variable-protected-checkbox">
{{ $options.i18n.protectedField }}
<p class="gl-text-secondary">
{{ $options.i18n.protectedDescription }}
</p>
</gl-form-checkbox>
<gl-form-checkbox data-testid="ci-variable-masked-checkbox">
{{ $options.i18n.maskedField }}
<p class="gl-text-secondary">{{ $options.i18n.maskedDescription }}</p>
</gl-form-checkbox>
<gl-form-checkbox data-testid="ci-variable-expanded-checkbox">
{{ $options.i18n.expandedField }}
<p class="gl-text-secondary">
<gl-sprintf :message="$options.i18n.expandedDescription" class="gl-text-secondary">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</gl-form-checkbox>
</gl-form-group>
<gl-form-combobox
v-model="key"
:token-list="$options.awsTokenList"
:label-text="$options.i18n.key"
class="gl-border-none gl-pb-0! gl-mb-n5"
data-testid="pipeline-form-ci-variable-key"
data-qa-selector="ci_variable_key_field"
/>
<gl-form-group
:label="$options.i18n.value"
label-for="ci-variable-value"
class="gl-border-none gl-mb-n2"
>
<gl-form-textarea
id="ci-variable-value"
class="gl-border-none gl-font-monospace!"
rows="3"
max-rows="10"
data-testid="pipeline-form-ci-variable-value"
data-qa-selector="ci_variable_value_field"
spellcheck="false"
/>
</gl-form-group>
<div class="gl-display-flex gl-justify-content-end">
<gl-button category="primary" class="gl-mr-3" data-testid="cancel-button" @click="close"
>{{ $options.i18n.cancel }}
</gl-button>
<gl-button category="primary" variant="confirm" data-testid="confirm-button"
>{{ $options.i18n.addVariable }}
</gl-button>
</div>
</gl-drawer>
</template>

View File

@ -241,7 +241,7 @@ export default {
this.resetVariableData();
this.resetValidationErrorEvents();
this.$emit('hideModal');
this.$emit('close-form');
},
resetVariableData() {
this.variable = { ...defaultVariableState };

View File

@ -1,13 +1,17 @@
<script>
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ADD_VARIABLE_ACTION, EDIT_VARIABLE_ACTION, VARIABLE_ACTIONS } from '../constants';
import CiVariableDrawer from './ci_variable_drawer.vue';
import CiVariableTable from './ci_variable_table.vue';
import CiVariableModal from './ci_variable_modal.vue';
export default {
components: {
CiVariableDrawer,
CiVariableTable,
CiVariableModal,
},
mixins: [glFeatureFlagsMixin()],
props: {
areEnvironmentsLoading: {
type: Boolean,
@ -62,23 +66,32 @@ export default {
};
},
computed: {
showModal() {
showForm() {
return VARIABLE_ACTIONS.includes(this.mode);
},
useDrawerForm() {
return this.glFeatures?.ciVariableDrawer;
},
showDrawer() {
return this.showForm && this.useDrawerForm;
},
showModal() {
return this.showForm && !this.useDrawerForm;
},
},
methods: {
addVariable(variable) {
this.$emit('add-variable', variable);
},
closeForm() {
this.mode = null;
},
deleteVariable(variable) {
this.$emit('delete-variable', variable);
},
updateVariable(variable) {
this.$emit('update-variable', variable);
},
hideModal() {
this.mode = null;
},
setSelectedVariable(variable = null) {
if (!variable) {
this.selectedVariable = {};
@ -118,10 +131,18 @@ export default {
:selected-variable="selectedVariable"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@hideModal="hideModal"
@close-form="closeForm"
@update-variable="updateVariable"
@search-environment-scope="$emit('search-environment-scope', $event)"
/>
<ci-variable-drawer
v-if="showDrawer"
:are-environments-loading="areEnvironmentsLoading"
:has-env-scope-query="hasEnvScopeQuery"
:mode="mode"
v-on="$listeners"
@close-form="closeForm"
/>
</div>
</div>
</template>

View File

@ -7,14 +7,6 @@ export const SORT_DIRECTIONS = {
ASC: 'KEY_ASC',
DESC: 'KEY_DESC',
};
// This const will be deprecated once we remove VueX from the section
export const displayText = {
variableText: __('Variable'),
fileText: __('File'),
allEnvironmentsText: __('All (default)'),
};
export const variableTypes = {
envType: 'ENV_VAR',
fileType: 'FILE',
@ -26,8 +18,8 @@ export const allEnvironments = {
};
export const variableOptions = [
{ value: variableTypes.envType, text: variableTypes.envType },
{ value: variableTypes.fileType, text: variableTypes.fileType },
{ value: variableTypes.envType, text: __('Variable (default)') },
{ value: variableTypes.fileType, text: __('File') },
];
export const defaultVariableState = {

View File

@ -1,9 +1,10 @@
<script>
import { GlButton, GlFormInput, GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import { GlButton, GlFormInput, GlModal, GlSprintf } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import csrf from '~/lib/utils/csrf';
import { sprintf, s__ } from '~/locale';
import { sprintf } from '~/locale';
import eventHub from '../event_hub';
import { I18N_DELETE_TAG_MODAL } from '../constants';
export default {
csrf,
@ -12,7 +13,6 @@ export default {
GlButton,
GlFormInput,
GlSprintf,
GlAlert,
},
data() {
return {
@ -94,57 +94,38 @@ export default {
this.$refs.modal.hide();
},
},
i18n: {
modalTitle: s__('TagsPage|Delete tag. Are you ABSOLUTELY SURE?'),
modalTitleProtectedTag: s__('TagsPage|Delete protected tag. Are you ABSOLUTELY SURE?'),
modalMessage: s__(
"TagsPage|You're about to permanently delete the tag %{strongStart}%{tagName}.%{strongEnd}",
),
modalMessageProtectedTag: s__(
"TagsPage|You're about to permanently delete the protected tag %{strongStart}%{tagName}.%{strongEnd}",
),
undoneWarning: s__(
'TagsPage|After you confirm and select %{strongStart}%{buttonText},%{strongEnd} you cannot recover this tag.',
),
cancelButtonText: s__('TagsPage|Cancel, keep tag'),
confirmationText: s__(
'TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} tag cannot be undone. Are you sure?',
),
confirmationTextProtectedTag: s__('TagsPage|Please type the following to confirm:'),
deleteButtonText: s__('TagsPage|Yes, delete tag'),
deleteButtonTextProtectedTag: s__('TagsPage|Yes, delete protected tag'),
},
i18n: I18N_DELETE_TAG_MODAL,
};
</script>
<template>
<gl-modal ref="modal" size="sm" :modal-id="modalId" :title="title">
<gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
<div data-testid="modal-message">
<gl-sprintf :message="message">
<template #strong="{ content }">
<strong> {{ content }} </strong>
</template>
</gl-sprintf>
</div>
</gl-alert>
<div data-testid="modal-message">
<gl-sprintf :message="message">
<template #strong="{ content }">
<strong> {{ content }} </strong>
</template>
</gl-sprintf>
</div>
<p class="gl-mt-4">
<gl-sprintf :message="confirmationText">
<template #strong="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</p>
<form ref="form" :action="path" method="post">
<div v-if="isProtected" class="gl-mt-4">
<p>
<gl-sprintf :message="undoneWarning">
<template #strong="{ content }">
<strong> {{ content }} </strong>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.i18n.confirmationTextProtectedTag">
<template #strong="{ content }">
{{ content }}
</template>
</gl-sprintf>
<code class="gl-white-space-pre-wrap"> {{ tagName }} </code>
<code> {{ tagName }} </code>
<gl-form-input
v-model="enteredTagName"
name="delete_tag_input"
@ -155,17 +136,6 @@ export default {
/>
</p>
</div>
<div v-else>
<p class="gl-mt-4">
<gl-sprintf :message="confirmationText">
<template #strong="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</p>
</div>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />

View File

@ -0,0 +1,37 @@
import { s__ } from '~/locale';
export const MODAL_TITLE = s__('TagsPage|Permanently delete tag?');
export const MODAL_TITLE_PROTECTED_TAG = s__('TagsPage|Permanently delete protected tag?');
export const MODAL_MESSAGE = s__(
'TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} tag cannot be undone.',
);
export const MODAL_MESSAGE_PROTECTED_TAG = s__(
'TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} protected tag cannot be undone.',
);
export const CANCEL_BUTTON_TEXT = s__('TagsPage|Cancel, keep tag');
export const CONFIRMATION_TEXT = s__('TagsPage|Are you sure you want to delete this tag?');
export const CONFIRMATION_TEXT_PROTECTED_TAG = s__(
'TagsPage|Please type the following to confirm:',
);
export const DELETE_BUTTON_TEXT = s__('TagsPage|Yes, delete tag');
export const DELETE_BUTTON_TEXT_PROTECTED_TAG = s__('TagsPage|Yes, delete protected tag');
export const I18N_DELETE_TAG_MODAL = {
modalTitle: MODAL_TITLE,
modalTitleProtectedTag: MODAL_TITLE_PROTECTED_TAG,
modalMessage: MODAL_MESSAGE,
modalMessageProtectedTag: MODAL_MESSAGE_PROTECTED_TAG,
cancelButtonText: CANCEL_BUTTON_TEXT,
confirmationText: CONFIRMATION_TEXT,
confirmationTextProtectedTag: CONFIRMATION_TEXT_PROTECTED_TAG,
deleteButtonText: DELETE_BUTTON_TEXT,
deleteButtonTextProtectedTag: DELETE_BUTTON_TEXT_PROTECTED_TAG,
};

View File

@ -135,3 +135,16 @@
.gl-fill-red-500 {
fill: $red-500;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3569
.gl-mb-n5 {
margin-bottom: -$gl-spacing-scale-5;
}
.gl-mb-n7 {
margin-bottom: -$gl-spacing-scale-7;
}
.gl-mb-n8 {
margin-bottom: -$gl-spacing-scale-8;
}

View File

@ -15,6 +15,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned

View File

@ -15,6 +15,7 @@ module Groups
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
urgency :low

View File

@ -14,6 +14,7 @@ module Projects
before_action do
push_frontend_feature_flag(:ci_variables_pages, current_user)
push_frontend_feature_flag(:ci_variable_drawer, current_user)
end
helper_method :highlight_badge

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'CiGroupEnvironmentScope'
description 'Ci/CD environment scope for a group.'
connection_type_class(Types::Ci::GroupEnvironmentScopeConnectionType)
connection_type_class Types::Ci::GroupEnvironmentScopeConnectionType
field :name, GraphQL::Types::String,
null: true,

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'CiGroupVariable'
description 'CI/CD variables for a group.'
connection_type_class(Types::Ci::GroupVariableConnectionType)
connection_type_class Types::Ci::GroupVariableConnectionType
implements(VariableInterface)
field :environment_scope, GraphQL::Types::String,

View File

@ -9,7 +9,7 @@ module Types
present_using ::Ci::BuildPresenter
connection_type_class(Types::LimitedCountableConnectionType)
connection_type_class Types::LimitedCountableConnectionType
expose_permissions Types::PermissionTypes::Ci::Job

View File

@ -7,7 +7,7 @@ module Types
description 'Represents a pipeline schedule'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
expose_permissions Types::PermissionTypes::Ci::PipelineSchedules

View File

@ -5,7 +5,7 @@ module Types
class PipelineType < BaseObject
graphql_name 'Pipeline'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_pipeline
present_using ::Ci::PipelinePresenter

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'CiProjectVariable'
description 'CI/CD variables for a project.'
connection_type_class(Types::Ci::ProjectVariableConnectionType)
connection_type_class Types::Ci::ProjectVariableConnectionType
implements(VariableInterface)
field :environment_scope, GraphQL::Types::String,

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'RecentFailures'
description 'Recent failure history of a test case.'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :count, GraphQL::Types::Int, null: true,
description: 'Number of times the test case has failed in the past 14 days.'

View File

@ -5,7 +5,7 @@ module Types
class RunnerManagerType < BaseObject
graphql_name 'CiRunnerManager'
connection_type_class(::Types::CountableConnectionType)
connection_type_class ::Types::CountableConnectionType
authorize :read_runner_manager

View File

@ -6,7 +6,7 @@ module Types
graphql_name 'CiRunner'
edge_type_class(RunnerWebUrlEdge)
connection_type_class(RunnerCountableConnectionType)
connection_type_class RunnerCountableConnectionType
authorize :read_runner
present_using ::Ci::RunnerPresenter

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'TestCase'
description 'Test case in pipeline test report.'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :status,
Types::Ci::TestCaseStatusEnum,

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'TestSuiteSummary'
description 'Test suite summary in a pipeline test report.'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :name, GraphQL::Types::String, null: true,
description: 'Name of the test suite.'

View File

@ -7,7 +7,7 @@ module Types
graphql_name 'TestSuite'
description 'Test suite in a pipeline test report.'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :name, GraphQL::Types::String, null: true,
description: 'Name of the test suite.'

View File

@ -7,7 +7,7 @@ module Types
authorize :read_cluster_agent
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :recorded_at,
Types::TimeType,

View File

@ -7,7 +7,7 @@ module Types
authorize :read_cluster_agent
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :cluster_agent,
Types::Clusters::AgentType,

View File

@ -7,7 +7,7 @@ module Types
authorize :read_cluster_agent
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :created_at,
Types::TimeType,

View File

@ -4,7 +4,7 @@ module Types
class IssueType < BaseObject
graphql_name 'Issue'
connection_type_class(Types::IssueConnectionType)
connection_type_class Types::IssueConnectionType
implements(Types::Notes::NoteableInterface)
implements(Types::CurrentUserTodos)

View File

@ -4,7 +4,7 @@ module Types
class LabelType < BaseObject
graphql_name 'Label'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_label

View File

@ -4,7 +4,7 @@ module Types
class MergeRequestType < BaseObject
graphql_name 'MergeRequest'
connection_type_class(Types::MergeRequestConnectionType)
connection_type_class Types::MergeRequestConnectionType
implements(Types::Notes::NoteableInterface)
implements(Types::CurrentUserTodos)

View File

@ -6,7 +6,7 @@ module Types
graphql_name 'PackageBase'
description 'Represents a package in the Package Registry'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_package

View File

@ -4,7 +4,7 @@ module Types
class ProjectType < BaseObject
graphql_name 'Project'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_project

View File

@ -5,7 +5,7 @@ module Types
graphql_name 'Release'
description 'Represents a release'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_release

View File

@ -4,7 +4,7 @@ module Types
class SavedReplyType < BaseObject
graphql_name 'SavedReply'
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
authorize :read_saved_replies

View File

@ -8,7 +8,7 @@ module Types
description 'Represents the snippet blob'
present_using SnippetBlobPresenter
connection_type_class(Types::Snippets::BlobConnectionType)
connection_type_class Types::Snippets::BlobConnectionType
field :rich_data, GraphQL::Types::String,
description: 'Blob highlighted data.',

View File

@ -7,7 +7,7 @@ module Types
authorize :read_terraform_state
connection_type_class(Types::CountableConnectionType)
connection_type_class Types::CountableConnectionType
field :id, GraphQL::Types::ID,
null: false,

View File

@ -4,7 +4,7 @@ module Types
class TimelogType < BaseObject
graphql_name 'Timelog'
connection_type_class(Types::TimeTracking::TimelogConnectionType)
connection_type_class Types::TimeTracking::TimelogConnectionType
authorize :read_issuable

View File

@ -0,0 +1,8 @@
---
name: ci_variable_drawer
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/126197
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/418005
milestone: '16.3'
type: development
group: group::pipeline security
default_enabled: false

View File

@ -9886,6 +9886,9 @@ msgstr ""
msgid "CiStatus|running"
msgstr ""
msgid "CiVariables|Add Variable"
msgstr ""
msgid "CiVariables|Attributes"
msgstr ""
@ -9898,9 +9901,15 @@ msgstr ""
msgid "CiVariables|Environments"
msgstr ""
msgid "CiVariables|Expand variable reference"
msgstr ""
msgid "CiVariables|Expanded"
msgstr ""
msgid "CiVariables|Export variable to pipelines running on protected branches and tags only."
msgstr ""
msgid "CiVariables|File"
msgstr ""
@ -9916,6 +9925,9 @@ msgstr ""
msgid "CiVariables|Key"
msgstr ""
msgid "CiVariables|Mask variable"
msgstr ""
msgid "CiVariables|Masked"
msgstr ""
@ -9925,6 +9937,9 @@ msgstr ""
msgid "CiVariables|Maximum number of variables reached."
msgstr ""
msgid "CiVariables|Protect variable"
msgstr ""
msgid "CiVariables|Protected"
msgstr ""
@ -9964,6 +9979,9 @@ msgstr ""
msgid "CiVariables|Value"
msgstr ""
msgid "CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements."
msgstr ""
msgid "CiVariables|Variables"
msgstr ""
@ -45544,7 +45562,7 @@ msgstr ""
msgid "Tags:"
msgstr ""
msgid "TagsPage|After you confirm and select %{strongStart}%{buttonText},%{strongEnd} you cannot recover this tag."
msgid "TagsPage|Are you sure you want to delete this tag?"
msgstr ""
msgid "TagsPage|Browse commits"
@ -45571,16 +45589,13 @@ msgstr ""
msgid "TagsPage|Delete protected tag"
msgstr ""
msgid "TagsPage|Delete protected tag. Are you ABSOLUTELY SURE?"
msgstr ""
msgid "TagsPage|Delete tag"
msgstr ""
msgid "TagsPage|Delete tag. Are you ABSOLUTELY SURE?"
msgid "TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} protected tag cannot be undone."
msgstr ""
msgid "TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} tag cannot be undone. Are you sure?"
msgid "TagsPage|Deleting the %{strongStart}%{tagName}%{strongEnd} tag cannot be undone."
msgstr ""
msgid "TagsPage|Do you want to create a release with the new tag? You can do that in the %{link_start}New release page%{link_end}."
@ -45610,6 +45625,12 @@ msgstr ""
msgid "TagsPage|Optionally, add a message to the tag. Leaving this blank creates a %{link_start}lightweight tag.%{link_end}"
msgstr ""
msgid "TagsPage|Permanently delete protected tag?"
msgstr ""
msgid "TagsPage|Permanently delete tag?"
msgstr ""
msgid "TagsPage|Please type the following to confirm:"
msgstr ""
@ -45637,12 +45658,6 @@ msgstr ""
msgid "TagsPage|Yes, delete tag"
msgstr ""
msgid "TagsPage|You're about to permanently delete the protected tag %{strongStart}%{tagName}.%{strongEnd}"
msgstr ""
msgid "TagsPage|You're about to permanently delete the tag %{strongStart}%{tagName}.%{strongEnd}"
msgstr ""
msgid "TagsPage|protected"
msgstr ""
@ -50660,6 +50675,9 @@ msgstr ""
msgid "Variable"
msgstr ""
msgid "Variable (default)"
msgstr ""
msgid "Variable name '%{variable}' must not start with '%{prefix}'"
msgstr ""

View File

@ -13,6 +13,7 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
@ -29,4 +30,14 @@ RSpec.describe 'Instance variables', :js, feature_category: :secrets_management
it_behaves_like 'variable list', is_admin: true
end
context 'when ci_variable_drawer FF is enabled' do
before do
stub_feature_flags(ci_variable_drawer: true)
visit page_path
wait_for_requests
end
it_behaves_like 'variable list drawer', is_admin: true
end
end

View File

@ -11,6 +11,8 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
before do
group.add_owner(user)
gitlab_sign_in(user)
stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
@ -27,4 +29,14 @@ RSpec.describe 'Group variables', :js, feature_category: :secrets_management do
it_behaves_like 'variable list'
end
context 'when ci_variable_drawer FF is enabled' do
before do
stub_feature_flags(ci_variable_drawer: true)
visit page_path
wait_for_requests
end
it_behaves_like 'variable list drawer'
end
end

View File

@ -12,6 +12,8 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
sign_in(user)
project.add_maintainer(user)
project.variables << variable
stub_feature_flags(ci_variable_drawer: false)
visit page_path
wait_for_requests
end
@ -49,4 +51,14 @@ RSpec.describe 'Project variables', :js, feature_category: :secrets_management d
expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
end
end
context 'when ci_variable_drawer FF is enabled' do
before do
stub_feature_flags(ci_variable_drawer: true)
visit page_path
wait_for_requests
end
it_behaves_like 'variable list drawer'
end
end

View File

@ -0,0 +1,69 @@
import { GlDrawer, GlFormSelect } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
import {
ADD_VARIABLE_ACTION,
variableOptions,
variableTypes,
} from '~/ci/ci_variable_list/constants';
describe('CI Variable Drawer', () => {
let wrapper;
const defaultProps = {
areEnvironmentsLoading: false,
hasEnvScopeQuery: true,
mode: ADD_VARIABLE_ACTION,
};
const createComponent = ({ mountFn = shallowMountExtended, props = {} } = {}) => {
wrapper = mountFn(CiVariableDrawer, {
propsData: {
...defaultProps,
...props,
},
provide: {
environmentScopeLink: '/help/environments',
},
});
};
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findTypeDropdown = () => wrapper.findComponent(GlFormSelect);
describe('validations', () => {
beforeEach(() => {
createComponent({ mountFn: mountExtended });
});
describe('type dropdown', () => {
it('adds each type option as a dropdown item', () => {
expect(findTypeDropdown().findAll('option')).toHaveLength(variableOptions.length);
variableOptions.forEach((v) => {
expect(findTypeDropdown().text()).toContain(v.text);
});
});
it('is set to environment variable by default', () => {
expect(findTypeDropdown().findAll('option').at(0).attributes('value')).toBe(
variableTypes.envType,
);
});
});
});
describe('drawer events', () => {
beforeEach(() => {
createComponent();
});
it('emits `close-form` when closing the drawer', async () => {
expect(wrapper.emitted('close-form')).toBeUndefined();
await findDrawer().vm.$emit('close');
expect(wrapper.emitted('close-form')).toHaveLength(1);
});
});
});

View File

@ -122,9 +122,9 @@ describe('Ci variable modal', () => {
expect(wrapper.emitted('add-variable')).toEqual([[currentVariable]]);
});
it('Dispatches the `hideModal` event when dismissing', () => {
it('Dispatches the `close-form` event when dismissing', () => {
findModal().vm.$emit('hidden');
expect(wrapper.emitted('hideModal')).toEqual([[]]);
expect(wrapper.emitted('close-form')).toEqual([[]]);
});
});
});
@ -313,9 +313,9 @@ describe('Ci variable modal', () => {
expect(wrapper.emitted('update-variable')).toEqual([[variable]]);
});
it('Propagates the `hideModal` event', () => {
it('Propagates the `close-form` event', () => {
findModal().vm.$emit('hidden');
expect(wrapper.emitted('hideModal')).toEqual([[]]);
expect(wrapper.emitted('close-form')).toEqual([[]]);
});
it('dispatches `delete-variable` with correct variable to delete', () => {

View File

@ -1,7 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
import {
ADD_VARIABLE_ACTION,
EDIT_VARIABLE_ACTION,
@ -27,15 +29,22 @@ describe('Ci variable table', () => {
variables: mockVariablesWithScopes(projectString),
};
const findCiVariableTable = () => wrapper.findComponent(ciVariableTable);
const findCiVariableModal = () => wrapper.findComponent(ciVariableModal);
const findCiVariableDrawer = () => wrapper.findComponent(CiVariableDrawer);
const findCiVariableTable = () => wrapper.findComponent(CiVariableTable);
const findCiVariableModal = () => wrapper.findComponent(CiVariableModal);
const createComponent = ({ props = {} } = {}) => {
const createComponent = ({ props = {}, featureFlags = {} } = {}) => {
wrapper = shallowMount(CiVariableSettings, {
propsData: {
...defaultProps,
...props,
},
provide: {
glFeatures: {
ciVariableDrawer: false,
...featureFlags,
},
},
});
};
@ -70,51 +79,51 @@ describe('Ci variable table', () => {
});
});
describe('modal mode', () => {
describe.each`
bool | flagStatus | elementName | findElement
${false} | ${'disabled'} | ${'modal'} | ${findCiVariableModal}
${true} | ${'enabled'} | ${'drawer'} | ${findCiVariableDrawer}
`('when ciVariableDrawer feature flag is $flagStatus', ({ bool, elementName, findElement }) => {
beforeEach(() => {
createComponent();
createComponent({ featureFlags: { ciVariableDrawer: bool } });
});
it('passes down ADD mode when receiving an empty variable', async () => {
it(`${elementName} is hidden by default`, () => {
expect(findElement().exists()).toBe(false);
});
it(`shows ${elementName} when adding a new variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
expect(findElement().exists()).toBe(true);
});
it('passes down EDIT mode when receiving a variable', async () => {
it(`shows ${elementName} when updating a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
});
describe('variable modal', () => {
beforeEach(() => {
createComponent();
expect(findElement().exists()).toBe(true);
});
it('is hidden by default', () => {
expect(findCiVariableModal().exists()).toBe(false);
});
it('shows modal when adding a new variable', async () => {
it(`hides ${elementName} when closing the form`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findCiVariableModal().exists()).toBe(true);
expect(findElement().isVisible()).toBe(true);
await findElement().vm.$emit('close-form');
expect(findElement().exists()).toBe(false);
});
it('shows modal when updating a variable', async () => {
it(`passes down ADD mode to ${elementName} when receiving an empty variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
expect(findElement().props('mode')).toBe(ADD_VARIABLE_ACTION);
});
it(`passes down EDIT mode to ${elementName} when receiving a variable`, async () => {
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
expect(findCiVariableModal().exists()).toBe(true);
});
it('hides modal when receiving the event from the modal', async () => {
await findCiVariableTable().vm.$emit('set-selected-variable');
await findCiVariableModal().vm.$emit('hideModal');
expect(findCiVariableModal().exists()).toBe(false);
expect(findElement().props('mode')).toBe(EDIT_VARIABLE_ACTION);
});
});

View File

@ -5,6 +5,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteTagModal from '~/tags/components/delete_tag_modal.vue';
import eventHub from '~/tags/event_hub';
import { I18N_DELETE_TAG_MODAL } from '~/tags/constants';
let wrapper;
@ -52,18 +53,17 @@ const findForm = () => wrapper.find('form');
describe('Delete tag modal', () => {
describe('Deleting a regular tag', () => {
const expectedTitle = 'Delete tag. Are you ABSOLUTELY SURE?';
const expectedMessage = "You're about to permanently delete the tag test-tag.";
const expectedMessage = 'Deleting the test-tag tag cannot be undone.';
beforeEach(() => {
createComponent();
});
it('renders the modal correctly', () => {
expect(findModal().props('title')).toBe(expectedTitle);
expect(findModal().props('title')).toBe(I18N_DELETE_TAG_MODAL.modalTitle);
expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
expect(findCancelButton().text()).toBe('Cancel, keep tag');
expect(findDeleteButton().text()).toBe('Yes, delete tag');
expect(findCancelButton().text()).toBe(I18N_DELETE_TAG_MODAL.cancelButtonText);
expect(findDeleteButton().text()).toBe(I18N_DELETE_TAG_MODAL.deleteButtonText);
expect(findForm().attributes('action')).toBe(path);
});
@ -92,11 +92,8 @@ describe('Delete tag modal', () => {
});
describe('Deleting a protected tag (for owner or maintainer)', () => {
const expectedTitleProtected = 'Delete protected tag. Are you ABSOLUTELY SURE?';
const expectedMessageProtected =
"You're about to permanently delete the protected tag test-tag.";
const expectedConfirmationText =
'After you confirm and select Yes, delete protected tag, you cannot recover this tag. Please type the following to confirm: test-tag';
const expectedMessage = 'Deleting the test-tag protected tag cannot be undone.';
const expectedConfirmationText = 'Please type the following to confirm: test-tag';
beforeEach(() => {
createComponent({ isProtected: true });
@ -104,11 +101,11 @@ describe('Delete tag modal', () => {
describe('rendering the modal correctly for a protected tag', () => {
it('sets the modal title for a protected tag', () => {
expect(findModal().props('title')).toBe(expectedTitleProtected);
expect(findModal().props('title')).toBe(I18N_DELETE_TAG_MODAL.modalTitleProtectedTag);
});
it('renders the correct text in the modal message', () => {
expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessageProtected);
expect(findModalMessage().text()).toMatchInterpolatedText(expectedMessage);
});
it('renders the protected tag name confirmation form with expected text and action', () => {
@ -117,8 +114,8 @@ describe('Delete tag modal', () => {
});
it('renders the buttons with the correct button text', () => {
expect(findCancelButton().text()).toBe('Cancel, keep tag');
expect(findDeleteButton().text()).toBe('Yes, delete protected tag');
expect(findCancelButton().text()).toBe(I18N_DELETE_TAG_MODAL.cancelButtonText);
expect(findDeleteButton().text()).toBe(I18N_DELETE_TAG_MODAL.deleteButtonTextProtectedTag);
});
});

View File

@ -50,6 +50,11 @@ RSpec.describe SwapTodosNoteIdToBigintForSelfManaged, feature_category: :databas
connection.execute('ALTER TABLE todos ADD COLUMN IF NOT EXISTS note_id_convert_to_bigint integer')
end
after do
connection = described_class.new.connection
connection.execute('ALTER TABLE todos DROP COLUMN IF EXISTS note_id_convert_to_bigint')
end
it 'does not swap the columns' do
# rubocop: disable RSpec/AnyInstanceOf
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)
@ -115,6 +120,11 @@ RSpec.describe SwapTodosNoteIdToBigintForSelfManaged, feature_category: :databas
BEGIN NEW."note_id_convert_to_bigint" := NEW."note_id"; RETURN NEW; END; $$;')
end
after do
connection = described_class.new.connection
connection.execute('ALTER TABLE todos DROP COLUMN IF EXISTS note_id_convert_to_bigint')
end
it 'swaps the columns' do
# rubocop: disable RSpec/AnyInstanceOf
allow_any_instance_of(described_class).to receive(:com_or_dev_or_test_but_not_jh?).and_return(false)

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'variable list drawer' do
it 'adds a new CI variable' do
click_button('Add variable')
# For now, we just check that the drawer is displayed
expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
# TODO: Add tests for ADDING a variable via drawer when feature is available
end
it 'edits a variable' do
page.within('[data-testid="ci-variable-table"]') do
click_button('Edit')
end
# For now, we just check that the drawer is displayed
expect(page).to have_selector('[data-testid="ci-variable-drawer"]')
# TODO: Add tests for EDITING a variable via drawer when feature is available
end
end