diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue new file mode 100644 index 00000000000..0ce11da658c --- /dev/null +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue @@ -0,0 +1,233 @@ + + + + + {{ $options.i18n.addVariable }} + + + + + + + + + {{ $options.i18n.environments }} + + + + + + + + + + + + + {{ $options.i18n.flags }} + + + + + + + + {{ $options.i18n.protectedField }} + + {{ $options.i18n.protectedDescription }} + + + + {{ $options.i18n.maskedField }} + {{ $options.i18n.maskedDescription }} + + + {{ $options.i18n.expandedField }} + + + + {{ content }} + + + + + + + + + + + {{ $options.i18n.cancel }} + + {{ $options.i18n.addVariable }} + + + + diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue index 3af48635f3f..5468e42b6b3 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_modal.vue @@ -241,7 +241,7 @@ export default { this.resetVariableData(); this.resetValidationErrorEvents(); - this.$emit('hideModal'); + this.$emit('close-form'); }, resetVariableData() { this.variable = { ...defaultVariableState }; diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue index b8a95f9081a..4faec24e19d 100644 --- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue +++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_settings.vue @@ -1,13 +1,17 @@ - - - - - {{ content }} - - - - + + + + {{ content }} + + + + + + + + {{ content }} + + + + - - - - {{ content }} - - - {{ content }} - {{ tagName }} + {{ tagName }} - - - - - - {{ content }} - - - - - diff --git a/app/assets/javascripts/tags/constants.js b/app/assets/javascripts/tags/constants.js new file mode 100644 index 00000000000..a8096a08a97 --- /dev/null +++ b/app/assets/javascripts/tags/constants.js @@ -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, +}; diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss index db9802eeefa..dbd01ffdbcf 100644 --- a/app/assets/stylesheets/utilities.scss +++ b/app/assets/stylesheets/utilities.scss @@ -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; +} diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb index f0b6d86d48d..be1edeb0d37 100644 --- a/app/controllers/admin/application_settings_controller.rb +++ b/app/controllers/admin/application_settings_controller.rb @@ -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 diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb index 4bbaf92b126..b4b2bddcaec 100644 --- a/app/controllers/groups/settings/ci_cd_controller.rb +++ b/app/controllers/groups/settings/ci_cd_controller.rb @@ -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 diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 9a128adb926..0845fbc9713 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -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 diff --git a/app/graphql/types/ci/group_environment_scope_type.rb b/app/graphql/types/ci/group_environment_scope_type.rb index 3a3a5a3f59f..0dd0ad963ac 100644 --- a/app/graphql/types/ci/group_environment_scope_type.rb +++ b/app/graphql/types/ci/group_environment_scope_type.rb @@ -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, diff --git a/app/graphql/types/ci/group_variable_type.rb b/app/graphql/types/ci/group_variable_type.rb index 7e2afba0d53..acb003b5977 100644 --- a/app/graphql/types/ci/group_variable_type.rb +++ b/app/graphql/types/ci/group_variable_type.rb @@ -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, diff --git a/app/graphql/types/ci/job_type.rb b/app/graphql/types/ci/job_type.rb index 02b10f3e4bd..22eb32993c5 100644 --- a/app/graphql/types/ci/job_type.rb +++ b/app/graphql/types/ci/job_type.rb @@ -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 diff --git a/app/graphql/types/ci/pipeline_schedule_type.rb b/app/graphql/types/ci/pipeline_schedule_type.rb index 904fa3f1c72..71a1f28ea38 100644 --- a/app/graphql/types/ci/pipeline_schedule_type.rb +++ b/app/graphql/types/ci/pipeline_schedule_type.rb @@ -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 diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb index 19d261853a7..ba638d4bc47 100644 --- a/app/graphql/types/ci/pipeline_type.rb +++ b/app/graphql/types/ci/pipeline_type.rb @@ -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 diff --git a/app/graphql/types/ci/project_variable_type.rb b/app/graphql/types/ci/project_variable_type.rb index a9679000511..8c1eb481774 100644 --- a/app/graphql/types/ci/project_variable_type.rb +++ b/app/graphql/types/ci/project_variable_type.rb @@ -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, diff --git a/app/graphql/types/ci/recent_failures_type.rb b/app/graphql/types/ci/recent_failures_type.rb index 0892cb2735c..37850c62658 100644 --- a/app/graphql/types/ci/recent_failures_type.rb +++ b/app/graphql/types/ci/recent_failures_type.rb @@ -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.' diff --git a/app/graphql/types/ci/runner_manager_type.rb b/app/graphql/types/ci/runner_manager_type.rb index 9c89b6537ea..b36c8f42862 100644 --- a/app/graphql/types/ci/runner_manager_type.rb +++ b/app/graphql/types/ci/runner_manager_type.rb @@ -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 diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb index 2baf64ca663..cb340675b72 100644 --- a/app/graphql/types/ci/runner_type.rb +++ b/app/graphql/types/ci/runner_type.rb @@ -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 diff --git a/app/graphql/types/ci/test_case_type.rb b/app/graphql/types/ci/test_case_type.rb index f88923215eb..78c70fbcc7c 100644 --- a/app/graphql/types/ci/test_case_type.rb +++ b/app/graphql/types/ci/test_case_type.rb @@ -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, diff --git a/app/graphql/types/ci/test_suite_summary_type.rb b/app/graphql/types/ci/test_suite_summary_type.rb index 8801501c8d4..a98c47c6a30 100644 --- a/app/graphql/types/ci/test_suite_summary_type.rb +++ b/app/graphql/types/ci/test_suite_summary_type.rb @@ -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.' diff --git a/app/graphql/types/ci/test_suite_type.rb b/app/graphql/types/ci/test_suite_type.rb index 8845338ed6d..a5cc6282abc 100644 --- a/app/graphql/types/ci/test_suite_type.rb +++ b/app/graphql/types/ci/test_suite_type.rb @@ -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.' diff --git a/app/graphql/types/clusters/agent_activity_event_type.rb b/app/graphql/types/clusters/agent_activity_event_type.rb index 1d0ec7c4959..8c7dfa9c10a 100644 --- a/app/graphql/types/clusters/agent_activity_event_type.rb +++ b/app/graphql/types/clusters/agent_activity_event_type.rb @@ -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, diff --git a/app/graphql/types/clusters/agent_token_type.rb b/app/graphql/types/clusters/agent_token_type.rb index 720ee2f685b..260c634e12e 100644 --- a/app/graphql/types/clusters/agent_token_type.rb +++ b/app/graphql/types/clusters/agent_token_type.rb @@ -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, diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb index 317a1aab628..c0989796141 100644 --- a/app/graphql/types/clusters/agent_type.rb +++ b/app/graphql/types/clusters/agent_type.rb @@ -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, diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 488e4d10cbc..8577d2b8dc5 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -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) diff --git a/app/graphql/types/label_type.rb b/app/graphql/types/label_type.rb index 05b703e60af..4848ee30950 100644 --- a/app/graphql/types/label_type.rb +++ b/app/graphql/types/label_type.rb @@ -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 diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index a142d8f215d..30878c17c6b 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -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) diff --git a/app/graphql/types/packages/package_base_type.rb b/app/graphql/types/packages/package_base_type.rb index 8dd2a4467d6..cc41169bcda 100644 --- a/app/graphql/types/packages/package_base_type.rb +++ b/app/graphql/types/packages/package_base_type.rb @@ -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 diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index a76603c5cdd..870b69d0ea2 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -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 diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb index 8516256b433..0bf723bcb1b 100644 --- a/app/graphql/types/release_type.rb +++ b/app/graphql/types/release_type.rb @@ -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 diff --git a/app/graphql/types/saved_reply_type.rb b/app/graphql/types/saved_reply_type.rb index 8c9f3d19810..74b3796ef8a 100644 --- a/app/graphql/types/saved_reply_type.rb +++ b/app/graphql/types/saved_reply_type.rb @@ -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 diff --git a/app/graphql/types/snippets/blob_type.rb b/app/graphql/types/snippets/blob_type.rb index bb4a0a64de8..2d1993225d1 100644 --- a/app/graphql/types/snippets/blob_type.rb +++ b/app/graphql/types/snippets/blob_type.rb @@ -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.', diff --git a/app/graphql/types/terraform/state_type.rb b/app/graphql/types/terraform/state_type.rb index be17fc41c2c..0870194a934 100644 --- a/app/graphql/types/terraform/state_type.rb +++ b/app/graphql/types/terraform/state_type.rb @@ -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, diff --git a/app/graphql/types/timelog_type.rb b/app/graphql/types/timelog_type.rb index 88baca028ef..2adf2847221 100644 --- a/app/graphql/types/timelog_type.rb +++ b/app/graphql/types/timelog_type.rb @@ -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 diff --git a/config/feature_flags/development/ci_variable_drawer.yml b/config/feature_flags/development/ci_variable_drawer.yml new file mode 100644 index 00000000000..ad451ab6414 --- /dev/null +++ b/config/feature_flags/development/ci_variable_drawer.yml @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 08b413f1d64..1ba69edc3bf 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb index 744d18a3b6d..91e7a46849c 100644 --- a/spec/features/admin_variables_spec.rb +++ b/spec/features/admin_variables_spec.rb @@ -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 diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index 3e87c90e7dc..b4a0678cb5f 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -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 diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb index c4f78bf4ea3..e2fa924af67 100644 --- a/spec/features/project_variables_spec.rb +++ b/spec/features/project_variables_spec.rb @@ -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 diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js new file mode 100644 index 00000000000..762c9611dac --- /dev/null +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js @@ -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); + }); + }); +}); diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js index d843646df16..445fb637076 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js @@ -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', () => { diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js index d72cfc5fc14..f5737c61eea 100644 --- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js +++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js @@ -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); }); }); diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js index 5a3104fad9b..682145c8d6c 100644 --- a/spec/frontend/tags/components/delete_tag_modal_spec.js +++ b/spec/frontend/tags/components/delete_tag_modal_spec.js @@ -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); }); }); diff --git a/spec/migrations/swap_todos_note_id_to_bigint_for_self_managed_spec.rb b/spec/migrations/swap_todos_note_id_to_bigint_for_self_managed_spec.rb index d576ec23554..525e4fbcd8d 100644 --- a/spec/migrations/swap_todos_note_id_to_bigint_for_self_managed_spec.rb +++ b/spec/migrations/swap_todos_note_id_to_bigint_for_self_managed_spec.rb @@ -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) diff --git a/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb b/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb new file mode 100644 index 00000000000..9f01c69608d --- /dev/null +++ b/spec/support/shared_examples/features/variable_list_drawer_shared_examples.rb @@ -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
+ {{ $options.i18n.protectedDescription }} +
{{ $options.i18n.maskedDescription }}
+ + + {{ content }} + + +
{{ content }}
+ + + + {{ content }} + + + +
- - - {{ content }} - - -
{{ content }} - {{ tagName }} + {{ tagName }}
{{ tagName }}
- - - - {{ content }} - - - -