diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-10 00:09:36 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-09-10 00:09:36 +0300 |
commit | cb25fb1a8e437d7a59f005eae401fdf40c8512bc (patch) | |
tree | 9b59813a1b544d8de76a22aa8e61118ba6992301 | |
parent | 764ecdaf4d65cb5730b7487ed8620bcf21e6b7d7 (diff) |
Add latest changes from gitlab-org/gitlab@master
-rw-r--r-- | app/controllers/application_controller.rb | 3 | ||||
-rw-r--r-- | app/helpers/gitlab_routing_helper.rb | 1 | ||||
-rw-r--r-- | app/helpers/routing/pseudonymization_helper.rb | 50 | ||||
-rw-r--r-- | config/feature_flags/ops/mask_page_urls.yml | 8 | ||||
-rw-r--r-- | doc/.vale/gitlab/spelling-exceptions.txt | 4 | ||||
-rw-r--r-- | doc/api/notification_settings.md | 2 | ||||
-rw-r--r-- | doc/development/documentation/redirects.md | 19 | ||||
-rw-r--r-- | doc/development/fe_guide/content_editor.md | 376 | ||||
-rw-r--r-- | doc/development/fe_guide/img/content_editor_highlevel_diagram.png | bin | 47794 -> 0 bytes | |||
-rw-r--r-- | doc/development/pipelines.md | 24 | ||||
-rw-r--r-- | spec/helpers/routing/pseudonymization_helper_spec.rb | 126 |
11 files changed, 524 insertions, 89 deletions
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 34bad74a9fc..02c963e432f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -62,7 +62,8 @@ class ApplicationController < ActionController::Base :bitbucket_import_enabled?, :bitbucket_import_configured?, :bitbucket_server_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?, - :manifest_import_enabled?, :phabricator_import_enabled? + :manifest_import_enabled?, :phabricator_import_enabled?, + :masked_page_url # Adds `no-store` to the DEFAULT_CACHE_CONTROL, to prevent security # concerns due to caching private data. diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb index 0f835e6881e..1be395437ea 100644 --- a/app/helpers/gitlab_routing_helper.rb +++ b/app/helpers/gitlab_routing_helper.rb @@ -16,6 +16,7 @@ module GitlabRoutingHelper include ::Routing::SnippetsHelper include ::Routing::WikiHelper include ::Routing::GraphqlHelper + include ::Routing::PseudonymizationHelper included do Gitlab::Routing.includes_helpers(self) end diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb new file mode 100644 index 00000000000..b83a2b30d06 --- /dev/null +++ b/app/helpers/routing/pseudonymization_helper.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module Routing + module PseudonymizationHelper + def masked_page_url + return unless Feature.enabled?(:mask_page_urls, type: :ops) + + mask_params(Rails.application.routes.recognize_path(request.original_fullpath)) + end + + private + + def mask_params(request_params) + return if request_params[:action] == 'new' + + namespace_type = request_params[:controller].split('/')[1] + + namespace_type.present? ? url_with_namespace_type(request_params, namespace_type) : url_without_namespace_type(request_params) + end + + def url_without_namespace_type(request_params) + masked_url = "#{request.protocol}#{request.host_with_port}/" + + masked_url += case request_params[:controller] + when 'groups' + "namespace:#{group.id}/" + when 'projects' + "namespace:#{project.namespace.id}/project:#{project.id}/" + when 'root' + '' + end + + masked_url + end + + def url_with_namespace_type(request_params, namespace_type) + masked_url = "#{request.protocol}#{request.host_with_port}/" + + if request_params.has_key?(:project_id) + masked_url += "namespace:#{project.namespace.id}/project:#{project.id}/-/#{namespace_type}/" + end + + if request_params.has_key?(:id) + masked_url += namespace_type == 'blob' ? ':repository_path' : request_params[:id] + end + + masked_url + end + end +end diff --git a/config/feature_flags/ops/mask_page_urls.yml b/config/feature_flags/ops/mask_page_urls.yml new file mode 100644 index 00000000000..a752d1c8796 --- /dev/null +++ b/config/feature_flags/ops/mask_page_urls.yml @@ -0,0 +1,8 @@ +--- +name: mask_page_urls +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69448 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340181 +milestone: '14.3' +type: ops +group: group::product intelligence +default_enabled: false diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index f3d0ce9c093..d397a436ff9 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -161,6 +161,10 @@ deprovisions dequarantine dequarantined dequarantining +deserialization +deserialize +deserializers +deserializes DevOps Dhall disambiguates diff --git a/doc/api/notification_settings.md b/doc/api/notification_settings.md index 390ba7dbd79..4b70a643263 100644 --- a/doc/api/notification_settings.md +++ b/doc/api/notification_settings.md @@ -199,7 +199,7 @@ Example responses: } ``` -Users on GitLab [Ultimate](https://about.gitlab.com/pricing/) also see the `new_epic` +Users on [GitLab Ultimate](https://about.gitlab.com/pricing/) also see the `new_epic` parameter: ```json diff --git a/doc/development/documentation/redirects.md b/doc/development/documentation/redirects.md index debb67976fb..4fd854a90c1 100644 --- a/doc/development/documentation/redirects.md +++ b/doc/development/documentation/redirects.md @@ -23,11 +23,20 @@ There are two types of redirects: - [GitLab Pages redirects](../../user/project/pages/redirects.md), for users who view the docs on [`docs.gitlab.com`](https://docs.gitlab.com). -The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md) -to regularly update and [clean up the redirects](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/raketasks.md#clean-up-redirects). -If you're a contributor, you may add a new redirect, but you don't need to delete -the old ones. This process is automatic and handled by the Technical -Writing team. + The Technical Writing team manages the [process](https://gitlab.com/gitlab-org/technical-writing/-/blob/main/.gitlab/issue_templates/tw-monthly-tasks.md) + to regularly update and [clean up the redirects](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/raketasks.md#clean-up-redirects). + If you're a contributor, you may add a new redirect, but you don't need to delete + the old ones. This process is automatic and handled by the Technical + Writing team. + +NOTE: +If the old page you're renaming doesn't exist in a stable branch, skip the +following steps and ask a Technical Writer to add the redirect in +[`redirects.yaml`](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/content/_data/redirects.yaml). +For example, if you add a new page on the 3rd of the month and then rename it before it gets +added in the stable branch on the 18th, the old page will never be part of the internal `/help`. +In that case, you can jump straight to the +[Pages redirect](https://gitlab.com/gitlab-org/gitlab-docs/-/blob/main/doc/maintenance.md#pages-redirects). To add a redirect: diff --git a/doc/development/fe_guide/content_editor.md b/doc/development/fe_guide/content_editor.md index 6cf4076bf83..956e7d0d56e 100644 --- a/doc/development/fe_guide/content_editor.md +++ b/doc/development/fe_guide/content_editor.md @@ -4,10 +4,10 @@ group: Editor info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments --- -# Content Editor **(FREE)** +# Content Editor development guidelines **(FREE)** The Content Editor is a UI component that provides a WYSIWYG editing -experience for [GitLab Flavored Markdown](../../user/markdown.md) (GFM) in the GitLab application. +experience for [GitLab Flavored Markdown](../../user/markdown.md) in the GitLab application. It also serves as the foundation for implementing Markdown-focused editors that target other engines, like static site generators. @@ -16,103 +16,339 @@ to build the Content Editor. These frameworks provide a level of abstraction on the native [`contenteditable`](https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Editable_content) web technology. -## Architecture remarks +## Usage guide -At a high level, the Content Editor: +Follow these instructions to include the Content Editor in a feature. -- Imports arbitrary Markdown. -- Renders it in a HTML editing area. -- Exports it back to Markdown with changes introduced by the user. +1. [Include the Content Editor component](#include-the-content-editor-component). +1. [Set and get Markdown](#set-and-get-markdown). +1. [Listen for changes](#listen-for-changes). -The Content Editor relies on the -[Markdown API endpoint](../../api/markdown.md) to transform Markdown -into HTML. It sends the Markdown input to the REST API and displays the API's -HTML output in the editing area. The editor exports the content back to Markdown -using a client-side library that serializes editable documents into Markdown. +### Include the Content Editor component -![Content Editor high level diagram](img/content_editor_highlevel_diagram.png) +Import the `ContentEditor` Vue component. We recommend using asynchronous named imports to +take advantage of caching, as the ContentEditor is a big dependency. -Check the [Content Editor technical design document](https://docs.google.com/document/d/1fKOiWpdHned4KOLVOOFYVvX1euEjMP5rTntUhpapdBg) -for more information about the design decisions that drive the development of the editor. - -**NOTE**: We also designed the Content Editor to be extensible. We intend to provide -more information about extension development for supporting new types of content in upcoming -milestones. - -## GitLab Flavored Markdown support +```html +<script> +export default { + components: { + ContentEditor: () => + import( + /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue' + ), + }, + // rest of the component definition +} +</script> +``` -The [GitLab Flavored Markdown](../../user/markdown.md) extends -the [CommonMark specification](https://spec.commonmark.org/0.29/) with support for a -variety of content types like diagrams, math expressions, and tables. Supporting -all GitLab Flavored Markdown content types in the Content Editor is a work in progress. For -the status of the ongoing development for CommonMark and GitLab Flavored Markdown support, read: +The Content Editor requires two properties: -- [Basic Markdown formatting extensions](https://gitlab.com/groups/gitlab-org/-/epics/5404) epic. -- [GitLab Flavored Markdown extensions](https://gitlab.com/groups/gitlab-org/-/epics/5438) epic. +- `renderMarkdown` is an asynchronous function that returns the response (String) of invoking the +[Markdown API](../../api/markdown.md). +- `uploadsPath` is a URL that points to a [GitLab upload service](../uploads.md#upload-encodings) + with `multipart/form-data` support. -## Usage +See the [`WikiForm.vue`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue#L207) +component for a production example of these two properties. -To include the Content Editor in your feature, import the `createContentEditor` factory -function and the `ContentEditor` Vue component. `createContentEditor` sets up an instance -of [tiptap's Editor class](https://www.tiptap.dev/api/editor/) with all the necessary -extensions to support editing GitLab Flavored Markdown content. It also creates -a Markdown serializer that allows exporting tiptap's document format to Markdown. +### Set and get Markdown -`createContentEditor` requires a `renderMarkdown` parameter invoked -by the editor every time it needs to convert Markdown to HTML. The Content Editor -does not provide a default value for this function yet. +The `ContentEditor` Vue component doesn't implement Vue data binding flow (`v-model`) +because setting and getting Markdown are expensive operations. Data binding would +trigger these operations every time the user interacts with the component. -**NOTE**: The Content Editor is in an early development stage. Usage and development -guidelines are subject to breaking changes in the upcoming months. +Instead, you should obtain an instance of the `ContentEditor` class by listening to the +`initialized` event: ```html <script> -import { GlButton } from '@gitlab/ui'; -import { createContentEditor, ContentEditor } from '~/content_editor'; -import { __ } from '~/locale'; import createFlash from '~/flash'; +import { __ } from '~/locale'; export default { - components: { - ContentEditor, - GlButton, + methods: { + async loadInitialContent(contentEditor) { + this.contentEditor = contentEditor; + + try { + await this.contentEditor.setSerializedContent(this.content); + } catch (e) { + createFlash(__('Could not load initial document')); + } + }, + submitChanges() { + const markdown = this.contentEditor.getSerializedContent(); + }, }, +}; +</script> +<template> + <content-editor + :render-markdown="renderMarkdown" + :uploads-path="pageInfo.uploadsPath" + @initialized="loadInitialContent" + /> +</template> +``` + +### Listen for changes + +You can still react to changes in the Content Editor. Reacting to changes helps +you know if the document is empty or dirty. Use the `@change` event handler for +this purpose. + +```html +<script> +export default { data() { return { - contentEditor: null, - } + empty: false, + }; }, - created() { - this.contentEditor = createContentEditor({ - renderMarkdown: (markdown) => Api.markdown({ text: markdown }), - }); - - try { - await this.contentEditor.setSerializedContent(this.content); - } catch (e) { - createFlash({ - message: __('There was an error loading content in the editor'), error: e - }); + methods: { + handleContentEditorChange({ empty }) { + this.empty = empty; } }, +}; +</script> +<template> + <div> + <content-editor + :render-markdown="renderMarkdown" + :uploads-path="pageInfo.uploadsPath" + @initialized="loadInitialContent" + @change="handleContentEditorChange" + /> + <gl-button :disabled="empty" @click="submitChanges"> + {{ __('Submit changes') }} + </gl-button> + </div> +</template> +``` + +## Implementation guide + +The Content Editor is composed of three main layers: + +- **The editing tools UI**, like the toolbar and the table structure editor. They + display the editor's state and mutate it by dispatching commands. +- **The Tiptap Editor object** manages the editor's state, + and exposes business logic as commands executed by the editing tools UI. +- **The Markdown serializer** transforms a Markdown source string into a ProseMirror + document and vice versa. + +### Editing tools UI + +The editing tools UI are Vue components that display the editor's state and +dispatch [commands](https://www.tiptap.dev/api/commands/#commands) to mutate it. +They are located in the `~/content_editor/components` directory. For example, +the **Bold** toolbar button displays the editor's state by becoming active when +the user selects bold text. This button also dispatches the `toggleBold` command +to format text as bold: + +```mermaid +sequenceDiagram + participant A as Editing tools UI + participant B as Tiptap object + A->>B: queries state/dispatches commands + B--)A: notifies state changes +``` + +#### Node views + +We implement [node views](https://www.tiptap.dev/guide/node-views/vue/#node-views-with-vue) +to provide inline editing tools for some content types, like tables and images. Node views +allow separating the presentation of a content type from its +[model](https://prosemirror.net/docs/guide/#doc.data_structures). Using a Vue component in +the presentation layer enables sophisticated editing experiences in the Content Editor. +Node views are located in `~/content_editor/components/wrappers`. + +#### Dispatch commands + +You can inject the Tiptap Editor object to Vue components to dispatch +commands. + +NOTE: +Do not implement logic that changes the editor's +state in Vue components. Encapsulate this logic in commands, and dispatch +the command from the component's methods. + +```html +<script> +export default { + inject: ['tiptapEditor'], methods: { - async save() { - await Api.updateContent({ - content: this.contentEditor.getSerializedContent(), - }); + execute() { + //Incorrect + const { state, view } = this.tiptapEditor.state; + const { tr, schema } = state; + tr.addMark(state.selection.from, state.selection.to, null, null, schema.mark('bold')); + + // Correct + this.tiptapEditor.chain().toggleBold().focus().run(); + }, + } +}; +</script> +<template> +``` + +#### Query editor's state + +Use the `EditorStateObserver` renderless component to react to changes in the +editor's state, such as when the document or the selection changes. You can listen to +the following events: + +- `docUpdate` +- `selectionUpdate` +- `transaction` +- `focus` +- `blur` +- `error`. + +Learn more about these events in [Tiptap's event guide](https://www.tiptap.dev/api/events/). + +```html +<script> +// Parts of the code has been hidden for efficiency +import EditorStateObserver from './editor_state_observer.vue'; + +export default { + components: { + EditorStateObserver, + }, + data() { + return { + error: null, + }; + }, + methods: { + displayError({ message }) { + this.error = message; + }, + dismissError() { + this.error = null; }, }, }; </script> <template> - <div> - <content-editor :content-editor="contentEditor" /> - <gl-button @click="save()">Save</gl-button> - </div> + <editor-state-observer @error="displayError"> + <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError"> + {{ error }} + </gl-alert> + </editor-state-observer> </template> ``` -Call `setSerializedContent` to set initial Markdown in the Editor. This method is -asynchronous because it makes an API request to render the Markdown input. -`getSerializedContent` returns a Markdown string that represents the serialized -version of the editable document. +### The Tiptap editor object + +The Tiptap [Editor](https://www.tiptap.dev/api/editor) class manages +the editor's state and encapsulates all the business logic that powers +the Content Editor. The Content Editor constructs a new instance of this class and +provides all the necessary extensions to support +[GitLab Flavored Markdown](../../user/markdown.md). + +#### Implement new extensions + +Extensions are the building blocks of the Content Editor. You can learn how to implement +new ones by reading [Tiptap's guide](https://www.tiptap.dev/guide/custom-extensions). +We recommend checking the list of built-in [nodes](https://www.tiptap.dev/api/nodes) and +[marks](https://www.tiptap.dev/api/marks) before implementing a new extension +from scratch. + +Store the Content Editor extensions in the `~/content_editor/extensions` directory. +When using a Tiptap's built-in extension, wrap it in a ES6 module inside this directory: + +```javascript +export { Bold as default } from '@tiptap/extension-bold'; +``` + +Use the `extend` method to customize the Extension's behavior: + +```javascript +import { HardBreak } from '@tiptap/extension-hard-break'; + +export default HardBreak.extend({ + addKeyboardShortcuts() { + return { + 'Shift-Enter': () => this.editor.commands.setHardBreak(), + }; + }, +}); +``` + +#### Register extensions + +Register the new extension in `~/content_editor/services/create_content_editor.js`. Import +the extension module and add it to the `builtInContentEditorExtensions` array: + +```javascript +import Emoji from '../extensions/emoji'; + +const builtInContentEditorExtensions = [ + Code, + CodeBlockHighlight, + Document, + Dropcursor, + Emoji, + // Other extensions +``` + +### The Markdown serializer + +The Markdown Serializer transforms a Markdown String to a +[ProseMirror document](https://prosemirror.net/docs/guide/#doc) and vice versa. + +#### Deserialization + +Deserialization is the process of converting Markdown to a ProseMirror document. +We take advantage of ProseMirror's +[HTML parsing and serialization capabilities](https://prosemirror.net/docs/guide/#schema.serialization_and_parsing) +by first rendering the Markdown as HTML using the [Markdown API endpoint](../../api/markdown.md): + +```mermaid +sequenceDiagram + participant A as Content Editor + participant E as Tiptap Object + participant B as Markdown Serializer + participant C as Markdown API + participant D as ProseMirror Parser + A->>B: deserialize(markdown) + B->>C: render(markdown) + C-->>B: html + B->>D: to document(html) + D-->>A: document + A->>E: setContent(document) +``` + +Deserializers live in the extension modules. Read Tiptap's +[parseHTML](https://www.tiptap.dev/guide/custom-extensions#parse-html) and +[addAttributes](https://www.tiptap.dev/guide/custom-extensions#attributes) documentation to +learn how to implement them. Titap's API is a wrapper around ProseMirror's +[schema spec API](https://prosemirror.net/docs/ref/#model.SchemaSpec). + +#### Serialization + +Serialization is the process of converting a ProseMirror document to Markdown. The Content +Editor uses [`prosemirror-markdown`](https://github.com/ProseMirror/prosemirror-markdown) +to serialize documents. We recommend reading the +[MarkdownSerializer](https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializer) +and [MarkdownSerializerState](https://github.com/ProseMirror/prosemirror-markdown#class-markdownserializerstate) +classes documentation before implementing a serializer: + +```mermaid +sequenceDiagram + participant A as Content Editor + participant B as Markdown Serializer + participant C as ProseMirror Markdown + A->>B: serialize(document) + B->>C: serialize(document, serializers) + C-->>A: markdown string +``` + +`prosemirror-markdown` requires implementing a serializer function for each content type supported +by the Content Editor. We implement serializers in `~/content_editor/services/markdown_serializer.js`. diff --git a/doc/development/fe_guide/img/content_editor_highlevel_diagram.png b/doc/development/fe_guide/img/content_editor_highlevel_diagram.png Binary files differdeleted file mode 100644 index 73a71cf5843..00000000000 --- a/doc/development/fe_guide/img/content_editor_highlevel_diagram.png +++ /dev/null diff --git a/doc/development/pipelines.md b/doc/development/pipelines.md index bd08379ca17..226c8fe9fda 100644 --- a/doc/development/pipelines.md +++ b/doc/development/pipelines.md @@ -64,7 +64,7 @@ graph LR click 1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8100542&udv=0" 1-2["docs-lint markdown (1.5 minutes)"]; click 1-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10224335&udv=0" - 1-3["docs-lint links (6 minutes)"]; + 1-3["docs-lint links (5 minutes)"]; click 1-3 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=8356757&udv=0" 1-4["ui-docs-links lint (2.5 minutes)"]; click 1-4 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=10823717&udv=1020379" @@ -104,7 +104,7 @@ graph RL; 1-18["kubesec-sast"]; 1-19["nodejs-scan-sast"]; 1-20["secrets-sast"]; - 1-21["static-analysis (30 minutes)"]; + 1-21["static-analysis (14 minutes)"]; click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0" class 1-3 criticalPath; @@ -123,7 +123,7 @@ graph RL; 2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6; end - 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (11 minutes)"]; + 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"]; class 2_2-2 criticalPath; click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0" 2_2-4["memory-on-boot (3.5 minutes)"]; @@ -152,14 +152,14 @@ graph RL; click 2_5-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations" end - 3_1-1["jest (16 minutes)"]; + 3_1-1["jest (14.5 minutes)"]; class 3_1-1 criticalPath; click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0" subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`"; 3_1-1 --> 2_2-2; end - 3_2-1["rspec:coverage (5.3 minutes)"]; + 3_2-1["rspec:coverage (4 minutes)"]; subgraph "Depends on `rspec` jobs"; 3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1; click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0" @@ -206,7 +206,7 @@ graph RL; 1-18["kubesec-sast"]; 1-19["nodejs-scan-sast"]; 1-20["secrets-sast"]; - 1-21["static-analysis (30 minutes)"]; + 1-21["static-analysis (14 minutes)"]; click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0" class 1-3 criticalPath; @@ -226,7 +226,7 @@ graph RL; 2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6; end - 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (11 minutes)"]; + 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"]; class 2_2-2 criticalPath; click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0" 2_2-4["memory-on-boot (3.5 minutes)"]; @@ -263,14 +263,14 @@ graph RL; click 2_6-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914314&udv=0" end - 3_1-1["jest (16 minutes)"]; + 3_1-1["jest (14.5 minutes)"]; class 3_1-1 criticalPath; click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0" subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`"; 3_1-1 --> 2_2-2; end - 3_2-1["rspec:coverage (5.3 minutes)"]; + 3_2-1["rspec:coverage (4 minutes)"]; subgraph "Depends on `rspec` jobs"; 3_2-1 -.->|"(don't use needs because of limitations)"| 2_5-1; click 3_2-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7248745&udv=0" @@ -283,7 +283,7 @@ graph RL; click 4_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910777&udv=0" end - 3_3-1["review-deploy (10.5 minutes)"]; + 3_3-1["review-deploy (9 minutes)"]; subgraph "Played by `review-build-cng`"; 3_3-1 --> 2_6-1; class 3_3-1 criticalPath; @@ -332,7 +332,7 @@ graph RL; 1-18["kubesec-sast"]; 1-19["nodejs-scan-sast"]; 1-20["secrets-sast"]; - 1-21["static-analysis (30 minutes)"]; + 1-21["static-analysis (14 minutes)"]; click 1-21 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914471&udv=0" class 1-5 criticalPath; @@ -350,7 +350,7 @@ graph RL; class 2_3-1 criticalPath; end - 2_4-1["package-and-qa (140 minutes)"]; + 2_4-1["package-and-qa (113 minutes)"]; subgraph "Needs `build-qa-image` & `build-assets-image`"; 2_4-1 --> 1-2 & 2_3-1; class 2_4-1 criticalPath; diff --git a/spec/helpers/routing/pseudonymization_helper_spec.rb b/spec/helpers/routing/pseudonymization_helper_spec.rb new file mode 100644 index 00000000000..f47e8185f07 --- /dev/null +++ b/spec/helpers/routing/pseudonymization_helper_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Routing::PseudonymizationHelper do + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:issue) { create(:issue, project: project) } + + let(:merge_request) { create(:merge_request, source_project: project) } + + before do + stub_feature_flags(mask_page_urls: true) + allow(helper).to receive(:group).and_return(group) + allow(helper).to receive(:project).and_return(project) + end + + shared_examples 'masked url' do + it 'generates masked page url' do + expect(helper.masked_page_url).to eq(masked_url) + end + end + + describe 'when url has params to mask' do + context 'with controller for MR' do + let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/merge_requests/#{merge_request.id}" } + + before do + allow(Rails.application.routes).to receive(:recognize_path).and_return({ + controller: "projects/merge_requests", + action: "show", + namespace_id: group.name, + project_id: project.name, + id: merge_request.id.to_s + }) + end + + it_behaves_like 'masked url' + end + + context 'with controller for issue' do + let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/issues/#{issue.id}" } + + before do + allow(Rails.application.routes).to receive(:recognize_path).and_return({ + controller: "projects/issues", + action: "show", + namespace_id: group.name, + project_id: project.name, + id: issue.id.to_s + }) + end + + it_behaves_like 'masked url' + end + + context 'with controller for groups with subgroups and project' do + let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/project:#{project.id}/"} + + before do + allow(helper).to receive(:group).and_return(subgroup) + allow(helper.project).to receive(:namespace).and_return(subgroup) + allow(Rails.application.routes).to receive(:recognize_path).and_return({ + controller: 'projects', + action: 'show', + namespace_id: subgroup.name, + id: project.name + }) + end + + it_behaves_like 'masked url' + end + + context 'with controller for groups and subgroups' do + let(:masked_url) { "http://test.host/namespace:#{subgroup.id}/"} + + before do + allow(helper).to receive(:group).and_return(subgroup) + allow(Rails.application.routes).to receive(:recognize_path).and_return({ + controller: 'groups', + action: 'show', + id: subgroup.name + }) + end + + it_behaves_like 'masked url' + end + + context 'with controller for blob with file path' do + let(:masked_url) { "http://test.host/namespace:#{group.id}/project:#{project.id}/-/blob/:repository_path" } + + before do + allow(Rails.application.routes).to receive(:recognize_path).and_return({ + controller: 'projects/blob', + action: 'show', + namespace_id: group.name, + project_id: project.name, + id: 'master/README.md' + }) + end + + it_behaves_like 'masked url' + end + end + + describe 'when url has no params to mask' do + let(:root_url) { 'http://test.host/' } + + context 'returns root url' do + it 'masked_page_url' do + expect(helper.masked_page_url).to eq(root_url) + end + end + end + + describe 'when feature flag is disabled' do + before do + stub_feature_flags(mask_page_urls: false) + end + + it 'returns nil' do + expect(helper.masked_page_url).to be_nil + end + end +end |