Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
764ecdaf4d
commit
cb25fb1a8e
11 changed files with 534 additions and 99 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
50
app/helpers/routing/pseudonymization_helper.rb
Normal file
50
app/helpers/routing/pseudonymization_helper.rb
Normal file
|
@ -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
|
8
config/feature_flags/ops/mask_page_urls.yml
Normal file
8
config/feature_flags/ops/mask_page_urls.yml
Normal file
|
@ -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
|
|
@ -161,6 +161,10 @@ deprovisions
|
|||
dequarantine
|
||||
dequarantined
|
||||
dequarantining
|
||||
deserialization
|
||||
deserialize
|
||||
deserializers
|
||||
deserializes
|
||||
DevOps
|
||||
Dhall
|
||||
disambiguates
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
|
||||
- [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.
|
||||
|
||||
## Usage
|
||||
|
||||
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.
|
||||
|
||||
`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.
|
||||
|
||||
**NOTE**: The Content Editor is in an early development stage. Usage and development
|
||||
guidelines are subject to breaking changes in the upcoming months.
|
||||
Import the `ContentEditor` Vue component. We recommend using asynchronous named imports to
|
||||
take advantage of caching, as the ContentEditor is a big dependency.
|
||||
|
||||
```html
|
||||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { createContentEditor, ContentEditor } from '~/content_editor';
|
||||
import { __ } from '~/locale';
|
||||
import createFlash from '~/flash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentEditor,
|
||||
GlButton,
|
||||
ContentEditor: () =>
|
||||
import(
|
||||
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
|
||||
),
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
contentEditor: null,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.contentEditor = createContentEditor({
|
||||
renderMarkdown: (markdown) => Api.markdown({ text: markdown }),
|
||||
});
|
||||
// rest of the component definition
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
try {
|
||||
await this.contentEditor.setSerializedContent(this.content);
|
||||
} catch (e) {
|
||||
createFlash({
|
||||
message: __('There was an error loading content in the editor'), error: e
|
||||
});
|
||||
}
|
||||
},
|
||||
The Content Editor requires two properties:
|
||||
|
||||
- `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.
|
||||
|
||||
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.
|
||||
|
||||
### Set and get Markdown
|
||||
|
||||
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.
|
||||
|
||||
Instead, you should obtain an instance of the `ContentEditor` class by listening to the
|
||||
`initialized` event:
|
||||
|
||||
```html
|
||||
<script>
|
||||
import createFlash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
methods: {
|
||||
async save() {
|
||||
await Api.updateContent({
|
||||
content: this.contentEditor.getSerializedContent(),
|
||||
});
|
||||
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 {
|
||||
empty: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleContentEditorChange({ empty }) {
|
||||
this.empty = empty;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<content-editor :content-editor="contentEditor" />
|
||||
<gl-button @click="save()">Save</gl-button>
|
||||
<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>
|
||||
```
|
||||
|
||||
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.
|
||||
## 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: {
|
||||
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>
|
||||
<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>
|
||||
```
|
||||
|
||||
### 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`.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 47 KiB |
|
@ -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;
|
||||
|
|
126
spec/helpers/routing/pseudonymization_helper_spec.rb
Normal file
126
spec/helpers/routing/pseudonymization_helper_spec.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue