Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-08-17 12:13:49 +00:00
parent e750680e89
commit 458b945df3
22 changed files with 414 additions and 291 deletions

View File

@ -1,5 +1,12 @@
<script>
import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
import {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
GlSafeHtmlDirective,
GlSprintf,
} from '@gitlab/ui';
import { formatDate } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import { getEventIcon } from './utils';
@ -12,6 +19,7 @@ export default {
timeUTC: __('%{time} UTC'),
},
components: {
GlButton,
GlDropdown,
GlDropdownItem,
GlIcon,
@ -83,7 +91,7 @@ export default {
no-caret
>
<gl-dropdown-item @click="$emit('delete')">
{{ $options.i18n.delete }}
<gl-button>{{ $options.i18n.delete }}</gl-button>
</gl-dropdown-item>
</gl-dropdown>
</div>

View File

@ -4,7 +4,7 @@ import { createAlert } from '~/flash';
import { sprintf } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import IncidentTimelineEventListItem from './timeline_events_list_item.vue';
import IncidentTimelineEventItem from './timeline_events_item.vue';
import deleteTimelineEvent from './graphql/queries/delete_timeline_event.mutation.graphql';
import { timelineListI18n } from './constants';
@ -12,7 +12,7 @@ export default {
name: 'IncidentTimelineEventList',
i18n: timelineListI18n,
components: {
IncidentTimelineEventListItem,
IncidentTimelineEventItem,
},
props: {
timelineEventLoading: {
@ -99,16 +99,21 @@ export default {
<div class="gl-pb-3 gl-border-gray-50 gl-border-1 gl-border-b-solid">
<strong class="gl-font-size-h2" data-testid="event-date">{{ eventDate }}</strong>
</div>
<ul class="notes main-notes-list gl-pl-n3">
<incident-timeline-event-list-item
<ul class="notes main-notes-list">
<li
v-for="(event, eventIndex) in events"
:key="event.id"
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
:is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
@delete="handleDelete(event)"
/>
:key="eventIndex"
class="timeline-entry-vertical-line note system-note note-wrapper gl-my-2! gl-pr-0!"
>
<incident-timeline-event-item
:key="event.id"
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
:is-last-item="isLastItem(dateGroupedEvents, groupIndex, events, eventIndex)"
@delete="handleDelete(event)"
/>
</li>
</ul>
</div>
</div>

View File

@ -26,33 +26,104 @@ function addBlockTags(blockTag, selected) {
return `${blockTag}\n${selected}\n${blockTag}`;
}
function lineBefore(text, textarea, trimNewlines = true) {
let split = text.substring(0, textarea.selectionStart);
if (trimNewlines) {
split = split.trim();
}
/**
* Returns the line of text that is before the first line
* of the current selection
*
* @param {String} text - the text of the targeted text area
* @param {Object} textArea - the targeted text area
* @returns {String}
*/
function lineBeforeSelection(text, textArea) {
let split = text.substring(0, textArea.selectionStart);
split = split.split('\n');
return split[split.length - 1];
// Last item, at -1, is the line where the start of selection is.
// Line before selection is therefore at -2
const lineBefore = split[split.length - 2];
return lineBefore === undefined ? '' : lineBefore;
}
function lineAfter(text, textarea, trimNewlines = true) {
let split = text.substring(textarea.selectionEnd);
if (trimNewlines) {
split = split.trim();
} else {
// remove possible leading newline to get at the real line
split = split.replace(/^\n/, '');
}
/**
* Returns the line of text that is after the last line
* of the current selection
*
* @param {String} text - the text of the targeted text area
* @param {Object} textArea - the targeted text area
* @returns {String}
*/
function lineAfterSelection(text, textArea) {
let split = text.substring(textArea.selectionEnd);
// remove possible leading newline to get at the real line
split = split.replace(/^\n/, '');
split = split.split('\n');
return split[0];
}
/**
* Returns the text lines that encompass the current selection
*
* @param {Object} textArea - the targeted text area
* @returns {Object}
*/
function linesFromSelection(textArea) {
const text = textArea.value;
const { selectionStart, selectionEnd } = textArea;
let startPos = text[selectionStart] === '\n' ? selectionStart - 1 : selectionStart;
startPos = text.lastIndexOf('\n', startPos) + 1;
let endPos = selectionEnd === selectionStart ? selectionEnd : selectionEnd - 1;
endPos = text.indexOf('\n', endPos);
if (endPos < 0) endPos = text.length;
const selectedRange = text.substring(startPos, endPos);
const lines = selectedRange.split('\n');
return {
lines,
selectionStart,
selectionEnd,
startPos,
endPos,
};
}
/**
* Set the selection of a textarea such that it maintains the
* previous selection before the lines were indented/outdented
*
* @param {Object} textArea - the targeted text area
* @param {Number} selectionStart - start position of original selection
* @param {Number} selectionEnd - end position of original selection
* @param {Number} lineStart - start pos of first line
* @param {Number} firstLineChange - number of characters changed on first line
* @param {Number} totalChanged - total number of characters changed
*/
function setNewSelectionRange(
textArea,
selectionStart,
selectionEnd,
lineStart,
firstLineChange,
totalChanged,
) {
let newStart = Math.max(lineStart, selectionStart + firstLineChange);
let newEnd = Math.max(lineStart, selectionEnd + totalChanged);
if (selectionStart === selectionEnd) {
newEnd = newStart;
} else if (selectionStart === lineStart) {
newStart = lineStart;
}
textArea.setSelectionRange(newStart, newEnd);
}
function convertMonacoSelectionToAceFormat(sel) {
return {
start: {
@ -95,7 +166,8 @@ function editorBlockTagText(text, blockTag, selected, editor) {
function blockTagText(text, textArea, blockTag, selected) {
const shouldRemoveBlock =
lineBefore(text, textArea) === blockTag && lineAfter(text, textArea) === blockTag;
lineBeforeSelection(text, textArea) === blockTag &&
lineAfterSelection(text, textArea) === blockTag;
if (shouldRemoveBlock) {
// To remove the block tag we have to select the line before & after
@ -314,66 +386,6 @@ function updateText({ textArea, tag, cursorOffset, blockTag, wrap, select, tagCo
});
}
/**
* Returns the text lines that encompass the current selection
*
* @param {Object} textArea - the targeted text area
* @returns {Object}
*/
function linesFromSelection(textArea) {
const text = textArea.value;
const { selectionStart, selectionEnd } = textArea;
let startPos = text[selectionStart] === '\n' ? selectionStart - 1 : selectionStart;
startPos = text.lastIndexOf('\n', startPos) + 1;
let endPos = selectionEnd === selectionStart ? selectionEnd : selectionEnd - 1;
endPos = text.indexOf('\n', endPos);
if (endPos < 0) endPos = text.length;
const selectedRange = text.substring(startPos, endPos);
const lines = selectedRange.split('\n');
return {
lines,
selectionStart,
selectionEnd,
startPos,
endPos,
};
}
/**
* Set the selection of a textarea such that it maintains the
* previous selection before the lines were indented/outdented
*
* @param {Object} textArea - the targeted text area
* @param {Number} selectionStart - start position of original selection
* @param {Number} selectionEnd - end position of original selection
* @param {Number} lineStart - start pos of first line
* @param {Number} firstLineChange - number of characters changed on first line
* @param {Number} totalChanged - total number of characters changed
*/
function setNewSelectionRange(
textArea,
selectionStart,
selectionEnd,
lineStart,
firstLineChange,
totalChanged,
) {
let newStart = Math.max(lineStart, selectionStart + firstLineChange);
let newEnd = Math.max(lineStart, selectionEnd + totalChanged);
if (selectionStart === selectionEnd) {
newEnd = newStart;
} else if (selectionStart === lineStart) {
newStart = lineStart;
}
textArea.setSelectionRange(newStart, newEnd);
}
/**
* Indents selected lines to the right by 2 spaces
*
@ -501,13 +513,13 @@ function handleSurroundSelectedText(e, textArea) {
/**
* Returns the content for a new line following a list item.
*
* @param {Object} result - regex match of the current line
* @param {Object?} nextLineResult - regex match of the next line
* @param {Object} listLineMatch - regex match of the current line
* @param {Object?} nextLineMatch - regex match of the next line
* @returns string with the new list item
*/
function continueOlText(result, nextLineResult) {
const { indent, leader } = result.groups;
const { indent: nextIndent, isOl: nextIsOl } = nextLineResult?.groups ?? {};
function continueOlText(listLineMatch, nextLineMatch) {
const { indent, leader } = listLineMatch.groups;
const { indent: nextIndent, isOl: nextIsOl } = nextLineMatch?.groups ?? {};
const [numStr, postfix = ''] = leader.split('.');
@ -521,20 +533,20 @@ function handleContinueList(e, textArea) {
if (!(e.key === 'Enter')) return;
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
if (textArea.selectionStart !== textArea.selectionEnd) return;
// prevent unintended line breaks were inserted using Japanese IME on MacOS
// prevent unintended line breaks inserted using Japanese IME on MacOS
if (compositioningNoteText) return;
const currentLine = lineBefore(textArea.value, textArea, false);
const result = currentLine.match(LIST_LINE_HEAD_PATTERN);
const firstSelectedLine = linesFromSelection(textArea).lines[0];
const listLineMatch = firstSelectedLine.match(LIST_LINE_HEAD_PATTERN);
if (result) {
const { leader, indent, content, isOl } = result.groups;
const prevLineEmpty = !content;
if (listLineMatch) {
const { leader, indent, content, isOl } = listLineMatch.groups;
const emptyListItem = !content;
if (prevLineEmpty) {
// erase previous empty list item - select the text and allow the
// natural line feed erase the text
textArea.selectionStart = textArea.selectionStart - result[0].length;
if (emptyListItem) {
// erase empty list item - select the text and allow the
// natural line feed to erase the text
textArea.selectionStart = textArea.selectionStart - listLineMatch[0].length;
return;
}
@ -542,12 +554,12 @@ function handleContinueList(e, textArea) {
// Behaviors specific to either `ol` or `ul`
if (isOl) {
const nextLine = lineAfter(textArea.value, textArea, false);
const nextLineResult = nextLine.match(LIST_LINE_HEAD_PATTERN);
const nextLine = lineAfterSelection(textArea.value, textArea);
const nextLineMatch = nextLine.match(LIST_LINE_HEAD_PATTERN);
itemToInsert = continueOlText(result, nextLineResult);
itemToInsert = continueOlText(listLineMatch, nextLineMatch);
} else {
if (currentLine.match(HR_PATTERN)) return;
if (firstSelectedLine.match(HR_PATTERN)) return;
itemToInsert = `${indent}${leader}`;
}

View File

@ -1,10 +1,11 @@
<script>
import { s__ } from '~/locale';
import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
import RunnerStatusStat from './runner_status_stat.vue';
export default {
components: {
RunnerStatusStat,
RunnerSingleStat,
RunnerUpgradeStatusStats: () =>
import('ee_component/runner/components/stat/runner_upgrade_status_stats.vue'),
},
@ -19,24 +20,58 @@ export default {
default: () => ({}),
},
},
computed: {
stats() {
return [
{
key: STATUS_ONLINE,
props: {
skip: this.statusCountSkip(STATUS_ONLINE),
variables: { ...this.variables, status: STATUS_ONLINE },
variant: 'success',
title: s__('Runners|Online runners'),
metaText: s__('Runners|online'),
},
},
{
key: STATUS_OFFLINE,
props: {
skip: this.statusCountSkip(STATUS_OFFLINE),
variables: { ...this.variables, status: STATUS_OFFLINE },
variant: 'muted',
title: s__('Runners|Offline runners'),
metaText: s__('Runners|offline'),
},
},
{
key: STATUS_STALE,
props: {
skip: this.statusCountSkip(STATUS_STALE),
variables: { ...this.variables, status: STATUS_STALE },
variant: 'warning',
title: s__('Runners|Stale runners'),
metaText: s__('Runners|stale'),
},
},
];
},
},
methods: {
statusCountSkip(status) {
// Show an empty result when we already filter by another status
return this.variables.status && this.variables.status !== status;
},
},
STATUS_LIST: [STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE],
};
</script>
<template>
<div class="gl-display-flex gl-flex-wrap gl-py-6">
<runner-status-stat
v-for="status in $options.STATUS_LIST"
:key="status"
class="gl-px-5"
:variables="variables"
<runner-single-stat
v-for="stat in stats"
:key="stat.key"
:scope="scope"
:status="status"
v-bind="stat.props"
class="gl-px-5"
/>
<runner-upgrade-status-stats

View File

@ -1,72 +0,0 @@
<script>
import { s__ } from '~/locale';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
import RunnerSingleStat from './runner_single_stat.vue';
export default {
components: {
RunnerSingleStat,
},
props: {
scope: {
type: String,
required: true,
},
variables: {
type: Object,
required: false,
default: () => ({}),
},
status: {
type: String,
required: true,
},
},
computed: {
countVariables() {
return { ...this.variables, status: this.status };
},
skip() {
// Status are mutually exclusive, skip displaying this total
// when filtering by an status different to this one
const { status } = this.variables;
return status && status !== this.status;
},
statProps() {
switch (this.status) {
case STATUS_ONLINE:
return {
variant: 'success',
title: s__('Runners|Online runners'),
metaText: s__('Runners|online'),
};
case STATUS_OFFLINE:
return {
variant: 'muted',
title: s__('Runners|Offline runners'),
metaText: s__('Runners|offline'),
};
case STATUS_STALE:
return {
variant: 'warning',
title: s__('Runners|Stale runners'),
metaText: s__('Runners|stale'),
};
default:
return {
title: s__('Runners|Runners'),
};
}
},
},
};
</script>
<template>
<runner-single-stat
v-if="statProps"
v-bind="statProps"
:scope="scope"
:variables="countVariables"
:skip="skip"
/>
</template>

View File

@ -22,7 +22,8 @@ module Users
namespace_storage_limit_banner_warning_threshold: 11, # EE-only
namespace_storage_limit_banner_alert_threshold: 12, # EE-only
namespace_storage_limit_banner_error_threshold: 13, # EE-only
usage_quota_trial_alert: 14 # EE-only
usage_quota_trial_alert: 14, # EE-only
preview_usage_quota_free_plan_alert: 15 # EE-only
}
validates :group, presence: true

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
presents ::ProjectHook, as: :project_hook
presents ::ProjectHook
def logs_details_path(log)
project_hook_hook_log_path(project, self, log)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
presents ::ServiceHook, as: :service_hook
presents ::ServiceHook
def logs_details_path(log)
project_settings_integration_hook_log_path(integration.project, integration, log)

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
presents ::WebHookLog, as: :web_hook_log
presents ::WebHookLog
def details_path
web_hook.present.logs_details_path(self)

View File

@ -1422,7 +1422,7 @@ To delete the LDAP group link, provide either a `cn` or a `filter`, but not both
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/290367) in GitLab 15.3.
List, add, and delete SAML group links.
List, get, add, and delete SAML group links.
### List SAML group links
@ -1432,9 +1432,78 @@ Lists SAML group links.
GET /groups/:id/saml_group_links
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
Supported attributes:
| Attribute | Type | Required | Description |
|:----------|:---------------|:---------|:-------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
If successful, returns [`200`](index.md#status-codes) and the following
response attributes:
| Attribute | Type | Description |
|:-------------------|:-------|:-------------------------------------------------------------------------------------|
| `[].name` | string | Name of the SAML group |
| `[].access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/saml_group_links"
```
Example response:
```json
[
{
"name": "saml-group-1",
"access_level": "Guest"
},
{
"name": "saml-group-2",
"access_level": "Maintainer"
}
]
```
### Get SAML group link
Get a SAML group link for the group.
```plaintext
GET /groups/:id/saml_group_links/:saml_group_name
```
Supported attributes:
| Attribute | Type | Required | Description |
|:-------------------|:---------------|:---------|:-------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `saml_group_name` | string | yes | Name of an SAML group |
If successful, returns [`200`](index.md#status-codes) and the following
response attributes:
| Attribute | Type | Description |
|:---------------|:-------|:-------------------------------------------------------------------------------------|
| `name` | string | Name of the SAML group |
| `access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group |
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/saml_group_links/saml-group-1"
```
Example response:
```json
{
"name": "saml-group-1",
"access_level": "Guest"
}
```
### Add SAML group link
@ -1444,11 +1513,36 @@ Adds a SAML group link for a group.
POST /groups/:id/saml_group_links
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `saml_group_name` | string | yes | The name of a SAML group |
| `access_level` | string | yes | Minimum [access level](members.md#valid-access-levels) for members of the SAML group |
Supported attributes:
| Attribute | Type | Required | Description |
|:-------------------|:---------------|:---------|:-------------------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `saml_group_name` | string | yes | Name of a SAML group |
| `access_level` | string | yes | Minimum [access level](members.md#valid-access-levels) for members of the SAML group |
If successful, returns [`201`](index.md#status-codes) and the following
response attributes:
| Attribute | Type | Description |
|:---------------|:-------|:-------------------------------------------------------------------------------------|
| `name` | string | Name of the SAML group |
| `access_level` | string | Minimum [access level](members.md#valid-access-levels) for members of the SAML group |
Example request:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/saml_group_links"
```
Example response:
```json
{
"name": "saml-group-1",
"access_level": "Guest"
}
```
### Delete SAML group link
@ -1458,10 +1552,20 @@ Deletes a SAML group link for the group.
DELETE /groups/:id/saml_group_links/:saml_group_name
```
| Attribute | Type | Required | Description |
| --------- | -------------- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `saml_group_name` | string | yes | The name of an SAML group |
Supported attributes:
| Attribute | Type | Required | Description |
|:-------------------|:---------------|:---------|:-------------------------------------------------------------------------|
| `id` | integer/string | yes | ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `saml_group_name` | string | yes | Name of an SAML group |
If successful, returns [`204`](index.md#status-codes) status code without any response body.
Example request:
```shell
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/saml_group_links/saml-group-1"
```
## Namespaces in groups

View File

@ -169,6 +169,20 @@ By default, impersonation is enabled. GitLab can be configured to [disable imper
![user impersonation button](img/impersonate_user_button_v13_8.png)
#### User identities
> The ability to see a user's SCIM identity was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/294608) in GitLab 15.3.
When using authentication providers, administrators can see the identities for a user:
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Overview > Users**.
1. From the list of users, select a user.
1. Select **Identities**.
This list shows the user's identities, including SCIM identities. Administrators can use this information to troubleshoot SCIM-related issues and confirm
the identities being used for an account.
#### User Permission Export **(PREMIUM SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1772) in GitLab 13.8.

View File

@ -220,6 +220,8 @@ It is important that this SCIM `id` and SCIM `externalId` are configured to the
### How do I verify user's SAML NameId matches the SCIM externalId
Admins can use the Admin Area to [list SCIM identities for a user](../../admin_area/#user-identities).
Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page.
A possible alternative is to use the [SCIM API](../../../api/scim.md#get-a-list-of-scim-provisioned-users) to manually retrieve the `externalId` we have stored for users, also called the `external_uid` or `NameId`.

View File

@ -154,5 +154,8 @@ to groups instead of projects. Bot users for groups:
- Do not count as licensed seats.
- Can have a maximum role of Owner for a group. For more information, see
[Create a group access token](../../../api/group_access_tokens.md#create-a-group-access-token).
- The username is set to `group_{project_id}_bot` for the first access token. For example, `project_123_bot`.
- The email is set to `group{group_id}_bot@noreply.{Gitlab.config.gitlab.host}`. For example, `group123_bot@noreply.example.com`.
- All other properties are similar to [bot users for projects](../../project/settings/project_access_tokens.md#bot-users-for-projects)
For more information, see [Bot users for projects](../../project/settings/project_access_tokens.md#bot-users-for-projects).

View File

@ -88,7 +88,7 @@ module API
requires :branch, type: String, desc: 'Name of the branch to commit into. To create a new branch, also provide either `start_branch` or `start_sha`, and optionally `start_project`.', allow_blank: false
requires :commit_message, type: String, desc: 'Commit message'
requires :actions, type: Array, desc: 'Actions to perform in commit' do
requires :action, type: String, desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze
requires :action, type: String, desc: 'The action to perform, `create`, `delete`, `move`, `update`, `chmod`', values: %w[create update move delete chmod].freeze, allow_blank: false
requires :file_path, type: String, desc: 'Full path to the file. Ex. `lib/class.rb`'
given action: ->(action) { action == 'move' } do
requires :previous_path, type: String, desc: 'Original full path to the file being moved. Ex. `lib/class1.rb`'

View File

@ -102,19 +102,19 @@ module Gitlab
# AlertManagement::Alert directly for read operations.
def alert_params
{
description: description&.truncate(::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
description: truncate(description, ::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
ended_at: ends_at,
environment: environment,
fingerprint: gitlab_fingerprint,
hosts: truncate_hosts(Array(hosts).flatten),
monitoring_tool: monitoring_tool&.truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH),
monitoring_tool: truncate(monitoring_tool, ::AlertManagement::Alert::TOOL_MAX_LENGTH),
payload: payload,
project_id: project.id,
prometheus_alert: gitlab_alert,
service: service&.truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH),
service: truncate(service, ::AlertManagement::Alert::SERVICE_MAX_LENGTH),
severity: severity,
started_at: starts_at,
title: title&.truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH)
title: truncate(title, ::AlertManagement::Alert::TITLE_MAX_LENGTH)
}.transform_values(&:presence).compact
end
@ -161,6 +161,10 @@ module Gitlab
SEVERITY_MAPPING
end
def truncate(value, length)
value.to_s.truncate(length)
end
def truncate_hosts(hosts)
return hosts if hosts.join.length <= ::AlertManagement::Alert::HOSTS_MAX_LENGTH

View File

@ -6285,9 +6285,6 @@ msgstr ""
msgid "Billing|Export list"
msgstr ""
msgid "Billing|From October 19, 2022, free groups will be limited to 5 members"
msgstr ""
msgid "Billing|Group invite"
msgstr ""
@ -6329,9 +6326,6 @@ msgstr ""
msgid "Billing|You are about to remove user %{username} from your subscription. If you continue, the user will be removed from the %{namespace} group and all its subgroups and projects. This action can't be undone."
msgstr ""
msgid "Billing|You can begin moving members in %{namespaceName} now. A member loses access to the group when you turn off %{strongStart}In a seat%{strongEnd}. If over 5 members have %{strongStart}In a seat%{strongEnd} enabled after October 19, 2022, we'll select the 5 members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach 5 members. The remaining members will get a status of Over limit and lose access to the group."
msgstr ""
msgid "Billing|Your group recently changed to use the Free plan. %{over_limit_message} You can free up space for new members by removing those who no longer need access or toggling them to over-limit. To get an unlimited number of members, you can %{link_start}upgrade%{link_end} to a paid tier."
msgstr ""
@ -44690,6 +44684,11 @@ msgstr ""
msgid "You can always edit this later"
msgstr ""
msgid "You can begin moving members in %{namespace_name} now. A member loses access to the group when you turn off %{strong_start}In a seat%{strong_end}. If over %{free_user_limit} member has %{strong_start}In a seat%{strong_end} enabled after October 19, 2022, we'll select the %{free_user_limit} member who maintains access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach %{free_user_limit} member. The remaining members will get a status of Over limit and lose access to the group."
msgid_plural "You can begin moving members in %{namespace_name} now. A member loses access to the group when you turn off %{strong_start}In a seat%{strong_end}. If over %{free_user_limit} members have %{strong_start}In a seat%{strong_end} enabled after October 19, 2022, we'll select the %{free_user_limit} members who maintain access. We'll first count members that have Owner and Maintainer roles, then the most recently active members until we reach %{free_user_limit} members. The remaining members will get a status of Over limit and lose access to the group."
msgstr[0] ""
msgstr[1] ""
msgid "You can check it in your %{pat_link_start}personal access tokens%{pat_link_end} settings."
msgstr ""

View File

@ -2,7 +2,7 @@ import timezoneMock from 'timezone-mock';
import { GlIcon, GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
import IncidentTimelineEventItem from '~/issues/show/components/incidents/timeline_events_item.vue';
import { mockEvents } from './mock_data';
describe('IncidentTimelineEventList', () => {
@ -10,7 +10,7 @@ describe('IncidentTimelineEventList', () => {
const mountComponent = ({ propsData, provide } = {}) => {
const { action, noteHtml, occurredAt } = mockEvents[0];
wrapper = mountExtended(IncidentTimelineEventListItem, {
wrapper = mountExtended(IncidentTimelineEventItem, {
propsData: {
action,
noteHtml,

View File

@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import IncidentTimelineEventList from '~/issues/show/components/incidents/timeline_events_list.vue';
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_list_item.vue';
import IncidentTimelineEventListItem from '~/issues/show/components/incidents/timeline_events_item.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import deleteTimelineEventMutation from '~/issues/show/components/incidents/graphql/queries/delete_timeline_event.mutation.graphql';

View File

@ -1,13 +1,13 @@
import { shallowMount, mount } from '@vue/test-utils';
import { s__ } from '~/locale';
import RunnerStats from '~/runner/components/stat/runner_stats.vue';
import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
import { INSTANCE_TYPE, STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '~/runner/constants';
describe('RunnerStats', () => {
let wrapper;
const findStatusStats = () => wrapper.findAllComponents(RunnerStatusStat).wrappers;
const findSingleStats = () => wrapper.findAllComponents(RunnerSingleStat).wrappers;
const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerStats, {
@ -55,8 +55,8 @@ describe('RunnerStats', () => {
const mockVariables = { paused: true };
createComponent({ props: { variables: mockVariables } });
findStatusStats().forEach((stat) => {
expect(stat.props('variables')).toEqual(mockVariables);
findSingleStats().forEach((stat) => {
expect(stat.props('variables')).toMatchObject(mockVariables);
});
});
});

View File

@ -1,66 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import RunnerStatusStat from '~/runner/components/stat/runner_status_stat.vue';
import RunnerSingleStat from '~/runner/components/stat/runner_single_stat.vue';
import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE, INSTANCE_TYPE } from '~/runner/constants';
describe('RunnerStatusStat', () => {
let wrapper;
const findRunnerSingleStat = () => wrapper.findComponent(RunnerSingleStat);
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(RunnerStatusStat, {
propsData: {
scope: INSTANCE_TYPE,
status: STATUS_ONLINE,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe.each`
status | variant | title | metaText
${STATUS_ONLINE} | ${'success'} | ${'Online runners'} | ${'online'}
${STATUS_OFFLINE} | ${'muted'} | ${'Offline runners'} | ${'offline'}
${STATUS_STALE} | ${'warning'} | ${'Stale runners'} | ${'stale'}
`('Renders a stat for status "$status"', ({ status, variant, title, metaText }) => {
beforeEach(() => {
createComponent({ props: { status } });
});
it('Renders text', () => {
expect(findRunnerSingleStat().attributes()).toMatchObject({
variant,
title,
metatext: metaText,
});
});
it('Passes filters', () => {
expect(findRunnerSingleStat().props('variables')).toEqual({ status });
});
it('Does not skip query with no filters', () => {
expect(findRunnerSingleStat().props('skip')).toEqual(false);
});
});
it('Merges filters', () => {
createComponent({ props: { status: STATUS_ONLINE, variables: { paused: true } } });
expect(findRunnerSingleStat().props('variables')).toEqual({
status: STATUS_ONLINE,
paused: true,
});
});
it('Skips query when other status is in the filters', () => {
createComponent({ props: { status: STATUS_ONLINE, variables: { status: STATUS_OFFLINE } } });
expect(findRunnerSingleStat().props('skip')).toEqual(true);
});
});

View File

@ -228,6 +228,46 @@ RSpec.describe Gitlab::AlertManagement::Payload::Base do
it { is_expected.to eq({ hosts: shortened_hosts, project_id: project.id }) }
end
end
context 'with present, non-string values for string fields' do
let_it_be(:stubs) do
{
description: { "description" => "description" },
monitoring_tool: ['datadog', 5],
service: 4356875,
title: true
}
end
before do
allow(parsed_payload).to receive_messages(stubs)
end
it 'casts values to strings' do
is_expected.to eq({
description: "{\"description\"=>\"description\"}",
monitoring_tool: "[\"datadog\", 5]",
service: '4356875',
project_id: project.id,
title: "true"
})
end
end
context 'with blank values for string fields' do
let_it_be(:stubs) do
{
description: nil,
monitoring_tool: '',
service: {},
title: []
}
end
it 'leaves the fields blank' do
is_expected.to eq({ project_id: project.id })
end
end
end
describe '#gitlab_fingerprint' do

View File

@ -979,6 +979,40 @@ RSpec.describe API::Commits do
end
end
context 'when action is missing' do
let(:params) do
{
branch: 'master',
commit_message: 'Invalid',
actions: [{ action: nil, file_path: 'files/ruby/popen.rb' }]
}
end
it 'responds with 400 bad request' do
post api(url, user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('actions[0][action] is empty')
end
end
context 'when action is not supported' do
let(:params) do
{
branch: 'master',
commit_message: 'Invalid',
actions: [{ action: 'unknown', file_path: 'files/ruby/popen.rb' }]
}
end
it 'responds with 400 bad request' do
post api(url, user), params: params
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq('actions[0][action] does not have a valid value')
end
end
context 'when committing into a fork as a maintainer' do
include_context 'merge request allowing collaboration'