Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-09-10 00:09:36 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-09-10 00:09:36 +0300
commitcb25fb1a8e437d7a59f005eae401fdf40c8512bc (patch)
tree9b59813a1b544d8de76a22aa8e61118ba6992301
parent764ecdaf4d65cb5730b7487ed8620bcf21e6b7d7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/helpers/gitlab_routing_helper.rb1
-rw-r--r--app/helpers/routing/pseudonymization_helper.rb50
-rw-r--r--config/feature_flags/ops/mask_page_urls.yml8
-rw-r--r--doc/.vale/gitlab/spelling-exceptions.txt4
-rw-r--r--doc/api/notification_settings.md2
-rw-r--r--doc/development/documentation/redirects.md19
-rw-r--r--doc/development/fe_guide/content_editor.md376
-rw-r--r--doc/development/fe_guide/img/content_editor_highlevel_diagram.pngbin47794 -> 0 bytes
-rw-r--r--doc/development/pipelines.md24
-rw-r--r--spec/helpers/routing/pseudonymization_helper_spec.rb126
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
deleted file mode 100644
index 73a71cf5843..00000000000
--- a/doc/development/fe_guide/img/content_editor_highlevel_diagram.png
+++ /dev/null
Binary files differ
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