Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-06-13 12:07:18 +00:00
parent 10e15ac3c2
commit c3eeb6a8d6
82 changed files with 1122 additions and 285 deletions

View File

@ -211,7 +211,7 @@ ping-appsec-for-sast-findings:
- .ping-appsec-for-sast-findings:rules
variables:
# Project Access Token bot ID for /gitlab-com/gl-security/appsec/sast-custom-rules
BOT_USER_ID: 13559989
BOT_USER_ID: 14406065
needs:
- semgrep-appsec-custom-rules
script:

View File

@ -5,7 +5,7 @@ import {
TYPENAME_MILESTONE,
TYPENAME_USER,
} from '~/graphql_shared/constants';
import { isGid, convertToGraphQLId } from '~/graphql_shared/utils';
import { isGid, convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
ListType,
MilestoneIDs,
@ -202,6 +202,38 @@ export function moveItemListHelper(item, fromList, toList) {
return updatedItem;
}
export function moveItemVariables({
iid,
epicId,
fromListId,
toListId,
moveBeforeId,
moveAfterId,
isIssue,
boardId,
itemToMove,
}) {
if (isIssue) {
return {
iid,
boardId,
projectPath: itemToMove.referencePath.split(/[#]/)[0],
moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined,
moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined,
fromListId: getIdFromGraphQLId(fromListId),
toListId: getIdFromGraphQLId(toListId),
};
}
return {
epicId,
boardId,
moveBeforeId,
moveAfterId,
fromListId,
toListId,
};
}
export function isListDraggable(list) {
return list.listType !== ListType.backlog && list.listType !== ListType.closed;
}

View File

@ -9,6 +9,7 @@ import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
DEFAULT_BOARD_LIST_ITEMS_SIZE,
toggleFormEventPrefix,
@ -16,6 +17,13 @@ import {
listIssuablesQueries,
ListType,
} from 'ee_else_ce/boards/constants';
import {
addItemToList,
removeItemFromList,
updateEpicsCount,
updateIssueCountAndWeight,
} from '../graphql/cache_updates';
import { shouldCloneCard, moveItemVariables } from '../boards_util';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
@ -37,7 +45,7 @@ export default {
GlIntersectionObserver,
BoardCardMoveToPosition,
},
mixins: [Tracking.mixin()],
mixins: [Tracking.mixin(), glFeatureFlagMixin()],
inject: [
'isEpicBoard',
'isGroupBoard',
@ -73,6 +81,8 @@ export default {
showEpicForm: false,
currentList: null,
isLoadingMore: false,
toListId: null,
toList: {},
};
},
apollo: {
@ -111,6 +121,29 @@ export default {
isSingleRequest: true,
},
},
toList: {
query() {
return listIssuablesQueries[this.issuableType].query;
},
variables() {
return {
id: this.toListId,
...this.listQueryVariables,
};
},
skip() {
return !this.toListId;
},
update(data) {
return data[this.boardType].board.lists.nodes[0];
},
context: {
isSingleRequest: true,
},
error() {
// handle error
},
},
},
computed: {
...mapState(['pageInfoByListId', 'listsFlags', 'isUpdateIssueOrderInProgress']),
@ -205,6 +238,9 @@ export default {
showMoveToPosition() {
return !this.disabled && this.list.listType !== ListType.closed;
},
shouldCloneCard() {
return shouldCloneCard(this.list.listType, this.toList.listType);
},
},
watch: {
boardListItems() {
@ -337,6 +373,19 @@ export default {
}
}
if (this.isApolloBoard) {
this.moveBoardItem(
{
epicId: itemId,
iid: itemIid,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
moveBeforeId,
moveAfterId,
},
newIndex,
);
} else {
this.moveItem({
itemId,
itemIid,
@ -346,6 +395,101 @@ export default {
moveBeforeId,
moveAfterId,
});
}
},
isItemInTheList(itemIid) {
const items = this.toList?.[`${this.issuableType}s`]?.nodes || [];
return items.some((item) => item.iid === itemIid);
},
async moveBoardItem(variables, newIndex) {
const { fromListId, toListId, iid } = variables;
this.toListId = toListId;
await this.$nextTick(); // we need this next tick to retrieve `toList` from Apollo cache
const itemToMove = this.boardListItems.find((item) => item.iid === iid);
if (this.shouldCloneCard && this.isItemInTheList(iid)) {
return;
}
try {
await this.$apollo.mutate({
mutation: listIssuablesQueries[this.issuableType].moveMutation,
variables: {
...moveItemVariables({
...variables,
isIssue: !this.isEpicBoard,
boardId: this.boardId,
itemToMove,
}),
withColor: this.isEpicBoard && this.glFeatures.epicColorHighlight,
},
update: (cache, { data: { issuableMoveList } }) =>
this.updateCacheAfterMovingItem({
issuableMoveList,
fromListId,
toListId,
newIndex,
cache,
}),
optimisticResponse: {
issuableMoveList: {
issuable: itemToMove,
errors: [],
},
},
});
} catch {
// handle error
}
},
updateCacheAfterMovingItem({ issuableMoveList, fromListId, toListId, newIndex, cache }) {
const { issuable } = issuableMoveList;
if (!this.shouldCloneCard) {
removeItemFromList({
query: listIssuablesQueries[this.issuableType].query,
variables: { ...this.listQueryVariables, id: fromListId },
boardType: this.boardType,
id: issuable.id,
issuableType: this.issuableType,
cache,
});
}
addItemToList({
query: listIssuablesQueries[this.issuableType].query,
variables: { ...this.listQueryVariables, id: toListId },
issuable,
newIndex,
boardType: this.boardType,
issuableType: this.issuableType,
cache,
});
this.updateCountAndWeight({ fromListId, toListId, issuable, cache });
},
updateCountAndWeight({ fromListId, toListId, issuable, isAddingIssue, cache }) {
if (!this.isEpicBoard) {
updateIssueCountAndWeight({
fromListId,
toListId,
filterParams: this.filterParams,
issuable,
shouldClone: isAddingIssue || this.shouldCloneCard,
cache,
});
} else {
const { issuableType, filterParams } = this;
updateEpicsCount({
issuableType,
toListId,
fromListId,
filterParams,
issuable,
shouldClone: this.shouldCloneCard,
cache,
});
}
},
},
};

View File

@ -10,9 +10,11 @@ import updateBoardListMutation from './graphql/board_list_update.mutation.graphq
import toggleListCollapsedMutation from './graphql/client/board_toggle_collapsed.mutation.graphql';
import issueSetSubscriptionMutation from './graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
import issueMoveListMutation from './graphql/issue_move_list.mutation.graphql';
import groupBoardQuery from './graphql/group_board.query.graphql';
import projectBoardQuery from './graphql/project_board.query.graphql';
import listIssuesQuery from './graphql/lists_issues.query.graphql';
import listDeferredQuery from './graphql/board_lists_deferred.query.graphql';
export const BoardType = {
project: 'project',
@ -72,6 +74,12 @@ export const listsQuery = {
},
};
export const listsDeferredQuery = {
[TYPE_ISSUE]: {
query: listDeferredQuery,
},
};
export const createListMutations = {
[TYPE_ISSUE]: {
mutation: createBoardListMutation,
@ -117,6 +125,7 @@ export const subscriptionQueries = {
export const listIssuablesQueries = {
[TYPE_ISSUE]: {
query: listIssuesQuery,
moveMutation: issueMoveListMutation,
},
};

View File

@ -0,0 +1,118 @@
import produce from 'immer';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import { listsDeferredQuery } from 'ee_else_ce/boards/constants';
export function removeItemFromList({ query, variables, boardType, id, issuableType, cache }) {
cache.updateQuery({ query, variables }, (sourceData) =>
produce(sourceData, (draftData) => {
const { nodes: items } = draftData[boardType].board.lists.nodes[0][`${issuableType}s`];
items.splice(
items.findIndex((item) => item.id === id),
1,
);
}),
);
}
export function addItemToList({
query,
variables,
boardType,
issuable,
newIndex,
issuableType,
cache,
}) {
cache.updateQuery({ query, variables }, (sourceData) =>
produce(sourceData, (draftData) => {
const { nodes: items } = draftData[boardType].board.lists.nodes[0][`${issuableType}s`];
items.splice(newIndex, 0, issuable);
}),
);
}
export function updateIssueCountAndWeight({
fromListId,
toListId,
filterParams,
issuable: issue,
shouldClone,
cache,
}) {
if (!shouldClone) {
cache.updateQuery(
{
query: listQuery,
variables: { id: fromListId, filters: filterParams },
},
({ boardList }) => ({
boardList: {
...boardList,
issuesCount: boardList.issuesCount - 1,
totalWeight: boardList.totalWeight - issue.weight,
},
}),
);
}
cache.updateQuery(
{
query: listQuery,
variables: { id: toListId, filters: filterParams },
},
({ boardList }) => ({
boardList: {
...boardList,
issuesCount: boardList.issuesCount + 1,
totalWeight: boardList.totalWeight + issue.weight,
},
}),
);
}
export function updateEpicsCount({
issuableType,
filterParams,
fromListId,
toListId,
issuable: epic,
shouldClone,
cache,
}) {
const epicWeight = epic.descendantWeightSum.openedIssues + epic.descendantWeightSum.closedIssues;
if (!shouldClone) {
cache.updateQuery(
{
query: listsDeferredQuery[issuableType].query,
variables: { id: fromListId, filters: filterParams },
},
({ epicBoardList }) => ({
epicBoardList: {
...epicBoardList,
metadata: {
epicsCount: epicBoardList.metadata.epicsCount - 1,
totalWeight: epicBoardList.metadata.totalWeight - epicWeight,
...epicBoardList.metadata,
},
},
}),
);
}
cache.updateQuery(
{
query: listsDeferredQuery[issuableType].query,
variables: { id: toListId, filters: filterParams },
},
({ epicBoardList }) => ({
epicBoardList: {
...epicBoardList,
metadata: {
epicsCount: epicBoardList.metadata.epicsCount + 1,
totalWeight: epicBoardList.metadata.totalWeight + epicWeight,
...epicBoardList.metadata,
},
},
}),
);
}

View File

@ -9,7 +9,7 @@ mutation issueMoveList(
$moveBeforeId: ID
$moveAfterId: ID
) {
issueMoveList(
issuableMoveList: issueMoveList(
input: {
projectPath: $projectPath
iid: $iid
@ -20,7 +20,7 @@ mutation issueMoveList(
moveAfterId: $moveAfterId
}
) {
issue {
issuable: issue {
...Issue
}
errors

View File

@ -602,8 +602,8 @@ export default {
cache,
{
data: {
issueMoveList: {
issue: { weight },
issuableMoveList: {
issuable: { weight },
},
},
},
@ -661,11 +661,11 @@ export default {
},
});
if (data?.issueMoveList?.errors.length || !data.issueMoveList) {
if (data?.issuableMoveList?.errors.length || !data.issuableMoveList) {
throw new Error('issueMoveList empty');
}
commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issueMoveList.issue });
commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issuableMoveList.issuable });
commit(types.MUTATE_ISSUE_IN_PROGRESS, false);
} catch {
commit(types.MUTATE_ISSUE_IN_PROGRESS, false);

View File

@ -4,14 +4,14 @@
* Used in the environments table.
*/
import { GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import setEnvironmentToDelete from '../graphql/mutations/set_environment_to_delete.mutation.graphql';
export default {
components: {
GlDropdownItem,
GlDisclosureDropdownItem,
},
directives: {
GlModalDirective,
@ -30,10 +30,14 @@ export default {
data() {
return {
isLoading: false,
};
item: {
text: s__('Environments|Delete environment'),
extraAttrs: {
variant: 'danger',
class: 'gl-text-red-500!',
},
i18n: {
title: s__('Environments|Delete environment'),
},
};
},
mounted() {
if (!this.graphql) {
@ -65,12 +69,10 @@ export default {
};
</script>
<template>
<gl-dropdown-item
<gl-disclosure-dropdown-item
v-gl-modal-directive.delete-environment-modal
:item="item"
:loading="isLoading"
variant="danger"
@click="onClick"
>
{{ $options.i18n.title }}
</gl-dropdown-item>
@action="onClick"
/>
</template>

View File

@ -3,14 +3,14 @@
* Renders a prevent auto-stop button.
* Used in environments table.
*/
import { GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
import eventHub from '../event_hub';
import cancelAutoStopMutation from '../graphql/mutations/cancel_auto_stop.mutation.graphql';
export default {
components: {
GlDropdownItem,
GlDisclosureDropdownItem,
},
props: {
autoStopUrl: {
@ -23,6 +23,11 @@ export default {
default: false,
},
},
data() {
return {
item: { text: __('Prevent auto-stopping') },
};
},
methods: {
onPinClick() {
if (this.graphql) {
@ -35,11 +40,8 @@ export default {
}
},
},
title: __('Prevent auto-stopping'),
};
</script>
<template>
<gl-dropdown-item @click="onPinClick">
{{ $options.title }}
</gl-dropdown-item>
<gl-disclosure-dropdown-item :item="item" @action="onPinClick" />
</template>

View File

@ -5,14 +5,14 @@
*
* Makes a post request when the button is clicked.
*/
import { GlModalDirective, GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem, GlModalDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import setEnvironmentToRollback from '../graphql/mutations/set_environment_to_rollback.mutation.graphql';
export default {
components: {
GlDropdownItem,
GlDisclosureDropdownItem,
},
directives: {
GlModal: GlModalDirective,
@ -41,12 +41,14 @@ export default {
},
},
computed: {
title() {
return this.isLastDeployment
data() {
return {
item: {
text: this.isLastDeployment
? s__('Environments|Re-deploy to environment')
: s__('Environments|Rollback environment');
: s__('Environments|Rollback environment'),
},
};
},
methods: {
@ -71,7 +73,5 @@ export default {
};
</script>
<template>
<gl-dropdown-item v-gl-modal.confirm-rollback-modal @click="onClick">
{{ title }}
</gl-dropdown-item>
<gl-disclosure-dropdown-item v-gl-modal.confirm-rollback-modal :item="item" @action="onClick" />
</template>

View File

@ -3,12 +3,12 @@
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
import { GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlDropdownItem,
GlDisclosureDropdownItem,
},
props: {
terminalPath: {
@ -22,11 +22,13 @@ export default {
default: false,
},
},
title: __('Terminal'),
data() {
return {
item: { text: __('Terminal'), href: this.terminalPath },
};
},
};
</script>
<template>
<gl-dropdown-item :href="terminalPath" :disabled="disabled">
{{ $options.title }}
</gl-dropdown-item>
<gl-disclosure-dropdown-item :item="item" :disabled="disabled" />
</template>

View File

@ -1,9 +1,9 @@
<script>
import {
GlCollapse,
GlDropdown,
GlBadge,
GlButton,
GlCollapse,
GlDisclosureDropdown,
GlLink,
GlSprintf,
GlTooltipDirective as GlTooltip,
@ -27,8 +27,8 @@ import KubernetesOverview from './kubernetes_overview.vue';
export default {
components: {
GlDisclosureDropdown,
GlCollapse,
GlDropdown,
GlBadge,
GlButton,
GlLink,
@ -284,14 +284,14 @@ export default {
graphql
/>
<gl-dropdown
<gl-disclosure-dropdown
v-if="hasExtraActions"
icon="ellipsis_v"
text-sr-only
:text="__('More actions')"
category="secondary"
no-caret
right
icon="ellipsis_v"
category="secondary"
placement="right"
:toggle-text="__('More actions')"
>
<rollback
v-if="retryPath"
@ -325,7 +325,7 @@ export default {
data-track-label="environment_delete"
graphql
/>
</gl-dropdown>
</gl-disclosure-dropdown>
</div>
</div>
</div>

View File

@ -1,16 +1,16 @@
<script>
import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { s__ } from '~/locale';
import { defaultIntegrationLevel, overrideDropdownDescriptions } from '~/integrations/constants';
const dropdownOptions = [
{
value: false,
value: 'default',
text: s__('Integrations|Use default settings'),
},
{
value: true,
value: 'custom',
text: s__('Integrations|Use custom settings'),
},
];
@ -19,8 +19,7 @@ export default {
dropdownOptions,
name: 'OverrideDropdown',
components: {
GlDropdown,
GlDropdownItem,
GlCollapsibleListbox,
GlLink,
},
props: {
@ -39,8 +38,10 @@ export default {
},
},
data() {
const selectedValue = this.override ? 'custom' : 'default';
return {
selected: dropdownOptions.find((x) => x.value === this.override),
selectedValue,
selectedOption: dropdownOptions.find((x) => x.value === selectedValue),
};
},
computed: {
@ -54,9 +55,10 @@ export default {
},
},
methods: {
onClick(option) {
this.selected = option;
this.$emit('change', option.value);
onSelect(value) {
this.selectedValue = value;
this.selectedOption = dropdownOptions.find((item) => item.value === value);
this.$emit('change', value === 'custom');
},
},
};
@ -73,14 +75,11 @@ export default {
}}</gl-link>
</span>
<input name="service[inherit_from_id]" :value="override ? '' : inheritFromId" type="hidden" />
<gl-dropdown :text="selected.text">
<gl-dropdown-item
v-for="option in $options.dropdownOptions"
:key="option.value"
@click="onClick(option)"
>
{{ option.text }}
</gl-dropdown-item>
</gl-dropdown>
<gl-collapsible-listbox
v-model="selectedValue"
:toggle-text="selectedOption.text"
:items="$options.dropdownOptions"
@select="onSelect"
/>
</div>
</template>

View File

@ -25,6 +25,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-unlimited-members-during-trial-alert',
'.js-branch-rules-info-callout',
'.js-new-navigation-callout',
'.js-code-suggestions-third-party-callout',
];
const initCallouts = () => {

View File

@ -0,0 +1,59 @@
# frozen_string_literal: true
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699
module Issues
module ForbidIssueTypeColumnUsage
extend ActiveSupport::Concern
ForbiddenColumnUsed = Class.new(StandardError)
included do
WorkItems::Type.base_types.each do |base_type, _value|
define_method "#{base_type}?".to_sym do
error_message = <<~ERROR
`#{model_name.element}.#{base_type}?` uses the `issue_type` column underneath. As we want to remove the column,
its usage is forbidden. You should use the `work_item_types` table instead.
# Before
#{model_name.element}.#{base_type}? => true
# After
#{model_name.element}.work_item_type.#{base_type}? => true
More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
ERROR
raise ForbiddenColumnUsed, error_message
end
define_singleton_method base_type.to_sym do
error = ForbiddenColumnUsed.new(
<<~ERROR
`#{name}.#{base_type}` uses the `issue_type` column underneath. As we want to remove the column,
its usage is forbidden. You should use the `work_item_types` table instead.
# Before
#{name}.#{base_type}
# After
#{name}.with_issue_type(:#{base_type})
More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
ERROR
)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(
error,
method_name: "#{name}.#{base_type}"
)
with_issue_type(base_type.to_sym)
end
end
end
end
end

View File

@ -21,7 +21,7 @@ class Integration < ApplicationRecord
asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker datadog discord
drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
unify_circuit webex_teams youtrack zentao
].freeze
@ -302,7 +302,7 @@ class Integration < ApplicationRecord
def self.project_specific_integration_names
names = PROJECT_SPECIFIC_INTEGRATION_NAMES.dup
names.delete('gitlab_slack_application') unless Gitlab::CurrentSettings.slack_app_enabled || Rails.env.test?
names.delete('gitlab_slack_application') unless Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?
names
end

View File

@ -0,0 +1,105 @@
# frozen_string_literal: true
module Integrations
class Telegram < BaseChatNotification
TELEGRAM_HOSTNAME = "https://api.telegram.org/bot%{token}/sendMessage"
field :token,
section: SECTION_TYPE_CONNECTION,
help: -> { s_('TelegramIntegration|Unique authentication token.') },
non_empty_password_title: -> { s_('TelegramIntegration|New token') },
non_empty_password_help: -> { s_('TelegramIntegration|Leave blank to use your current token.') },
placeholder: '123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11',
exposes_secrets: true,
is_secret: true,
required: true
field :room,
title: 'Channel identifier',
section: SECTION_TYPE_CONFIGURATION,
help: "Unique identifier for the target chat or the username of the target channel (format: @channelusername)",
placeholder: '@channelusername',
required: true
with_options if: :activated? do
validates :token, :room, presence: true
end
before_validation :set_webhook
def title
'Telegram'
end
def description
s_("TelegramIntegration|Send notifications about project events to Telegram.")
end
def self.to_param
'telegram'
end
def help
docs_link = ActionController::Base.helpers.link_to(
_('Learn more.'),
Rails.application.routes.url_helpers.help_page_url('user/project/integrations/telegram'),
target: '_blank',
rel: 'noopener noreferrer'
)
format(s_("TelegramIntegration|Send notifications about project events to Telegram. %{docs_link}"),
docs_link: docs_link.html_safe
)
end
def fields
self.class.fields + build_event_channels
end
def self.supported_events
super - ['deployment']
end
def sections
[
{
type: SECTION_TYPE_CONNECTION,
title: s_('Integrations|Connection details'),
description: help
},
{
type: SECTION_TYPE_TRIGGER,
title: s_('Integrations|Trigger'),
description: s_('Integrations|An event will be triggered when one of the following items happen.')
},
{
type: SECTION_TYPE_CONFIGURATION,
title: s_('Integrations|Notification settings'),
description: s_('Integrations|Configure the scope of notifications.')
}
]
end
private
def set_webhook
self.webhook = format(TELEGRAM_HOSTNAME, token: token) if token.present?
end
def notify(message, _opts)
body = {
text: message.summary,
chat_id: room,
parse_mode: 'markdown'
}
header = { 'Content-Type' => 'application/json' }
response = Gitlab::HTTP.post(webhook, headers: header, body: Gitlab::Json.dump(body))
response if response.success?
end
def custom_data(data)
super(data).merge(markdown: true)
end
end
end

View File

@ -39,7 +39,6 @@ class Issue < ApplicationRecord
DueNextMonthAndPreviousTwoWeeks = DueDateStruct.new('Due Next Month And Previous Two Weeks', 'next_month_and_previous_two_weeks').freeze
IssueTypeOutOfSyncError = Class.new(StandardError)
ForbiddenColumnUsed = Class.new(StandardError)
SORTING_PREFERENCE_FIELD = :issues_sort
MAX_BRANCH_TEMPLATE = 255
@ -138,28 +137,8 @@ class Issue < ApplicationRecord
validate :issue_type_attribute_present
enum issue_type: WorkItems::Type.base_types
# TODO: Remove with https://gitlab.com/gitlab-org/gitlab/-/issues/402699
WorkItems::Type.base_types.each do |base_type, _value|
define_method "#{base_type}?".to_sym do
error_message = <<~ERROR
`#{base_type}?` uses the `issue_type` column underneath. As we want to remove the column,
its usage is forbidden. You should use the `work_item_types` table instead.
# Before
issue.requirement? => true
# After
issue.work_item_type.requirement? => true
More details in https://gitlab.com/groups/gitlab-org/-/epics/10529
ERROR
raise ForbiddenColumnUsed, error_message
end
end
include ::Issues::ForbidIssueTypeColumnUsage
alias_method :issuing_parent, :project
alias_attribute :issuing_parent_id, :project_id

View File

@ -57,7 +57,6 @@ class Namespace < ApplicationRecord
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: 'User'
belongs_to :organization, class_name: 'Organizations::Organization'
belongs_to :parent, class_name: "Namespace"
has_many :children, -> { where(type: Group.sti_name) }, class_name: "Namespace", foreign_key: :parent_id

View File

@ -8,9 +8,6 @@ module Organizations
before_destroy :check_if_default_organization
has_many :namespaces
has_many :groups
validates :name,
presence: true,
length: { maximum: 255 }

View File

@ -219,6 +219,7 @@ class Project < ApplicationRecord
has_one :slack_slash_commands_integration, class_name: 'Integrations::SlackSlashCommands'
has_one :squash_tm_integration, class_name: 'Integrations::SquashTm'
has_one :teamcity_integration, class_name: 'Integrations::Teamcity'
has_one :telegram_integration, class_name: 'Integrations::Telegram'
has_one :unify_circuit_integration, class_name: 'Integrations::UnifyCircuit'
has_one :webex_teams_integration, class_name: 'Integrations::WebexTeams'
has_one :youtrack_integration, class_name: 'Integrations::Youtrack'

View File

@ -70,7 +70,8 @@ module Users
repository_storage_limit_banner_warning_threshold: 68, # EE-only
repository_storage_limit_banner_alert_threshold: 69, # EE-only
repository_storage_limit_banner_error_threshold: 70, # EE-only
new_navigation_callout: 71
new_navigation_callout: 71,
code_suggestions_third_party_callout: 72 # EE-only
}
validates :feature_name,

View File

@ -7,7 +7,7 @@
= f.text_field :username, name: :username, autocomplete: :username, class: 'form-control gl-form-input', title: _('This field is required.'), autofocus: 'autofocus', required: true
.form-group
= f.label :password, _('Password')
= f.password_field :password, name: :password, autocomplete: :current_password, class: 'form-control gl-form-input js-password', data: { id: 'password', name: 'password' }
= f.text_field :vue_password_placeholder, class: 'form-control gl-form-input js-password', data: { id: "#{:crowd}_password", name: 'password' }
- if render_remember_me
= f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { name: :remember_me, autocomplete: 'off' }

View File

@ -6,10 +6,10 @@
= gitlab_ui_form_for(provider, url: omniauth_callback_path(:user, provider), html: { class: 'gl-p-5 gl-show-field-errors', aria: { live: 'assertive' }, data: { testid: 'new_ldap_user' }}) do |f|
.form-group
= f.label :username, _('Username')
= f.text_field :username, name: :username, autocomplete: :username, class: 'form-control gl-form-input top', title: _('This field is required.'), autofocus: 'autofocus', data: { qa_selector: 'username_field' }, required: true
= f.text_field :username, name: :username, autocomplete: :username, class: 'form-control gl-form-input', title: _('This field is required.'), autofocus: 'autofocus', data: { qa_selector: 'username_field' }, required: true
.form-group
= f.label :password, _('Password')
= f.password_field :password, name: :password, autocomplete: :current_password, class: 'form-control gl-form-input js-password', data: { id: "#{provider}-password", name: 'password', qa_selector: 'password_field' }
= f.text_field :vue_password_placeholder, class: 'form-control gl-form-input js-password', data: { id: "#{provider}_password", name: 'password', qa_selector: 'password_field' }
- if render_remember_me
= f.gitlab_ui_checkbox_component :remember_me, _('Remember me'), checkbox_options: { name: :remember_me, autocomplete: 'off' }

View File

@ -20,6 +20,7 @@
.mobile-overlay
= dispensable_render_if_exists 'layouts/header/verification_reminder'
.alert-wrapper.gl-force-block-formatting-context
= yield :code_suggestions_third_party_alert
= dispensable_render 'shared/new_nav_announcement'
= dispensable_render 'shared/outdated_browser'
= dispensable_render_if_exists "layouts/header/licensed_user_count_threshold"

View File

@ -21,6 +21,7 @@
= dispensable_render_if_exists "shared/web_hooks/group_web_hook_disabled_alert"
= dispensable_render_if_exists "shared/code_suggestions_alert"
= dispensable_render_if_exists "shared/code_suggestions_third_party_alert", source: @group
= dispensable_render_if_exists "shared/free_user_cap_alert", source: @group
= dispensable_render_if_exists "shared/unlimited_members_during_trial_alert", resource: @group

View File

@ -23,6 +23,7 @@
= dispensable_render_if_exists "shared/web_hooks/web_hook_disabled_alert"
= dispensable_render_if_exists "projects/code_suggestions_alert", project: @project
= dispensable_render_if_exists "projects/code_suggestions_third_party_alert", project: @project
= dispensable_render_if_exists "projects/free_user_cap_alert", project: @project
= dispensable_render_if_exists 'shared/unlimited_members_during_trial_alert', resource: @project

View File

@ -1,7 +1,7 @@
---
name: ci_include_components
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/109154
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/39064
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390646
milestone: '15.9'
type: development
group: group::pipeline authoring

View File

@ -247,10 +247,6 @@ ml_candidates:
- table: ci_builds
column: ci_build_id
on_delete: async_nullify
namespaces:
- table: organizations
column: organization_id
on_delete: async_nullify
p_ci_builds_metadata:
- table: projects
column: project_id

View File

@ -0,0 +1,21 @@
---
key_path: counts.projects_telegram_active
description: Count of projects with active integrations for Telegram
product_section: dev
product_stage: manage
product_group: integrations
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879
time_frame: all
data_source: database
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts.projects_inheriting_telegram_active
description: Count of active projects inheriting integrations for Telegram
product_section: dev
product_stage: manage
product_group: integrations
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879
time_frame: all
data_source: database
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts.instances_telegram_active
description: Count of active instance-level integrations for Telegram
product_section: dev
product_stage: manage
product_group: integrations
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879
time_frame: all
data_source: database
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts.groups_telegram_active
description: Count of groups with active integrations for Telegram
product_section: dev
product_stage: manage
product_group: integrations
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879
time_frame: all
data_source: database
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -0,0 +1,21 @@
---
key_path: counts.groups_inheriting_telegram_active
description: Count of active groups inheriting integrations for Telegram
product_section: dev
product_stage: manage
product_group: integrations
value_type: number
status: active
milestone: "16.1"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879
time_frame: all
data_source: database
data_category: optional
performance_indicator_type: []
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate

View File

@ -50,6 +50,7 @@ classes:
- Integrations::SlackSlashCommands
- Integrations::SquashTm
- Integrations::Teamcity
- Integrations::Telegram
- Integrations::UnifyCircuit
- Integrations::WebexTeams
- Integrations::Youtrack

View File

@ -1,11 +1,7 @@
# frozen_string_literal: true
class AddOrganizationIdToNamespace < Gitlab::Database::Migration[2.1]
DEFAULT_ORGANIZATION_ID = 1
enable_lock_retries!
def change
add_column :namespaces, :organization_id, :bigint, default: DEFAULT_ORGANIZATION_ID, null: true # rubocop:disable Migration/AddColumnsToWideTables
# no-op
end
end

View File

@ -1,15 +1,11 @@
# frozen_string_literal: true
class TrackOrganizationRecordChanges < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers::LooseForeignKeyHelpers
enable_lock_retries!
def up
track_record_deletions(:organizations)
# no-op
end
def down
untrack_record_deletions(:organizations)
# no-op
end
end

View File

@ -1,13 +1,11 @@
# frozen_string_literal: true
class PrepareIndexForOrgIdOnNamespaces < Gitlab::Database::Migration[2.1]
INDEX_NAME = 'index_namespaces_on_organization_id'
def up
prepare_async_index :namespaces, :organization_id, name: INDEX_NAME
# no-op
end
def down
unprepare_async_index :namespaces, :organization_id, name: INDEX_NAME
# no-op
end
end

View File

@ -18878,8 +18878,7 @@ CREATE TABLE namespaces (
push_rule_id bigint,
shared_runners_enabled boolean DEFAULT true NOT NULL,
allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL,
traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL,
organization_id bigint DEFAULT 1
traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL
);
CREATE SEQUENCE namespaces_id_seq
@ -35140,8 +35139,6 @@ CREATE TRIGGER namespaces_loose_fk_trigger AFTER DELETE ON namespaces REFERENCIN
CREATE TRIGGER nullify_merge_request_metrics_build_data_on_update BEFORE UPDATE ON merge_request_metrics FOR EACH ROW EXECUTE FUNCTION nullify_merge_request_metrics_build_data();
CREATE TRIGGER organizations_loose_fk_trigger AFTER DELETE ON organizations REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER p_ci_builds_loose_fk_trigger AFTER DELETE ON p_ci_builds REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();
CREATE TRIGGER projects_loose_fk_trigger AFTER DELETE ON projects REFERENCING OLD TABLE AS old_table FOR EACH STATEMENT EXECUTE FUNCTION insert_into_loose_foreign_keys_deleted_records();

View File

@ -26176,6 +26176,7 @@ State of a Sentry error.
| <a id="servicetypeslack_slash_commands_service"></a>`SLACK_SLASH_COMMANDS_SERVICE` | SlackSlashCommandsService type. |
| <a id="servicetypesquash_tm_service"></a>`SQUASH_TM_SERVICE` | SquashTmService type. |
| <a id="servicetypeteamcity_service"></a>`TEAMCITY_SERVICE` | TeamcityService type. |
| <a id="servicetypetelegram_service"></a>`TELEGRAM_SERVICE` | TelegramService type. |
| <a id="servicetypeunify_circuit_service"></a>`UNIFY_CIRCUIT_SERVICE` | UnifyCircuitService type. |
| <a id="servicetypewebex_teams_service"></a>`WEBEX_TEAMS_SERVICE` | WebexTeamsService type. |
| <a id="servicetypeyoutrack_service"></a>`YOUTRACK_SERVICE` | YoutrackService type. |
@ -26359,6 +26360,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumci_deprecation_warning_for_types_keyword"></a>`CI_DEPRECATION_WARNING_FOR_TYPES_KEYWORD` | Callout feature name for ci_deprecation_warning_for_types_keyword. |
| <a id="usercalloutfeaturenameenumcloud_licensing_subscription_activation_banner"></a>`CLOUD_LICENSING_SUBSCRIPTION_ACTIVATION_BANNER` | Callout feature name for cloud_licensing_subscription_activation_banner. |
| <a id="usercalloutfeaturenameenumcluster_security_warning"></a>`CLUSTER_SECURITY_WARNING` | Callout feature name for cluster_security_warning. |
| <a id="usercalloutfeaturenameenumcode_suggestions_third_party_callout"></a>`CODE_SUGGESTIONS_THIRD_PARTY_CALLOUT` | Callout feature name for code_suggestions_third_party_callout. |
| <a id="usercalloutfeaturenameenumcreate_runner_workflow_banner"></a>`CREATE_RUNNER_WORKFLOW_BANNER` | Callout feature name for create_runner_workflow_banner. |
| <a id="usercalloutfeaturenameenumeoa_bronze_plan_banner"></a>`EOA_BRONZE_PLAN_BANNER` | Callout feature name for eoa_bronze_plan_banner. |
| <a id="usercalloutfeaturenameenumfeature_flags_new_version"></a>`FEATURE_FLAGS_NEW_VERSION` | Callout feature name for feature_flags_new_version. |

View File

@ -412,6 +412,50 @@ Get Datadog integration settings for a project.
GET /projects/:id/integrations/datadog
```
## Telegram
Telegram chat tool.
### Create/Edit Telegram integration
Set the Telegram integration for a project.
```plaintext
PUT /projects/:id/integrations/telegram
```
Parameters:
| Parameter | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `token` | string | true | The Telegram bot token. For example, `123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11`. |
| `room` | string | true | Unique identifier for the target chat or the username of the target channel (in the format `@channelusername`) |
| `push_events` | boolean | true | Enable notifications for push events |
| `issues_events` | boolean | true | Enable notifications for issue events |
| `confidential_issues_events` | boolean | true | Enable notifications for confidential issue events |
| `merge_requests_events` | boolean | true | Enable notifications for merge request events |
| `tag_push_events` | boolean | true | Enable notifications for tag push events |
| `note_events` | boolean | true | Enable notifications for note events |
| `confidential_note_events` | boolean | true | Enable notifications for confidential note events |
| `pipeline_events` | boolean | true | Enable notifications for pipeline events |
| `wiki_page_events` | boolean | true | Enable notifications for wiki page events |
### Disable Telegram integration
Disable the Telegram integration for a project. Integration settings are reset.
```plaintext
DELETE /projects/:id/integrations/telegram
```
### Get Telegram integration settings
Get Telegram integration settings for a project.
```plaintext
GET /projects/:id/integrations/telegram
```
## Unify Circuit
Unify Circuit RTC and collaboration tool.

View File

@ -10,7 +10,8 @@ All methods require administrator authorization.
You can configure the URL endpoint of the system hooks from the GitLab user interface:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. Select **System Hooks** (`/admin/hooks`).
Read more about [system hooks](../administration/system_hooks.md).

View File

@ -80,7 +80,8 @@ response attributes:
Example request:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/endpoint?parameters"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/endpoint?parameters"
```
Example response:
@ -201,9 +202,13 @@ For information about writing attribute descriptions, see the [GraphQL API descr
- Wherever needed use this personal access token: `<your_access_token>`.
- Always put the request first. `GET` is the default so you don't have to
include it.
- Use long option names (`--header` instead of `-H`) for legibility. (Tested in
[`scripts/lint-doc.sh`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/scripts/lint-doc.sh).)
- Wrap the URL in double quotes (`"`).
- Prefer to use examples using the personal access token and don't pass data of
username and password.
- For legibility, use the <code>&#92;</code> character and indentation to break long single-line
commands apart into multiple lines.
| Methods | Description |
|:------------------------------------------------|:-------------------------------------------------------|
@ -227,7 +232,8 @@ relevant style guide sections on [Fake user information](styleguide/index.md#fak
Get the details of a group:
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/gitlab-org"
curl --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/groups/gitlab-org"
```
### cURL example with parameters passed in the URL
@ -235,7 +241,8 @@ curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/a
Create a new project under the authenticated user's namespace:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects?name=foo"
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects?name=foo"
```
### Post data using cURL's `--data`
@ -245,7 +252,9 @@ can use cURL's `--data` option. The example below will create a new project
`foo` under the authenticated user's namespace.
```shell
curl --data "name=foo" --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects"
curl --data "name=foo" \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects"
```
### Post data using JSON content
@ -254,8 +263,11 @@ This example creates a new group. Be aware of the use of single (`'`) and double
(`"`) quotes.
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --header "Content-Type: application/json" \
--data '{"path": "my-group", "name": "My group"}' "https://gitlab.example.com/api/v4/groups"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{"path": "my-group", "name": "My group"}' \
"https://gitlab.example.com/api/v4/groups"
```
For readability, you can also set up the `--data` by using the following format:
@ -277,8 +289,11 @@ Instead of using JSON or URL-encoding data, you can use `multipart/form-data` wh
properly handles data encoding:
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" --form "title=ssh-key" \
--form "key=ssh-rsa AAAAB3NzaC1yc2EA..." "https://gitlab.example.com/api/v4/users/25/keys"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--form "title=ssh-key" \
--form "key=ssh-rsa AAAAB3NzaC1yc2EA..." \
"https://gitlab.example.com/api/v4/users/25/keys"
```
The above example is run by and administrator and will add an SSH public key
@ -292,7 +307,9 @@ contains spaces in its title. Observe how spaces are escaped using the `%20`
ASCII code.
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20GitLab"
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
"https://gitlab.example.com/api/v4/projects/42/issues?title=Hello%20GitLab"
```
Use `%2F` for slashes (`/`).
@ -304,6 +321,9 @@ exclude specific users when requesting a list of users for a project, you would
do something like this:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --data "skip_users[]=<user_id>" \
--data "skip_users[]=<user_id>" "https://gitlab.example.com/api/v4/projects/<project_id>/users"
curl --request PUT \
--header "PRIVATE-TOKEN: <your_access_token>"
--data "skip_users[]=<user_id>" \
--data "skip_users[]=<user_id>" \
"https://gitlab.example.com/api/v4/projects/<project_id>/users"
```

View File

@ -522,6 +522,11 @@ When using code block style:
[list of supported languages and lexers](https://github.com/rouge-ruby/rouge/wiki/List-of-supported-languages-and-lexers)
for available syntax highlighters. Use `plaintext` if no better hint is available.
#### cURL commands in code blocks
See [cURL commands](../restful_api_styleguide.md#curl-commands) for information
about styling cURL commands.
## Lists
- Do not use a period if the phrase is not a full sentence.

View File

@ -22,7 +22,7 @@ To meet GitLab for Open Source Program requirements, first add an OSI-approved o
To add a license to a project:
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. On the overview page, select **Add LICENSE**. If the license you want is not available as a license template, manually copy the entire, unaltered [text of your chosen license](https://opensource.org/licenses/) into the `LICENSE` file. GitLab defaults to **All rights reserved** if users do not perform this action.
![Add license](img/add-license.png)
@ -45,7 +45,7 @@ Benefits of the GitLab Open Source Program apply to all projects in a GitLab nam
#### Screenshot 1: License overview
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. On the left sidebar, select your project avatar. If you haven't specified an avatar for your project, the avatar displays as a single letter.
1. Take a screenshot of the project overview that clearly displays the license you've chosen for your project.
@ -53,8 +53,8 @@ Benefits of the GitLab Open Source Program apply to all projects in a GitLab nam
#### Screenshot 2: License contents
1. On the top bar, select **Main menu > Projects** and find your project.
1. On the left sidebar, select **Repository** and locate the project's `LICENSE` file.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1.Select **Code > Repository** and locate the project's `LICENSE` file.
1. Take a screenshot of the contents of the file. Make sure the screenshot includes the title of the license.
![License file](img/license-file.png)
@ -63,8 +63,8 @@ Benefits of the GitLab Open Source Program apply to all projects in a GitLab nam
To be eligible for the GitLab Open Source Program, projects must be publicly visible. To check your project's public visibility settings:
1. On the top bar, select **Main menu > Projects** and find your project.
1. From the left sidebar, select **Settings > General**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your project.
1. Select **Settings > General**.
1. Expand **Visibility, project features, permissions**.
1. From the **Project visibility** dropdown list, select **Public**.
1. Select the **Users can request access** checkbox.

View File

@ -48,8 +48,8 @@ Prerequisite:
To see the status of your GitLab SaaS subscription:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Billing**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Billing**.
The following information is displayed:
@ -99,8 +99,8 @@ In this case, they would see only the features available to that subscription.
To view a list of seats being used:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Usage Quotas**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Usage Quotas**.
1. On the **Seats** tab, view usage information.
The data in seat usage listing, **Seats in use**, and **Seats in subscription** are updated live.
@ -108,8 +108,8 @@ The counts for **Max seats used** and **Seats owed** are updated once per day.
To view your subscription information and a summary of seat counts:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Billing**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Billing**.
The usage statistics are updated once per day, which may cause
a difference between the information in the **Usage Quotas** page and the **Billing page**.
@ -136,8 +136,8 @@ For example:
To export seat usage data as a CSV file:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Billing**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Billing**.
1. Under **Seats currently in use**, select **See usage**.
1. Select **Export list**.
@ -197,8 +197,8 @@ The following is emailed to you:
To remove a billable user from your subscription:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Billing**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Billing**.
1. In the **Seats currently in use** section, select **See usage**.
1. In the row for the user you want to remove, on the right side, select the ellipsis and **Remove user**.
1. Re-type the username and select **Remove user**.
@ -424,8 +424,8 @@ main quota. You can find pricing for additional storage on the
To purchase additional storage for your group on GitLab SaaS:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Settings > Usage Quotas**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Settings > Usage Quotas**.
1. Select **Storage** tab.
1. Select **Purchase more storage**.
1. Complete the details.

View File

@ -36,8 +36,9 @@ Prorated charges are not possible without a quarterly usage report.
You can view users for your license and determine if you've gone over your subscription.
1. On the top bar, select **Main menu > Admin**.
1. On the left menu, select **Users**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. Select **Users**.
The lists of users are displayed.
@ -216,8 +217,9 @@ Example of a license sync request:
You can manually synchronize your subscription details at any time.
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Subscription**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. Select **Subscription**.
1. In the **Subscription details** section, select **Sync subscription details**.
A job is queued. When the job finishes, the subscription details are updated.
@ -226,8 +228,9 @@ A job is queued. When the job finishes, the subscription details are updated.
If you are an administrator, you can view the status of your subscription:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Subscription**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. Select **Subscription**.
The **Subscription** page includes the following details:
@ -250,8 +253,9 @@ It also displays the following information:
If you are an administrator, you can export your license usage into a CSV:
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Subscription**.
1. On the left sidebar, expand the top-most chevron (**{chevron-down}**).
1. Select **Admin Area**.
1. Select **Subscription**.
1. In the upper-right corner, select **Export license usage file**.
This file contains the information GitLab uses to manually process quarterly reconciliations or renewals. If your instance is firewalled or an offline environment, you must provide GitLab with this information.

View File

@ -282,8 +282,8 @@ To create group links via filter:
LDAP user permissions can be manually overridden by an administrator. To override a user's permissions:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**. If LDAP synchronization
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**. If LDAP synchronization
has granted a user a role with:
- More permissions than the parent group membership, that user is displayed as having
[direct membership](../project/members/index.md#display-direct-members) of the group.

View File

@ -181,8 +181,8 @@ In lists of group members, entries can display the following badges:
You can search for members by name, username, or email.
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Above the list of members, in the **Filter members** box, enter search criteria.
1. To the right of the **Filter members** box, select the magnifying glass (**{search}**).
@ -190,8 +190,8 @@ You can search for members by name, username, or email.
You can sort members by **Account**, **Access granted**, **Max role**, or **Last sign-in**.
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Above the list of members, in the upper-right corner, from the **Account** list, select
the criteria to filter by.
1. To switch the sort between ascending and descending, to the right of the **Account** list, select the
@ -205,8 +205,8 @@ Prerequisite:
- You must have the Owner role.
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Select **Invite members**.
1. Fill in the fields.
- The role applies to all projects in the group. For more information, see [permissions](../permissions.md).
@ -231,8 +231,8 @@ Prerequisites:
To remove a member from a group:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Next to the member you want to remove, select **Remove member**.
1. Optional. On the **Remove member** confirmation box:
- To remove direct user membership from subgroups and projects, select the **Also remove direct user membership from subgroups and projects** checkbox.

View File

@ -41,13 +41,13 @@ You can change the owner of a group. Each group must always have at least one
member with the Owner role.
- As an administrator:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Give a different member the **Owner** role.
1. Refresh the page. You can now remove the **Owner** role from the original owner.
- As the current group's owner:
1. On the top bar, select **Main menu > Groups** and find your group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Members**.
1. Give a different member the **Owner** role.
1. Have the new owner sign in and remove the **Owner** role from you.
@ -120,7 +120,7 @@ To share a given group, for example, `Frontend` with another group, for example,
`Engineering`:
1. Go to the `Frontend` group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, select **Manage > Members**.
1. Select **Invite a group**.
1. In the **Select a group to invite** list, select `Engineering`.
1. Select a [role](../permissions.md) as maximum access level.
@ -206,8 +206,8 @@ To disable group mentions:
You can export a list of members in a group or subgroup as a CSV.
1. On the top bar, select **Main menu > Groups** and find your group or subgroup.
1. On the left sidebar, select either **Group information > Members** or **Subgroup information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group or subgroup.
1. On the left sidebar, **Manage > Members**.
1. Select **Export as CSV**.
1. After the CSV file has been generated, it is emailed as an attachment to the user that requested it.
@ -496,8 +496,8 @@ Changes to [group wikis](../project/wiki/group.md) do not appear in group activi
You can view the most recent actions taken in a group, either in your browser or in an RSS feed:
1. On the top bar, select **Main menu > Groups > View all groups** and find your group.
1. On the left sidebar, select **Group information > Activity**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. On the left sidebar, select **Manage > Activity**.
To view the activity feed in Atom format, select the
**RSS** (**{rss}**) icon.

View File

@ -26,7 +26,7 @@ Prerequisites:
To unban a user:
1. Go to the top-level group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, select **Manage > Members**.
1. Select the **Banned** tab.
1. For the account you want to unban, select **Unban**.
@ -43,6 +43,6 @@ Prerequisites:
To manually ban a user:
1. Go to the top-level group.
1. On the left sidebar, select **Group information > Members**.
1. On the left sidebar, select **Manage > Members**.
1. Next to the member you want to ban, select the vertical ellipsis (**{ellipsis_v}**).
1. From the dropdown list, select **Ban member**.

View File

@ -160,8 +160,8 @@ Group permissions for a member can be changed only by:
To see if a member has inherited the permissions from a parent group:
1. On the top bar, select **Main menu > Groups** and find the group.
1. Select **Group information > Members**.
1. On the left sidebar, at the top, select **Search GitLab** (**{search}**) to find your group.
1. Select **Manage > Members**.
Members list for an example subgroup _Four_:

View File

@ -81,6 +81,7 @@ You can configure the following integrations.
| [Slack notifications](slack.md) | Send notifications about project events to Slack. | **{dotted-circle}** No |
| [Slack slash commands](slack_slash_commands.md) | Enable slash commands in a workspace. | **{dotted-circle}** No |
| [Squash TM](squash_tm.md) | Update Squash TM requirements when GitLab issues are modified. | **{check-circle}** Yes |
| [Telegram](telegram.md) | Send notifications about project events to Telegram. | **{dotted-circle}** No |
| [Unify Circuit](unify_circuit.md) | Send notifications about project events to Unify Circuit. | **{dotted-circle}** No |
| [Webex Teams](webex_teams.md) | Receive events notifications. | **{dotted-circle}** No |
| [YouTrack](youtrack.md) | Use YouTrack as the issue tracker. | **{dotted-circle}** No |

View File

@ -0,0 +1,55 @@
---
stage: Manage
group: Import and Integrate
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
---
# Telegram **(FREE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/122879) in GitLab 16.1.
You can configure GitLab to send notifications to a Telegram chat or channel.
To set up the Telegram integration, you must:
1. [Create a Telegram bot](#create-a-telegram-bot).
1. [Configure the Telegram bot](#configure-the-telegram-bot).
1. [Set up the Telegram integration in GitLab](#set-up-the-telegram-integration-in-gitlab).
## Create a Telegram bot
To create a bot in Telegram:
1. Start a new chat with `@BotFather`.
1. [Create a new bot](https://core.telegram.org/bots/features#creating-a-new-bot) as described in the Telegram documentation.
When you create a bot, `BotFather` provides you with an API token. Keep this token secure as you need it to authenticate the bot in Telegram.
## Configure the Telegram bot
To configure the bot in Telegram:
1. Add the bot as an administrator to a new or existing channel.
1. Assign the bot `Post Messages` rights to receive events.
1. Create an identifier for the channel.
## Set up the Telegram integration in GitLab
After you invite the bot to a Telegram channel, you can configure GitLab to send notifications:
1. To enable the integration:
- **For your group or project:**
1. On the top bar, select **Main menu** and find your group or project.
1. on the left sidebar, select **Settings > Integrations**.
- **For your instance:**
1. On the top bar, select **Main menu > Admin**.
1. On the left sidebar, select **Settings > Integrations**.
1. Select **Telegram**.
1. In **Enable integration**, select the **Active** checkbox.
1. In **New token**, [paste the token value from the Telegram bot](#create-a-telegram-bot).
1. In the **Trigger** section, select the checkboxes for the GitLab events you want to receive in Telegram.
1. In **Channel identifier**, [paste the channel identifier from the Telegram channel](#configure-the-telegram-bot).
- To get a private channel ID, use the [`getUpdates`](https://core.telegram.org/bots/api#getupdates) method.
1. Optional. Select **Test settings**.
1. Select **Save changes**.
The Telegram channel can now receive all selected GitLab events.

View File

@ -1143,7 +1143,8 @@ Payload example:
"key": "NESTOR_PROD_ENVIRONMENT",
"value": "us-west-1"
}
]
],
"url": "http://example.com/gitlab-org/gitlab-test/-/pipelines/31"
},
"merge_request": {
"id": 1,

View File

@ -183,8 +183,10 @@ Code Suggestions do not prevent you from writing code in your IDE.
### Internet connectivity
Code Suggestions only work when you have internet connectivity and can access GitLab.com.
Code Suggestions are not available for self-managed customers, nor customers operating within an offline environment.
To use Code Suggestions:
- On GitLab.com, you must have an internet connection and be able to access GitLab.
- In GitLab 16.1 and later, on self-managed GitLab, you must have an internet connection. Code Suggestions does not work with offline environments.
### Model accuracy and quality

View File

@ -918,6 +918,21 @@ module API
desc: 'The password of the user'
}
],
'telegram' => [
{
required: true,
name: :token,
type: String,
desc: 'The Telegram chat token. For example, 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11'
},
{
required: true,
name: :room,
type: String,
desc: 'Unique identifier for the target chat or username of the target channel (in the format @channelusername)'
},
chat_notification_events
].flatten,
'unify-circuit' => [
{
required: true,

View File

@ -77,7 +77,8 @@ module Gitlab
finished_at: pipeline.finished_at,
duration: pipeline.duration,
queued_duration: pipeline.queued_duration,
variables: pipeline.variables.map(&:hook_attrs)
variables: pipeline.variables.map(&:hook_attrs),
url: Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline)
}
end

View File

@ -19,6 +19,7 @@ module Sidebars
[
:security_dashboard,
:vulnerability_report,
:dependency_list,
:audit_events,
:compliance,
:scan_policies

View File

@ -9551,6 +9551,9 @@ msgstr ""
msgid "Choose file…"
msgstr ""
msgid "Choose protected branch"
msgstr ""
msgid "Choose the top-level group for your repository imports."
msgstr ""
@ -11091,6 +11094,12 @@ msgstr ""
msgid "CodeSuggestionsSM|Your personal access token from GitLab.com. See the %{link_start}documentation%{link_end} for information on creating a personal access token."
msgstr ""
msgid "CodeSuggestionsThirdPartyAlert|%{code_suggestions_link_start}Code Suggestions%{link_end} now uses third-party AI services to provide higher quality suggestions. You can %{third_party_link_start}disable third-party services%{link_end} for your group, or disable Code Suggestions entirely in %{profile_settings_link_start}your user profile%{link_end}."
msgstr ""
msgid "CodeSuggestionsThirdPartyAlert|We use third-party AI services to improve Code Suggestions."
msgstr ""
msgid "CodeSuggestions|%{link_start}What are code suggestions?%{link_end}"
msgstr ""
@ -42007,6 +42016,12 @@ msgstr ""
msgid "Select projects"
msgstr ""
msgid "Select protected branch"
msgstr ""
msgid "Select protected branches"
msgstr ""
msgid "Select report"
msgstr ""
@ -43739,6 +43754,9 @@ msgstr ""
msgid "Specific branches"
msgstr ""
msgid "Specific protected branches"
msgstr ""
msgid "Specified URL cannot be used: \"%{reason}\""
msgstr ""
@ -45147,6 +45165,21 @@ msgstr ""
msgid "TeamcityIntegration|Trigger TeamCity CI after every push to the repository, except branch delete"
msgstr ""
msgid "TelegramIntegration|Leave blank to use your current token."
msgstr ""
msgid "TelegramIntegration|New token"
msgstr ""
msgid "TelegramIntegration|Send notifications about project events to Telegram."
msgstr ""
msgid "TelegramIntegration|Send notifications about project events to Telegram. %{docs_link}"
msgstr ""
msgid "TelegramIntegration|Unique authentication token."
msgstr ""
msgid "Telephone number"
msgstr ""

View File

@ -30,8 +30,12 @@ RUN set -eux; \
apt-get autoclean -y
# Clone GDK and install system dependencies, purge system git
ARG GDK_SHA
ENV GDK_SHA=${GDK_SHA:-main}
RUN set -eux; \
git -c advice.detachedHead=false clone --depth 1 --branch ${GDK_BRANCH_OR_TAG:-main} https://gitlab.com/gitlab-org/gitlab-development-kit.git; \
git -c advice.detachedHead=false clone --depth 1 https://gitlab.com/gitlab-org/gitlab-development-kit.git; \
git -C gitlab-development-kit fetch --depth 1 origin ${GDK_SHA}; \
git -C gitlab-development-kit -c advice.detachedHead=false checkout ${GDK_SHA}; \
mkdir -p gitlab-development-kit/gitlab && chown -R gdk:gdk gitlab-development-kit; \
apt-get update && apt-get install -y --no-install-recommends $(grep -o '^[^#]*' gitlab-development-kit/packages_debian.txt); \
apt-get remove -y git git-lfs; \

View File

@ -9,10 +9,22 @@ SHA_TAG="${CI_COMMIT_SHA}"
BRANCH_TAG="${CI_COMMIT_REF_SLUG}"
BASE_TAG=$([ "${BUILD_GDK_BASE}" == "true" ] && echo "${SHA_TAG}" || echo "master")
if [[ -z "${GDK_SHA}" ]]; then
GDK_SHA=$(git ls-remote https://gitlab.com/gitlab-org/gitlab-development-kit.git main | cut -f 1)
fi
if [[ -n "${CI}" ]]; then
OUTPUT_OPTION="--push"
else
OUTPUT_OPTION="--load"
fi
function build_image() {
local image=$1
local target=$2
echoinfo "Using GDK at SHA ${GDK_SHA}"
docker buildx build \
--cache-to="type=inline" \
--cache-from="${image}:${BRANCH_TAG}" \
@ -23,7 +35,8 @@ function build_image() {
--tag="${image}:${SHA_TAG}" \
--tag="${image}:${BRANCH_TAG}" \
--build-arg="BASE_TAG=${BASE_TAG}" \
--push \
--build-arg="GDK_SHA=${GDK_SHA:-main}" \
${OUTPUT_OPTION} \
.
}

View File

@ -319,6 +319,15 @@ FactoryBot.define do
token { 'squash_tm_token' }
end
factory :telegram_integration, class: 'Integrations::Telegram' do
project
type { 'Integrations::Telegram' }
active { true }
token { '123456:ABC-DEF1234' }
room { '@channel' }
end
# this is for testing storing values inside properties, which is deprecated and will be removed in
# https://gitlab.com/gitlab-org/gitlab/issues/29404
trait :without_properties_callback do

View File

@ -86,10 +86,11 @@ RSpec.describe 'Profile > Password', feature_category: :user_profile do
Rails.application.reload_routes!
end
it 'renders 404' do
it 'renders 404', :js do
visit edit_profile_password_path
expect(page).to have_gitlab_http_status(:not_found)
expect(page).to have_title('Not Found')
expect(page).to have_content('Page Not Found')
end
end
end

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'User uses inherited settings', :js, feature_category: :integrations do
include JiraIntegrationHelpers
include ListboxHelpers
include_context 'project integration activation'
@ -24,8 +25,7 @@ RSpec.describe 'User uses inherited settings', :js, feature_category: :integrati
expect(page).to have_field('Web URL', with: parent_settings[:url], readonly: true)
expect(page).to have_field('New API token or password', with: '', readonly: true)
click_on 'Use default settings'
click_on 'Use custom settings'
select_from_listbox('Use custom settings', from: 'Use default settings')
expect(page).not_to have_button('Use default settings')
expect(page).to have_field('Web URL', with: project_settings[:url], readonly: false)
@ -55,8 +55,7 @@ RSpec.describe 'User uses inherited settings', :js, feature_category: :integrati
expect(page).to have_field('URL', with: project_settings[:url], readonly: false)
expect(page).to have_field('New API token or password', with: '', readonly: false)
click_on 'Use custom settings'
click_on 'Use default settings'
select_from_listbox('Use default settings', from: 'Use custom settings')
expect(page).not_to have_button('Use custom settings')
expect(page).to have_field('URL', with: parent_settings[:url], readonly: true)

View File

@ -39,7 +39,7 @@ export default function createComponent({
Vue.use(Vuex);
const fakeApollo = createMockApollo([
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse({ issuesCount }))],
...apolloQueryHandlers,
]);

View File

@ -964,11 +964,14 @@ export const issueBoardListsQueryResponse = {
},
};
export const boardListQueryResponse = (issuesCount = 20) => ({
export const boardListQueryResponse = ({
listId = 'gid://gitlab/List/5',
issuesCount = 20,
} = {}) => ({
data: {
boardList: {
__typename: 'BoardList',
id: 'gid://gitlab/BoardList/5',
id: listId,
totalWeight: 5,
issuesCount,
},

View File

@ -1340,8 +1340,8 @@ describe('updateIssueOrder', () => {
};
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: rawIssue,
issuableMoveList: {
issuable: rawIssue,
errors: [],
},
},
@ -1355,8 +1355,8 @@ describe('updateIssueOrder', () => {
it('should commit MUTATE_ISSUE_SUCCESS mutation when successful', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: rawIssue,
issuableMoveList: {
issuable: rawIssue,
errors: [],
},
},
@ -1387,8 +1387,8 @@ describe('updateIssueOrder', () => {
it('should commit SET_ERROR and dispatch undoMoveIssueCard', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
issueMoveList: {
issue: {},
issuableMoveList: {
issuable: {},
errors: [{ foo: 'bar' }],
},
},

View File

@ -1,4 +1,4 @@
import { GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@ -21,7 +21,7 @@ describe('External URL Component', () => {
});
};
const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
describe('event hub', () => {
beforeEach(() => {
@ -30,13 +30,13 @@ describe('External URL Component', () => {
it('should render a dropdown item to delete the environment', () => {
expect(findDropdownItem().exists()).toBe(true);
expect(wrapper.text()).toEqual('Delete environment');
expect(findDropdownItem().attributes('variant')).toBe('danger');
expect(findDropdownItem().props('item').text).toBe('Delete environment');
expect(findDropdownItem().props('item').extraAttrs.variant).toBe('danger');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
findDropdownItem().vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', resolvedEnvironment);
});
});
@ -55,13 +55,13 @@ describe('External URL Component', () => {
it('should render a dropdown item to delete the environment', () => {
expect(findDropdownItem().exists()).toBe(true);
expect(wrapper.text()).toEqual('Delete environment');
expect(findDropdownItem().attributes('variant')).toBe('danger');
expect(findDropdownItem().props('item').text).toBe('Delete environment');
expect(findDropdownItem().props('item').extraAttrs.variant).toBe('danger');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
findDropdownItem().vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToDelete,
variables: { environment: resolvedEnvironment },

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import cancelAutoStopMutation from '~/environments/graphql/mutations/cancel_auto_stop.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
@ -18,6 +18,8 @@ describe('Pin Component', () => {
const autoStopUrl = '/root/auto-stop-env-test/-/environments/38/cancel_auto_stop';
const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
describe('without graphql', () => {
beforeEach(() => {
factory({
@ -28,14 +30,13 @@ describe('Pin Component', () => {
});
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
expect(findDropdownItem().props('item').text).toBe('Prevent auto-stopping');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const item = wrapper.findComponent(GlDropdownItem);
item.vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
});
@ -57,14 +58,13 @@ describe('Pin Component', () => {
});
it('should render the component with descriptive text', () => {
expect(wrapper.text()).toBe('Prevent auto-stopping');
expect(findDropdownItem().props('item').text).toBe('Prevent auto-stopping');
});
it('should emit onPinClick when clicked', () => {
jest.spyOn(mockApollo.defaultClient, 'mutate');
const item = wrapper.findComponent(GlDropdownItem);
item.vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(mockApollo.defaultClient.mutate).toHaveBeenCalledWith({
mutation: cancelAutoStopMutation,

View File

@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GlDropdownItem } from '@gitlab/ui';
import { GlDisclosureDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
import eventHub from '~/environments/event_hub';
@ -8,10 +8,14 @@ import setEnvironmentToRollback from '~/environments/graphql/mutations/set_envir
import createMockApollo from 'helpers/mock_apollo_helper';
describe('Rollback Component', () => {
let wrapper;
const retryUrl = 'https://gitlab.com/retry';
const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
it('Should render Re-deploy label when isLastDeployment is true', () => {
const wrapper = shallowMount(RollbackComponent, {
wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: true,
@ -19,11 +23,11 @@ describe('Rollback Component', () => {
},
});
expect(wrapper.text()).toBe('Re-deploy to environment');
expect(findDropdownItem().props('item').text).toBe('Re-deploy to environment');
});
it('Should render Rollback label when isLastDeployment is false', () => {
const wrapper = shallowMount(RollbackComponent, {
wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: false,
@ -31,12 +35,12 @@ describe('Rollback Component', () => {
},
});
expect(wrapper.text()).toBe('Rollback environment');
expect(findDropdownItem().props('item').text).toBe('Rollback environment');
});
it('should emit a "rollback" event on button click', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
const wrapper = shallowMount(RollbackComponent, {
wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
environment: {
@ -44,9 +48,8 @@ describe('Rollback Component', () => {
},
},
});
const button = wrapper.findComponent(GlDropdownItem);
button.vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(eventHubSpy).toHaveBeenCalledWith('requestRollbackEnvironment', {
retryUrl,
@ -63,7 +66,8 @@ describe('Rollback Component', () => {
const environment = {
name: 'test',
};
const wrapper = shallowMount(RollbackComponent, {
wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
graphql: true,
@ -71,8 +75,8 @@ describe('Rollback Component', () => {
},
apolloProvider,
});
const button = wrapper.findComponent(GlDropdownItem);
button.vm.$emit('click');
findDropdownItem().vm.$emit('action');
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
mutation: setEnvironmentToRollback,

View File

@ -17,7 +17,7 @@ describe('Terminal Component', () => {
});
it('should render a link to open a web terminal with the provided path', () => {
const link = wrapper.findByRole('menuitem', { name: __('Terminal') });
const link = wrapper.findByRole('link', { name: __('Terminal') });
expect(link.attributes('href')).toBe(terminalPath);
});

View File

@ -201,7 +201,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('shows the option to rollback/re-deploy if available', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', {
const rollback = wrapper.findByRole('button', {
name: s__('Environments|Re-deploy to environment'),
});
@ -214,7 +214,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', {
const rollback = wrapper.findByRole('button', {
name: s__('Environments|Re-deploy to environment'),
});
@ -240,7 +240,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
it('shows the option to pin the environment if there is an autostop date', () => {
const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
const pin = wrapper.findByRole('button', { name: __('Prevent auto-stopping') });
expect(pin.exists()).toBe(true);
});
@ -260,7 +260,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('does not show the option to pin the environment if there is no autostop date', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
const pin = wrapper.findByRole('button', { name: __('Prevent auto-stopping') });
expect(pin.exists()).toBe(false);
});
@ -295,7 +295,7 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('does not show the option to pin the environment if there is no autostop date', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const pin = wrapper.findByRole('menuitem', { name: __('Prevent auto-stopping') });
const pin = wrapper.findByRole('button', { name: __('Prevent auto-stopping') });
expect(pin.exists()).toBe(false);
});
@ -319,17 +319,17 @@ describe('~/environments/components/new_environment_item.vue', () => {
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
const terminal = wrapper.findByRole('link', { name: __('Terminal') });
expect(rollback.exists()).toBe(true);
expect(terminal.exists()).toBe(true);
});
it('does not show the link to the terminal if not set up', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', { name: __('Terminal') });
const terminal = wrapper.findByRole('link', { name: __('Terminal') });
expect(rollback.exists()).toBe(false);
expect(terminal.exists()).toBe(false);
});
});
@ -342,21 +342,21 @@ describe('~/environments/components/new_environment_item.vue', () => {
apolloProvider: createApolloProvider(),
});
const rollback = wrapper.findByRole('menuitem', {
const deleteTrigger = wrapper.findByRole('button', {
name: s__('Environments|Delete environment'),
});
expect(rollback.exists()).toBe(true);
expect(deleteTrigger.exists()).toBe(true);
});
it('does not show the button to delete the environment if not possible', () => {
wrapper = createWrapper({ apolloProvider: createApolloProvider() });
const rollback = wrapper.findByRole('menuitem', {
const deleteTrigger = wrapper.findByRole('button', {
name: s__('Environments|Delete environment'),
});
expect(rollback.exists()).toBe(false);
expect(deleteTrigger.exists()).toBe(false);
});
});

View File

@ -1,4 +1,4 @@
import { GlDropdown, GlLink } from '@gitlab/ui';
import { GlCollapsibleListbox, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
@ -27,14 +27,14 @@ describe('OverrideDropdown', () => {
};
const findGlLink = () => wrapper.findComponent(GlLink);
const findGlDropdown = () => wrapper.findComponent(GlDropdown);
const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
describe('template', () => {
describe('override prop is true', () => {
it('renders GlToggle as disabled', () => {
createComponent();
expect(findGlDropdown().props('text')).toBe('Use custom settings');
expect(findGlCollapsibleListbox().props('toggleText')).toBe('Use custom settings');
});
});
@ -42,7 +42,7 @@ describe('OverrideDropdown', () => {
it('renders GlToggle as disabled', () => {
createComponent({ override: false });
expect(findGlDropdown().props('text')).toBe('Use default settings');
expect(findGlCollapsibleListbox().props('toggleText')).toBe('Use default settings');
});
});

View File

@ -33,6 +33,7 @@ RSpec.describe Gitlab::DataBuilder::Pipeline do
expect(attributes[:iid]).to eq(pipeline.iid)
expect(attributes[:source]).to eq(pipeline.source)
expect(attributes[:status]).to eq(pipeline.status)
expect(attributes[:url]).to eq(Gitlab::Routing.url_helpers.project_pipeline_url(pipeline.project, pipeline))
expect(attributes[:detailed_status]).to eq('passed')
expect(build_data).to be_a(Hash)
expect(build_data[:id]).to eq(build.id)

View File

@ -765,6 +765,7 @@ project:
- freeze_periods
- pumble_integration
- webex_teams_integration
- telegram_integration
- build_report_results
- vulnerability_statistic
- vulnerability_historical_statistics

View File

@ -17,6 +17,7 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::SecureMenu, feature_category
expect(items.map(&:item_id)).to eq([
:security_dashboard,
:vulnerability_report,
:dependency_list,
:audit_events,
:compliance,
:scan_policies

View File

@ -0,0 +1,53 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Integrations::Telegram, feature_category: :integrations do
it_behaves_like "chat integration", "Telegram" do
let(:payload) do
{
text: be_present
}
end
end
describe 'validations' do
context 'when integration is active' do
before do
subject.active = true
end
it { is_expected.to validate_presence_of(:token) }
it { is_expected.to validate_presence_of(:room) }
end
context 'when integration is inactive' do
before do
subject.active = false
end
it { is_expected.not_to validate_presence_of(:token) }
it { is_expected.not_to validate_presence_of(:room) }
end
end
describe 'before_validation :set_webhook' do
context 'when token is not present' do
let(:integration) { build(:telegram_integration, token: nil) }
it 'does not set webhook value' do
expect(integration.webhook).to eq(nil)
expect(integration).not_to be_valid
end
end
context 'when token is present' do
let(:integration) { create(:telegram_integration) }
it 'sets webhook value' do
expect(integration).to be_valid
expect(integration.webhook).to eq("https://api.telegram.org/bot123456:ABC-DEF1234/sendMessage")
end
end
end
end

View File

@ -2111,15 +2111,48 @@ RSpec.describe Issue, feature_category: :team_planning do
end
describe 'issue_type enum generated methods' do
using RSpec::Parameterized::TableSyntax
describe '#<issue_type>?' do
let_it_be(:issue) { create(:issue, project: reusable_project) }
where(issue_type: WorkItems::Type.base_types.keys)
with_them do
it 'raises an error if called' do
expect { issue.public_send("#{issue_type}?".to_sym) }.to raise_error(Issue::ForbiddenColumnUsed)
expect { issue.public_send("#{issue_type}?".to_sym) }.to raise_error(
Issue::ForbiddenColumnUsed,
a_string_matching(/`issue\.#{issue_type}\?` uses the `issue_type` column underneath/)
)
end
end
end
describe '.<issue_type> scopes' do
where(issue_type: WorkItems::Type.base_types.keys)
with_them do
it 'raises an error if called' do
expect { Issue.public_send(issue_type.to_sym) }.to raise_error(
Issue::ForbiddenColumnUsed,
a_string_matching(/`Issue\.#{issue_type}` uses the `issue_type` column underneath/)
)
end
context 'when called in a production environment' do
before do
stub_rails_env('production')
end
it 'returns issues scoped by type instead of raising an error' do
issue = create(
:issue,
issue_type: issue_type,
work_item_type: WorkItems::Type.default_by_type(issue_type),
project: reusable_project
)
expect(Issue.public_send(issue_type.to_sym)).to contain_exactly(issue)
end
end
end
end
end

View File

@ -15,7 +15,6 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
let(:repository_storage) { 'default' }
describe 'associations' do
it { is_expected.to belong_to(:organization).class_name('Organizations::Organization') }
it { is_expected.to have_many :projects }
it { is_expected.to have_many :project_statistics }
it { is_expected.to belong_to :parent }
@ -2746,11 +2745,4 @@ RSpec.describe Namespace, feature_category: :groups_and_projects do
end
end
end
context 'with loose foreign key on organization_id' do
it_behaves_like 'cleanup by a loose foreign key' do
let!(:parent) { create(:organization) }
let!(:model) { create(:namespace, organization: parent) }
end
end
end

View File

@ -6,11 +6,6 @@ RSpec.describe Organizations::Organization, type: :model, feature_category: :cel
let_it_be(:organization) { create(:organization) }
let_it_be(:default_organization) { create(:organization, :default) }
describe 'associations' do
it { is_expected.to have_many :namespaces }
it { is_expected.to have_many :groups }
end
describe 'validations' do
subject { create(:organization) }

View File

@ -49,6 +49,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
it { is_expected.to have_one(:microsoft_teams_integration) }
it { is_expected.to have_one(:mattermost_integration) }
it { is_expected.to have_one(:hangouts_chat_integration) }
it { is_expected.to have_one(:telegram_integration) }
it { is_expected.to have_one(:unify_circuit_integration) }
it { is_expected.to have_one(:pumble_integration) }
it { is_expected.to have_one(:webex_teams_integration) }