Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
5947e68ac3
commit
6431ee6152
|
@ -0,0 +1,10 @@
|
|||
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN = '[[';
|
||||
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN = 'TOC';
|
||||
export const TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN = ']]';
|
||||
export const TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN = '[TOC]';
|
||||
|
||||
export const MDAST_TEXT_NODE = 'text';
|
||||
export const MDAST_EMPHASIS_NODE = 'emphasis';
|
||||
export const MDAST_PARAGRAPH_NODE = 'paragraph';
|
||||
|
||||
export const GLFM_TABLE_OF_CONTENTS_NODE = 'tableOfContents';
|
|
@ -0,0 +1,85 @@
|
|||
import { first, last } from 'lodash';
|
||||
import { u } from 'unist-builder';
|
||||
import { visitParents, SKIP, CONTINUE } from 'unist-util-visit-parents';
|
||||
import {
|
||||
TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN,
|
||||
TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN,
|
||||
TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN,
|
||||
TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN,
|
||||
MDAST_TEXT_NODE,
|
||||
MDAST_EMPHASIS_NODE,
|
||||
MDAST_PARAGRAPH_NODE,
|
||||
GLFM_TABLE_OF_CONTENTS_NODE,
|
||||
} from '../constants';
|
||||
|
||||
const isTOCTextNode = ({ type, value }) =>
|
||||
type === MDAST_TEXT_NODE && value === TABLE_OF_CONTENTS_DOUBLE_BRACKET_MIDDLE_TOKEN;
|
||||
|
||||
const isTOCEmphasisNode = ({ type, children }) =>
|
||||
type === MDAST_EMPHASIS_NODE && children.length === 1 && isTOCTextNode(first(children));
|
||||
|
||||
const isTOCDoubleSquareBracketOpenTokenTextNode = ({ type, value }) =>
|
||||
type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_OPEN_TOKEN;
|
||||
|
||||
const isTOCDoubleSquareBracketCloseTokenTextNode = ({ type, value }) =>
|
||||
type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_DOUBLE_BRACKET_CLOSE_TOKEN;
|
||||
|
||||
/*
|
||||
* Detects table of contents declaration with syntax [[_TOC_]]
|
||||
*/
|
||||
const isTableOfContentsDoubleSquareBracketSyntax = ({ children }) => {
|
||||
if (children.length !== 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [firstChild, middleChild, lastChild] = children;
|
||||
|
||||
return (
|
||||
isTOCDoubleSquareBracketOpenTokenTextNode(firstChild) &&
|
||||
isTOCEmphasisNode(middleChild) &&
|
||||
isTOCDoubleSquareBracketCloseTokenTextNode(lastChild)
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
* Detects table of contents declaration with syntax [TOC]
|
||||
*/
|
||||
const isTableOfContentsSingleSquareBracketSyntax = ({ children }) => {
|
||||
if (children.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [firstChild] = children;
|
||||
const { type, value } = firstChild;
|
||||
|
||||
return type === MDAST_TEXT_NODE && value.trim() === TABLE_OF_CONTENTS_SINGLE_BRACKET_TOKEN;
|
||||
};
|
||||
|
||||
const isTableOfContentsNode = (node) =>
|
||||
node.type === MDAST_PARAGRAPH_NODE &&
|
||||
(isTableOfContentsDoubleSquareBracketSyntax(node) ||
|
||||
isTableOfContentsSingleSquareBracketSyntax(node));
|
||||
|
||||
export default () => {
|
||||
return (tree) => {
|
||||
visitParents(tree, (node, ancestors) => {
|
||||
const parent = last(ancestors);
|
||||
|
||||
if (!parent) {
|
||||
return CONTINUE;
|
||||
}
|
||||
|
||||
if (isTableOfContentsNode(node)) {
|
||||
const index = parent.children.indexOf(node);
|
||||
|
||||
parent.children[index] = u(GLFM_TABLE_OF_CONTENTS_NODE, {
|
||||
position: node.position,
|
||||
});
|
||||
}
|
||||
|
||||
return SKIP;
|
||||
});
|
||||
|
||||
return tree;
|
||||
};
|
||||
};
|
|
@ -6,6 +6,8 @@ import remarkFrontmatter from 'remark-frontmatter';
|
|||
import remarkGfm from 'remark-gfm';
|
||||
import remarkRehype, { all } from 'remark-rehype';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
import glfmTableOfContents from './glfm_extensions/table_of_contents';
|
||||
import * as glfmMdastToHastHandlers from './mdast_to_hast_handlers/glfm_mdast_to_hast_handlers';
|
||||
|
||||
const skipFrontmatterHandler = (language) => (h, node) =>
|
||||
h(node.position, 'frontmatter', { language }, [{ type: 'text', value: node.value }]);
|
||||
|
@ -65,19 +67,22 @@ const skipRenderingHandlers = {
|
|||
all(h, node),
|
||||
);
|
||||
},
|
||||
tableOfContents: (h, node) => h(node.position, 'tableOfContents'),
|
||||
toml: skipFrontmatterHandler('toml'),
|
||||
yaml: skipFrontmatterHandler('yaml'),
|
||||
json: skipFrontmatterHandler('json'),
|
||||
};
|
||||
|
||||
const createParser = ({ skipRendering = [] }) => {
|
||||
const createParser = ({ skipRendering }) => {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkFrontmatter, ['yaml', 'toml', { type: 'json', marker: ';' }])
|
||||
.use(glfmTableOfContents)
|
||||
.use(remarkRehype, {
|
||||
allowDangerousHtml: true,
|
||||
handlers: {
|
||||
...glfmMdastToHastHandlers,
|
||||
...pick(skipRenderingHandlers, skipRendering),
|
||||
},
|
||||
})
|
||||
|
@ -99,13 +104,13 @@ const compilerFactory = (renderer) =>
|
|||
* tree in any desired representation
|
||||
*
|
||||
* @param {String} params.markdown Markdown to parse
|
||||
* @param {(tree: MDast -> any)} params.renderer A function that accepts mdast
|
||||
* @param {Function} params.renderer A function that accepts mdast
|
||||
* AST tree and returns an object of any type that represents the result of
|
||||
* rendering the tree. See the references below to for more information
|
||||
* about MDast.
|
||||
*
|
||||
* MDastTree documentation https://github.com/syntax-tree/mdast
|
||||
* @returns {Promise<any>} Returns a promise with the result of rendering
|
||||
* @returns {Promise} Returns a promise with the result of rendering
|
||||
* the MDast tree
|
||||
*/
|
||||
export const render = async ({ markdown, renderer, skipRendering = [] }) => {
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
export const tableOfContents = (h, node) => h(node.position, 'nav');
|
|
@ -7,6 +7,7 @@ import Poll from '~/lib/utils/poll';
|
|||
import StatusIcon from '../extensions/status_icon.vue';
|
||||
import ActionButtons from '../action_buttons.vue';
|
||||
import { EXTENSION_ICONS } from '../../constants';
|
||||
import ContentSection from './widget_content_section.vue';
|
||||
|
||||
const FETCH_TYPE_COLLAPSED = 'collapsed';
|
||||
const FETCH_TYPE_EXPANDED = 'expanded';
|
||||
|
@ -17,6 +18,7 @@ export default {
|
|||
StatusIcon,
|
||||
GlButton,
|
||||
GlLoadingIcon,
|
||||
ContentSection,
|
||||
},
|
||||
directives: {
|
||||
GlTooltip: GlTooltipDirective,
|
||||
|
@ -92,15 +94,16 @@ export default {
|
|||
isCollapsed: true,
|
||||
isLoading: false,
|
||||
isLoadingExpandedContent: false,
|
||||
error: null,
|
||||
summaryError: null,
|
||||
contentError: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
collapseButtonLabel() {
|
||||
return sprintf(this.isCollapsed ? __('Show details') : __('Hide details'));
|
||||
},
|
||||
statusIcon() {
|
||||
return this.error ? EXTENSION_ICONS.failed : this.statusIconName;
|
||||
summaryStatusIcon() {
|
||||
return this.summaryError ? this.$options.failedStatusIcon : this.statusIconName;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
@ -114,7 +117,7 @@ export default {
|
|||
try {
|
||||
await this.fetch(this.fetchCollapsedData, FETCH_TYPE_COLLAPSED);
|
||||
} catch {
|
||||
this.error = this.errorText;
|
||||
this.summaryError = this.errorText;
|
||||
}
|
||||
|
||||
this.isLoading = false;
|
||||
|
@ -130,12 +133,12 @@ export default {
|
|||
},
|
||||
async fetchExpandedContent() {
|
||||
this.isLoadingExpandedContent = true;
|
||||
this.error = null;
|
||||
this.contentError = null;
|
||||
|
||||
try {
|
||||
await this.fetch(this.fetchExpandedData, FETCH_TYPE_EXPANDED);
|
||||
} catch {
|
||||
this.error = this.errorText;
|
||||
this.contentError = this.errorText;
|
||||
|
||||
// Reset these values so that we allow refetching
|
||||
this.isExpandedForTheFirstTime = true;
|
||||
|
@ -178,20 +181,26 @@ export default {
|
|||
});
|
||||
},
|
||||
},
|
||||
failedStatusIcon: EXTENSION_ICONS.failed,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="media-section" data-testid="widget-extension">
|
||||
<div class="media gl-p-5">
|
||||
<status-icon :level="1" :name="widgetName" :is-loading="isLoading" :icon-name="statusIcon" />
|
||||
<status-icon
|
||||
:level="1"
|
||||
:name="widgetName"
|
||||
:is-loading="isLoading"
|
||||
:icon-name="summaryStatusIcon"
|
||||
/>
|
||||
<div
|
||||
class="media-body gl-display-flex gl-flex-direction-row! gl-align-self-center"
|
||||
data-testid="widget-extension-top-level"
|
||||
>
|
||||
<div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
|
||||
<slot v-if="!error" name="summary">{{ isLoading ? loadingText : summary }}</slot>
|
||||
<span v-else>{{ error }}</span>
|
||||
<span v-if="summaryError">{{ summaryError }}</span>
|
||||
<slot v-else name="summary">{{ isLoading ? loadingText : summary }}</slot>
|
||||
</div>
|
||||
<action-buttons
|
||||
v-if="actionButtons.length > 0"
|
||||
|
@ -217,14 +226,24 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isCollapsed"
|
||||
v-if="!isCollapsed || contentError"
|
||||
class="mr-widget-grouped-section gl-relative"
|
||||
data-testid="widget-extension-collapsed-section"
|
||||
>
|
||||
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
|
||||
<gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
|
||||
</div>
|
||||
<slot v-else name="content">{{ content }}</slot>
|
||||
<content-section
|
||||
v-else-if="contentError"
|
||||
class="report-block-container"
|
||||
:status-icon-name="$options.failedStatusIcon"
|
||||
:widget-name="widgetName"
|
||||
>
|
||||
{{ contentError }}
|
||||
</content-section>
|
||||
<slot v-else name="content">
|
||||
{{ content }}
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
<script>
|
||||
import { EXTENSION_ICONS } from '../../constants';
|
||||
import StatusIcon from '../extensions/status_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIcon,
|
||||
},
|
||||
props: {
|
||||
statusIconName: {
|
||||
type: String,
|
||||
default: '',
|
||||
required: false,
|
||||
validator: (value) => value === '' || Object.keys(EXTENSION_ICONS).indexOf(value) > -1,
|
||||
},
|
||||
widgetName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="gl-px-7">
|
||||
<div class="gl-pl-4 gl-display-flex">
|
||||
<status-icon
|
||||
v-if="statusIconName"
|
||||
:level="2"
|
||||
:name="widgetName"
|
||||
:icon-name="statusIconName"
|
||||
/>
|
||||
<slot name="default"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -142,6 +142,16 @@ module TodosHelper
|
|||
todos_filter_params.values.none?
|
||||
end
|
||||
|
||||
def no_todos_messages
|
||||
[
|
||||
s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
|
||||
s_('Todos|Isn\'t an empty To-Do List beautiful?'),
|
||||
s_('Todos|Give yourself a pat on the back!'),
|
||||
s_('Todos|Nothing left to do. High five!'),
|
||||
s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"')
|
||||
]
|
||||
end
|
||||
|
||||
def todos_filter_path(options = {})
|
||||
without = options.delete(:without)
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
.text-content.gl-text-center
|
||||
- if todos_filter_empty?
|
||||
%h4
|
||||
= Gitlab.config.gitlab.no_todos_messages.sample
|
||||
= no_todos_messages.sample
|
||||
%p
|
||||
= (s_("Todos|Are you looking for things to do? Take a look at %{strongStart}%{openIssuesLinkStart}open issues%{openIssuesLinkEnd}%{strongEnd}, contribute to %{strongStart}%{mergeRequestLinkStart}a merge request%{mergeRequestLinkEnd}%{mergeRequestLinkEnd}%{strongEnd}, or mention someone in a comment to automatically assign them a new to-do item.") % { strongStart: '<strong>', strongEnd: '</strong>', openIssuesLinkStart: "<a href=\"#{issues_dashboard_path}\">", openIssuesLinkEnd: '</a>', mergeRequestLinkStart: "<a href=\"#{merge_requests_dashboard_path}\">", mergeRequestLinkEnd: '</a>' }).html_safe
|
||||
- else
|
||||
|
|
|
@ -214,7 +214,6 @@ Settings.gitlab['import_sources'] ||= Gitlab::ImportSources.values
|
|||
Settings.gitlab['trusted_proxies'] ||= []
|
||||
Settings.gitlab['content_security_policy'] ||= {}
|
||||
Settings.gitlab['allowed_hosts'] ||= []
|
||||
Settings.gitlab['no_todos_messages'] ||= YAML.load_file(Rails.root.join('config', 'no_todos_messages.yml'))
|
||||
Settings.gitlab['impersonation_enabled'] ||= true if Settings.gitlab['impersonation_enabled'].nil?
|
||||
Settings.gitlab['usage_ping_enabled'] = true if Settings.gitlab['usage_ping_enabled'].nil?
|
||||
Settings.gitlab['max_request_duration_seconds'] ||= 57
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
key_path: usage_activity_by_stage.manage.count_user_auth
|
||||
description: Number of unique user logins
|
||||
product_section: dev
|
||||
product_stage: manage
|
||||
product_group: authentication_and_authorization
|
||||
product_category: system_access
|
||||
value_type: number
|
||||
status: active
|
||||
milestone: "15.4"
|
||||
introduced_by_url: "https://gitlab.com/gitlab-org/gitlab/-/merge_requests/96321"
|
||||
time_frame: all
|
||||
data_source: database
|
||||
instrumentation_class: CountUserAuthMetric
|
||||
data_category: optional
|
||||
performance_indicator_type: []
|
||||
distribution:
|
||||
- ce
|
||||
- ee
|
||||
tier:
|
||||
- free
|
||||
- premium
|
||||
- ultimate
|
|
@ -1,11 +0,0 @@
|
|||
# When the todo list on the user's dashboard becomes empty, a random message
|
||||
# from the list below will be shown.
|
||||
#
|
||||
# If you come up with a fun one, please feel free to contribute it to GitLab!
|
||||
# https://about.gitlab.com/contributing/
|
||||
---
|
||||
- Good job! Looks like you don't have anything left on your To-Do List
|
||||
- Isn't an empty To-Do List beautiful?
|
||||
- Give yourself a pat on the back!
|
||||
- Nothing left to do. High five!
|
||||
- Henceforth, you shall be known as "To-Do Destroyer"
|
|
@ -1157,16 +1157,13 @@ A site profile contains:
|
|||
- **Target URL**: The URL that DAST runs against.
|
||||
- **Excluded URLs**: A comma-separated list of URLs to exclude from the scan.
|
||||
- **Request headers**: A comma-separated list of HTTP request headers, including names and values. These headers are added to every request made by DAST.
|
||||
- **Authentication (for website)**:
|
||||
- **Authentication**:
|
||||
- **Authenticated URL**: The URL of the page containing the sign-in HTML form on the target website. The username and password are submitted with the login form to create an authenticated scan.
|
||||
- **Username**: The username used to authenticate to the website.
|
||||
- **Password**: The password used to authenticate to the website.
|
||||
- **Username form field**: The name of username field at the sign-in HTML form.
|
||||
- **Password form field**: The name of password field at the sign-in HTML form.
|
||||
- **Submit form field**: The `id` or `name` of the element that when clicked submits the sign-in HTML form.
|
||||
- **Authentication (for API scan)**:
|
||||
- **Username**: The username used to authenticate to the API.
|
||||
- **Password**: The password used to authenticate to the API.
|
||||
|
||||
When an API site type is selected, a [host override](#host-override) is used to ensure the API being scanned is on the same host as the target. This is done to reduce the risk of running an active scan against the wrong API.
|
||||
|
||||
|
|
|
@ -212,7 +212,17 @@ module API
|
|||
|
||||
recheck_mergeability_of(merge_requests: merge_requests) unless options[:skip_merge_status_recheck]
|
||||
|
||||
present_cached merge_requests, expires_in: 8.hours, cache_context: -> (mr) { "#{current_user&.cache_key}:#{mr.merge_status}" }, **options
|
||||
present_cached merge_requests,
|
||||
expires_in: 8.hours,
|
||||
cache_context: -> (mr) do
|
||||
[
|
||||
current_user&.cache_key,
|
||||
mr.merge_status,
|
||||
mr.merge_request_assignees.map(&:cache_key),
|
||||
mr.merge_request_reviewers.map(&:cache_key)
|
||||
].join(":")
|
||||
end,
|
||||
**options
|
||||
end
|
||||
|
||||
desc 'Create a merge request' do
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Analytics
|
||||
# This class generates a date => value hash without gaps in the data points.
|
||||
#
|
||||
# Simple usage:
|
||||
#
|
||||
# > # We have the following data for the last 5 day:
|
||||
# > input = { 3.days.ago.to_date => 10, Date.today => 5 }
|
||||
#
|
||||
# > # Format this data, so we can chart the complete date range:
|
||||
# > Gitlab::Analytics::DateFiller.new(input, from: 4.days.ago, to: Date.today, default_value: 0).fill
|
||||
# > {
|
||||
# > Sun, 28 Aug 2022=>0,
|
||||
# > Mon, 29 Aug 2022=>10,
|
||||
# > Tue, 30 Aug 2022=>0,
|
||||
# > Wed, 31 Aug 2022=>0,
|
||||
# > Thu, 01 Sep 2022=>5
|
||||
# > }
|
||||
#
|
||||
# Parameters:
|
||||
#
|
||||
# **input**
|
||||
# A Hash containing data for the series or the chart. The key is a Date object
|
||||
# or an object which can be converted to Date.
|
||||
#
|
||||
# **from**
|
||||
# Start date of the range
|
||||
#
|
||||
# **to**
|
||||
# End date of the range
|
||||
#
|
||||
# **period**
|
||||
# Specifies the period in wich the dates should be generated. Options:
|
||||
#
|
||||
# - :day, generate date-value pair for each day in the given period
|
||||
# - :week, generate date-value pair for each week (beginning of the week date)
|
||||
# - :month, generate date-value pair for each week (beginning of the month date)
|
||||
#
|
||||
# Note: the Date objects in the `input` should follow the same pattern (beginning of ...)
|
||||
#
|
||||
# **default_value**
|
||||
#
|
||||
# Which value use when the `input` Hash does not contain data for the given day.
|
||||
#
|
||||
# **date_formatter**
|
||||
#
|
||||
# How to format the dates in the resulting hash.
|
||||
class DateFiller
|
||||
DEFAULT_DATE_FORMATTER = -> (date) { date }
|
||||
PERIOD_STEPS = {
|
||||
day: 1.day,
|
||||
week: 1.week,
|
||||
month: 1.month
|
||||
}.freeze
|
||||
|
||||
def initialize(
|
||||
input,
|
||||
from:,
|
||||
to:,
|
||||
period: :day,
|
||||
default_value: nil,
|
||||
date_formatter: DEFAULT_DATE_FORMATTER)
|
||||
@input = input.transform_keys(&:to_date)
|
||||
@from = from.to_date
|
||||
@to = to.to_date
|
||||
@period = period
|
||||
@default_value = default_value
|
||||
@date_formatter = date_formatter
|
||||
end
|
||||
|
||||
def fill
|
||||
data = {}
|
||||
|
||||
current_date = from
|
||||
loop do
|
||||
transformed_date = transform_date(current_date)
|
||||
break if transformed_date > to
|
||||
|
||||
formatted_date = date_formatter.call(transformed_date)
|
||||
|
||||
value = input.delete(transformed_date)
|
||||
data[formatted_date] = value.nil? ? default_value : value
|
||||
|
||||
current_date = (current_date + PERIOD_STEPS.fetch(period)).to_date
|
||||
end
|
||||
|
||||
raise "Input contains values which doesn't fall under the given period!" if input.any?
|
||||
|
||||
data
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :input, :from, :to, :period, :default_value, :date_formatter
|
||||
|
||||
def transform_date(date)
|
||||
case period
|
||||
when :day
|
||||
date.beginning_of_day.to_date
|
||||
when :week
|
||||
date.beginning_of_week.to_date
|
||||
when :month
|
||||
date.beginning_of_month.to_date
|
||||
else
|
||||
raise "Unknown period given: #{period}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountUserAuthMetric < DatabaseMetric
|
||||
operation :distinct_count, column: :user_id
|
||||
|
||||
relation do
|
||||
AuthenticationEvent.success
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -41117,6 +41117,18 @@ msgstr ""
|
|||
msgid "Todos|Filter by project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Give yourself a pat on the back!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Good job! Looks like you don't have anything left on your To-Do List"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Henceforth, you shall be known as \"To-Do Destroyer\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Isn't an empty To-Do List beautiful?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|It's how you always know what to work on next."
|
||||
msgstr ""
|
||||
|
||||
|
@ -41126,6 +41138,9 @@ msgstr ""
|
|||
msgid "Todos|Nothing is on your to-do list. Nice work!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Nothing left to do. High five!"
|
||||
msgstr ""
|
||||
|
||||
msgid "Todos|Undo mark all as done"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -172,6 +172,7 @@
|
|||
"three": "^0.143.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"unified": "^10.1.2",
|
||||
"unist-builder": "^3.0.0",
|
||||
"unist-util-visit-parents": "^5.1.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"uuid": "8.1.0",
|
||||
|
|
|
@ -24,12 +24,6 @@ describe('gfm', () => {
|
|||
};
|
||||
|
||||
describe('render', () => {
|
||||
it('processes Commonmark and provides an ast to the renderer function', async () => {
|
||||
const result = await markdownToAST('This is text');
|
||||
|
||||
expect(result.type).toBe('root');
|
||||
});
|
||||
|
||||
it('transforms raw HTML into individual nodes in the AST', async () => {
|
||||
const result = await markdownToAST('<strong>This is bold text</strong>');
|
||||
|
||||
|
@ -46,6 +40,13 @@ describe('gfm', () => {
|
|||
);
|
||||
});
|
||||
|
||||
describe('with custom renderer', () => {
|
||||
it('processes Commonmark and provides an ast to the renderer function', async () => {
|
||||
const result = await markdownToAST('This is text');
|
||||
|
||||
expect(result.type).toBe('root');
|
||||
});
|
||||
|
||||
it('returns the result of executing the renderer function', async () => {
|
||||
const rendered = { value: 'rendered tree' };
|
||||
|
||||
|
@ -58,7 +59,9 @@ describe('gfm', () => {
|
|||
|
||||
expect(result).toEqual(rendered);
|
||||
});
|
||||
});
|
||||
|
||||
describe('footnote references and footnote definitions', () => {
|
||||
describe('when skipping the rendering of footnote reference and definition nodes', () => {
|
||||
it('transforms footnotes into footnotedefinition and footnotereference tags', async () => {
|
||||
const result = await markdownToAST(
|
||||
|
@ -96,7 +99,9 @@ describe('gfm', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('code blocks', () => {
|
||||
describe('when skipping the rendering of code blocks', () => {
|
||||
it('transforms code nodes into codeblock html tags', async () => {
|
||||
const result = await markdownToAST(
|
||||
|
@ -119,7 +124,9 @@ console.log('Hola');
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('reference definitions', () => {
|
||||
describe('when skipping the rendering of reference definitions', () => {
|
||||
it('transforms code nodes into codeblock html tags', async () => {
|
||||
const result = await markdownToAST(
|
||||
|
@ -151,7 +158,9 @@ console.log('Hola');
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('link and image references', () => {
|
||||
describe('when skipping the rendering of link and image references', () => {
|
||||
it('transforms linkReference and imageReference nodes into html tags', async () => {
|
||||
const result = await markdownToAST(
|
||||
|
@ -231,6 +240,7 @@ console.log('Hola');
|
|||
});
|
||||
});
|
||||
|
||||
describe('frontmatter', () => {
|
||||
describe('when skipping the rendering of frontmatter types', () => {
|
||||
it.each`
|
||||
type | input
|
||||
|
@ -258,4 +268,42 @@ console.log('Hola');
|
|||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('table of contents', () => {
|
||||
it.each`
|
||||
markdown
|
||||
${'[[_TOC_]]'}
|
||||
${' [[_TOC_]]'}
|
||||
${'[[_TOC_]] '}
|
||||
${'[TOC]'}
|
||||
${' [TOC]'}
|
||||
${'[TOC] '}
|
||||
`('parses $markdown and produces a table of contents section', async ({ markdown }) => {
|
||||
const result = await markdownToAST(markdown);
|
||||
|
||||
expectInRoot(
|
||||
result,
|
||||
expect.objectContaining({
|
||||
type: 'element',
|
||||
tagName: 'nav',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when skipping the rendering of table of contents', () => {
|
||||
it('transforms table of contents nodes into html tableofcontents tags', async () => {
|
||||
const result = await markdownToAST('[[_TOC_]]', ['tableOfContents']);
|
||||
|
||||
expectInRoot(
|
||||
result,
|
||||
expect.objectContaining({
|
||||
type: 'element',
|
||||
tagName: 'tableofcontents',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import WidgetContentSection from '~/vue_merge_request_widget/components/widget/widget_content_section.vue';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
|
||||
|
||||
describe('~/vue_merge_request_widget/components/widget/widget_content_section.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
const findStatusIcon = () => wrapper.findComponent(StatusIcon);
|
||||
|
||||
const createComponent = ({ propsData, slots } = {}) => {
|
||||
wrapper = shallowMountExtended(WidgetContentSection, {
|
||||
propsData: {
|
||||
widgetName: 'MyWidget',
|
||||
...propsData,
|
||||
},
|
||||
slots,
|
||||
});
|
||||
};
|
||||
|
||||
it('does not render the status icon when it is not provided', () => {
|
||||
createComponent();
|
||||
expect(findStatusIcon().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('renders the status icon when provided', () => {
|
||||
createComponent({ propsData: { statusIconName: 'failed' } });
|
||||
expect(findStatusIcon().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders the default slot', () => {
|
||||
createComponent({
|
||||
slots: {
|
||||
default: 'Hello world',
|
||||
},
|
||||
});
|
||||
|
||||
expect(wrapper.findByText('Hello world').exists()).toBe(true);
|
||||
});
|
||||
});
|
|
@ -43,7 +43,7 @@ describe('MR Widget', () => {
|
|||
createComponent({ propsData: { fetchCollapsedData } });
|
||||
await waitForPromises();
|
||||
expect(fetchCollapsedData).toHaveBeenCalled();
|
||||
expect(wrapper.vm.error).toBe(null);
|
||||
expect(wrapper.vm.summaryError).toBe(null);
|
||||
});
|
||||
|
||||
it('sets the error text when fetch method fails', async () => {
|
||||
|
|
|
@ -258,6 +258,21 @@ RSpec.describe TodosHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#no_todos_messages' do
|
||||
context 'when getting todos messsages' do
|
||||
it 'return these sentences' do
|
||||
expected_sentences = [
|
||||
s_('Todos|Good job! Looks like you don\'t have anything left on your To-Do List'),
|
||||
s_('Todos|Isn\'t an empty To-Do List beautiful?'),
|
||||
s_('Todos|Give yourself a pat on the back!'),
|
||||
s_('Todos|Nothing left to do. High five!'),
|
||||
s_('Todos|Henceforth, you shall be known as "To-Do Destroyer"')
|
||||
]
|
||||
expect(helper.no_todos_messages).to eq(expected_sentences)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#todo_author_display?' do
|
||||
using RSpec::Parameterized::TableSyntax
|
||||
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
# frozen_string_literal: true
|
||||
require 'fast_spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Analytics::DateFiller do
|
||||
let(:default_value) { 0 }
|
||||
let(:formatter) { Gitlab::Analytics::DateFiller::DEFAULT_DATE_FORMATTER }
|
||||
|
||||
subject(:filler_result) do
|
||||
described_class.new(data,
|
||||
from: from,
|
||||
to: to,
|
||||
period: period,
|
||||
default_value: default_value,
|
||||
date_formatter: formatter).fill.to_a
|
||||
end
|
||||
|
||||
context 'when unknown period is given' do
|
||||
it 'raises error' do
|
||||
input = { 3.days.ago.to_date => 10, Date.today => 5 }
|
||||
|
||||
expect do
|
||||
described_class.new(input, from: 4.days.ago, to: Date.today, period: :unknown).fill
|
||||
end.to raise_error(/Unknown period given/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when period=:day' do
|
||||
let(:from) { Date.new(2021, 5, 25) }
|
||||
let(:to) { Date.new(2021, 6, 5) }
|
||||
let(:period) { :day }
|
||||
|
||||
let(:expected_result) do
|
||||
{
|
||||
Date.new(2021, 5, 25) => 1,
|
||||
Date.new(2021, 5, 26) => default_value,
|
||||
Date.new(2021, 5, 27) => default_value,
|
||||
Date.new(2021, 5, 28) => default_value,
|
||||
Date.new(2021, 5, 29) => default_value,
|
||||
Date.new(2021, 5, 30) => default_value,
|
||||
Date.new(2021, 5, 31) => default_value,
|
||||
Date.new(2021, 6, 1) => default_value,
|
||||
Date.new(2021, 6, 2) => default_value,
|
||||
Date.new(2021, 6, 3) => 10,
|
||||
Date.new(2021, 6, 4) => default_value,
|
||||
Date.new(2021, 6, 5) => default_value
|
||||
}
|
||||
end
|
||||
|
||||
let(:data) do
|
||||
{
|
||||
Date.new(2021, 6, 3) => 10, # deliberatly not sorted
|
||||
Date.new(2021, 5, 27) => nil,
|
||||
Date.new(2021, 5, 25) => 1
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to eq(expected_result.to_a) }
|
||||
|
||||
context 'when a custom default value is given' do
|
||||
let(:default_value) { 'MISSING' }
|
||||
|
||||
it do
|
||||
is_expected.to eq(expected_result.to_a)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a custom date formatter is given' do
|
||||
let(:formatter) { -> (date) { date.to_s } }
|
||||
|
||||
it do
|
||||
expected_result.transform_keys!(&:to_s)
|
||||
|
||||
is_expected.to eq(expected_result.to_a)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the data contains dates outside of the requested period' do
|
||||
before do
|
||||
data[Date.new(2022, 6, 1)] = 5
|
||||
end
|
||||
|
||||
it 'raises error' do
|
||||
expect { filler_result }.to raise_error(/Input contains values which doesn't/)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when period=:week' do
|
||||
let(:from) { Date.new(2021, 5, 16) }
|
||||
let(:to) { Date.new(2021, 6, 7) }
|
||||
let(:period) { :week }
|
||||
let(:data) do
|
||||
{
|
||||
Date.new(2021, 5, 24) => nil,
|
||||
Date.new(2021, 6, 7) => 10
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected_result) do
|
||||
{
|
||||
Date.new(2021, 5, 10) => 0,
|
||||
Date.new(2021, 5, 17) => 0,
|
||||
Date.new(2021, 5, 24) => 0,
|
||||
Date.new(2021, 5, 31) => 0,
|
||||
Date.new(2021, 6, 7) => 10
|
||||
}
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq(expected_result.to_a)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when period=:month' do
|
||||
let(:from) { Date.new(2021, 5, 1) }
|
||||
let(:to) { Date.new(2021, 7, 1) }
|
||||
let(:period) { :month }
|
||||
let(:data) do
|
||||
{
|
||||
Date.new(2021, 5, 1) => 100
|
||||
}
|
||||
end
|
||||
|
||||
let(:expected_result) do
|
||||
{
|
||||
Date.new(2021, 5, 1) => 100,
|
||||
Date.new(2021, 6, 1) => 0,
|
||||
Date.new(2021, 7, 1) => 0
|
||||
}
|
||||
end
|
||||
|
||||
it do
|
||||
is_expected.to eq(expected_result.to_a)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Usage::Metrics::Instrumentations::CountUserAuthMetric do
|
||||
context 'with all time frame' do
|
||||
let(:expected_value) { 2 }
|
||||
|
||||
before do
|
||||
user = create(:user)
|
||||
user2 = create(:user)
|
||||
create(:authentication_event, user: user, provider: :ldapmain, result: :success)
|
||||
create(:authentication_event, user: user2, provider: :ldapsecondary, result: :success)
|
||||
create(:authentication_event, user: user2, provider: :group_saml, result: :success)
|
||||
create(:authentication_event, user: user2, provider: :group_saml, result: :success)
|
||||
create(:authentication_event, user: user, provider: :group_saml, result: :failed)
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: 'all', data_source: 'database' }
|
||||
end
|
||||
|
||||
context 'with 28d time frame' do
|
||||
let(:expected_value) { 1 }
|
||||
|
||||
before do
|
||||
user = create(:user)
|
||||
user2 = create(:user)
|
||||
|
||||
create(:authentication_event, created_at: 1.year.ago, user: user, provider: :ldapmain, result: :success)
|
||||
create(:authentication_event, created_at: 1.week.ago, user: user2, provider: :ldapsecondary, result: :success)
|
||||
end
|
||||
|
||||
it_behaves_like 'a correct instrumented metric value', { time_frame: '28d', data_source: 'database' }
|
||||
end
|
||||
end
|
|
@ -37,6 +37,10 @@ RSpec.describe Gitlab::UsageDataMetrics do
|
|||
expect(subject[:usage_activity_by_stage][:plan]).to include(:issues)
|
||||
end
|
||||
|
||||
it 'includes usage_activity_by_stage metrics' do
|
||||
expect(subject[:usage_activity_by_stage][:manage]).to include(:count_user_auth)
|
||||
end
|
||||
|
||||
it 'includes usage_activity_by_stage_monthly keys' do
|
||||
expect(subject[:usage_activity_by_stage_monthly][:plan]).to include(:issues)
|
||||
end
|
||||
|
|
|
@ -215,14 +215,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
|
|||
groups: 2,
|
||||
users_created: 10,
|
||||
omniauth_providers: ['google_oauth2'],
|
||||
user_auth_by_provider: { 'group_saml' => 2, 'ldap' => 4, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
|
||||
user_auth_by_provider: {
|
||||
'group_saml' => 2,
|
||||
'ldap' => 4,
|
||||
'standard' => 0,
|
||||
'two-factor' => 0,
|
||||
'two-factor-via-u2f-device' => 0,
|
||||
"two-factor-via-webauthn-device" => 0
|
||||
}
|
||||
)
|
||||
expect(described_class.usage_activity_by_stage_manage(described_class.monthly_time_range_db_params)).to include(
|
||||
events: be_within(error_rate).percent_of(2),
|
||||
groups: 1,
|
||||
users_created: 6,
|
||||
omniauth_providers: ['google_oauth2'],
|
||||
user_auth_by_provider: { 'group_saml' => 1, 'ldap' => 2, 'standard' => 0, 'two-factor' => 0, 'two-factor-via-u2f-device' => 0, "two-factor-via-webauthn-device" => 0 }
|
||||
user_auth_by_provider: {
|
||||
'group_saml' => 1,
|
||||
'ldap' => 2,
|
||||
'standard' => 0,
|
||||
'two-factor' => 0,
|
||||
'two-factor-via-u2f-device' => 0,
|
||||
"two-factor-via-webauthn-device" => 0
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -1023,6 +1023,22 @@ RSpec.describe API::MergeRequests do
|
|||
it_behaves_like 'a non-cached MergeRequest api request', 1
|
||||
end
|
||||
|
||||
context 'when the assignees change' do
|
||||
before do
|
||||
merge_request.assignees << create(:user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a non-cached MergeRequest api request', 1
|
||||
end
|
||||
|
||||
context 'when the reviewers change' do
|
||||
before do
|
||||
merge_request.reviewers << create(:user)
|
||||
end
|
||||
|
||||
it_behaves_like 'a non-cached MergeRequest api request', 1
|
||||
end
|
||||
|
||||
context 'when another user requests' do
|
||||
before do
|
||||
sign_in(user2)
|
||||
|
|
Loading…
Reference in New Issue