diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-15 21:07:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-15 21:07:42 +0000 |
commit | 2b5079efdb7c4e7d5a607d95747ddeb0b8af9678 (patch) | |
tree | 6d226593a137e111c7d4075ec22d6c4d328c3b57 | |
parent | 5ff1f808adf841bca979cb2fac6bdfa9c449d028 (diff) | |
download | gitlab-ce-2b5079efdb7c4e7d5a607d95747ddeb0b8af9678.tar.gz |
Add latest changes from gitlab-org/gitlab@master
59 files changed, 798 insertions, 134 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml index 5f0b59af9d7..8fcea840cfe 100644 --- a/.gitlab/ci/rules.gitlab-ci.yml +++ b/.gitlab/ci/rules.gitlab-ci.yml @@ -1463,6 +1463,12 @@ .qa:rules:package-and-test-sidebar: rules: + - <<: *if-not-canonical-namespace + when: never + - <<: *if-not-ee + when: never + - <<: *if-merge-request-labels-pipeline-expedite + when: never - <<: *if-merge-request changes: *code-patterns when: manual diff --git a/.rubocop_todo/gitlab/namespaced_class.yml b/.rubocop_todo/gitlab/namespaced_class.yml index 925b5188c8b..3a81130a47c 100644 --- a/.rubocop_todo/gitlab/namespaced_class.yml +++ b/.rubocop_todo/gitlab/namespaced_class.yml @@ -244,6 +244,7 @@ Gitlab/NamespacedClass: - 'app/models/notification_setting.rb' - 'app/models/oauth_access_grant.rb' - 'app/models/oauth_access_token.rb' + - 'app/models/organization.rb' - 'app/models/out_of_context_discussion.rb' - 'app/models/pages_deployment.rb' - 'app/models/pages_domain.rb' diff --git a/.rubocop_todo/layout/argument_alignment.yml b/.rubocop_todo/layout/argument_alignment.yml index 4e4d3537c9e..bdd3a15489d 100644 --- a/.rubocop_todo/layout/argument_alignment.yml +++ b/.rubocop_todo/layout/argument_alignment.yml @@ -1451,15 +1451,6 @@ Layout/ArgumentAlignment: - 'ee/spec/services/vulnerabilities/user_notes_count_service_spec.rb' - 'ee/spec/services/vulnerability_feedback/create_service_spec.rb' - 'ee/spec/services/vulnerability_merge_request_links/create_service_spec.rb' - - 'ee/spec/support/shared_examples/audit/audit_event_type_stream_shared_examples.rb' - - 'ee/spec/support/shared_examples/controllers/analytics/cycle_analytics/shared_stage_shared_examples.rb' - - 'ee/spec/support/shared_examples/features/credentials_inventory_shared_examples.rb' - - 'ee/spec/support/shared_examples/features/password_complexity_shared_examples.rb' - - 'ee/spec/support/shared_examples/graphql/dast/dast_profile_schedule_shared_examples.rb' - - 'ee/spec/support/shared_examples/graphql/mutations/set_multiple_assignees_shared_examples.rb' - - 'ee/spec/support/shared_examples/services/geo/geo_request_service_shared_examples.rb' - - 'ee/spec/support/shared_examples/services/search_service_shared_examples.rb' - - 'ee/spec/support/shared_examples/services/vulnerabilities/removes_dismissal_feedback_from_associated_findings_shared_example.rb' - 'ee/spec/tasks/gitlab/elastic_rake_spec.rb' - 'lib/api/access_requests.rb' - 'lib/api/admin/plan_limits.rb' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index abaf97ff662..a32468aaacf 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -d87e76a602d9a5400c80363c8cfa878a3409c292 +11825b3ad89525b194cc4095e581eef843377cdb diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue index 66957365261..eabf4749e9c 100644 --- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue +++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue @@ -17,6 +17,7 @@ export default { GlButton, }, mixins: [glFeatureFlagMixin(), Tracking.mixin()], + inject: ['aiChatAvailable'], props: { showDrawer: { type: Boolean, @@ -31,6 +32,11 @@ export default { required: true, }, }, + computed: { + isAiConfigChatAvailable() { + return this.glFeatures.aiCiConfigGenerator && this.aiChatAvailable; + }, + }, methods: { toggleDrawer() { if (this.showDrawer) { @@ -96,7 +102,7 @@ export default { {{ $options.i18n.jobAssistant }} </gl-button> <gl-button - v-if="glFeatures.aiCiConfigGenerator" + v-if="isAiConfigChatAvailable" icon="bulb" size="small" data-testid="ai-assistant-drawer-toggle" diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js index 09acd805410..b8d6c27435d 100644 --- a/app/assets/javascripts/ci/pipeline_editor/index.js +++ b/app/assets/javascripts/ci/pipeline_editor/index.js @@ -46,6 +46,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { usesExternalConfig, validateTabIllustrationPath, ymlHelpPagePath, + aiChatAvailable, } = el.dataset; const configurationPaths = Object.fromEntries( @@ -115,6 +116,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => { el, apolloProvider, provide: { + aiChatAvailable: parseBoolean(aiChatAvailable), ciConfigPath, ciExamplesHelpPagePath, ciHelpPagePath, diff --git a/app/assets/javascripts/design_management/components/list/item.vue b/app/assets/javascripts/design_management/components/list/item.vue index f52486f0629..9c1bcf5bf90 100644 --- a/app/assets/javascripts/design_management/components/list/item.vue +++ b/app/assets/javascripts/design_management/components/list/item.vue @@ -133,7 +133,11 @@ export default { <div class="card-body gl-p-0 gl-display-flex gl-align-items-center gl-justify-content-center gl-overflow-hidden gl-relative" > - <div v-if="icon.name" data-testid="design-event" class="gl-top-5 gl-right-5 gl-absolute"> + <div + v-if="icon.name" + data-testid="design-event" + class="gl-absolute gl-top-3 gl-right-3 gl-mr-1" + > <span :title="icon.tooltip" :aria-label="icon.tooltip"> <gl-icon :name="icon.name" @@ -165,11 +169,11 @@ export default { /> </gl-intersection-observer> </div> - <div class="card-footer gl-display-flex gl-w-full"> + <div class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4"> <div class="gl-display-flex gl-flex-direction-column str-truncated-100"> <span v-gl-tooltip - class="gl-font-weight-bold str-truncated-100" + class="gl-font-weight-semibold str-truncated-100" data-qa-selector="design_file_name" :data-testid="`design-img-filename-${id}`" :title="filename" diff --git a/app/assets/javascripts/design_management/pages/index.vue b/app/assets/javascripts/design_management/pages/index.vue index e270613e4eb..dcc65c957fe 100644 --- a/app/assets/javascripts/design_management/pages/index.vue +++ b/app/assets/javascripts/design_management/pages/index.vue @@ -141,6 +141,12 @@ export default { } return 'col-12'; }, + designContentWrapperClass() { + if (this.hasDesigns) { + return 'gl-bg-gray-10 gl-border gl-border-t-0 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-5'; + } + return null; + }, }, mounted() { if (this.$route.path === '/designs') { @@ -364,7 +370,7 @@ export default { </gl-alert> <header v-if="showToolbar" - class="gl-display-flex gl-my-0 gl-text-gray-900" + class="gl-border gl-px-5 gl-py-4 gl-display-flex gl-justify-content-space-between gl-bg-white gl-rounded-base gl-rounded-bottom-left-none! gl-rounded-bottom-right-none!" data-testid="design-toolbar-wrapper" > <div @@ -418,7 +424,7 @@ export default { </div> </div> </header> - <div> + <div :class="designContentWrapperClass"> <gl-loading-icon v-if="isLoading" size="lg" /> <gl-alert v-else-if="error" variant="danger" :dismissible="false"> {{ __('An error occurred while loading designs. Please try again.') }} @@ -483,7 +489,7 @@ export default { v-if="canSelectDesign(design.filename)" :checked="isDesignSelected(design.filename)" type="checkbox" - class="design-checkbox" + class="design-checkbox gl-absolute gl-top-4 gl-left-6 gl-ml-2" data-qa-selector="design_checkbox" :data-qa-design="design.filename" @change="changeSelectedDesigns(design.filename)" diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue index 5de6e04d827..93c249dffeb 100644 --- a/app/assets/javascripts/super_sidebar/components/menu_section.vue +++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue @@ -69,6 +69,7 @@ export default { <template> <component :is="tag"> + <hr v-if="separated" aria-hidden="true" class="gl-mx-4 gl-my-2" /> <button class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-py-3 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus" :class="computedLinkClasses" @@ -117,6 +118,5 @@ export default { /> </slot> </gl-collapse> - <hr v-if="separated" aria-hidden="true" class="gl-mx-4 gl-my-2" /> </component> </template> diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue index 4fc86e41ef2..ccd739c8bb1 100644 --- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue +++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue @@ -23,6 +23,11 @@ export default { required: false, default: () => [], }, + separated: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { @@ -69,7 +74,7 @@ export default { <menu-section :item="sectionItem" :expanded="expanded" - :separated="true" + :separated="separated" @collapse-toggle="expanded = !expanded" > <draggable diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue index 12abd727ef0..08af9232107 100644 --- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue +++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue @@ -89,6 +89,9 @@ export default { supportsPins() { return PANELS_WITH_PINS.includes(this.panelType); }, + hasStaticItems() { + return this.staticItems.length > 0; + }, }, methods: { createPin(itemId) { @@ -126,34 +129,50 @@ export default { Sentry.captureException(e); }); }, + isSection(navItem) { + return navItem.items?.length; + }, }, }; </script> <template> <nav class="gl-p-2 gl-relative"> - <template v-if="staticItems.length > 0"> - <ul class="gl-p-0 gl-m-0"> - <nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static /> - </ul> - <hr aria-hidden="true" class="gl-my-2 gl-mx-4" /> - </template> + <ul v-if="hasStaticItems" class="gl-p-0 gl-m-0"> + <nav-item v-for="item in staticItems" :key="item.id" :item="item" is-static /> + </ul> <pinned-section v-if="supportsPins" + separated :items="pinnedItems" @pin-remove="destroyPin" @pin-reorder="movePin" /> + <hr + v-if="supportsPins" + aria-hidden="true" + class="gl-my-2 gl-mx-4" + data-testid="main-menu-separator" + /> <ul class="gl-p-0 gl-list-style-none"> - <component - :is="item.items && item.items.length ? 'MenuSection' : 'NavItem'" - v-for="item in nonStaticItems" - :key="item.id" - :item="item" - tag="li" - @pin-add="createPin" - @pin-remove="destroyPin" - /> + <template v-for="item in nonStaticItems"> + <menu-section + v-if="isSection(item)" + :key="item.id" + :item="item" + :separated="item.separated" + @pin-add="createPin" + @pin-remove="destroyPin" + /> + <nav-item + v-else + :key="item.id" + :item="item" + tag="li" + @pin-add="createPin" + @pin-remove="destroyPin" + /> + </template> </ul> </nav> </template> diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue index 3f5ff526e91..6ae30e9b084 100644 --- a/app/assets/javascripts/work_items/components/widget_wrapper.vue +++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue @@ -45,7 +45,7 @@ export default { <template> <div id="tasks" - class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4" + class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-5" > <div class="gl-pl-5 gl-pr-4 gl-py-4 gl-display-flex gl-justify-content-space-between gl-bg-white gl-rounded-base" diff --git a/app/assets/stylesheets/page_bundles/design_management.scss b/app/assets/stylesheets/page_bundles/design_management.scss index f56eb4ae6fb..b42e6fd85fa 100644 --- a/app/assets/stylesheets/page_bundles/design_management.scss +++ b/app/assets/stylesheets/page_bundles/design_management.scss @@ -8,12 +8,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16); background: transparent; } -.design-checkbox { - position: absolute; - top: $gl-padding; - left: 30px; -} - .layout-page.design-detail-layout { max-height: 100vh; } diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb index 6e5e13ef0b6..7aef208447a 100644 --- a/app/helpers/projects/ml/experiments_helper.rb +++ b/app/helpers/projects/ml/experiments_helper.rb @@ -5,27 +5,6 @@ module Projects require 'json' include ActionView::Helpers::NumberHelper - def show_candidate_view_model(candidate) - data = { - candidate: { - params: candidate.params, - metrics: candidate.latest_metrics, - info: { - iid: candidate.iid, - eid: candidate.eid, - path_to_artifact: link_to_artifact(candidate), - experiment_name: candidate.experiment.name, - path_to_experiment: link_to_experiment(candidate.project, candidate.experiment), - path: link_to_details(candidate), - status: candidate.status - }, - metadata: candidate.metadata - } - } - - Gitlab::Json.generate(data) - end - def experiment_as_data(experiment) data = { name: experiment.name, diff --git a/app/models/organization.rb b/app/models/organization.rb index 73a7e84305f..cfbbbf1183e 100644 --- a/app/models/organization.rb +++ b/app/models/organization.rb @@ -1,6 +1,26 @@ # frozen_string_literal: true -# rubocop: disable Gitlab/NamespacedClass class Organization < ApplicationRecord + DEFAULT_ORGANIZATION_ID = 1 + + scope :without_default, -> { where.not(id: DEFAULT_ORGANIZATION_ID) } + + before_destroy :check_if_default_organization + + validates :name, + presence: true, + length: { maximum: 255 }, + uniqueness: { case_sensitive: false } + + def default? + id == DEFAULT_ORGANIZATION_ID + end + + private + + def check_if_default_organization + return unless default? + + raise ActiveRecord::RecordNotDestroyed, _('Cannot delete the default organization') + end end -# rubocop: enable Gitlab/NamespacedClass diff --git a/app/presenters/ml/candidate_details_presenter.rb b/app/presenters/ml/candidate_details_presenter.rb new file mode 100644 index 00000000000..58ec2aee471 --- /dev/null +++ b/app/presenters/ml/candidate_details_presenter.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Ml + class CandidateDetailsPresenter + include Rails.application.routes.url_helpers + + def initialize(candidate) + @candidate = candidate + end + + def present + data = { + candidate: { + info: { + iid: candidate.iid, + eid: candidate.eid, + path_to_artifact: link_to_artifact, + experiment_name: candidate.experiment.name, + path_to_experiment: link_to_experiment, + path: link_to_details, + status: candidate.status, + ci_job: job_info + }, + params: candidate.params, + metrics: candidate.latest_metrics, + metadata: candidate.metadata + } + } + + Gitlab::Json.generate(data) + end + + private + + attr_reader :candidate + + def job_info + return unless candidate.from_ci? + + build = candidate.ci_build + + { + path: project_job_path(build.project, build), + name: build.name, + **user_info(build.user) || {}, + **mr_info(build.pipeline.merge_request) || {} + } + end + + def user_info(user) + return unless user + + { + user: { + path: user_path(user), + username: user.username + } + } + end + + def mr_info(mr) + return unless mr + + { + merge_request: { + path: project_merge_request_path(mr.project, mr), + title: mr.title + } + } + end + + def link_to_artifact + artifact = candidate.artifact + + return unless artifact.present? + + project_package_path(candidate.project, artifact) + end + + def link_to_details + project_ml_candidate_path(candidate.project, candidate.iid) + end + + def link_to_experiment + project_ml_experiment_path(candidate.project, candidate.experiment.iid) + end + end +end diff --git a/app/views/projects/ml/candidates/show.html.haml b/app/views/projects/ml/candidates/show.html.haml index aea74ecfb48..5d5059551d3 100644 --- a/app/views/projects/ml/candidates/show.html.haml +++ b/app/views/projects/ml/candidates/show.html.haml @@ -3,5 +3,6 @@ - add_to_breadcrumbs experiment.name, project_ml_experiment_path(@project, experiment.iid) - breadcrumb_title "Candidate #{@candidate.iid}" - add_page_specific_style 'page_bundles/ml_experiment_tracking' +- presenter = ::Ml::CandidateDetailsPresenter.new(@candidate) -#js-show-ml-candidate{ data: { view_model: show_candidate_view_model(@candidate) } } +#js-show-ml-candidate{ data: { view_model: presenter.present } } diff --git a/config/feature_flags/development/realtime_approvals.yml b/config/feature_flags/development/realtime_approvals.yml index b2a4131b0f6..adb818e52bf 100644 --- a/config/feature_flags/development/realtime_approvals.yml +++ b/config/feature_flags/development/realtime_approvals.yml @@ -5,4 +5,4 @@ rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/399539' milestone: '16.0' type: development group: group::code review -default_enabled: false +default_enabled: true diff --git a/db/docs/design_management_repository_states.yml b/db/docs/design_management_repository_states.yml new file mode 100644 index 00000000000..3a8fa59fb42 --- /dev/null +++ b/db/docs/design_management_repository_states.yml @@ -0,0 +1,10 @@ +--- +table_name: design_management_repository_states +classes: +classes: [] +feature_categories: +- geo_replication +description: Separate table for Design Repository verification states +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113958 +milestone: '16.0' +gitlab_schema: gitlab_main diff --git a/db/migrate/20230322164031_create_design_management_repository_states.rb b/db/migrate/20230322164031_create_design_management_repository_states.rb new file mode 100644 index 00000000000..0c906a45ae8 --- /dev/null +++ b/db/migrate/20230322164031_create_design_management_repository_states.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class CreateDesignManagementRepositoryStates < Gitlab::Database::Migration[2.1] + VERIFICATION_STATE_INDEX_NAME = "index_design_management_repository_states_on_verification_state" + PENDING_VERIFICATION_INDEX_NAME = "index_design_management_repository_states_pending_verification" + FAILED_VERIFICATION_INDEX_NAME = "index_design_management_repository_states_failed_verification" + NEEDS_VERIFICATION_INDEX_NAME = "index_design_management_repository_states_needs_verification" + + enable_lock_retries! + + def up + create_table :design_management_repository_states, id: false do |t| + t.datetime_with_timezone :verification_started_at + t.datetime_with_timezone :verification_retry_at + t.datetime_with_timezone :verified_at + t.references :design_management_repository, primary_key: true, default: nil, index: false, + foreign_key: { on_delete: :cascade } + t.integer :verification_state, default: 0, limit: 2, null: false + t.integer :verification_retry_count, default: 0, limit: 2, null: false + t.binary :verification_checksum, using: 'verification_checksum::bytea' + t.text :verification_failure, limit: 255 + + t.index :verification_state, name: VERIFICATION_STATE_INDEX_NAME + t.index :verified_at, + where: "(verification_state = 0)", + order: { verified_at: 'ASC NULLS FIRST' }, + name: PENDING_VERIFICATION_INDEX_NAME + t.index :verification_retry_at, + where: "(verification_state = 3)", + order: { verification_retry_at: 'ASC NULLS FIRST' }, + name: FAILED_VERIFICATION_INDEX_NAME + t.index :verification_state, + where: "(verification_state = 0 OR verification_state = 3)", + name: NEEDS_VERIFICATION_INDEX_NAME + end + end + + def down + drop_table :design_management_repository_states + end +end diff --git a/db/migrate/20230509085428_change_organizations_sequence.rb b/db/migrate/20230509085428_change_organizations_sequence.rb new file mode 100644 index 00000000000..59ec8c6e1ea --- /dev/null +++ b/db/migrate/20230509085428_change_organizations_sequence.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +class ChangeOrganizationsSequence < Gitlab::Database::Migration[2.1] + def up + # Modify sequence for organizations.id so id '1' is never automatically taken + execute "ALTER SEQUENCE organizations_id_seq START WITH 1000 MINVALUE 1000 RESTART" + end + + def down + execute "ALTER SEQUENCE organizations_id_seq START WITH 1 MINVALUE 1" + end +end diff --git a/db/migrate/20230509115525_add_name_to_organization.rb b/db/migrate/20230509115525_add_name_to_organization.rb new file mode 100644 index 00000000000..d77fa84a70c --- /dev/null +++ b/db/migrate/20230509115525_add_name_to_organization.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# rubocop:disable Migration/AddLimitToTextColumns, Migration/AddIndex +# limit is added in 20230515111314_add_text_limit_on_organization_name.rb +class AddNameToOrganization < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'unique_organizations_on_name_lower' + + def up + add_column :organizations, :name, :text, null: false, default: '' + + add_index :organizations, 'lower(name)', name: INDEX_NAME, unique: true + end + + def down + remove_column :organizations, :name, if_exists: true + end +end +# rubocop:enable Migration/AddLimitToTextColumns, Migration/AddIndex diff --git a/db/migrate/20230509131736_add_default_organization.rb b/db/migrate/20230509131736_add_default_organization.rb new file mode 100644 index 00000000000..a63e7171f53 --- /dev/null +++ b/db/migrate/20230509131736_add_default_organization.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class AddDefaultOrganization < Gitlab::Database::Migration[2.1] + restrict_gitlab_migration gitlab_schema: :gitlab_main + + class Organization < MigrationRecord + end + + def up + Organization.create(id: 1, name: 'Default') + end + + def down + Organization.where(id: 1).delete_all + end +end diff --git a/db/migrate/20230515111314_add_text_limit_on_organization_name.rb b/db/migrate/20230515111314_add_text_limit_on_organization_name.rb new file mode 100644 index 00000000000..c0b687fab94 --- /dev/null +++ b/db/migrate/20230515111314_add_text_limit_on_organization_name.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddTextLimitOnOrganizationName < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_text_limit :organizations, :name, 255 + end + + def down + remove_text_limit :organizations, :name + end +end diff --git a/db/schema_migrations/20230322164031 b/db/schema_migrations/20230322164031 new file mode 100644 index 00000000000..57478bae5b3 --- /dev/null +++ b/db/schema_migrations/20230322164031 @@ -0,0 +1 @@ +4610b001f48cc89d79a769231d73f7fe579ffe8753a276e32ddd4cc15ad5bf25
\ No newline at end of file diff --git a/db/schema_migrations/20230509085428 b/db/schema_migrations/20230509085428 new file mode 100644 index 00000000000..cf7214ceadc --- /dev/null +++ b/db/schema_migrations/20230509085428 @@ -0,0 +1 @@ +6179fe3d8c419c58e028fc1fe5d554678976229eff88f087beec174cb669d4ce
\ No newline at end of file diff --git a/db/schema_migrations/20230509115525 b/db/schema_migrations/20230509115525 new file mode 100644 index 00000000000..e3c0ada40cd --- /dev/null +++ b/db/schema_migrations/20230509115525 @@ -0,0 +1 @@ +92b70129d19796653569fb730be43ea6eed7dacbce224e1323124fdf03b0a0b0
\ No newline at end of file diff --git a/db/schema_migrations/20230509131736 b/db/schema_migrations/20230509131736 new file mode 100644 index 00000000000..593c9495eb2 --- /dev/null +++ b/db/schema_migrations/20230509131736 @@ -0,0 +1 @@ +f9545a27756e5ca05220ffebcf89e8268e0231cbd8c7af0a89d13c70f5a070ec
\ No newline at end of file diff --git a/db/schema_migrations/20230515111314 b/db/schema_migrations/20230515111314 new file mode 100644 index 00000000000..d2d7d2c94c4 --- /dev/null +++ b/db/schema_migrations/20230515111314 @@ -0,0 +1 @@ +2a011d12459e0c21832df777569a12f4f2bbdaa5f57da7dc3823147f948d7772
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 75087462e3a..8dea1331b6e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -15321,6 +15321,18 @@ CREATE SEQUENCE design_management_repositories_id_seq ALTER SEQUENCE design_management_repositories_id_seq OWNED BY design_management_repositories.id; +CREATE TABLE design_management_repository_states ( + verification_started_at timestamp with time zone, + verification_retry_at timestamp with time zone, + verified_at timestamp with time zone, + design_management_repository_id bigint NOT NULL, + verification_state smallint DEFAULT 0 NOT NULL, + verification_retry_count smallint DEFAULT 0 NOT NULL, + verification_checksum bytea, + verification_failure text, + CONSTRAINT check_bf1387c28b CHECK ((char_length(verification_failure) <= 255)) +); + CREATE TABLE design_management_versions ( id bigint NOT NULL, sha bytea NOT NULL, @@ -19179,13 +19191,15 @@ ALTER SEQUENCE operations_user_lists_id_seq OWNED BY operations_user_lists.id; CREATE TABLE organizations ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, - updated_at timestamp with time zone NOT NULL + updated_at timestamp with time zone NOT NULL, + name text DEFAULT ''::text NOT NULL, + CONSTRAINT check_d130d769e0 CHECK ((char_length(name) <= 255)) ); CREATE SEQUENCE organizations_id_seq - START WITH 1 + START WITH 1000 INCREMENT BY 1 - NO MINVALUE + MINVALUE 1000 NO MAXVALUE CACHE 1; @@ -27046,6 +27060,9 @@ ALTER TABLE ONLY design_management_designs_versions ALTER TABLE ONLY design_management_repositories ADD CONSTRAINT design_management_repositories_pkey PRIMARY KEY (id); +ALTER TABLE ONLY design_management_repository_states + ADD CONSTRAINT design_management_repository_states_pkey PRIMARY KEY (design_management_repository_id); + ALTER TABLE ONLY design_management_versions ADD CONSTRAINT design_management_versions_pkey PRIMARY KEY (id); @@ -30560,6 +30577,14 @@ CREATE INDEX index_design_management_designs_versions_on_version_id ON design_ma CREATE UNIQUE INDEX index_design_management_repositories_on_project_id ON design_management_repositories USING btree (project_id); +CREATE INDEX index_design_management_repository_states_failed_verification ON design_management_repository_states USING btree (verification_retry_at NULLS FIRST) WHERE (verification_state = 3); + +CREATE INDEX index_design_management_repository_states_needs_verification ON design_management_repository_states USING btree (verification_state) WHERE ((verification_state = 0) OR (verification_state = 3)); + +CREATE INDEX index_design_management_repository_states_on_verification_state ON design_management_repository_states USING btree (verification_state); + +CREATE INDEX index_design_management_repository_states_pending_verification ON design_management_repository_states USING btree (verified_at NULLS FIRST) WHERE (verification_state = 0); + CREATE INDEX index_design_management_versions_on_author_id ON design_management_versions USING btree (author_id) WHERE (author_id IS NOT NULL); CREATE INDEX index_design_management_versions_on_issue_id ON design_management_versions USING btree (issue_id); @@ -33170,6 +33195,8 @@ CREATE UNIQUE INDEX unique_index_on_system_note_metadata_id ON resource_link_eve CREATE UNIQUE INDEX unique_merge_request_metrics_by_merge_request_id ON merge_request_metrics USING btree (merge_request_id); +CREATE UNIQUE INDEX unique_organizations_on_name_lower ON organizations USING btree (lower(name)); + CREATE UNIQUE INDEX unique_packages_project_id_and_name_and_version_when_debian ON packages_packages USING btree (project_id, name, version) WHERE ((package_type = 9) AND (status <> 4)); CREATE UNIQUE INDEX unique_postgres_async_fk_validations_name_and_table_name ON postgres_async_foreign_key_validations USING btree (name, table_name); @@ -37021,6 +37048,9 @@ ALTER TABLE ONLY requirements_management_test_reports ALTER TABLE ONLY pool_repositories ADD CONSTRAINT fk_rails_d2711daad4 FOREIGN KEY (source_project_id) REFERENCES projects(id) ON DELETE SET NULL; +ALTER TABLE ONLY design_management_repository_states + ADD CONSTRAINT fk_rails_d2a258cc5a FOREIGN KEY (design_management_repository_id) REFERENCES design_management_repositories(id) ON DELETE CASCADE; + ALTER TABLE ONLY web_hooks ADD CONSTRAINT fk_rails_d35697648e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; diff --git a/doc/topics/release_your_application.md b/doc/topics/release_your_application.md index 819ed0a1224..e8dba553cab 100644 --- a/doc/topics/release_your_application.md +++ b/doc/topics/release_your_application.md @@ -26,6 +26,7 @@ release features incrementally. - [Auto Deploy](autodevops/stages.md#auto-deploy) is the DevOps stage dedicated to software deployment using GitLab CI/CD. Auto Deploy has built-in support for EC2 and ECS deployments. - Deploy to Kubernetes clusters by using the [GitLab agent](../user/clusters/agent/install/index.md). +- View an example of [how to structure a GitOps deployment repository](../user/clusters/agent/gitops/example_repository_structure.md). - Use Docker images to run AWS commands from GitLab CI/CD, and a template to facilitate [deployment to AWS](../ci/cloud_deployment). - Use GitLab CI/CD to target any type of infrastructure accessible by GitLab Runner. diff --git a/doc/user/clusters/agent/gitops/example_repository_structure.md b/doc/user/clusters/agent/gitops/example_repository_structure.md new file mode 100644 index 00000000000..93c05e40bfd --- /dev/null +++ b/doc/user/clusters/agent/gitops/example_repository_structure.md @@ -0,0 +1,78 @@ +--- +stage: Deploy +group: Environments +info: An example of how to structure a repository for GitOps deployments +--- + +# Example GitOps repository structure **(FREE)** + +This page describes an example structure for a project that builds and deploys an application +to a Kubernetes cluster with [GitOps](https://about.gitlab.com/topics/gitops) and the +[GitLab agent for Kubernetes](../../agent/gitops.md). + +You can find an example project that uses this structure +[in this GitLab repository](https://gitlab.com/tigerwnz/minimal-gitops-app). You can use the example project +as a starting point to create your own deployment project. + +## Deployment workflow + +The default branch is the single source of truth for your application and the +Kubernetes manifests that deploy it. To be reflected in a Kubernetes cluster, +a code or configuration change must exist in the default branch. + +A GitLab agent for Kubernetes is installed in every Kubernetes cluster. The agent +is configured to sync manifests from a corresponding branch in the repository. +These branches represent the state of each cluster, and contain only commits that +exist in the default branch. + +Changes are deployed by merging the default branch into the branch of a cluster. +The agent that watches the branch picks up the change and syncs it to the cluster. + +For the actual deployment, the example project uses the GitLab agent for Kubernetes, +but you can also use other GitOps tools. + +### Review apps + +Ephemeral environments such as [review apps](../../../../ci/review_apps/index.md) +are deployed differently. Their configuration does not exist on the default branch, +and the changes are not meant to be deployed to a permanent environment. Review app +manifests are generated and deployed in a merge request feature branch, which is removed +when the MR is merged. + +## Example deployment + +The example project deploys to two permanent environments, staging and production, +which each have a dedicated Kubernetes cluster. A third cluster is used for ephemeral +review apps. + +Each cluster has a corresponding branch that represents the current state of the cluster: +`_gitlab/agents/staging`, `_gitlab/agents/production` and `_gitlab/agents/review`. Each branch is +[protected](../../../../user/project/protected_branches.md) and +a [project access token](../../../../user/project/settings/project_access_tokens.md) +is created for each branch with a configuration that allows only the corresponding token to push to the branch. +This ensures that environment branches are updated only through the configured process. + +Deployment branches are updated by CI/CD jobs. The access token that allows pushing to each +branch is configured as a [CI/CD variable](../../../../ci/variables/index.md). These variables +are protected, and only available to pipelines running on a protected branch. +The CI/CD job merges the default branch `main` into the deployment branch, and pushes +the deployment branch back to the repository using the provided token. To preserve the +commit history between both branches, the CI/CD job uses a fast-forward merge. + +Each cluster has an agent for Kubernetes, and each agent is configured to +[sync manifests from the branch corresponding to its cluster](../gitops.md#gitops-configuration-reference). +In your own project, you can different GitOps tool like Flux, or use the same configuration to deploy +to virtual machines with GitLab CI/CD. + +### Application changes + +The example project follows this process to deploy an application change: + +1. A new feature branch is created with the desired changes. The pipeline builds an image, + runs the test suite, and deploy the changes to a review app in the `review` cluster. +1. The feature branch is merged to `main` and the review app is removed. +1. Manifests are updated on `main` (either directly or via merge request) to point to an updated + version of the deployed image. The pipeline automatically merges `main` into the `_gitlab/agents/staging` + branch, which updates the `staging` cluster. +1. The `production` job is triggered manually, and merges `main` into the `_gitlab/agents/production` branch, + deploying to the `production` cluster. diff --git a/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml index 733ba4e4954..1ed4cd86e82 100644 --- a/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST-On-Demand-Scan.gitlab-ci.yml @@ -13,7 +13,7 @@ stages: - dast variables: - DAST_VERSION: 3 + DAST_VERSION: 4 # Setting this variable will affect all Security templates # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products" diff --git a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml index 6e1d96d4add..792bd7f666b 100644 --- a/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.gitlab-ci.yml @@ -22,7 +22,7 @@ # List of available variables: https://docs.gitlab.com/ee/user/application_security/dast/#available-variables variables: - DAST_VERSION: 3 + DAST_VERSION: 4 # Setting this variable will affect all Security templates # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products" diff --git a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml index de8a21819cc..d1d1c4d7e52 100644 --- a/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml @@ -22,7 +22,7 @@ # List of available variables: https://docs.gitlab.com/ee/user/application_security/dast/#available-variables variables: - DAST_VERSION: 3 + DAST_VERSION: 4 # Setting this variable will affect all Security templates # (SAST, Dependency Scanning, ...) SECURE_ANALYZERS_PREFIX: "$CI_TEMPLATE_REGISTRY_HOST/security-products" diff --git a/lib/sidebars/admin/menus/admin_settings_menu.rb b/lib/sidebars/admin/menus/admin_settings_menu.rb index 163c32ad0a9..4656e0f33e2 100644 --- a/lib/sidebars/admin/menus/admin_settings_menu.rb +++ b/lib/sidebars/admin/menus/admin_settings_menu.rb @@ -35,6 +35,11 @@ module Sidebars { 'data-qa-selector': 'admin_settings_menu_link' } end + override :separated? + def separated? + true + end + private def general_settings_menu_item diff --git a/lib/sidebars/groups/menus/settings_menu.rb b/lib/sidebars/groups/menus/settings_menu.rb index 76c5f9c16a5..b8f345c1ed5 100644 --- a/lib/sidebars/groups/menus/settings_menu.rb +++ b/lib/sidebars/groups/menus/settings_menu.rb @@ -52,6 +52,11 @@ module Sidebars true end + override :separated? + def separated? + true + end + private def general_menu_item diff --git a/lib/sidebars/menu.rb b/lib/sidebars/menu.rb index 432cc6ebc8b..5f9255c06d0 100644 --- a/lib/sidebars/menu.rb +++ b/lib/sidebars/menu.rb @@ -66,6 +66,11 @@ module Sidebars @renderable_items ||= @items.select(&:render?) end + # Defines whether menu is separated from others with a top separator + def separated? + false + end + # Returns a tree-like representation of itself and all # renderable menu entries, with additional information # on whether the item(s) have an active route @@ -79,7 +84,8 @@ module Sidebars link: link, is_active: is_active, pill_count: has_pill? ? pill_count : nil, - items: items + items: items, + separated: separated? } end diff --git a/lib/sidebars/projects/menus/settings_menu.rb b/lib/sidebars/projects/menus/settings_menu.rb index 5138c1534b8..9ed142f7f60 100644 --- a/lib/sidebars/projects/menus/settings_menu.rb +++ b/lib/sidebars/projects/menus/settings_menu.rb @@ -49,6 +49,11 @@ module Sidebars true end + override :separated? + def separated? + true + end + private def general_menu_item diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b2d1169af7b..99cda6ff71b 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8751,6 +8751,9 @@ msgstr "" msgid "Cannot delete the default framework" msgstr "" +msgid "Cannot delete the default organization" +msgstr "" + msgid "Cannot have multiple Jira imports running at the same time" msgstr "" diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb index d79befc6837..b39475b481d 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb @@ -1,10 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :orchestrated, :skip_live_env, product_group: :container_registry, quarantine: { - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/399556', - type: :flaky - } do + RSpec.describe 'Package', :orchestrated, :skip_live_env, product_group: :container_registry do describe 'Self-managed Container Registry' do include Support::Helpers::MaskToken @@ -48,7 +45,6 @@ module QA after do runner.remove_via_api! - project.remove_via_api! end context "when tls is disabled" do diff --git a/spec/factories/organizations.rb b/spec/factories/organizations.rb index a6684a8f95f..7ff0493d140 100644 --- a/spec/factories/organizations.rb +++ b/spec/factories/organizations.rb @@ -1,5 +1,16 @@ # frozen_string_literal: true FactoryBot.define do - factory :organization + factory :organization do + sequence(:name) { |n| "Organization ##{n}" } + + trait :default do + id { Organization::DEFAULT_ORGANIZATION_ID } + name { 'Default' } + initialize_with do + # Ensure we only use one default organization + Organization.find_by(id: Organization::DEFAULT_ORGANIZATION_ID) || new(**attributes) + end + end + end end diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js index 2861fc35342..f1a5c4169fb 100644 --- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js @@ -11,12 +11,25 @@ describe('CI Editor Header', () => { let wrapper; let trackingSpy = null; - const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => { + const createComponent = ({ + showDrawer = false, + showJobAssistantDrawer = false, + showAiAssistantDrawer = false, + aiChatAvailable = false, + aiCiConfigGenerator = false, + } = {}) => { wrapper = extendedWrapper( shallowMount(CiEditorHeader, { + provide: { + aiChatAvailable, + glFeatures: { + aiCiConfigGenerator, + }, + }, propsData: { showDrawer, showJobAssistantDrawer, + showAiAssistantDrawer, }, }), ); @@ -24,6 +37,7 @@ describe('CI Editor Header', () => { const findLinkBtn = () => wrapper.findByTestId('template-repo-link'); const findHelpBtn = () => wrapper.findByTestId('drawer-toggle'); + const findAiAssistnantBtn = () => wrapper.findByTestId('ai-assistant-drawer-toggle'); afterEach(() => { unmockTracking(); @@ -39,7 +53,29 @@ describe('CI Editor Header', () => { label, }); }; + describe('Ai Assistant toggle button', () => { + describe('when feature is unavailable', () => { + it('should not show ai button when feature toggle is off', () => { + createComponent({ aiChatAvailable: true }); + mockTracking(undefined, wrapper.element, jest.spyOn); + expect(findAiAssistnantBtn().exists()).toBe(false); + }); + + it('should not show ai button when feature is unavailable', () => { + createComponent({ aiCiConfigGenerator: true }); + mockTracking(undefined, wrapper.element, jest.spyOn); + expect(findAiAssistnantBtn().exists()).toBe(false); + }); + }); + describe('when feature is available', () => { + it('should show ai button', () => { + createComponent({ aiCiConfigGenerator: true, aiChatAvailable: true }); + mockTracking(undefined, wrapper.element, jest.spyOn); + expect(findAiAssistnantBtn().exists()).toBe(true); + }); + }); + }); describe('link button', () => { beforeEach(() => { createComponent(); diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js index cbdf01105c7..471b033913b 100644 --- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js +++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js @@ -57,6 +57,7 @@ describe('Pipeline editor tabs component', () => { isNewCiConfigFile: true, showDrawer: false, showJobAssistantDrawer: false, + showAiAssistantDrawer: false, ...props, }, data() { @@ -65,6 +66,7 @@ describe('Pipeline editor tabs component', () => { }; }, provide: { + aiChatAvailable: false, ciConfigPath: '/path/to/ci-config', ciLintPath: mockCiLintPath, currentBranch: 'main', diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js index 7ec6d4c6a01..576263d5418 100644 --- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js +++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js @@ -41,6 +41,7 @@ describe('Pipeline editor home wrapper', () => { ...props, }, provide: { + aiChatAvailable: false, projectFullPath: '', totalBranches: 19, glFeatures: { diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap index 7773950708f..9451f35ac5b 100644 --- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap +++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap @@ -38,13 +38,13 @@ exports[`Design management list item component with notes renders item with mult </div> <div - class="card-footer gl-display-flex gl-w-full" + class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4" > <div class="gl-display-flex gl-flex-direction-column str-truncated-100" > <span - class="gl-font-weight-bold str-truncated-100" + class="gl-font-weight-semibold str-truncated-100" data-qa-selector="design_file_name" data-testid="design-img-filename-1" title="test" @@ -118,13 +118,13 @@ exports[`Design management list item component with notes renders item with sing </div> <div - class="card-footer gl-display-flex gl-w-full" + class="card-footer gl-display-flex gl-w-full gl-bg-white gl-py-3 gl-px-4" > <div class="gl-display-flex gl-flex-direction-column str-truncated-100" > <span - class="gl-font-weight-bold str-truncated-100" + class="gl-font-weight-semibold str-truncated-100" data-qa-selector="design_file_name" data-testid="design-img-filename-1" title="test" diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap index ef1ed9bee51..7da0652faba 100644 --- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap +++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap @@ -9,7 +9,9 @@ exports[`Design management index page designs renders error 1`] = ` <!----> - <div> + <div + class="gl-bg-gray-10 gl-border gl-border-t-0 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-5" + > <gl-alert-stub dismisslabel="Dismiss" primarybuttonlink="" @@ -41,7 +43,9 @@ exports[`Design management index page designs renders loading icon 1`] = ` <!----> - <div> + <div + class="gl-bg-gray-10 gl-border gl-border-t-0 gl-rounded-bottom-left-base gl-rounded-bottom-right-base gl-px-5" + > <gl-loading-icon-stub color="dark" label="Loading" diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js index 26b146f0c8b..9b726b620dd 100644 --- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js +++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js @@ -1,8 +1,16 @@ import { mountExtended } from 'helpers/vue_test_utils_helper'; import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue'; +import PinnedSection from '~/super_sidebar/components/pinned_section.vue'; import { PANELS_WITH_PINS } from '~/super_sidebar/constants'; import { sidebarData } from '../mock_data'; +const menuItems = [ + { id: 1, title: 'No subitems' }, + { id: 2, title: 'With subitems', items: [{ id: 21, title: 'Pinned subitem' }] }, + { id: 3, title: 'Empty subitems array', items: [] }, + { id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] }, +]; + describe('SidebarMenu component', () => { let wrapper; @@ -17,14 +25,10 @@ describe('SidebarMenu component', () => { }); }; - describe('computed', () => { - const menuItems = [ - { id: 1, title: 'No subitems' }, - { id: 2, title: 'With subitems', items: [{ id: 21, title: 'Pinned subitem' }] }, - { id: 3, title: 'Empty subitems array', items: [] }, - { id: 4, title: 'Also with subitems', items: [{ id: 41, title: 'Subitem' }] }, - ]; + const findPinnedSection = () => wrapper.findComponent(PinnedSection); + const findMainMenuSeparator = () => wrapper.findByTestId('main-menu-separator'); + describe('computed', () => { describe('supportsPins', () => { it('is true for the project sidebar', () => { createWrapper({ ...sidebarData, panel_type: 'project' }); @@ -148,4 +152,33 @@ describe('SidebarMenu component', () => { }); }); }); + + describe('Menu separators', () => { + it('should add the separator above pinned section', () => { + createWrapper({ + ...sidebarData, + current_menu_items: menuItems, + panel_type: 'project', + }); + expect(findPinnedSection().props('separated')).toBe(true); + }); + + it('should add the separator above main menu items when there is a pinned section', () => { + createWrapper({ + ...sidebarData, + current_menu_items: menuItems, + panel_type: PANELS_WITH_PINS[0], + }); + expect(findMainMenuSeparator().exists()).toBe(true); + }); + + it('should NOT add the separator above main menu items when there is no pinned section', () => { + createWrapper({ + ...sidebarData, + current_menu_items: menuItems, + panel_type: 'explore', + }); + expect(findMainMenuSeparator().exists()).toBe(false); + }); + }); }); diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb index 1a0f2e10a20..b45882d9888 100644 --- a/spec/helpers/ci/pipeline_editor_helper_spec.rb +++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Ci::PipelineEditorHelper do let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } describe 'can_view_pipeline_editor?' do subject { helper.can_view_pipeline_editor?(project) } @@ -62,6 +63,10 @@ RSpec.describe Ci::PipelineEditorHelper do .to receive(:image_path) .with('illustrations/project-run-CICD-pipelines-sm.svg') .and_return('illustrations/validate.svg') + + allow(helper) + .to receive(:current_user) + .and_return(user) end subject(:pipeline_editor_data) { helper.js_pipeline_editor_data(project) } diff --git a/spec/helpers/projects/ml/experiments_helper_spec.rb b/spec/helpers/projects/ml/experiments_helper_spec.rb index e0fbc8ac488..021d518a329 100644 --- a/spec/helpers/projects/ml/experiments_helper_spec.rb +++ b/spec/helpers/projects/ml/experiments_helper_spec.rb @@ -98,41 +98,6 @@ RSpec.describe Projects::Ml::ExperimentsHelper, feature_category: :mlops do end end - describe '#show_candidate_view_model' do - let(:candidate) { candidate0 } - - subject { Gitlab::Json.parse(helper.show_candidate_view_model(candidate))['candidate'] } - - it 'generates the correct params' do - expect(subject['params']).to include( - hash_including('name' => 'param1', 'value' => 'p1'), - hash_including('name' => 'param2', 'value' => 'p2') - ) - end - - it 'generates the correct metrics' do - expect(subject['metrics']).to include( - hash_including('name' => 'metric1', 'value' => 0.1), - hash_including('name' => 'metric2', 'value' => 0.2), - hash_including('name' => 'metric3', 'value' => 0.3) - ) - end - - it 'generates the correct info' do - expected_info = { - 'iid' => candidate.iid, - 'eid' => candidate.eid, - 'path_to_artifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}", - 'experiment_name' => candidate.experiment.name, - 'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", - 'status' => 'running', - 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}" - } - - expect(subject['info']).to include(expected_info) - end - end - describe '#experiments_as_data' do let(:experiments) { [experiment] } diff --git a/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb b/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb index be23dd4d25b..4c9f603e99f 100644 --- a/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb +++ b/spec/lib/sidebars/admin/menus/admin_settings_menu_spec.rb @@ -6,7 +6,8 @@ RSpec.describe Sidebars::Admin::Menus::AdminSettingsMenu, feature_category: :nav it_behaves_like 'Admin menu', link: '/admin/application_settings/general', title: s_('Admin|Settings'), - icon: 'settings' + icon: 'settings', + separated: true it_behaves_like 'Admin menu with sub menus' end diff --git a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb index c5246fe93dd..bc30d7628af 100644 --- a/spec/lib/sidebars/groups/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/groups/menus/settings_menu_spec.rb @@ -25,6 +25,12 @@ RSpec.describe Sidebars::Groups::Menus::SettingsMenu, :with_license do end end + describe '#separated?' do + it 'returns true' do + expect(menu.separated?).to be true + end + end + describe 'Menu items' do subject { menu.renderable_items.find { |e| e.item_id == item_id } } diff --git a/spec/lib/sidebars/menu_spec.rb b/spec/lib/sidebars/menu_spec.rb index 21aec10ec27..4f77cb3aed4 100644 --- a/spec/lib/sidebars/menu_spec.rb +++ b/spec/lib/sidebars/menu_spec.rb @@ -54,6 +54,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do link: "foo2", is_active: true, pill_count: nil, + separated: false, items: [ { id: 'id1', @@ -87,6 +88,7 @@ RSpec.describe Sidebars::Menu, feature_category: :navigation do link: nil, is_active: false, pill_count: 'foo', + separated: false, items: [] }) end diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb index c7aca0fb97e..4be99892631 100644 --- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb @@ -22,6 +22,12 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do end end + describe '#separated?' do + it 'returns true' do + expect(subject.separated?).to be true + end + end + describe 'Menu items' do subject { described_class.new(context).renderable_items.find { |e| e.item_id == item_id } } diff --git a/spec/migrations/20230509131736_add_default_organization_spec.rb b/spec/migrations/20230509131736_add_default_organization_spec.rb new file mode 100644 index 00000000000..539216c57ee --- /dev/null +++ b/spec/migrations/20230509131736_add_default_organization_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe AddDefaultOrganization, feature_category: :cell do + let(:organization) { table(:organizations) } + + it "correctly migrates up and down" do + reversible_migration do |migration| + migration.before -> { + expect(organization.where(id: 1, name: 'Default')).to be_empty + } + migration.after -> { + expect(organization.where(id: 1, name: 'Default')).not_to be_empty + } + end + end +end diff --git a/spec/models/organization_spec.rb b/spec/models/organization_spec.rb index 9966a7132ce..e1aac88e640 100644 --- a/spec/models/organization_spec.rb +++ b/spec/models/organization_spec.rb @@ -2,9 +2,97 @@ require 'spec_helper' -# rubocop: disable Lint/EmptyBlock -# rubocop: disable RSpec/EmptyExampleGroup RSpec.describe Organization, type: :model, feature_category: :cell do + let_it_be(:organization) { create(:organization) } + let_it_be(:default_organization) { create(:organization, :default) } + + describe 'validations' do + subject { create(:organization) } + + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).case_insensitive } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + end + + context 'when using scopes' do + describe '.without_default' do + it 'excludes default organization' do + expect(described_class.without_default).not_to include(default_organization) + end + + it 'includes other organizations organization' do + expect(described_class.without_default).to include(organization) + end + end + end + + describe '#id' do + context 'when organization is default' do + it 'has id 1' do + expect(default_organization.id).to eq(1) + end + end + + context 'when organization is not default' do + it 'does not have id 1' do + expect(organization.id).not_to eq(1) + end + end + end + + describe '#destroy!' do + context 'when trying to delete the default organization' do + it 'raises an error' do + expect do + default_organization.destroy! + end.to raise_error(ActiveRecord::RecordNotDestroyed, _('Cannot delete the default organization')) + end + end + + context 'when trying to delete a non-default organization' do + let(:to_be_removed) { create(:organization) } + + it 'does not raise error' do + expect { to_be_removed.destroy! }.not_to raise_error + end + end + end + + describe '#destroy' do + context 'when trying to delete the default organization' do + it 'returns false' do + expect(default_organization.destroy).to eq(false) + end + end + + context 'when trying to delete a non-default organization' do + let(:to_be_removed) { create(:organization) } + + it 'returns true' do + expect(to_be_removed.destroy).to eq(to_be_removed) + end + end + end + + describe '#default?' do + context 'when organization is default' do + it 'returns true' do + expect(default_organization.default?).to eq(true) + end + end + + context 'when organization is not default' do + it 'returns false' do + expect(organization.default?).to eq(false) + end + end + end + + describe '#name' do + context 'when organization is default' do + it 'returns Default' do + expect(default_organization.name).to eq('Default') + end + end + end end -# rubocop: enable RSpec/EmptyExampleGroup -# rubocop: enable Lint/EmptyBlock diff --git a/spec/presenters/ml/candidate_details_presenter_spec.rb b/spec/presenters/ml/candidate_details_presenter_spec.rb new file mode 100644 index 00000000000..d83ffbc7129 --- /dev/null +++ b/spec/presenters/ml/candidate_details_presenter_spec.rb @@ -0,0 +1,111 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Ml::CandidateDetailsPresenter, feature_category: :mlops do + let_it_be(:project) { create(:project, :private) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:user) { project.creator } + let_it_be(:experiment) { create(:ml_experiments, user: user, project: project) } # rubocop:disable RSpec/FactoryBot/AvoidCreate + let_it_be(:candidate) do + create(:ml_candidates, :with_artifact, experiment: experiment, user: user, project: project) # rubocop:disable RSpec/FactoryBot/AvoidCreate + end + + let_it_be(:metrics) do + [ + build_stubbed(:ml_candidate_metrics, name: 'metric1', value: 0.1, candidate: candidate), + build_stubbed(:ml_candidate_metrics, name: 'metric2', value: 0.2, candidate: candidate), + build_stubbed(:ml_candidate_metrics, name: 'metric3', value: 0.3, candidate: candidate) + ] + end + + let_it_be(:params) do + [ + build_stubbed(:ml_candidate_params, name: 'param1', value: 'p1', candidate: candidate), + build_stubbed(:ml_candidate_params, name: 'param2', value: 'p2', candidate: candidate) + ] + end + + subject { Gitlab::Json.parse(described_class.new(candidate).present)['candidate'] } + + before do + allow(candidate).to receive(:latest_metrics).and_return(metrics) + allow(candidate).to receive(:params).and_return(params) + end + + describe '#execute' do + context 'when candidate has metrics, params and artifacts' do + it 'generates the correct params' do + expect(subject['params']).to include( + hash_including('name' => 'param1', 'value' => 'p1'), + hash_including('name' => 'param2', 'value' => 'p2') + ) + end + + it 'generates the correct metrics' do + expect(subject['metrics']).to include( + hash_including('name' => 'metric1', 'value' => 0.1), + hash_including('name' => 'metric2', 'value' => 0.2), + hash_including('name' => 'metric3', 'value' => 0.3) + ) + end + + it 'generates the correct info' do + expected_info = { + 'iid' => candidate.iid, + 'eid' => candidate.eid, + 'path_to_artifact' => "/#{project.full_path}/-/packages/#{candidate.artifact.id}", + 'experiment_name' => candidate.experiment.name, + 'path_to_experiment' => "/#{project.full_path}/-/ml/experiments/#{experiment.iid}", + 'status' => 'running', + 'path' => "/#{project.full_path}/-/ml/candidates/#{candidate.iid}" + } + + expect(subject['info']).to include(expected_info) + end + end + + context 'when candidate has job' do + let_it_be(:pipeline) { build_stubbed(:ci_pipeline, project: project, user: user) } + let_it_be(:build) { candidate.ci_build = build_stubbed(:ci_build, pipeline: pipeline, user: user) } + + it 'generates the correct ci' do + expected_info = { + 'path' => "/#{project.full_path}/-/jobs/#{build.id}", + 'name' => 'test', + 'user' => { + 'path' => "/#{pipeline.user.username}", + 'username' => pipeline.user.username + } + } + + expect(subject.dig('info', 'ci_job')).to include(expected_info) + end + + context 'when build user is nil' do + it 'does not include build user info' do + expected_info = { + 'path' => "/#{project.full_path}/-/jobs/#{build.id}", + 'name' => 'test' + } + + allow(build).to receive(:user).and_return(nil) + + expect(subject.dig('info', 'ci_job')).to eq(expected_info) + end + end + + context 'and job is from MR' do + let_it_be(:mr) { pipeline.merge_request = build_stubbed(:merge_request, source_project: project) } + + it 'generates the correct ci' do + expected_info = { + 'path' => "/#{project.full_path}/-/merge_requests/#{mr.iid}", + 'title' => mr.title + } + + expect(subject.dig('info', 'ci_job', 'merge_request')).to include(expected_info) + end + end + end + end +end diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index a53e1e1002c..ceb567e54c4 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -372,6 +372,7 @@ module TestEnv def seed_db Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions + FactoryBot.create(:organization, :default) end private diff --git a/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb index a9fd66528bd..f913c6b8a9e 100644 --- a/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb +++ b/spec/support/shared_examples/lib/sidebars/admin/menus/admin_menus_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'Admin menu' do |link:, title:, icon:| +RSpec.shared_examples 'Admin menu' do |link:, title:, icon:, separated: false| let_it_be(:user) { build(:user, :admin) } before do @@ -23,6 +23,10 @@ RSpec.shared_examples 'Admin menu' do |link:, title:, icon:| expect(subject.sprite_icon).to be icon end + it 'renders the separator if needed' do + expect(subject.separated?).to be separated + end + describe '#render?' do context 'when user is admin' do it 'renders' do |