diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-16 09:08:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-16 09:08:02 +0000 |
commit | 613868af23d7c0e09210857518895edd6678f5e9 (patch) | |
tree | 90b5ba583bbec4cb2a1eef3b34b2df1bb13de50f | |
parent | b78b8c1103e1e9542891a1c333c8abcd4d7e10ab (diff) | |
download | gitlab-ce-613868af23d7c0e09210857518895edd6678f5e9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
37 files changed, 471 insertions, 118 deletions
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue index 0afee7bebe0..f2550d753d6 100644 --- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue +++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue @@ -1,6 +1,5 @@ <script> import { isEmpty } from 'lodash'; -import { GlButtonGroup } from '@gitlab/ui'; import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql'; import { EDITOR_TOOLBAR_BUTTON_GROUPS } from '~/editor/constants'; import SourceEditorToolbarButton from './source_editor_toolbar_button.vue'; @@ -9,7 +8,6 @@ export default { name: 'SourceEditorToolbar', components: { SourceEditorToolbarButton, - GlButtonGroup, }, data() { return { @@ -52,31 +50,34 @@ export default { <section v-if="isVisible" id="se-toolbar" - class="gl-py-3 gl-px-5 gl-bg-white gl-border-b gl-display-flex gl-align-items-center" + class="file-buttons gl-display-flex gl-align-items-center gl-justify-content-end" > - <gl-button-group v-if="hasGroupItems($options.groups.file)"> + <div v-if="hasGroupItems($options.groups.file)"> <source-editor-toolbar-button v-for="item in getGroupItems($options.groups.file)" :key="item.id" :button="item" @click="$emit('click', item)" /> - </gl-button-group> - <gl-button-group v-if="hasGroupItems($options.groups.edit)"> + </div> + <div + v-if="hasGroupItems($options.groups.edit)" + class="md-header-toolbar gl-display-flex gl-flex-wrap gl-gap-3 gl-ml-auto" + > <source-editor-toolbar-button v-for="item in getGroupItems($options.groups.edit)" :key="item.id" :button="item" @click="$emit('click', item)" /> - </gl-button-group> - <gl-button-group v-if="hasGroupItems($options.groups.settings)" class="gl-ml-auto"> + </div> + <div v-if="hasGroupItems($options.groups.settings)" class="gl-align-self-start"> <source-editor-toolbar-button v-for="item in getGroupItems($options.groups.settings)" :key="item.id" :button="item" @click="$emit('click', item)" /> - </gl-button-group> + </div> </section> </template> diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue index 38f586f0773..996ecea04e5 100644 --- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue +++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue @@ -30,6 +30,15 @@ export default { showButton() { return Object.entries(this.button).length > 0; }, + showLabel() { + if (this.button.category === 'tertiary' && this.button.icon) { + return false; + } + return true; + }, + isSelected() { + return this.button.category === 'tertiary' && this.button.selected; + }, }, mounted() { if (this.button.data) { @@ -55,11 +64,12 @@ export default { :category="button.category" :variant="button.variant" type="button" - :selected="button.selected" + :selected="isSelected" :icon="icon" :title="label" :aria-label="label" :class="button.class" @click="clickHandler($event)" - /> + ><template v-if="showLabel">{{ label }}</template></gl-button + > </template> diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js index 8ec83e4df1c..905126cae52 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js +++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js @@ -151,6 +151,7 @@ export class SourceEditorExtension { instance.toolbar.updateItem(EXTENSION_SOFTWRAP_ID, { selected: !isSoftWrapped, }); + document.querySelector('.soft-wrap-toggle')?.blur(); } }, }; diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js index 9ec1a97ba1a..60aa00da861 100644 --- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js +++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js @@ -14,7 +14,6 @@ import { EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY, EXTENSION_MARKDOWN_PREVIEW_LABEL, EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL, - EDITOR_TOOLBAR_BUTTON_GROUPS, } from '../constants'; const fetchPreview = (text, previewMarkdownPath) => { @@ -58,9 +57,6 @@ export class EditorMarkdownPreviewExtension { this.toolbarButtons = []; this.setupPreviewAction(instance); - if (instance.toolbar) { - this.setupToolbar(instance); - } const debouncedResizeHandler = debounce((entries) => { for (const entry of entries) { @@ -104,25 +100,6 @@ export class EditorMarkdownPreviewExtension { instance.layout({ width, height }); } - setupToolbar(instance) { - this.toolbarButtons = [ - { - id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, - label: EXTENSION_MARKDOWN_PREVIEW_LABEL, - icon: 'live-preview', - selected: false, - group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings, - category: 'primary', - selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL, - onClick: () => instance.togglePreview(), - data: { - qaSelector: 'editor_toolbar_button', - }, - }, - ]; - instance.toolbar.addItems(this.toolbarButtons); - } - togglePreviewLayout(instance) { const { width } = instance.getLayoutInfo(); let newWidth; diff --git a/app/assets/stylesheets/page_bundles/editor.scss b/app/assets/stylesheets/page_bundles/editor.scss index 9e9723d2e5a..0c1979424b1 100644 --- a/app/assets/stylesheets/page_bundles/editor.scss +++ b/app/assets/stylesheets/page_bundles/editor.scss @@ -110,11 +110,13 @@ .file-buttons { display: flex; - flex-direction: column; + flex-direction: row; + justify-content: space-between; width: 100%; + padding: $gl-padding-8 0 0; .md-header-toolbar { - margin: $gl-padding 0; + margin-left: 0; } .soft-wrap-toggle { @@ -129,6 +131,17 @@ } } +@include media-breakpoint-down(sm) { + .file-editor .file-buttons { + flex-direction: column; + padding: 0; + + .md-header-toolbar { + margin: $gl-padding-8 0; + } + } +} + .blob-new-page-title, .blob-edit-page-title { margin: 19px 0 21px; diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb index 345d7bdbca8..ef3144f6f8c 100644 --- a/app/controllers/profiles/webauthn_registrations_controller.rb +++ b/app/controllers/profiles/webauthn_registrations_controller.rb @@ -4,8 +4,7 @@ class Profiles::WebauthnRegistrationsController < Profiles::ApplicationControlle feature_category :system_access def destroy - webauthn_registration = current_user.webauthn_registrations.find(params[:id]) - webauthn_registration.destroy + Webauthn::DestroyService.new(current_user, current_user, params[:id]).execute redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted WebAuthn device.") end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index e053fc0453c..0328d262dc7 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -5,18 +5,32 @@ module Emails def new_issue_email(recipient_id, issue_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_new_thread(@issue, issue_thread_options(@issue.author_id, reason)) + mail_new_thread( + @issue, + issue_thread_options( + @issue.author_id, + reason, + confidentiality: @issue.confidential? + ) + ) end def issue_due_email(recipient_id, issue_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_answer_thread(@issue, issue_thread_options(@issue.author_id, reason)) + mail_answer_thread(@issue, issue_thread_options(@issue.author_id, reason, confidentiality: @issue.confidential?)) end def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end # rubocop: disable CodeReuse/ActiveRecord @@ -26,7 +40,14 @@ module Emails @previous_assignees = [] @previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any? - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end # rubocop: enable CodeReuse/ActiveRecord @@ -35,7 +56,14 @@ module Emails @updated_by = User.find(updated_by_user_id) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id, reason = nil) @@ -43,13 +71,27 @@ module Emails @label_names = label_names @labels_url = project_labels_url(@project) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil) setup_issue_mail(issue_id, recipient_id) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil) @@ -57,9 +99,14 @@ module Emails @milestone = milestone @milestone_url = milestone_url(@milestone) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason).merge({ - template_name: 'changed_milestone_email' - })) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ).merge({ template_name: 'changed_milestone_email' }) + ) end def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil) @@ -67,7 +114,14 @@ module Emails @issue_status = status @updated_by = User.find(updated_by_user_id) - mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason)) + mail_answer_thread( + @issue, + issue_thread_options( + updated_by_user_id, + reason, + confidentiality: @issue.confidential? + ) + ) end def issue_moved_email(recipient, issue, new_issue, updated_by_user, reason = nil) @@ -76,7 +130,14 @@ module Emails @new_issue = new_issue @new_project = new_issue.project @can_access_project = recipient.can?(:read_project, @new_project) - mail_answer_thread(issue, issue_thread_options(updated_by_user.id, reason)) + mail_answer_thread( + issue, + issue_thread_options( + updated_by_user.id, + reason, + confidentiality: @issue.confidential? + ) + ) end def issue_cloned_email(recipient, issue, new_issue, updated_by_user, reason = nil) @@ -86,7 +147,14 @@ module Emails @issue = issue @new_issue = new_issue @can_access_project = recipient.can?(:read_project, @new_issue.project) - mail_answer_thread(issue, issue_thread_options(updated_by_user.id, reason)) + mail_answer_thread( + issue, + issue_thread_options( + updated_by_user.id, + reason, + confidentiality: @issue.confidential? + ) + ) end def import_issues_csv_email(user_id, project_id, results) @@ -115,12 +183,14 @@ module Emails @sent_notification = SentNotification.record(@issue, recipient_id, reply_key) end - def issue_thread_options(sender_id, reason) + def issue_thread_options(sender_id, reason, confidentiality: false) + confidentiality = false if confidentiality.nil? { from: sender(sender_id), to: @recipient.notification_email_for(@project.group), subject: subject("#{@issue.title} (##{@issue.iid})"), - 'X-GitLab-NotificationReason' => reason + 'X-GitLab-NotificationReason' => reason, + 'X-GitLab-ConfidentialIssue' => confidentiality } end end diff --git a/app/services/webauthn/destroy_service.rb b/app/services/webauthn/destroy_service.rb new file mode 100644 index 00000000000..afad2680d42 --- /dev/null +++ b/app/services/webauthn/destroy_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Webauthn + class DestroyService < BaseService + attr_reader :webauthn_registration, :user, :current_user + + def initialize(current_user, user, webauthn_registrations_id) + @current_user = current_user + @user = user + @webauthn_registration = user.webauthn_registrations.find(webauthn_registrations_id) + end + + def execute + return error(_('You are not authorized to perform this action')) unless authorized? + + webauthn_registration.destroy + user.reset_backup_codes! if last_two_factor_registration? + end + + private + + def last_two_factor_registration? + user.webauthn_registrations.empty? && !user.otp_required_for_login? + end + + def authorized? + current_user.can?(:disable_two_factor, user) + end + end +end diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index 621cd251bdf..68520d36858 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -26,7 +26,10 @@ dismiss_key: @project.id, human_access: human_access } } - - unless Feature.enabled?(:source_editor_toolbar, current_user) + - if Feature.enabled?(:source_editor_toolbar, current_user) + #editor-toolbar + + - else .file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end - if is_markdown .md-header.gl-display-flex.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2 @@ -40,8 +43,6 @@ = _("Soft wrap") .file-editor.code - - if Feature.enabled?(:source_editor_toolbar, current_user) - #editor-toolbar .js-edit-mode-pane#editor{ data: { 'editor-loading': true, qa_selector: 'source_editor_preview_container' } }< %pre.editor-loading-content= params[:content] || local_assigns[:blob_data] - if local_assigns[:path] diff --git a/config/feature_flags/development/product_intelligence_database_event_tracking.yml b/config/feature_flags/development/product_intelligence_database_event_tracking.yml index 545cdc47f2b..63b53996eea 100644 --- a/config/feature_flags/development/product_intelligence_database_event_tracking.yml +++ b/config/feature_flags/development/product_intelligence_database_event_tracking.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368976 milestone: '15.3' type: development group: group::product intelligence -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml b/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml index 2e15d86004b..825f684ed8c 100644 --- a/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml +++ b/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/403041 milestone: '16.0' type: development group: group::analytics instrumentation -default_enabled: false +default_enabled: true diff --git a/db/post_migrate/20230511132140_create_component_id_index.rb b/db/post_migrate/20230511132140_create_component_id_index.rb new file mode 100644 index 00000000000..3b466010f7c --- /dev/null +++ b/db/post_migrate/20230511132140_create_component_id_index.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class CreateComponentIdIndex < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + INDEX_NAME = 'index_sbom_occurrences_on_project_id_component_id' + + def up + return if index_exists_by_name?(:sbom_occurrences, INDEX_NAME) + + add_concurrent_index :sbom_occurrences, [:project_id, :component_id], name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :sbom_sources, INDEX_NAME + end +end diff --git a/db/schema_migrations/20230511132140 b/db/schema_migrations/20230511132140 new file mode 100644 index 00000000000..e90a58e2e28 --- /dev/null +++ b/db/schema_migrations/20230511132140 @@ -0,0 +1 @@ +a9fc2734da81d15b8c6bdb3812c22e7c5a24d6729b98bf9118e6c8a9c85a6712
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 8dea1331b6e..9e64191eb74 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -32339,6 +32339,8 @@ CREATE INDEX index_sbom_occurrences_on_project_id ON sbom_occurrences USING btre CREATE INDEX index_sbom_occurrences_on_project_id_and_id ON sbom_occurrences USING btree (project_id, id); +CREATE INDEX index_sbom_occurrences_on_project_id_component_id ON sbom_occurrences USING btree (project_id, component_id); + CREATE INDEX index_sbom_occurrences_on_source_id ON sbom_occurrences USING btree (source_id); CREATE UNIQUE INDEX index_sbom_occurrences_on_uuid ON sbom_occurrences USING btree (uuid); diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md index 7fab97f76da..3533237f946 100644 --- a/doc/administration/logs/index.md +++ b/doc/administration/logs/index.md @@ -1034,6 +1034,18 @@ can be used. } ``` +## `llm.log` **(ULTIMATE SAAS)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120506) in GitLab 16.0. + +The `llm.log` file logs information related to +[AI features](../../user/ai_features.md). + +Depending on your installation method, this file is located at: + +- Omnibus GitLab: `/var/log/gitlab/gitlab-rails/llm.log` +- Installations from source: `/home/git/gitlab/log/llm.log` + ## Registry logs For Omnibus GitLab installations, Container Registry logs are in `/var/log/gitlab/registry/current`. diff --git a/doc/administration/sidekiq/sidekiq_troubleshooting.md b/doc/administration/sidekiq/sidekiq_troubleshooting.md index 8b95a9f6f0a..cce9420f455 100644 --- a/doc/administration/sidekiq/sidekiq_troubleshooting.md +++ b/doc/administration/sidekiq/sidekiq_troubleshooting.md @@ -494,6 +494,41 @@ has number of drawbacks, as mentioned in [Why Ruby's Timeout is dangerous (and T > > Nobody writes code to defend against an exception being raised on literally any line. That's not even possible. So Thread.raise is basically like a sneak attack on your code that could result in almost anything. It would probably be okay if it were pure-functional code that did not modify any state. But this is Ruby, so that's unlikely :) +## Manually trigger a cron job + +By visiting `/admin/background_jobs`, you can look into what jobs are scheduled/running/pending on your instance. + +You can trigger a cron job from the UI by selecting the "Enqueue Now" button. To trigger a cron job programmatically first open a [Rails console](../operations/rails_console.md). + +To find the cron job you want to test: + +```irb +job = Sidekiq::Cron::Job.find('job-name') + +# get status of job: +job.status + +# enqueue job right now! +job.enque! +``` + +For example, to trigger the `update_all_mirrors_worker` cron job that updates the repository mirrors: + +```irb +irb(main):001:0> job = Sidekiq::Cron::Job.find('update_all_mirrors_worker') +=> +#<Sidekiq::Cron::Job:0x00007f147f84a1d0 +... +irb(main):002:0> job.status +=> "enabled" +irb(main):003:0> job.enque! +=> 257 +``` + +The list of available jobs can be found in the [workers](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/workers) directory. + +For more information about Sidekiq jobs, see the [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron#work-with-job) documentation. + ## Omnibus GitLab 14.0 and later: remove the `sidekiq-cluster` service Omnibus GitLab instances that were configured to run `sidekiq-cluster` prior to GitLab 14.0 diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index c52d14f59fe..9a7724932eb 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -19474,6 +19474,7 @@ four standard [pagination arguments](#connection-pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="projectdependenciescomponentnames"></a>`componentNames` | [`[String!]`](#string) | Filter dependencies by component names. | | <a id="projectdependenciespackagemanagers"></a>`packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. | | <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. | diff --git a/doc/architecture/blueprints/remote_development/index.md b/doc/architecture/blueprints/remote_development/index.md index f214ef56967..e2647551a95 100644 --- a/doc/architecture/blueprints/remote_development/index.md +++ b/doc/architecture/blueprints/remote_development/index.md @@ -42,6 +42,64 @@ As a [new Software Developer to a team such as Sasha](https://about.gitlab.com/h ![User Flow](img/remote_dev_15_7.png) +## Architecture + +```plantuml +@startuml +node "Kubernetes" { + [Ingress Controller] --> [GitLab Workspaces Proxy] : Decrypt Traffic + + note right of "Ingress Controller" + Customers can choose + an ingress controller + of their choice + end note + + note top of "GitLab Workspaces Proxy" + Authenticate and + authorize user traffic + end note + + [GitLab Workspaces Proxy] ..> [Workspace n] : Forward traffic\nfor workspace n + [GitLab Workspaces Proxy] ..> [Workspace 2] : Forward traffic\nfor workspace 2 + [GitLab Workspaces Proxy] --> [Workspace 1] : Forward traffic\nfor workspace 1 + + [Agentk] .up.> [Workspace n] : Applies kubernetes resources\nfor workspace n + [Agentk] .up.> [Workspace 2] : Applies kubernetes resources\nfor workspace 2 + [Agentk] .up.> [Workspace 1] : Applies kubernetes resources\nfor workspace 1 + + [Agentk] --> [Kubernetes API Server] : Interact and get/apply\nKubernetes resources +} + +node "GitLab" { + [Nginx] --> [GitLab Rails] : Forward + [GitLab Rails] --> [Postgres] : Access database + [GitLab Rails] --> [Gitaly] : Fetch files + [KAS] -up-> [GitLab Rails] : Proxy +} + +[Agentk] -up-> [KAS] : Initiate reconciliation loop +"Load Balancer IP" --> [Ingress Controller] +[Browser] --> [Nginx] : Browse GitLab +[Browser] -right-> "Domain IP" : Browse workspace URL +"Domain IP" .right.> "Load Balancer IP" +[GitLab Workspaces Proxy] ..> [GitLab Rails] : Authenticate and authorize\nthe user accessing the workspace. + +note top of "Domain IP" + For local development, workspace URL + is [workspace-name].workspaces.localdev.me + which resolves to localhost (127.0.0.1) +end note + +note top of "Load Balancer IP" + For local development, + it includes all local loopback interfaces + e.g. 127.0.0.1, 172.16.123.1, 192.168.0.1, etc. +end note + +@enduml +``` + ## Terminology We use the following terms to describe components and properties of the Remote Development architecture. diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md index 6ed1d59c3e0..25da99addc1 100644 --- a/doc/development/ai_features.md +++ b/doc/development/ai_features.md @@ -89,6 +89,15 @@ To populate the embedding database for GitLab chat: 1. Open a rails console 1. Run [this script](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/10588#note_1373586079) to populate the embedding database +### Debugging + +To gather more insights about the full request, use the `Gitlab::Llm::Logger` file to debug logs. +To follow the debugging messages related to the AI requests on the abstraction layer, you can use: + +```shell +tail -f log/llm.log +``` + ### Configure GCP Vertex access In order to obtain a GCP service key for local development, please follow the steps below: diff --git a/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png b/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png Binary files differnew file mode 100644 index 00000000000..6521da25754 --- /dev/null +++ b/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png diff --git a/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png b/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png Binary files differnew file mode 100644 index 00000000000..58f39fdfb10 --- /dev/null +++ b/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png diff --git a/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png b/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png Binary files differnew file mode 100644 index 00000000000..cfce3bf0743 --- /dev/null +++ b/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png diff --git a/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png b/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png Binary files differnew file mode 100644 index 00000000000..c7c32fed841 --- /dev/null +++ b/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png diff --git a/doc/tutorials/boards_for_teams/index.md b/doc/tutorials/boards_for_teams/index.md index e37bf5a2d31..9b34f580e05 100644 --- a/doc/tutorials/boards_for_teams/index.md +++ b/doc/tutorials/boards_for_teams/index.md @@ -31,12 +31,9 @@ After you set up everything, the two teams will be able to hand off issues from 1. A product designer on the UX team: 1. Checks the `Workflow::Ready for design` list on the **UX workflow** board and decides to work on the profile page redesign. - <!-- Image: UX board with lists: - ~Workflow::Ready for design, - ~Workflow::Design - ~Workflow::Ready for development --> + ![Issue board called "UX workflow" with three columns and three issues](img/ux_board_filled_v16_0.png) - 1. Assigns themselves to the issue. + 1. Assigns themselves to the **Redesign user profile page** issue. 1. Drags the issue card to the `Workflow::Design` list. The previous workflow label is automatically removed. 1. Creates the ✨new designs✨. 1. [Adds the designs to the issue](../../user/project/issues/design_management.md). @@ -45,12 +42,9 @@ After you set up everything, the two teams will be able to hand off issues from 1. A developer on the Frontend team: 1. Checks the `Workflow::Ready for development` list on the **Frontend workflow** board and chooses an issue to work on. - <!-- Image: Frontend board, scoped to ~Frontend, with lists: - ~Workflow::Ready for development - ~Workflow::In development - ~Workflow::Complete --> + ![Issue board called "Frontend workflow" with three columns and three issues](img/frontend_board_filled_v16_0.png) - 1. Assigns themselves to the issue. + 1. Assigns themselves to the **Redesign user profile page** issue. 1. Drags the issue card to the `Workflow::In development` list. The previous workflow label is automatically removed. 1. Adds the frontend code in a [merge request](../../user/project/merge_requests/index.md). 1. Adds the `Workflow::Complete` label. @@ -140,15 +134,14 @@ To create the **UX workflow** issue board: 1. In the **Title field**, enter `UX workflow`. 1. Clear the **Show the Open list** and **Show the Closed list** checkboxes. 1. Select **Create board**. You should see an empty board. - - <!-- Image: empty UX workflow board --> - 1. Create a list for the `Workflow::Ready for design` label: 1. In the upper-left corner of the issue board page, select **Create list**. 1. In the column that appears, from the **Value** dropdown list, select the `Workflow::Ready for design` label. 1. Select **Add to board**. 1. Repeat the previous step for labels `Workflow::Design` and `Workflow::Ready for development`. +![Issue board called "UX workflow" with three columns and no issues](img/ux_board_empty_v16_0.png) + To create the **Frontend workflow** board: 1. In the upper-left corner of the issue board page, select the dropdown list with the current board name. @@ -164,6 +157,8 @@ To create the **Frontend workflow** board: 1. Select **Add to board**. 1. Repeat the previous step for labels `Workflow::In development` and `Workflow::Complete`. +![Issue board called "Frontend workflow" with three columns and no issues](img/frontend_board_empty_v16_0.png) + For now, lists in both your boards should be empty. Next, you'll populate them with some issues. ## Create issues for features @@ -193,9 +188,9 @@ Repeat these steps to create a few more issues with the same labels. You should now see at least one issue there, ready for your product designers to start working on! -<!-- Image: UX workflow board with at least one issue in the `Workflow::Ready for design` list --> - Congratulations! Now your teams can start collaborating on amazing software. +As a next step, you can try out [the goal workflow](#the-goal-workflow) for yourself using these boards, +simulating the two teams interacting. ## Learn more about project management in GitLab diff --git a/doc/tutorials/plan_and_track.md b/doc/tutorials/plan_and_track.md index 35b552cdaa5..0206c462cb2 100644 --- a/doc/tutorials/plan_and_track.md +++ b/doc/tutorials/plan_and_track.md @@ -15,4 +15,5 @@ issues, epics, and more. | [Create a project from a template](https://gitlab.com/projects/new#create_from_template) | Choose a project template and create a project with files to get you started. | | | [Migrate to GitLab](../user/project/import/index.md) | If you are coming to GitLab from another platform, you can import or convert your projects. | | | [Run an agile iteration](agile_sprint/index.md) | Use group, projects, and iterations to run an agile development iteration. | +| [Set up issue boards for team hand-off](boards_for_teams/index.md) | Use issue boards and scoped labels to set up collaboration across many teams. | **{star}** | | <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Epics and Issue Boards](https://www.youtube.com/watch?v=I1bFIAQBHB8) | Find out how to use epics and issue boards for project management. | | diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md index 89366c73b16..b60ff8471bf 100644 --- a/doc/user/profile/notifications.md +++ b/doc/user/profile/notifications.md @@ -362,6 +362,7 @@ The following table lists all GitLab-specific email headers: | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `List-Id` | The path of the project in an RFC 2919 mailing list identifier. You can use it for email organization with filters. | | `X-GitLab-(Resource)-ID` | The ID of the resource the notification is for. The resource, for example, can be `Issue`, `MergeRequest`, `Commit`, or another such resource. | +| `X-GitLab-ConfidentialIssue` | The boolean value indicating issue confidentiality for notifications. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222908) in GitLab 16.0. | | `X-GitLab-Discussion-ID` | The ID of the thread the comment belongs to, in notification emails for comments. | | `X-GitLab-Group-Id` | The group's ID. Only present on notification emails for [epics](../group/epics/index.md). | | `X-GitLab-Group-Path` | The group's path. Only present on notification emails for [epics](../group/epics/index.md) | diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index bb1609a74e5..18721c0582d 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -131,6 +131,16 @@ To commit changes in the Web IDE: 1. Select **Commit & Push**. 1. Commit to the current branch, or create a new branch. +## Create a merge request + +To create a merge request: + +1. [Commit the changes](index.md#commit-changes). +1. In the pop-up notification in the lower-right corner, select `Create Merge Request`. +This action opens a new window with the [merge request creation form](../merge_requests/index.md). + +To access missed notifications, see the [IDE notification tips](index.md#access-missed-ide-notifications). + ## Use the command palette In the Web IDE, you can access many commands through the command palette. @@ -203,3 +213,10 @@ You cannot use interactive web terminals to interact with a runner. However, you can use a terminal to install dependencies and compile and debug code. For more information about configuring a workspace that supports interactive web terminals, see [remote development](../remote_development/index.md). + +## Access missed IDE notifications + +Actions that you take in the Web IDE trigger notifications that pop up in the lower-right corner. If you missed the pop-up: + +1. In the lower-right corner, select the bell icon (**{notifications}**). This opens a list of notifications. +1. Select the notification you want to access. diff --git a/qa/Gemfile b/qa/Gemfile index 6379f65db3d..e8d3a435766 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -11,7 +11,7 @@ gem 'capybara-screenshot', '~> 1.0.26' gem 'rake', '~> 13', '>= 13.0.6' gem 'rspec', '~> 3.12' # 4.9.1 drops Ruby 2.7 support. We can upgrade further after we drop Ruby 2.7 support. -gem 'selenium-webdriver', '= 4.9.1' +gem 'selenium-webdriver', '= 4.9.0' gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default gem 'rest-client', '~> 2.1.0' gem 'rspec-retry', '~> 0.6.2', require: 'rspec/retry' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index b8f4880a436..9b2b8acfb87 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -269,7 +269,7 @@ GEM sawyer (0.9.2) addressable (>= 2.3.5) faraday (>= 0.17.3, < 3) - selenium-webdriver (4.9.1) + selenium-webdriver (4.9.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) @@ -345,7 +345,7 @@ DEPENDENCIES rspec-retry (~> 0.6.2) rspec_junit_formatter (~> 0.6.0) ruby-debug-ide (~> 0.7.3) - selenium-webdriver (= 4.9.1) + selenium-webdriver (= 4.9.0) slack-notifier (~> 2.4) terminal-table (~> 3.0.2) warning (~> 1.3) diff --git a/qa/qa/page/file/edit.rb b/qa/qa/page/file/edit.rb index e66019279ce..ccf163cd1b4 100644 --- a/qa/qa/page/file/edit.rb +++ b/qa/qa/page/file/edit.rb @@ -8,10 +8,6 @@ module QA include Shared::CommitButton include Shared::Editor - view 'app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js' do - element :editor_toolbar_button, "qaSelector: 'editor_toolbar_button'" # rubocop:disable QA/ElementWithPattern - end - def has_markdown_preview?(component, content) within_element(:source_editor_preview_container) do has_css?(component, exact_text: content) @@ -23,10 +19,6 @@ module QA raise ElementNotFound, %("Couldn't find #{component} element with content '#{content}') end - - def click_editor_toolbar - click_element(:editor_toolbar_button) - end end end end diff --git a/spec/controllers/profiles/webauthn_registrations_controller_spec.rb b/spec/controllers/profiles/webauthn_registrations_controller_spec.rb index 0c475039963..949de9d0b90 100644 --- a/spec/controllers/profiles/webauthn_registrations_controller_spec.rb +++ b/spec/controllers/profiles/webauthn_registrations_controller_spec.rb @@ -10,11 +10,27 @@ RSpec.describe Profiles::WebauthnRegistrationsController do end describe '#destroy' do - it 'deletes the given webauthn registration' do - registration_to_delete = user.webauthn_registrations.first + let(:webauthn_id) { user.webauthn_registrations.first.id } - expect { delete :destroy, params: { id: registration_to_delete.id } }.to change { user.webauthn_registrations.count }.by(-1) - expect(response).to be_redirect + subject { delete :destroy, params: { id: webauthn_id } } + + it 'redirects to the profile two factor authentication page' do + subject + + expect(response).to redirect_to profile_two_factor_auth_path + end + + it 'destroys the webauthn registration' do + expect { subject }.to change { user.webauthn_registrations.count }.by(-1) + end + + it 'calls the Webauthn::DestroyService' do + service = double + + expect(Webauthn::DestroyService).to receive(:new).with(user, user, webauthn_id.to_s).and_return(service) + expect(service).to receive(:execute) + + subject end end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 351583b7ef6..448d937f30e 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -124,6 +124,8 @@ FactoryBot.define do transient { registrations_count { 5 } } after(:create) do |user, evaluator| + user.generate_otp_backup_codes! + create_list(:webauthn_registration, evaluator.registrations_count, user: user) end end diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 6e335871ed1..7474f416146 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -83,29 +83,20 @@ RSpec.describe 'Editing file blob', :js, feature_category: :projects do end context 'blob edit toolbar' do - toolbar_buttons = [ - "Add bold text", - "Add italic text", - "Add strikethrough text", - "Insert a quote", - "Insert code", - "Add a link", - "Add a bullet list", - "Add a numbered list", - "Add a checklist", - "Add a collapsible section", - "Add a table" - ] - - it "does not have any buttons" do - stub_feature_flags(source_editor_toolbar: true) - visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) - buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]') - expect(buttons.length).to eq(0) - end - - it "has defined set of toolbar buttons when the flag is off" do - stub_feature_flags(source_editor_toolbar: false) + def has_toolbar_buttons + toolbar_buttons = [ + "Add bold text", + "Add italic text", + "Add strikethrough text", + "Insert a quote", + "Insert code", + "Add a link", + "Add a bullet list", + "Add a numbered list", + "Add a checklist", + "Add a collapsible section", + "Add a table" + ] visit project_edit_blob_path(project, tree_join(branch, readme_file_path)) buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]') expect(buttons.length).to eq(toolbar_buttons.length) @@ -113,6 +104,16 @@ RSpec.describe 'Editing file blob', :js, feature_category: :projects do expect(buttons[i]['title']).to include(button_title) end end + + it "has defined set of toolbar buttons when the flag is on" do + stub_feature_flags(source_editor_toolbar: true) + has_toolbar_buttons + end + + it "has defined set of toolbar buttons when the flag is off" do + stub_feature_flags(source_editor_toolbar: false) + has_toolbar_buttons + end end context 'from blob file path' do diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js index b5944a52af7..1e592f435e4 100644 --- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js +++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js @@ -7,6 +7,7 @@ import { buildButton } from './helpers'; describe('Source Editor Toolbar button', () => { let wrapper; const defaultBtn = buildButton(); + const tertiaryBtnWithIcon = buildButton({ category: 'tertiary' }); const findButton = () => wrapper.findComponent(GlButton); @@ -41,6 +42,16 @@ describe('Source Editor Toolbar button', () => { const btn = findButton(); expect(btn.exists()).toBe(true); expect(btn.props()).toMatchObject(defaultProps); + expect(btn.text()).toBe('Foo Bar Button'); + }); + + it('does not render button for tertiary button with icon', () => { + createComponent({ + button: { + tertiaryBtnWithIcon, + }, + }); + expect(findButton().text()).toBe(''); }); it('renders a button based on the props passed', () => { diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js index b1b8173188c..70bc1dee0ee 100644 --- a/spec/frontend/editor/source_editor_extension_base_spec.js +++ b/spec/frontend/editor/source_editor_extension_base_spec.js @@ -19,12 +19,12 @@ describe('The basis for an Source Editor extension', () => { const findLine = (num) => { return document.querySelector(`.${EXTENSION_BASE_LINE_NUMBERS_CLASS}:nth-child(${num})`); }; - const generateLines = () => { + const generateFixture = () => { let res = ''; for (let line = 1, lines = 5; line <= lines; line += 1) { res += `<div class="${EXTENSION_BASE_LINE_NUMBERS_CLASS}">${line}</div>`; } - return res; + return `<span class="soft-wrap-toggle"></span>${res}`; }; const generateEventMock = ({ line = defaultLine, el = null } = {}) => { return { @@ -51,7 +51,7 @@ describe('The basis for an Source Editor extension', () => { }; beforeEach(() => { - setHTMLFixture(generateLines()); + setHTMLFixture(generateFixture()); event = generateEventMock(); }); @@ -156,12 +156,13 @@ describe('The basis for an Source Editor extension', () => { describe('toggleSoftwrap', () => { let instance; - beforeEach(() => { instance = createInstance(); instance.toolbar = toolbar; instance.use({ definition: SourceEditorExtension }); + + jest.spyOn(document.querySelector('.soft-wrap-toggle'), 'blur'); }); it.each` @@ -183,6 +184,7 @@ describe('The basis for an Source Editor extension', () => { expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, { selected: expectSelected, }); + expect(document.querySelector('.soft-wrap-toggle').blur).toHaveBeenCalled(); }, ); }); diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js index fb5fce92482..512b298bbbd 100644 --- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js +++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js @@ -206,9 +206,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => { it('removes the registered buttons from the toolbar', () => { expect(instance.toolbar.removeItems).not.toHaveBeenCalled(); instance.unuse(extension); - expect(instance.toolbar.removeItems).toHaveBeenCalledWith([ - EXTENSION_MARKDOWN_PREVIEW_ACTION_ID, - ]); + expect(instance.toolbar.removeItems).toHaveBeenCalledWith([]); }); it('disposes the modelChange listener and does not fetch preview on content changes', () => { diff --git a/spec/services/webauthn/destroy_service_spec.rb b/spec/services/webauthn/destroy_service_spec.rb new file mode 100644 index 00000000000..dd04601ccf0 --- /dev/null +++ b/spec/services/webauthn/destroy_service_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Webauthn::DestroyService, feature_category: :system_access do + let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) } + let(:current_user) { user } + + describe '#execute' do + let(:webauthn_id) { user.webauthn_registrations.first.id } + + subject { described_class.new(current_user, user, webauthn_id).execute } + + context 'with only one webauthn method enabled' do + context 'when another user is calling the service' do + context 'for a user without permissions' do + let(:current_user) { create(:user) } + + it 'does not destry the webauthn registration' do + expect { subject }.not_to change { user.webauthn_registrations.count } + end + + it 'does not remove the user backup codes' do + expect { subject }.not_to change { user.otp_backup_codes } + end + + it 'returns error' do + expect(subject[:status]).to eq(:error) + end + end + + context 'for an admin' do + it 'destroys the webauthn registration' do + expect { subject }.to change { user.webauthn_registrations.count }.by(-1) + end + + it 'removes the user backup codes' do + subject + + expect(user.otp_backup_codes).to be_nil + end + end + end + + context 'when current user is calling the service' do + context 'when there is also OTP enabled' do + before do + user.otp_required_for_login = true + user.otp_secret = User.generate_otp_secret(32) + user.otp_grace_period_started_at = Time.current + user.generate_otp_backup_codes! + user.save! + end + + it 'removes the webauth registrations' do + expect { subject }.to change { user.webauthn_registrations.count }.by(-1) + end + + it 'does not remove the user backup codes' do + expect { subject }.not_to change { user.otp_backup_codes } + end + end + end + end + + context 'with multiple webauthn methods enabled' do + before do + create(:webauthn_registration, user: user) + end + + it 'destroys the webauthn registration' do + expect { subject }.to change { user.webauthn_registrations.count }.by(-1) + end + + it 'does not remove the user backup codes' do + expect { subject }.not_to change { user.otp_backup_codes } + end + end + end +end |