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>2022-04-27 03:08:37 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-27 03:08:37 +0300
commitb9a77f953c360d82777f8f1652b5cdf369779605 (patch)
tree70dba8351315c44e1078d0d144da77aa7ecca12e
parent413c6d807b260f6086fdd05c01618f459335d465 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js475
-rw-r--r--app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js87
-rw-r--r--app/assets/javascripts/lib/gfm/index.js4
-rw-r--r--app/controllers/admin/background_migrations_controller.rb1
-rw-r--r--app/models/project_pages_metadatum.rb2
-rw-r--r--db/docs/project_feature_usages.yml4
-rw-r--r--db/docs/taggings.yml5
-rw-r--r--db/docs/tags.yml3
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/monitoring/performance/index.md1
-rw-r--r--doc/administration/monitoring/performance/request_profiling.md44
-rw-r--r--doc/development/documentation/styleguide/index.md1
-rw-r--r--doc/development/performance.md1
-rw-r--r--doc/user/admin_area/index.md6
-rw-r--r--package.json4
-rw-r--r--spec/frontend/content_editor/services/remark_markdown_deserializer_spec.js309
-rw-r--r--spec/frontend/lib/gfm/index_spec.js6
-rw-r--r--yarn.lock17
19 files changed, 902 insertions, 71 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 7c018f78928..39359d7b827 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -77,7 +77,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "v5.5.0"
+ GITLAB_HELM_CHART_REF: "v5.10.0"
environment:
name: review/${CI_COMMIT_REF_SLUG}${FREQUENCY}
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
diff --git a/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
new file mode 100644
index 00000000000..b6a3e0bc26a
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/hast_to_prosemirror_converter.js
@@ -0,0 +1,475 @@
+/**
+ * This module implements a function that converts a Hast Abstract
+ * Syntax Tree (AST) to a ProseMirror document.
+ *
+ * It is based on the prosemirror-markdown’s from_markdown module
+ * https://github.com/ProseMirror/prosemirror-markdown/blob/master/src/from_markdown.js.
+ *
+ * It deviates significantly from the original because
+ * prosemirror-markdown supports converting an markdown-it AST instead of a
+ * HAST one. It also adds sourcemap attributes automatically to every
+ * ProseMirror node and mark created during the conversion process.
+ *
+ * We recommend becoming familiar with HAST and ProseMirror documents to
+ * facilitate the understanding of the behavior implemented in this module.
+ *
+ * Unist syntax tree documentation: https://github.com/syntax-tree/unist
+ * Hast tree documentation: https://github.com/syntax-tree/hast
+ * ProseMirror document documentation: https://prosemirror.net/docs/ref/#model.Document_Structure
+ * visit-parents documentation: https://github.com/syntax-tree/unist-util-visit-parents
+ */
+
+import { Mark } from 'prosemirror-model';
+import { visitParents } from 'unist-util-visit-parents';
+import { toString } from 'hast-util-to-string';
+import { isFunction } from 'lodash';
+
+/**
+ * Merges two ProseMirror text nodes if both text nodes
+ * have the same set of marks.
+ *
+ * @param {ProseMirror.Node} a first ProseMirror node
+ * @param {ProseMirror.Node} b second ProseMirror node
+ * @returns {model.Node} A new text node that results from combining
+ * the text of the two text node parameters or null.
+ */
+function maybeMerge(a, b) {
+ if (a && a.isText && b && b.isText && Mark.sameSet(a.marks, b.marks)) {
+ return a.withText(a.text + b.text);
+ }
+
+ return null;
+}
+
+/**
+ * Creates an object that contains sourcemap position information
+ * included in a Hast Abstract Syntax Tree. The Content
+ * Editor uses the sourcemap information to restore the
+ * original source of a node when the user doesn’t change it.
+ *
+ * Unist syntax tree documentation: https://github.com/syntax-tree/unist
+ * Hast node documentation: https://github.com/syntax-tree/hast
+ *
+ * @param {HastNode} hastNode A Hast node
+ * @param {String} source Markdown source file
+ *
+ * @returns It returns an object with the following attributes:
+ *
+ * - sourceMapKey: A string that uniquely identifies what is
+ * the position of the hast node in the Markdown source file.
+ * - sourceMarkdown: A node’s original Markdown source extrated
+ * from the Markdown source file.
+ */
+function createSourceMapAttributes(hastNode, source) {
+ const { position } = hastNode;
+
+ return {
+ sourceMapKey: `${position.start.offset}:${position.end.offset}`,
+ sourceMarkdown: source.substring(position.start.offset, position.end.offset),
+ };
+}
+
+/**
+ * Compute ProseMirror node’s attributes from a Hast node.
+ * By default, this function includes sourcemap position
+ * information in the object returned.
+ *
+ * Other attributes are retrieved by invoking a getAttrs
+ * function provided by the ProseMirror node factory spec.
+ *
+ * @param {*} proseMirrorNodeSpec ProseMirror node spec object
+ * @param {HastNode} hastNode A hast node
+ * @param {Array<HastNode>} hastParents All the ancestors of the hastNode
+ * @param {String} source Markdown source file’s content
+ *
+ * @returns An object that contains a ProseMirror node’s attributes
+ */
+function getAttrs(proseMirrorNodeSpec, hastNode, hastParents, source) {
+ const { getAttrs: specGetAttrs } = proseMirrorNodeSpec;
+
+ return {
+ ...createSourceMapAttributes(hastNode, source),
+ ...(isFunction(specGetAttrs) ? specGetAttrs(hastNode, hastParents, source) : {}),
+ };
+}
+
+/**
+ * Keeps track of the Hast -> ProseMirror conversion process.
+ *
+ * When the `openNode` method is invoked, it adds the node to a stack
+ * data structure. When the `closeNode` method is invoked, it removes the
+ * last element from the Stack, creates a ProseMirror node, and adds that
+ * ProseMirror node to the previous node in the Stack.
+ *
+ * For example, given a Hast tree with three levels of nodes:
+ *
+ * - blockquote
+ * - paragraph
+ * - text
+ *
+ * 3. text
+ * 2. paragraph
+ * 1. blockquote
+ *
+ * Calling `closeNode` will fold the text node into paragraph. A 2nd
+ * call to this method will fold "paragraph" into "blockquote".
+ *
+ * Mark state
+ *
+ * When the `openMark` method is invoked, this class adds the Mark to a `MarkSet`
+ * object. When a text node is added, it assigns all the opened marks to that text
+ * node and cleans the marks. It takes care of merging text nodes with the same
+ * set of marks as well.
+ */
+class HastToProseMirrorConverterState {
+ constructor() {
+ this.stack = [];
+ this.marks = Mark.none;
+ }
+
+ /**
+ * Gets the first element of the node stack
+ */
+ get top() {
+ return this.stack[this.stack.length - 1];
+ }
+
+ /**
+ * Detects if the node stack is empty
+ */
+ get empty() {
+ return this.stack.length === 0;
+ }
+
+ /**
+ * Creates a text node and adds it to
+ * the top node in the stack.
+ *
+ * It applies the marks stored temporarily
+ * by calling the `addMark` method. After
+ * the text node is added, it clears the mark
+ * set afterward.
+ *
+ * If the top block node has a text
+ * node with the same set of marks as the
+ * text node created, this method merges
+ * both text nodes
+ *
+ * @param {ProseMirror.Schema} schema ProseMirror schema
+ * @param {String} text Text
+ * @returns
+ */
+ addText(schema, text) {
+ if (!text) return;
+ const nodes = this.top.content;
+ const last = nodes[nodes.length - 1];
+ const node = schema.text(text, this.marks);
+ const merged = maybeMerge(last, node);
+
+ if (last && merged) {
+ nodes[nodes.length - 1] = merged;
+ } else {
+ nodes.push(node);
+ }
+
+ this.closeMarks();
+ }
+
+ /**
+ * Adds a mark to the set of marks stored temporarily
+ * until addText is called.
+ * @param {*} markType
+ * @param {*} attrs
+ */
+ openMark(markType, attrs) {
+ this.marks = markType.create(attrs).addToSet(this.marks);
+ }
+
+ /**
+ * Empties the temporary Mark set.
+ */
+ closeMarks() {
+ this.marks = Mark.none;
+ }
+
+ /**
+ * Adds a node to the stack data structure.
+ *
+ * @param {Schema.NodeType} type ProseMirror Schema for the node
+ * @param {HastNode} hastNode Hast node from which the ProseMirror node will be created
+ * @param {*} attrs Node’s attributes
+ * @param {*} factorySpec The factory spec used to create the node factory
+ */
+ openNode(type, hastNode, attrs, factorySpec) {
+ this.stack.push({ type, attrs, content: [], hastNode, factorySpec });
+ }
+
+ /**
+ * Removes the top ProseMirror node from the
+ * conversion stack and adds the node to the
+ * previous element.
+ * @returns
+ */
+ closeNode() {
+ const { type, attrs, content } = this.stack.pop();
+ const node = type.createAndFill(attrs, content);
+
+ if (!node) return null;
+
+ if (this.marks.length) {
+ this.marks = Mark.none;
+ }
+
+ if (!this.empty) {
+ this.top.content.push(node);
+ }
+
+ return node;
+ }
+
+ closeUntil(hastNode) {
+ while (hastNode !== this.top?.hastNode) {
+ this.closeNode();
+ }
+ }
+}
+
+/**
+ * Create ProseMirror node/mark factories based on one or more
+ * factory specifications.
+ *
+ * Note: Read `createProseMirrorDocFromMdastTree` documentation
+ * for instructions about how to define these specifications.
+ *
+ * @param {model.ProseMirrorSchema} schema A ProseMirror schema used to create the
+ * ProseMirror nodes and marks.
+ * @param {Object} proseMirrorFactorySpecs ProseMirror nodes factory specifications.
+ * @param {String} source Markdown source file’s content
+ *
+ * @returns An object that contains ProseMirror node factories
+ */
+const createProseMirrorNodeFactories = (schema, proseMirrorFactorySpecs, source) => {
+ const handlers = {
+ root: (state, hastNode) => state.openNode(schema.topNodeType, hastNode, {}),
+ text: (state, hastNode) => {
+ const { factorySpec } = state.top;
+
+ if (/^\s+$/.test(hastNode.value)) {
+ return;
+ }
+
+ if (factorySpec.wrapTextInParagraph === true) {
+ state.openNode(schema.nodeType('paragraph'));
+ state.addText(schema, hastNode.value);
+ state.closeNode();
+ } else {
+ state.addText(schema, hastNode.value);
+ }
+ },
+ };
+
+ for (const [hastNodeTagName, factorySpec] of Object.entries(proseMirrorFactorySpecs)) {
+ if (factorySpec.block) {
+ handlers[hastNodeTagName] = (state, hastNode, parent, ancestors) => {
+ const nodeType = schema.nodeType(
+ isFunction(factorySpec.block)
+ ? factorySpec.block(hastNode, parent, ancestors)
+ : factorySpec.block,
+ );
+
+ state.closeUntil(parent);
+ state.openNode(
+ nodeType,
+ hastNode,
+ getAttrs(factorySpec, hastNode, parent, source),
+ factorySpec,
+ );
+
+ /**
+ * If a getContent function is provided, we immediately close
+ * the node to delegate content processing to this function.
+ * */
+ if (isFunction(factorySpec.getContent)) {
+ state.addText(
+ schema,
+ factorySpec.getContent({ hastNode, hastNodeText: toString(hastNode) }),
+ );
+ state.closeNode();
+ }
+ };
+ } else if (factorySpec.inline) {
+ const nodeType = schema.nodeType(factorySpec.inline);
+ handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ state.closeUntil(parent);
+ state.openNode(
+ nodeType,
+ hastNode,
+ getAttrs(factorySpec, hastNode, parent, source),
+ factorySpec,
+ );
+ // Inline nodes do not have children therefore they are immediately closed
+ state.closeNode();
+ };
+ } else if (factorySpec.mark) {
+ const markType = schema.marks[factorySpec.mark];
+ handlers[hastNodeTagName] = (state, hastNode, parent) => {
+ state.openMark(markType, getAttrs(factorySpec, hastNode, parent, source));
+
+ if (factorySpec.inlineContent) {
+ state.addText(schema, hastNode.value);
+ }
+ };
+ } else {
+ throw new RangeError(`Unrecognized node factory spec ${JSON.stringify(factorySpec)}`);
+ }
+ }
+
+ return handlers;
+};
+
+/**
+ * Converts a Hast AST to a ProseMirror document based on a series
+ * of specifications that describe how to map all the nodes of the former
+ * to ProseMirror nodes or marks.
+ *
+ * The specification object describes how to map a Hast node to a ProseMirror node or mark.
+ * The converter will trigger an error if it doesn’t find a specification
+ * for a Hast node while traversing the AST.
+ *
+ * The object should have the following shape:
+ *
+ * {
+ * [hastNode.tagName]: {
+ * [block|node|mark]: [ProseMirror.Node.name],
+ * ...configurationOptions
+ * }
+ * }
+ *
+ * Where each property in the object represents a HAST node with a given tag name, for example:
+ *
+ * {
+ * h1: {},
+ * h2: {},
+ * table: {},
+ * strong: {},
+ * // etc
+ * }
+ *
+ * You can specify the type of ProseMirror object adding one the following
+ * properties:
+ *
+ * 1. "block": A ProseMirror node that contains one or more children.
+ * 2. "inline": A ProseMirror node that doesn’t contain any children although
+ * it can have inline content like a code block or a reference.
+ * 3. "mark": A ProseMirror mark.
+ *
+ * The value of that property should be the name of the ProseMirror node or mark, i.e:
+ *
+ * {
+ * h1: {
+ * block: 'heading',
+ * },
+ * h2: {
+ * block: 'heading',
+ * },
+ * img: {
+ * node: 'image',
+ * },
+ * strong: {
+ * mark: 'bold',
+ * }
+ * }
+ *
+ * You can compute a ProseMirror’s node or mark name based on the HAST node
+ * by passing a function instead of a String. The converter invokes the function
+ * and provides a HAST node object:
+ *
+ * {
+ * list: {
+ * block: (hastNode) => {
+ * let type = 'bulletList';
+
+ * if (hastNode.children.some(isTaskItem)) {
+ * type = 'taskList';
+ * } else if (hastNode.ordered) {
+ * type = 'orderedList';
+ * }
+
+ * return type;
+ * }
+ * }
+ * }
+ *
+ * Configuration options
+ * ----------------------
+ *
+ * You can customize the conversion process for every node or mark
+ * setting the following properties in the specification object:
+ *
+ * **getAttrs**
+ *
+ * Computes a ProseMirror node or mark attributes. The converter will invoke
+ * `getAttrs` with the following parameters:
+ *
+ * 1. hastNode: The hast node
+ * 2. hasParents: All the hast node’s ancestors up to the root node
+ * 3. source: Markdown source file’s content
+ *
+ * **wrapTextInParagraph**
+ *
+ * This property only applies to block nodes. If a block node contains text,
+ * it will wrap that text in a paragraph. This is useful for ProseMirror block
+ * nodes that don’t allow text directly such as list items and tables.
+ *
+ * **skipChildren**
+ *
+ * Skips a hast node’s children while traversing the tree.
+ *
+ * **getContent**
+ *
+ * Allows to pass a custom function that returns the content of a block node. The
+ * Content is limited to a single text node therefore the function should return
+ * a String value.
+ *
+ * Use this property along skipChildren to provide custom processing of child nodes
+ * for a block node.
+ *
+ * @param {model.Document_Schema} params.schema A ProseMirror schema that specifies the shape
+ * of the ProseMirror document.
+ * @param {Object} params.factorySpec A factory specification as described above
+ * @param {Hast} params.tree https://github.com/syntax-tree/hast
+ * @param {String} params.source Markdown source from which the MDast tree was generated
+ *
+ * @returns A ProseMirror document
+ */
+export const createProseMirrorDocFromMdastTree = ({ schema, factorySpecs, tree, source }) => {
+ const proseMirrorNodeFactories = createProseMirrorNodeFactories(schema, factorySpecs, source);
+ const state = new HastToProseMirrorConverterState();
+
+ visitParents(tree, (hastNode, ancestors) => {
+ const parent = ancestors[ancestors.length - 1];
+ const skipChildren = factorySpecs[hastNode.tagName]?.skipChildren;
+
+ const handler = proseMirrorNodeFactories[hastNode.tagName || hastNode.type];
+
+ if (!handler) {
+ throw new Error(
+ `Hast node of type "${
+ hastNode.tagName || hastNode.type
+ }" not supported by this converter. Please, provide an specification.`,
+ );
+ }
+
+ handler(state, hastNode, parent, ancestors);
+
+ return skipChildren === true ? 'skip' : true;
+ });
+
+ let doc;
+
+ do {
+ doc = state.closeNode();
+ } while (!state.empty);
+
+ return doc;
+};
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
new file mode 100644
index 00000000000..ef0a6ce0fac
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -0,0 +1,87 @@
+import { isString } from 'lodash';
+import { render } from '~/lib/gfm';
+import { createProseMirrorDocFromMdastTree } from './hast_to_prosemirror_converter';
+
+const factorySpecs = {
+ blockquote: { block: 'blockquote' },
+ p: { block: 'paragraph' },
+ li: { block: 'listItem', wrapTextInParagraph: true },
+ ul: { block: 'bulletList' },
+ ol: { block: 'orderedList' },
+ h1: {
+ block: 'heading',
+ getAttrs: () => ({ level: 1 }),
+ },
+ h2: {
+ block: 'heading',
+ getAttrs: () => ({ level: 2 }),
+ },
+ h3: {
+ block: 'heading',
+ getAttrs: () => ({ level: 3 }),
+ },
+ h4: {
+ block: 'heading',
+ getAttrs: () => ({ level: 4 }),
+ },
+ h5: {
+ block: 'heading',
+ getAttrs: () => ({ level: 5 }),
+ },
+ h6: {
+ block: 'heading',
+ getAttrs: () => ({ level: 6 }),
+ },
+ pre: {
+ block: 'codeBlock',
+ skipChildren: true,
+ getContent: ({ hastNodeText }) => hastNodeText,
+ getAttrs: (hastNode) => {
+ const languageClass = hastNode.children[0]?.properties.className?.[0];
+ const language = isString(languageClass) ? languageClass.replace('language-', '') : '';
+
+ return { language };
+ },
+ },
+ hr: { inline: 'horizontalRule' },
+ img: {
+ inline: 'image',
+ getAttrs: (hastNode) => ({
+ src: hastNode.properties.src,
+ title: hastNode.properties.title,
+ alt: hastNode.properties.alt,
+ }),
+ },
+ br: { inline: 'hardBreak' },
+ code: { mark: 'code' },
+ em: { mark: 'italic' },
+ i: { mark: 'italic' },
+ strong: { mark: 'bold' },
+ b: { mark: 'bold' },
+ a: {
+ mark: 'link',
+ getAttrs: (hastNode) => ({
+ href: hastNode.properties.href,
+ title: hastNode.properties.title,
+ }),
+ },
+};
+
+export default () => {
+ return {
+ deserialize: async ({ schema, content: markdown }) => {
+ const document = await render({
+ markdown,
+ renderer: (tree) =>
+ createProseMirrorDocFromMdastTree({
+ schema,
+ factorySpecs,
+ tree,
+ source: markdown,
+ }),
+ });
+
+ return { document, languages: [] };
+ },
+ };
+};
diff --git a/app/assets/javascripts/lib/gfm/index.js b/app/assets/javascripts/lib/gfm/index.js
index 07388f1fdfa..4e704eb69b2 100644
--- a/app/assets/javascripts/lib/gfm/index.js
+++ b/app/assets/javascripts/lib/gfm/index.js
@@ -32,7 +32,7 @@ const compilerFactory = (renderer) =>
* the MDast tree
*/
export const render = async ({ markdown, renderer }) => {
- const { value } = await createParser().use(compilerFactory(renderer)).process(markdown);
+ const { result } = await createParser().use(compilerFactory(renderer)).process(markdown);
- return value;
+ return result;
};
diff --git a/app/controllers/admin/background_migrations_controller.rb b/app/controllers/admin/background_migrations_controller.rb
index 0730d372c8c..03a9b436453 100644
--- a/app/controllers/admin/background_migrations_controller.rb
+++ b/app/controllers/admin/background_migrations_controller.rb
@@ -2,6 +2,7 @@
class Admin::BackgroundMigrationsController < Admin::ApplicationController
feature_category :database
+ urgency :low
def index
@relations_by_tab = {
diff --git a/app/models/project_pages_metadatum.rb b/app/models/project_pages_metadatum.rb
index dc1e9319340..7a3ece4bc92 100644
--- a/app/models/project_pages_metadatum.rb
+++ b/app/models/project_pages_metadatum.rb
@@ -8,8 +8,6 @@ class ProjectPagesMetadatum < ApplicationRecord
self.primary_key = :project_id
- ignore_columns :artifacts_archive_id, remove_with: '15.0', remove_after: '2022-04-22'
-
belongs_to :project, inverse_of: :pages_metadatum
belongs_to :pages_deployment
diff --git a/db/docs/project_feature_usages.yml b/db/docs/project_feature_usages.yml
index 51e8191c6e5..b3182de243b 100644
--- a/db/docs/project_feature_usages.yml
+++ b/db/docs/project_feature_usages.yml
@@ -3,7 +3,7 @@ table_name: project_feature_usages
classes:
- ProjectFeatureUsage
feature_categories:
-- product_analytics
-description: TODO
+- integrations
+description: Track Jira DVCS usage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/21db9a55e200b23a5a47251e9df46fd548c74559
milestone: '11.8'
diff --git a/db/docs/taggings.yml b/db/docs/taggings.yml
index a13108e705a..71078ab9c19 100644
--- a/db/docs/taggings.yml
+++ b/db/docs/taggings.yml
@@ -3,7 +3,8 @@ table_name: taggings
classes:
- ActsAsTaggableOn::Tagging
feature_categories:
+- continuous_integration
- runner
-description: TODO
+description: Taggings applied to arbitrary models based on entries in the 'tags' table
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b946da44695c9c8fe8867bb87bcdf801c52177d3
-milestone: "<6.0"
+milestone: "1.2"
diff --git a/db/docs/tags.yml b/db/docs/tags.yml
index 2d4820d6095..9ae2a4361ff 100644
--- a/db/docs/tags.yml
+++ b/db/docs/tags.yml
@@ -3,7 +3,8 @@ table_name: tags
classes:
- ActsAsTaggableOn::Tag
feature_categories:
+- continuous_integration
- runner
-description: TODO
+description: Tags applied to arbitrary models through the 'taggings' table
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/b946da44695c9c8fe8867bb87bcdf801c52177d3
milestone: "<6.0"
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 73ea4a8e1d0..b094bd59e58 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -203,7 +203,6 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Enable Performance Monitoring](monitoring/performance/gitlab_configuration.md): Enable GitLab Performance Monitoring.
- [GitLab performance monitoring with Prometheus](monitoring/prometheus/index.md): Configure GitLab and Prometheus for measuring performance metrics.
- [GitLab performance monitoring with Grafana](monitoring/performance/grafana_configuration.md): Configure GitLab to visualize time series metrics through graphs and dashboards.
- - [Request Profiling](monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
- [Performance Bar](monitoring/performance/performance_bar.md): Get performance information for the current page.
## Analytics
diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md
index 20fad8baf91..b063a20dc7f 100644
--- a/doc/administration/monitoring/performance/index.md
+++ b/doc/administration/monitoring/performance/index.md
@@ -17,7 +17,6 @@ documents to understand and properly configure GitLab Performance Monitoring:
- [Prometheus documentation](../prometheus/index.md)
- [Grafana Install/Configuration](grafana_configuration.md)
- [Performance bar](performance_bar.md)
-- [Request profiling](request_profiling.md)
## Introduction to GitLab Performance Monitoring
diff --git a/doc/administration/monitoring/performance/request_profiling.md b/doc/administration/monitoring/performance/request_profiling.md
deleted file mode 100644
index 6cd20132092..00000000000
--- a/doc/administration/monitoring/performance/request_profiling.md
+++ /dev/null
@@ -1,44 +0,0 @@
----
-stage: Monitor
-group: Respond
-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
----
-
-# Request profiling (DEPRECATED) **(FREE SELF)**
-
-> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/352488) in GitLab 14.8, and planned for removal in GitLab 15.0.
-
-WARNING:
-This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/352488)
-in GitLab 14.8, and is planned for removal in GitLab 15.0.
-
-To profile a request:
-
-1. Sign in to GitLab as an Administrator or a user with the [Maintainer role](../../../user/permissions.md).
-1. In the navigation bar, click **Admin area**.
-1. Go to **Monitoring > Requests Profiles**.
-1. In the **Requests Profiles** section, copy the token.
-1. Pass the headers `X-Profile-Token: <token>` and `X-Profile-Mode: <mode>`(where
- `<mode>` can be `execution` or `memory`) to the request you want to profile. When
- passing headers, you can use:
-
- - Browser extensions such as the
- [ModHeader](https://chrome.google.com/webstore/detail/modheader/idgpnmonknjnojddfkpgkljpfnnfcklj)
- Chrome extension.
- - `curl`. For example:
-
- ```shell
- curl --header 'X-Profile-Token: <token>' --header 'X-Profile-Mode: <mode>' "https://gitlab.example.com/group/project"
- ```
-
- Profiled requests can take longer than usual.
-
-After the request completes, you can view the profiling output from the
-**Monitoring > Requests Profiles** administration page:
-
-![Profiling output](img/request_profile_result.png)
-
-## Cleaning up profiled requests
-
-The output from profiled requests is cleared out once each day through a
-Sidekiq worker.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 80e5dd18726..1ae3aa4adb6 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -276,7 +276,6 @@ You can use these fake tokens as examples:
| Trigger token | `be20d8dcc028677c931e04f3871a9b` |
| Webhook secret token | `6XhDroRcYPM5by_h-HLY` |
| Health check token | `Tu7BgjR9qeZTEyRzGG2P` |
-| Request profile token | `7VgpS4Ax5utVD2esNstz` |
### Contractions
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 1e3e0570206..716a0d5199a 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -75,7 +75,6 @@ GitLab provides built-in tools to help improve performance and availability:
- [Profiling](profiling.md).
- [Distributed Tracing](distributed_tracing.md)
- [GitLab Performance Monitoring](../administration/monitoring/performance/index.md).
-- [Request Profiling](../administration/monitoring/performance/request_profiling.md).
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
- [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
- [Service measurement](service_measurement.md) for measuring and logging service execution.
diff --git a/doc/user/admin_area/index.md b/doc/user/admin_area/index.md
index f57672d3d36..de36018e194 100644
--- a/doc/user/admin_area/index.md
+++ b/doc/user/admin_area/index.md
@@ -25,7 +25,7 @@ The Admin Area is made up of the following sections:
| Section | Description |
|:-----------------------------------------------|:------------|
| **{overview}** [Overview](#overview-section) | View your GitLab [Dashboard](#admin-area-dashboard), and administer [projects](#administering-projects), [users](#administering-users), [groups](#administering-groups), [topics](#administering-topics), [jobs](#administering-jobs), [runners](#administering-runners), and [Gitaly servers](#administering-gitaly-servers). |
-| **{monitor}** Monitoring | View GitLab [system information](#system-information), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), [requests profiles](#requests-profiles), and [audit events](#audit-events). |
+| **{monitor}** Monitoring | View GitLab [system information](#system-information), and information on [background jobs](#background-jobs), [logs](#logs), [health checks](monitoring/health_check.md), and [audit events](#audit-events). |
| **{messages}** Messages | Send and manage [broadcast messages](broadcast_messages.md) for your users. |
| **{hook}** System Hooks | Configure [system hooks](../../administration/system_hooks.md) for many events. |
| **{applications}** Applications | Create system [OAuth applications](../../integration/oauth_provider.md) for integrations with other services. |
@@ -430,10 +430,6 @@ For details of these log files and their contents, see [Log system](../../admini
The content of each log file is listed in chronological order. To minimize performance issues, a maximum 2000 lines of each log file are shown.
-### Requests Profiles
-
-The **Requests Profiles** page contains the token required for profiling. For more details, see [Request Profiling](../../administration/monitoring/performance/request_profiling.md).
-
### Audit Events **(PREMIUM SELF)**
The **Audit Events** page lists changes made within the GitLab server. With this information you can control, analyze, and track every change.
diff --git a/package.json b/package.json
index 88973c5e4e9..c1b4d670938 100644
--- a/package.json
+++ b/package.json
@@ -58,7 +58,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "2.10.0",
"@gitlab/ui": "39.2.1",
- "@gitlab/visual-review-tools": "1.6.1",
+ "@gitlab/visual-review-tools": "1.7.0",
"@rails/actioncable": "6.1.4-7",
"@rails/ujs": "6.1.4-7",
"@sentry/browser": "5.30.0",
@@ -129,6 +129,7 @@
"fuzzaldrin-plus": "^0.6.0",
"graphql": "^15.7.2",
"graphql-tag": "^2.11.0",
+ "hast-util-to-string": "^2.0.0",
"highlight.js": "^11.3.1",
"immer": "^7.0.7",
"ipaddr.js": "^1.9.1",
@@ -179,6 +180,7 @@
"three-stl-loader": "^1.0.4",
"timeago.js": "^4.0.2",
"unified": "^10.1.2",
+ "unist-util-visit-parents": "^5.1.0",
"url-loader": "^4.1.1",
"uuid": "8.1.0",
"visibilityjs": "^1.2.4",
diff --git a/spec/frontend/content_editor/services/remark_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/remark_markdown_deserializer_spec.js
new file mode 100644
index 00000000000..66ca1587ec6
--- /dev/null
+++ b/spec/frontend/content_editor/services/remark_markdown_deserializer_spec.js
@@ -0,0 +1,309 @@
+import Bold from '~/content_editor/extensions/bold';
+import Blockquote from '~/content_editor/extensions/blockquote';
+import BulletList from '~/content_editor/extensions/bullet_list';
+import Code from '~/content_editor/extensions/code';
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import DescriptionItem from '~/content_editor/extensions/description_item';
+import DescriptionList from '~/content_editor/extensions/description_list';
+import Details from '~/content_editor/extensions/details';
+import DetailsContent from '~/content_editor/extensions/details_content';
+import Division from '~/content_editor/extensions/division';
+import Figure from '~/content_editor/extensions/figure';
+import FigureCaption from '~/content_editor/extensions/figure_caption';
+import HardBreak from '~/content_editor/extensions/hard_break';
+import Heading from '~/content_editor/extensions/heading';
+import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
+import Image from '~/content_editor/extensions/image';
+import Italic from '~/content_editor/extensions/italic';
+import Link from '~/content_editor/extensions/link';
+import ListItem from '~/content_editor/extensions/list_item';
+import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
+import createRemarkMarkdownDeserializer from '~/content_editor/services/remark_markdown_deserializer';
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+const tiptapEditor = createTestEditor({
+ extensions: [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ HardBreak,
+ Heading,
+ HorizontalRule,
+ Image,
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ ],
+});
+
+const {
+ builders: {
+ doc,
+ blockquote,
+ bold,
+ bulletList,
+ code,
+ codeBlock,
+ heading,
+ hardBreak,
+ horizontalRule,
+ image,
+ italic,
+ link,
+ listItem,
+ orderedList,
+ paragraph,
+ },
+} = createDocBuilder({
+ tiptapEditor,
+ names: {
+ blockquote: { nodeType: Blockquote.name },
+ bold: { markType: Bold.name },
+ bulletList: { nodeType: BulletList.name },
+ code: { markType: Code.name },
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ details: { nodeType: Details.name },
+ detailsContent: { nodeType: DetailsContent.name },
+ division: { nodeType: Division.name },
+ descriptionItem: { nodeType: DescriptionItem.name },
+ descriptionList: { nodeType: DescriptionList.name },
+ figure: { nodeType: Figure.name },
+ figureCaption: { nodeType: FigureCaption.name },
+ hardBreak: { nodeType: HardBreak.name },
+ heading: { nodeType: Heading.name },
+ horizontalRule: { nodeType: HorizontalRule.name },
+ image: { nodeType: Image.name },
+ italic: { nodeType: Italic.name },
+ link: { markType: Link.name },
+ listItem: { nodeType: ListItem.name },
+ orderedList: { nodeType: OrderedList.name },
+ paragraph: { nodeType: Paragraph.name },
+ },
+});
+
+describe('content_editor/services/remark_markdown_deserializer', () => {
+ let deserializer;
+
+ beforeEach(() => {
+ deserializer = createRemarkMarkdownDeserializer();
+ });
+
+ it.each([
+ {
+ markdown: '__bold text__',
+ doc: doc(paragraph(bold('bold text'))),
+ },
+ {
+ markdown: '**bold text**',
+ doc: doc(paragraph(bold('bold text'))),
+ },
+ {
+ markdown: '<strong>bold text</strong>',
+ doc: doc(paragraph(bold('bold text'))),
+ },
+ {
+ markdown: '<b>bold text</b>',
+ doc: doc(paragraph(bold('bold text'))),
+ },
+ {
+ markdown: '_italic text_',
+ doc: doc(paragraph(italic('italic text'))),
+ },
+ {
+ markdown: '*italic text*',
+ doc: doc(paragraph(italic('italic text'))),
+ },
+ {
+ markdown: '<em>italic text</em>',
+ doc: doc(paragraph(italic('italic text'))),
+ },
+ {
+ markdown: '<i>italic text</i>',
+ doc: doc(paragraph(italic('italic text'))),
+ },
+ {
+ markdown: '`inline code`',
+ doc: doc(paragraph(code('inline code'))),
+ },
+ {
+ markdown: '**`inline code bold`**',
+ doc: doc(paragraph(bold(code('inline code bold')))),
+ },
+ {
+ markdown: '[GitLab](https://gitlab.com "Go to GitLab")',
+ doc: doc(paragraph(link({ href: 'https://gitlab.com', title: 'Go to GitLab' }, 'GitLab'))),
+ },
+ {
+ markdown: '**[GitLab](https://gitlab.com "Go to GitLab")**',
+ doc: doc(
+ paragraph(bold(link({ href: 'https://gitlab.com', title: 'Go to GitLab' }, 'GitLab'))),
+ ),
+ },
+ {
+ markdown: `
+This is a paragraph with a\\
+hard line break`,
+ doc: doc(paragraph('This is a paragraph with a', hardBreak(), '\nhard line break')),
+ },
+ {
+ markdown: '![GitLab Logo](https://gitlab.com/logo.png "GitLab Logo")',
+ doc: doc(
+ paragraph(
+ image({ src: 'https://gitlab.com/logo.png', alt: 'GitLab Logo', title: 'GitLab Logo' }),
+ ),
+ ),
+ },
+ {
+ markdown: '---',
+ doc: doc(horizontalRule()),
+ },
+ {
+ markdown: '***',
+ doc: doc(horizontalRule()),
+ },
+ {
+ markdown: '___',
+ doc: doc(horizontalRule()),
+ },
+ {
+ markdown: '<hr>',
+ doc: doc(horizontalRule()),
+ },
+ {
+ markdown: '# Heading 1',
+ doc: doc(heading({ level: 1 }, 'Heading 1')),
+ },
+ {
+ markdown: '## Heading 2',
+ doc: doc(heading({ level: 2 }, 'Heading 2')),
+ },
+ {
+ markdown: '### Heading 3',
+ doc: doc(heading({ level: 3 }, 'Heading 3')),
+ },
+ {
+ markdown: '#### Heading 4',
+ doc: doc(heading({ level: 4 }, 'Heading 4')),
+ },
+ {
+ markdown: '##### Heading 5',
+ doc: doc(heading({ level: 5 }, 'Heading 5')),
+ },
+ {
+ markdown: '###### Heading 6',
+ doc: doc(heading({ level: 6 }, 'Heading 6')),
+ },
+ {
+ markdown: `
+Heading
+one
+======
+`,
+ doc: doc(heading({ level: 1 }, 'Heading\none')),
+ },
+ {
+ markdown: `
+Heading
+two
+-------
+`,
+ doc: doc(heading({ level: 2 }, 'Heading\ntwo')),
+ },
+ {
+ markdown: `
+- List item 1
+- List item 2
+`,
+ doc: doc(bulletList(listItem(paragraph('List item 1')), listItem(paragraph('List item 2')))),
+ },
+ {
+ markdown: `
+1. List item 1
+1. List item 2
+`,
+ doc: doc(orderedList(listItem(paragraph('List item 1')), listItem(paragraph('List item 2')))),
+ },
+ {
+ markdown: `
+- List item 1
+ - Sub list item 1
+`,
+ doc: doc(
+ bulletList(
+ listItem(paragraph('List item 1\n'), bulletList(listItem(paragraph('Sub list item 1')))),
+ ),
+ ),
+ },
+ {
+ markdown: `
+- List item 1 paragraph 1
+
+ List item 1 paragraph 2
+- List item 2
+`,
+ doc: doc(
+ bulletList(
+ listItem(paragraph('List item 1 paragraph 1'), paragraph('List item 1 paragraph 2')),
+ listItem(paragraph('List item 2')),
+ ),
+ ),
+ },
+ {
+ markdown: `
+> This is a blockquote
+`,
+ doc: doc(blockquote(paragraph('This is a blockquote'))),
+ },
+ {
+ markdown: `
+> - List item 1
+> - List item 2
+`,
+ doc: doc(
+ blockquote(
+ bulletList(listItem(paragraph('List item 1')), listItem(paragraph('List item 2'))),
+ ),
+ ),
+ },
+ {
+ markdown: `
+ const fn = () => 'GitLab';
+`,
+ doc: doc(codeBlock({ language: '' }, "const fn = () => 'GitLab';\n")),
+ },
+ {
+ markdown: `
+\`\`\`javascript
+ const fn = () => 'GitLab';
+\`\`\`\
+`,
+ doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n")),
+ },
+ {
+ markdown: `
+\`\`\`
+\`\`\`\
+`,
+ doc: doc(codeBlock({ language: '' }, '')),
+ },
+ {
+ markdown: `
+\`\`\`javascript
+ const fn = () => 'GitLab';
+
+
+\`\`\`\
+`,
+ doc: doc(codeBlock({ language: 'javascript' }, " const fn = () => 'GitLab';\n\n\n")),
+ },
+ ])('deserializes %s correctly', async ({ markdown, doc: expectedDoc }) => {
+ const { schema } = tiptapEditor;
+ const { document } = await deserializer.deserialize({ schema, content: markdown });
+
+ expect(document.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+});
diff --git a/spec/frontend/lib/gfm/index_spec.js b/spec/frontend/lib/gfm/index_spec.js
index 5c72b5a51a7..c9a480e9943 100644
--- a/spec/frontend/lib/gfm/index_spec.js
+++ b/spec/frontend/lib/gfm/index_spec.js
@@ -33,14 +33,16 @@ describe('gfm', () => {
});
it('returns the result of executing the renderer function', async () => {
+ const rendered = { value: 'rendered tree' };
+
const result = await render({
markdown: '<strong>This is bold text</strong>',
renderer: () => {
- return 'rendered tree';
+ return rendered;
},
});
- expect(result).toBe('rendered tree');
+ expect(result).toEqual(rendered);
});
});
});
diff --git a/yarn.lock b/yarn.lock
index ca94db51272..200dab1ca33 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -991,10 +991,10 @@
url-search-params-polyfill "^5.0.0"
vue-runtime-helpers "^1.1.2"
-"@gitlab/visual-review-tools@1.6.1":
- version "1.6.1"
- resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.6.1.tgz#0d8f3ff9f51b05f7c80b9a107727703d48997e4e"
- integrity sha512-vY8K1igwZFoEOmU0h4E7XTLlilsQ4ylPr27O01UsSe6ZTKi6oEMREsRAEpNIUgRlxUARCsf+Opp4pgSFzFkFcw==
+"@gitlab/visual-review-tools@1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.0.tgz#18a59911484ddbbee433d3701316b5ab8e4077a5"
+ integrity sha512-o9gM3W6guSl00aS0hJcXePuR/mkmq38F5FhUgTlMBkB5+R68aO87md3cSvSMfJin0MjPlWktBNAfaz+y5CQ39g==
"@graphql-eslint/eslint-plugin@3.10.2":
version "3.10.2"
@@ -6613,6 +6613,13 @@ hast-util-to-parse5@^7.0.0:
web-namespaces "^2.0.0"
zwitch "^2.0.0"
+hast-util-to-string@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a"
+ integrity sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A==
+ dependencies:
+ "@types/hast" "^2.0.0"
+
hastscript@^7.0.0:
version "7.0.2"
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.0.2.tgz#d811fc040817d91923448a28156463b2e40d590a"
@@ -12366,7 +12373,7 @@ unist-util-visit-parents@^4.0.0:
"@types/unist" "^2.0.0"
unist-util-is "^5.0.0"
-unist-util-visit-parents@^5.0.0:
+unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.0:
version "5.1.0"
resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.0.tgz#44bbc5d25f2411e7dfc5cecff12de43296aa8521"
integrity sha512-y+QVLcY5eR/YVpqDsLf/xh9R3Q2Y4HxkZTp7ViLDU6WtJCEcPmRzW1gpdWDCDIqIlhuPDXOgttqPlykrHYDekg==