Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									10e15ac3c2
								
							
						
					
					
						commit
						c3eeb6a8d6
					
				|  | @ -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: | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|         }); | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  |  | |||
|  | @ -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, | ||||
|   }, | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|         }, | ||||
|       }, | ||||
|     }), | ||||
|   ); | ||||
| } | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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); | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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 = () => { | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 } | ||||
|  |  | |||
|  | @ -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' | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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' } | ||||
|  |  | |||
|  | @ -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' } | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  | @ -50,6 +50,7 @@ classes: | |||
| - Integrations::SlackSlashCommands | ||||
| - Integrations::SquashTm | ||||
| - Integrations::Teamcity | ||||
| - Integrations::Telegram | ||||
| - Integrations::UnifyCircuit | ||||
| - Integrations::WebexTeams | ||||
| - Integrations::Youtrack | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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(); | ||||
|  |  | |||
|  | @ -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. | | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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). | ||||
|  |  | |||
|  | @ -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>\</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" | ||||
| ``` | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
| 
 | ||||
|  | ||||
|  | @ -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. | ||||
| 
 | ||||
|  | ||||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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. | ||||
|  |  | |||
|  | @ -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**. | ||||
|  |  | |||
|  | @ -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_: | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | | ||||
|  |  | |||
|  | @ -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. | ||||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ module Sidebars | |||
|           [ | ||||
|             :security_dashboard, | ||||
|             :vulnerability_report, | ||||
|             :dependency_list, | ||||
|             :audit_events, | ||||
|             :compliance, | ||||
|             :scan_policies | ||||
|  |  | |||
|  | @ -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 "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -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; \ | ||||
|  |  | |||
|  | @ -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} \ | ||||
|     . | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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, | ||||
|   ]); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
|     }, | ||||
|  |  | |||
|  | @ -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' }], | ||||
|         }, | ||||
|       }, | ||||
|  |  | |||
|  | @ -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 }, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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'); | ||||
|       }); | ||||
|     }); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -765,6 +765,7 @@ project: | |||
| - freeze_periods | ||||
| - pumble_integration | ||||
| - webex_teams_integration | ||||
| - telegram_integration | ||||
| - build_report_results | ||||
| - vulnerability_statistic | ||||
| - vulnerability_historical_statistics | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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 | ||||
|  |  | |||
|  | @ -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) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue