Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e750680e89
commit
458b945df3
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`.
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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`'
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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,
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
Loading…
Reference in New Issue