diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 00:08:01 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-23 00:08:01 +0300 |
commit | c50e042a392687730db9b8c2607883485b258ae4 (patch) | |
tree | 519b069aa0a400241a2f8dc0f900f09625e3d8ed | |
parent | 7e2f555a6dc37839727dee130d8ed4421b680d42 (diff) |
Add latest changes from gitlab-org/gitlab@master
68 files changed, 690 insertions, 422 deletions
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue index 4d04b5d4b14..149c69e5307 100644 --- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue +++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue @@ -20,7 +20,7 @@ export default { }, computed: { paused() { - return !this.runner.active; + return this.runner.paused; }, }, }; diff --git a/app/assets/javascripts/ci/runner/components/runner_header.vue b/app/assets/javascripts/ci/runner/components/runner_header.vue index 874c234ca4c..9e29dc7a52e 100644 --- a/app/assets/javascripts/ci/runner/components/runner_header.vue +++ b/app/assets/javascripts/ci/runner/components/runner_header.vue @@ -25,9 +25,6 @@ export default { }, }, computed: { - paused() { - return !this.runner.active; - }, heading() { const id = getIdFromGraphQLId(this.runner.id); return sprintf(I18N_DETAILS_TITLE, { runner_id: id }); diff --git a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue index a27af232e97..d16c8f98bad 100644 --- a/app/assets/javascripts/ci/runner/components/runner_pause_button.vue +++ b/app/assets/javascripts/ci/runner/components/runner_pause_button.vue @@ -1,6 +1,6 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; +import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; import { createAlert } from '~/alert'; import { captureException } from '~/ci/runner/sentry_utils'; import { I18N_PAUSE, I18N_PAUSE_TOOLTIP, I18N_RESUME, I18N_RESUME_TOOLTIP } from '../constants'; @@ -31,14 +31,14 @@ export default { }; }, computed: { - isActive() { - return this.runner.active; + isPaused() { + return this.runner.paused; }, icon() { - return this.isActive ? 'pause' : 'play'; + return this.isPaused ? 'play' : 'pause'; }, label() { - return this.isActive ? I18N_PAUSE : I18N_RESUME; + return this.isPaused ? I18N_RESUME : I18N_PAUSE; }, buttonContent() { if (this.compact) { @@ -56,7 +56,7 @@ export default { // Prevent a "sticky" tooltip: If this button is disabled, // mouseout listeners don't run leaving the tooltip stuck if (!this.updating) { - return this.isActive ? I18N_PAUSE_TOOLTIP : I18N_RESUME_TOOLTIP; + return this.isPaused ? I18N_RESUME_TOOLTIP : I18N_PAUSE_TOOLTIP; } return ''; }, @@ -67,7 +67,7 @@ export default { try { const input = { id: this.runner.id, - active: !this.isActive, + paused: !this.isPaused, }; const { @@ -75,7 +75,7 @@ export default { runnerUpdate: { errors }, }, } = await this.$apollo.mutate({ - mutation: runnerToggleActiveMutation, + mutation: runnerTogglePausedMutation, variables: { input, }, diff --git a/app/assets/javascripts/ci/runner/components/runner_update_form.vue b/app/assets/javascripts/ci/runner/components/runner_update_form.vue index 0b05969a551..aebddc70646 100644 --- a/app/assets/javascripts/ci/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/ci/runner/components/runner_update_form.vue @@ -134,12 +134,7 @@ export default { </template> <template v-else> <div class="gl-mb-5"> - <gl-form-checkbox - v-model="model.active" - data-testid="runner-field-paused" - :value="false" - :unchecked-value="true" - > + <gl-form-checkbox v-model="model.paused" data-testid="runner-field-paused"> {{ __('Paused') }} <template #help> {{ s__('Runners|Stop the runner from accepting new jobs.') }} diff --git a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql index d18b80511fb..41ec9967d90 100644 --- a/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/edit/runner_fields_shared.fragment.graphql @@ -2,7 +2,7 @@ fragment RunnerFieldsShared on CiRunner { id shortSha runnerType - active + paused accessLevel runUntagged locked diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql index 4eebcd01be6..624980f58af 100644 --- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql @@ -7,7 +7,7 @@ fragment ListItemShared on CiRunner { shortSha version ipAddress - active + paused locked jobCount tagList diff --git a/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql index 9b15570dbc0..e862a20750f 100644 --- a/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql +++ b/app/assets/javascripts/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql @@ -1,11 +1,11 @@ # Mutation executed for the pause/resume button in the # runner list and details views. -mutation runnerToggleActive($input: RunnerUpdateInput!) { +mutation runnerTogglePaused($input: RunnerUpdateInput!) { runnerUpdate(input: $input) { runner { id - active + paused } errors } diff --git a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql index bd53fb29bd0..87d92b8e263 100644 --- a/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql +++ b/app/assets/javascripts/ci/runner/graphql/show/runner_details_shared.fragment.graphql @@ -2,7 +2,7 @@ fragment RunnerDetailsShared on CiRunner { id shortSha runnerType - active + paused accessLevel runUntagged locked diff --git a/app/assets/javascripts/ci/runner/runner_update_form_utils.js b/app/assets/javascripts/ci/runner/runner_update_form_utils.js index 3b519fa7d71..6f6c9f64af0 100644 --- a/app/assets/javascripts/ci/runner/runner_update_form_utils.js +++ b/app/assets/javascripts/ci/runner/runner_update_form_utils.js @@ -4,7 +4,7 @@ export const runnerToModel = (runner) => { description, maximumTimeout, accessLevel, - active, + paused, locked, runUntagged, tagList = [], @@ -15,7 +15,7 @@ export const runnerToModel = (runner) => { description, maximumTimeout, accessLevel, - active, + paused, locked, runUntagged, tagList: tagList.join(', '), diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue index 8eb8654cddd..10ac4c5383b 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue @@ -1,5 +1,14 @@ <script> -import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; +import { + GlAlert, + GlLink, + GlTable, + GlDropdownItem, + GlDropdown, + GlButton, + GlFormCheckbox, + GlLoadingIcon, +} from '@gitlab/ui'; import { last } from 'lodash'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { __, s__ } from '~/locale'; @@ -9,21 +18,26 @@ import { packageTypeToTrackCategory } from '~/packages_and_registries/package_re import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { + FETCH_PACKAGE_FILES_ERROR_MESSAGE, + GRAPHQL_PACKAGE_FILES_PAGE_SIZE, REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION, SELECT_PACKAGE_FILE_TRACKING_ACTION, TRACKING_LABEL_PACKAGE_ASSET, TRACKING_ACTION_EXPAND_PACKAGE_ASSET, } from '~/packages_and_registries/package_registry/constants'; +import getPackageFilesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql'; export default { name: 'PackageFiles', components: { + GlAlert, GlLink, GlTable, GlDropdown, GlDropdownItem, GlFormCheckbox, GlButton, + GlLoadingIcon, FileIcon, TimeAgoTooltip, FileSha, @@ -40,14 +54,36 @@ export default { required: false, default: false, }, + packageId: { + type: String, + required: true, + }, + packageType: { + type: String, + required: true, + }, + }, + apollo: { packageFiles: { - type: Array, - required: false, - default: () => [], + query: getPackageFilesQuery, + context: { + isSingleRequest: true, + }, + variables() { + return this.queryVariables; + }, + update(data) { + return data.package?.packageFiles?.nodes || []; + }, + error() { + this.fetchPackageFilesError = true; + }, }, }, data() { return { + fetchPackageFilesError: false, + packageFiles: [], selectedReferences: [], }; }, @@ -56,7 +92,7 @@ export default { return this.selectedReferences.length > 0; }, areAllFilesSelected() { - return this.packageFiles.every(this.isSelected); + return this.packageFiles.length > 0 && this.packageFiles.every(this.isSelected); }, filesTableRows() { return this.packageFiles.map((pf) => ({ @@ -68,10 +104,8 @@ export default { hasSelectedSomeFiles() { return this.areFilesSelected && !this.areAllFilesSelected; }, - showCommitColumn() { - // note that this is always false for now since we do not return - // pipelines associated to files for performance concerns - return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); + loading() { + return this.$apollo.queries.packageFiles.loading || this.isLoading; }, filesTableHeaderFields() { return [ @@ -86,11 +120,6 @@ export default { label: __('Name'), }, { - key: 'commit', - label: __('Commit'), - hide: !this.showCommitColumn, - }, - { key: 'size', label: __('Size'), }, @@ -108,6 +137,12 @@ export default { }, ].filter((c) => !c.hide); }, + queryVariables() { + return { + id: this.packageId, + first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE, + }; + }, tracking() { return { category: packageTypeToTrackCategory(this.packageType), @@ -142,6 +177,7 @@ export default { deleteFile: __('Delete asset'), deleteSelected: s__('PackageRegistry|Delete selected'), moreActionsText: __('More actions'), + fetchPackageFilesErrorMessage: FETCH_PACKAGE_FILES_ERROR_MESSAGE, }, }; </script> @@ -151,8 +187,8 @@ export default { <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> <h3 class="gl-font-lg gl-mt-5">{{ __('Assets') }}</h3> <gl-button - v-if="canDelete" - :disabled="isLoading || !areFilesSelected" + v-if="!fetchPackageFilesError && canDelete" + :disabled="loading || !areFilesSelected" category="secondary" variant="danger" data-testid="delete-selected" @@ -161,7 +197,16 @@ export default { {{ $options.i18n.deleteSelected }} </gl-button> </div> + <gl-alert + v-if="fetchPackageFilesError" + variant="danger" + @dismiss="fetchPackageFilesError = false" + > + {{ $options.i18n.fetchPackageFilesErrorMessage }} + </gl-alert> <gl-table + v-else + :busy="loading" :fields="filesTableHeaderFields" :items="filesTableRows" show-empty @@ -171,6 +216,9 @@ export default { :tbody-tr-attr="{ 'data-testid': 'file-row' }" @row-selected="updateSelectedReferences" > + <template #table-busy> + <gl-loading-icon size="lg" class="gl-my-5" /> + </template> <template #head(checkbox)="{ selectAllRows, clearSelected }"> <gl-form-checkbox v-if="canDelete" @@ -218,16 +266,6 @@ export default { </gl-link> </template> - <template #cell(commit)="{ item }"> - <gl-link - v-if="item.pipeline && item.pipeline" - :href="item.pipeline.commitPath" - class="gl-text-gray-500" - data-testid="commit-link" - >{{ item.pipeline.sha }} - </gl-link> - </template> - <template #cell(created)="{ item }"> <time-ago-tooltip :time="item.createdAt" /> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index b4276d69ed6..80712c2991c 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -102,6 +102,9 @@ export const FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE = s__( export const FETCH_PACKAGE_METADATA_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while fetching the package metadata.', ); +export const FETCH_PACKAGE_FILES_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while fetching package assets.', +); export const DELETE_PACKAGES_TRACKING_ACTION = 'delete_packages'; export const REQUEST_DELETE_PACKAGES_TRACKING_ACTION = 'request_delete_packages'; @@ -232,3 +235,4 @@ export const REQUEST_FORWARDING_HELP_PAGE_PATH = helpPagePath( ); export const GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE = 10; +export const GRAPHQL_PACKAGE_FILES_PAGE_SIZE = 100; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js index 39e5da54509..d05ff5daad4 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js @@ -21,6 +21,9 @@ export const apolloProvider = new VueApollo({ keyArgs: false, merge: mergeVariables, }, + packageFiles: { + merge: mergeVariables, + }, }, }, }, diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 984996b829a..e5ef9265f3e 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -52,13 +52,7 @@ query getPackageDetails($id: PackagesPackageID!) { } nodes { id - fileMd5 - fileName - fileSha1 - fileSha256 size - createdAt - downloadPath } } versions { diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql new file mode 100644 index 00000000000..7851cd39200 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql @@ -0,0 +1,17 @@ +query getPackageFiles($id: PackagesPackageID!, $first: Int) { + package(id: $id) { + id + packageFiles(first: $first) { + nodes { + id + fileMd5 + fileName + fileSha1 + fileSha256 + size + createdAt + downloadPath + } + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index 40e175fab99..48a45956ef1 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -158,8 +158,8 @@ export default { isLoading() { return this.$apollo.queries.packageEntity.loading; }, - packageFilesLoading() { - return this.isLoading || this.mutationLoading; + packageFilesMutationLoading() { + return this.mutationLoading; }, isValidPackage() { return this.isLoading || Boolean(this.packageEntity.name); @@ -360,7 +360,7 @@ export default { <gl-tabs> <gl-tab :title="__('Detail')"> - <div v-if="!isLoading" data-qa-selector="package_information_content"> + <div data-qa-selector="package_information_content"> <package-history :package-entity="packageEntity" :project-name="projectName" /> <installation-commands :package-entity="packageEntity" /> @@ -370,16 +370,17 @@ export default { :package-id="packageEntity.id" :package-type="packageType" /> - </div> - <package-files - v-if="showFiles" - :can-delete="packageEntity.canDestroy" - :is-loading="packageFilesLoading" - :package-files="packageFiles" - @download-file="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)" - @delete-files="handleFileDelete" - /> + <package-files + v-if="showFiles" + :can-delete="packageEntity.canDestroy" + :is-loading="packageFilesMutationLoading" + :package-id="packageEntity.id" + :package-type="packageType" + @download-file="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)" + @delete-files="handleFileDelete" + /> + </div> </gl-tab> <gl-tab v-if="showDependencies"> diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql index 5bdafa15f72..c1f994ece24 100644 --- a/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql +++ b/app/assets/javascripts/pipelines/graphql/queries/get_failed_jobs.query.graphql @@ -3,7 +3,7 @@ query getFailedJobs($fullPath: ID!, $pipelineIid: ID!) { id pipeline(iid: $pipelineIid) { id - jobs(statuses: FAILED, retried: false) { + jobs(statuses: FAILED, retried: false, jobKind: BUILD) { nodes { status detailedStatus { diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb index b1ec6b8ba32..94cc417af22 100644 --- a/app/models/concerns/issuable.rb +++ b/app/models/concerns/issuable.rb @@ -654,6 +654,7 @@ module Issuable def read_ability_for(participable_source) return super if participable_source == self + return super if participable_source.is_a?(Note) && participable_source.system? name = participable_source.try(:issuable_ability_name) || :read_issuable_participables diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb index 318d30d8b2e..b4f2282fd7a 100644 --- a/app/models/integrations/jira.rb +++ b/app/models/integrations/jira.rb @@ -374,9 +374,9 @@ module Integrations private def jira_issue_match_regex - match_regex = (jira_issue_regex.presence || Gitlab::Regex.jira_issue_key_regex) + return /\b#{jira_issue_prefix}(?<issue>#{Gitlab::Regex.jira_issue_key_regex})/ if jira_issue_regex.blank? - /\b#{jira_issue_prefix}(?<issue>#{match_regex})/ + Gitlab::UntrustedRegexp.new("\\b#{jira_issue_prefix}(?P<issue>#{jira_issue_regex})") end def parse_project_from_issue_key(issue_key) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index b7e39423e85..e4b2b81005c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -420,7 +420,7 @@ class MergeRequest < ApplicationRecord includes(:metrics) end - scope :with_jira_issue_keys, -> { where('title ~ :regex OR merge_requests.description ~ :regex', regex: Gitlab::Regex.jira_issue_key_regex.source) } + scope :with_jira_issue_keys, -> { where('title ~ :regex OR merge_requests.description ~ :regex', regex: Gitlab::Regex.jira_issue_key_regex(expression_escape: '\m').source) } scope :review_requested, -> do where(reviewers_subquery.exists) diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml index 1f742279756..09eeaa8386d 100644 --- a/app/views/layouts/group.html.haml +++ b/app/views/layouts/group.html.haml @@ -20,6 +20,7 @@ = render 'groups/invite_members_modal', group: @group = 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/free_user_cap_alert", source: @group = dispensable_render_if_exists "shared/unlimited_members_during_trial_alert", resource: @group diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml index 31d02324e68..bb67d8bce14 100644 --- a/app/views/layouts/project.html.haml +++ b/app/views/layouts/project.html.haml @@ -22,6 +22,7 @@ = render 'projects/invite_members_modal', project: @project = 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/free_user_cap_alert", project: @project = dispensable_render_if_exists 'shared/unlimited_members_during_trial_alert', resource: @project diff --git a/app/views/shared/issuable/form/_metadata.html.haml b/app/views/shared/issuable/form/_metadata.html.haml index b27fd8ab7d2..1dc24d205d0 100644 --- a/app/views/shared/issuable/form/_metadata.html.haml +++ b/app/views/shared/issuable/form/_metadata.html.haml @@ -6,12 +6,12 @@ - if @add_related_issue .form-group - .form-check - = check_box_tag :add_related_issue, @add_related_issue.iid, true, class: 'form-check-input' - = label_tag :add_related_issue, class: 'form-check-label' do + = render Pajamas::CheckboxTagComponent.new(name: :add_related_issue, value: @add_related_issue.iid, checked: true) do |c| + = c.label do - add_related_issue_link = link_to "\##{@add_related_issue.iid}", issue_path(@add_related_issue), class: ['has-tooltip'], title: @add_related_issue.title #{_('Relate to %{issuable_type} %{add_related_issue_link}').html_safe % { issuable_type: @add_related_issue.issue_type, add_related_issue_link: add_related_issue_link }} - %p.text-muted= _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type } + = c.help_text do + = _('Adds this %{issuable_type} as related to the %{issuable_type} it was created from') % { issuable_type: @add_related_issue.issue_type } - if issuable.respond_to?(:confidential) && can?(current_user, :set_confidentiality, issuable) .form-group diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md index 013c4515268..3c8b9bf187f 100644 --- a/doc/administration/monitoring/prometheus/index.md +++ b/doc/administration/monitoring/prometheus/index.md @@ -190,6 +190,7 @@ To use an external Prometheus server: # Rails nodes gitlab_exporter['listen_address'] = '0.0.0.0' gitlab_exporter['listen_port'] = '9168' + registry['debug_addr'] = '0.0.0.0:5001' # Sidekiq nodes sidekiq['listen_address'] = '0.0.0.0' @@ -205,14 +206,12 @@ To use an external Prometheus server: # ... prometheus_listen_addr: '0.0.0.0:9236', } + + # Pgbouncer nodes + pgbouncer_exporter['listen_address'] = '0.0.0.0:9188' ``` 1. Install and set up a dedicated Prometheus instance, if necessary, using the [official installation instructions](https://prometheus.io/docs/prometheus/latest/installation/). -1. Add the Prometheus server IP address to the [monitoring IP allowlist](../ip_allowlist.md). For example: - - ```ruby - gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '192.168.0.1'] - ``` 1. On **all** GitLab Rails(Puma, Sidekiq) servers, set the Prometheus server IP address and listen port. For example: @@ -232,6 +231,15 @@ To use an external Prometheus server: } ``` +1. To allow the Prometheus server to fetch from the [GitLab metrics](#gitlab-metrics) endpoint, add the Prometheus +server IP address to the [monitoring IP allowlist](../ip_allowlist.md): + + ```ruby + gitlab_rails['monitoring_whitelist'] = ['127.0.0.0/8', '192.168.0.1'] + ``` + +1. As we are setting each bundled service's [exporter](#bundled-software-metrics) to listen on a network address, +update the firewall on the instance to only allow traffic from your Prometheus IP for the exporters enabled. A full reference list of exporter services and their respective ports can be found [here](../../package_information/defaults.md#ports). 1. [Reconfigure GitLab](../../restart_gitlab.md#omnibus-gitlab-reconfigure) to apply the changes. 1. Edit the Prometheus server's configuration file. 1. Add each node's exporters to the Prometheus server's diff --git a/doc/administration/package_information/defaults.md b/doc/administration/package_information/defaults.md index 96b56388ea9..ac183afdc2f 100644 --- a/doc/administration/package_information/defaults.md +++ b/doc/administration/package_information/defaults.md @@ -53,6 +53,8 @@ by default: | Gitaly | Yes | Socket | Port (8075) | 8075 or 9999 (TLS) | | Gitaly exporter | Yes | Port | X | 9236 | | Praefect | No | Port | X | 2305 or 3305 (TLS) | +| GitLab Workhorse exporter | Yes | Port | X | 9229 | +| Registry exporter | No | Port | X | 5001 | Legend: diff --git a/doc/ci/environments/index.md b/doc/ci/environments/index.md index f4d155369e9..137bc883072 100644 --- a/doc/ci/environments/index.md +++ b/doc/ci/environments/index.md @@ -61,6 +61,12 @@ To search environments by name: - For example when the name is `review/test-app`, search term `test` matches `review/test-app`. - Also searching with the folder name prefixed like `review/test` matches `review/test-app`. +## CI/CD variables + +To customize your environments and deployments, you can use any of the +[predefined CI/CD variables](../../ci/variables/predefined_variables.md), +and define custom CI/CD variables. + ## Types of environments An environment is either static or dynamic: @@ -123,7 +129,8 @@ deploy_staging: ### Create a dynamic environment -To create a dynamic environment, you use [CI/CD variables](../variables/index.md) that are unique to each pipeline. +To create a dynamic environment, you use [CI/CD variables](#cicd-variables) that are +unique to each pipeline. Prerequisites: @@ -158,6 +165,79 @@ deploy_review_app: - main ``` +#### Set a dynamic environment URL + +Some external hosting platforms generate a random URL for each deployment, for example: +`https://94dd65b.amazonaws.com/qa-lambda-1234567`. That makes it difficult to reference the URL in +the `.gitlab-ci.yml` file. + +To address this problem, you can configure a deployment job to report back a set of +variables. These variables include the URL that was dynamically generated by the external service. +GitLab supports the [dotenv (`.env`)](https://github.com/bkeepers/dotenv) file format, +and expands the `environment:url` value with variables defined in the `.env` file. + +To use this feature, specify the +[`artifacts:reports:dotenv`](../yaml/artifacts_reports.md#artifactsreportsdotenv) keyword in `.gitlab-ci.yml`. + +You can also specify a static part of the URL at `environment:url`, such as +`https://$DYNAMIC_ENVIRONMENT_URL`. If the value of `DYNAMIC_ENVIRONMENT_URL` is `example.com`, the +final result is `https://example.com`. + +The assigned URL for the `review/your-branch-name` environment is visible in the UI. + +<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> +For an overview, see [Set dynamic URLs after a job finished](https://youtu.be/70jDXtOf4Ig). + +In the following example a review app creates a new environment for each merge request: + +- The `review` job is triggered by every push, and creates or updates an environment named + `review/your-branch-name`. The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`. +- When the `review` job finishes, GitLab updates the `review/your-branch-name` environment's URL. + It parses the `deploy.env` report artifact, registers a list of variables as runtime-created, + expands the `environment:url: $DYNAMIC_ENVIRONMENT_URL` and sets it to the environment + URL. + +```yaml +review: + script: + - DYNAMIC_ENVIRONMENT_URL=$(deploy-script) # In script, get the environment URL. + - echo "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" >> deploy.env # Add the value to a dotenv file. + artifacts: + reports: + dotenv: deploy.env # Report back dotenv file to rails. + environment: + name: review/$CI_COMMIT_REF_SLUG + url: $DYNAMIC_ENVIRONMENT_URL # and set the variable produced in script to `environment:url` + on_stop: stop_review + +stop_review: + script: + - ./teardown-environment + when: manual + environment: + name: review/$CI_COMMIT_REF_SLUG + action: stop +``` + +Note the following: + +- `stop_review` doesn't generate a dotenv report artifact, so it doesn't recognize the + `DYNAMIC_ENVIRONMENT_URL` environment variable. Therefore you shouldn't set `environment:url` in the + `stop_review` job. +- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update + the environment URL. +- If the script that runs in `stop_review` exists only in your repository and therefore can't use + `GIT_STRATEGY: none`, configure [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md) + for these jobs. This ensures that runners can fetch the repository even after a feature branch is + deleted. For more information, see [Ref Specs for Runners](../pipelines/index.md#ref-specs-for-runners). + +NOTE: +For Windows runners, you should use the PowerShell `Add-Content` command to write to `.env` files. + +```powershell +Add-Content -Path deploy.env -Value "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" +``` + ### Rename an environment > - Renaming an environment by using the UI was [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68550) in GitLab 14.3. @@ -268,105 +348,6 @@ job page, it's displayed above the job trace: Learn how to release production changes to only a portion of your Kubernetes pods with [incremental rollouts](../environments/incremental_rollouts.md). -## CI/CD variables for environments and deployments - -When you create an environment, you specify the name and URL. - -If you want to use the name or URL in another job, you can use: - -- `$CI_ENVIRONMENT_NAME`. The name defined in the `.gitlab-ci.yml` file. -- `$CI_ENVIRONMENT_SLUG`. A "cleaned-up" version of the name, suitable for use in URL and DNS, for example. - This variable is guaranteed to be unique. -- `$CI_ENVIRONMENT_URL`. The environment's URL, which was specified in the - `.gitlab-ci.yml` file or automatically assigned. - -If you change the name of an existing environment, the: - -- `$CI_ENVIRONMENT_NAME` variable is updated with the new environment name. -- `$CI_ENVIRONMENT_SLUG` variable remains unchanged to prevent unintended side - effects. - -## Set dynamic environment URLs after a job finishes - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17066) in GitLab 12.9. - -In a job script, you can specify a static environment URL. -However, there may be times when you want a dynamic URL. For example, -if you deploy a Review App to an external hosting -service that generates a random URL per deployment, like `https://94dd65b.amazonaws.com/qa-lambda-1234567`. -In this case, you don't know the URL before the deployment script finishes. -If you want to use the environment URL in GitLab, you would have to update it manually. - -To address this problem, you can configure a deployment job to report back a set of -variables. These variables include the URL that was dynamically-generated by the external service. -GitLab supports the [dotenv (`.env`)](https://github.com/bkeepers/dotenv) file format, -and expands the `environment:url` value with variables defined in the `.env` file. - -To use this feature, specify the -[`artifacts:reports:dotenv`](../yaml/artifacts_reports.md#artifactsreportsdotenv) keyword in `.gitlab-ci.yml`. - -<i class="fa fa-youtube-play youtube" aria-hidden="true"></i> -For an overview, see [Set dynamic URLs after a job finished](https://youtu.be/70jDXtOf4Ig). - -### Example of setting dynamic environment URLs - -The following example shows a Review App that creates a new environment -for each merge request. The `review` job is triggered by every push, and -creates or updates an environment named `review/your-branch-name`. -The environment URL is set to `$DYNAMIC_ENVIRONMENT_URL`: - -```yaml -review: - script: - - DYNAMIC_ENVIRONMENT_URL=$(deploy-script) # In script, get the environment URL. - - echo "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" >> deploy.env # Add the value to a dotenv file. - artifacts: - reports: - dotenv: deploy.env # Report back dotenv file to rails. - environment: - name: review/$CI_COMMIT_REF_SLUG - url: $DYNAMIC_ENVIRONMENT_URL # and set the variable produced in script to `environment:url` - on_stop: stop_review - -stop_review: - script: - - ./teardown-environment - when: manual - environment: - name: review/$CI_COMMIT_REF_SLUG - action: stop -``` - -As soon as the `review` job finishes, GitLab updates the `review/your-branch-name` -environment's URL. -It parses the `deploy.env` report artifact, registers a list of variables as runtime-created, -uses it for expanding `environment:url: $DYNAMIC_ENVIRONMENT_URL` and sets it to the environment URL. -You can also specify a static part of the URL at `environment:url`, such as -`https://$DYNAMIC_ENVIRONMENT_URL`. If the value of `DYNAMIC_ENVIRONMENT_URL` is -`example.com`, the final result is `https://example.com`. - -The assigned URL for the `review/your-branch-name` environment is visible in the UI. - -Note the following: - -- `stop_review` doesn't generate a dotenv report artifact, so it doesn't recognize the - `DYNAMIC_ENVIRONMENT_URL` environment variable. Therefore you shouldn't set `environment:url` in the - `stop_review` job. -- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update - the environment URL. -- If the script that runs in `stop_review` exists only in your repository and therefore can't use - `GIT_STRATEGY: none`, configure [merge request pipelines](../../ci/pipelines/merge_request_pipelines.md) - for these jobs. This ensures that runners can fetch the repository even after a feature branch is - deleted. For more information, see [Ref Specs for Runners](../pipelines/index.md#ref-specs-for-runners). - -NOTE: -For Windows runners, using `echo` to write to `.env` files may fail. Using the PowerShell `Add-Content`command -helps in such cases. For example: - -```powershell -Add-Content -Path deploy.env -Value "DYNAMIC_ENVIRONMENT_URL=$DYNAMIC_ENVIRONMENT_URL" -``` - ## Track newly included merge requests per deployment GitLab can track newly included merge requests per deployment. diff --git a/doc/ci/yaml/artifacts_reports.md b/doc/ci/yaml/artifacts_reports.md index 78c9e98c33f..37cb7efdf94 100644 --- a/doc/ci/yaml/artifacts_reports.md +++ b/doc/ci/yaml/artifacts_reports.md @@ -189,7 +189,7 @@ GitLab can display the results of one or more reports in: The `dotenv` report collects a set of environment variables as artifacts. The collected variables are registered as runtime-created variables of the job, -which you can use to [set dynamic environment URLs after a job finishes](../environments/index.md#set-dynamic-environment-urls-after-a-job-finishes). +which you can use to [set dynamic environment URLs after a job finishes](../environments/index.md#set-a-dynamic-environment-url). If duplicate environment variables are present in a `dotenv` report: diff --git a/doc/development/vs_code_debugging.md b/doc/development/vs_code_debugging.md index 08aa4688bfd..129eddf853b 100644 --- a/doc/development/vs_code_debugging.md +++ b/doc/development/vs_code_debugging.md @@ -6,12 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w # VS Code debugging -This document describes how to set up Rails debugging in [VS Code](https://code.visualstudio.com/). +This document describes how to set up Rails debugging in [Visual Studio Code (VSCode)](https://code.visualstudio.com/) using the [GitLab Development Kit (GDK)](contributing/first_contribution.md#step-1-configure-the-gitlab-development-kit). ## Setup 1. Install the `debug` gem by running `gem install debug` inside your `gitlab` folder. -1. Add the following configuration to your `.vscode/tasks.json` file: +1. Install the [VSCode Ruby rdbg Debugger](https://marketplace.visualstudio.com/items?itemName=KoichiSasada.vscode-rdbg) extension to add support for the `rdbg` debugger type to VSCode. +1. In case you want to automatically stop and start GitLab and its associated Ruby Rails server, you may add the following VSCode task to your configuration under the `.vscode/tasks.json` file: ```json { @@ -20,7 +21,7 @@ This document describes how to set up Rails debugging in [VS Code](https://code. { "label": "start rdbg", "type": "shell", - "command": "gdk stop rails-web && GITLAB_RAILS_RACK_TIMEOUT_ENABLE_LOGGING=false PUMA_SINGLE_MODE=true rdbg --open -c -- bin/rails s", + "command": "gdk stop rails-web && GITLAB_RAILS_RACK_TIMEOUT_ENABLE_LOGGING=false PUMA_SINGLE_MODE=true rdbg --open -c bin/rails server", "isBackground": true, "problemMatcher": { "owner": "rails", @@ -42,26 +43,29 @@ This document describes how to set up Rails debugging in [VS Code](https://code. ```json { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, see https://go.microsoft.com/fwlink/?linkid=830387. "version": "0.2.0", "configurations": [ { "type": "rdbg", "name": "Attach with rdbg", "request": "attach", + + // remove the following "preLaunchTask" if you do not wish to stop and start + // GitLab via VS Code but manually on a separate terminal. "preLaunchTask": "start rdbg" } ] } ``` +WARNING: +The VSCode Ruby extension might have issues finding the correct Ruby installation and the appropriate `rdbg` command. In this case, add `"rdbgPath": "/home/user/.asdf/shims/` (in the case of asdf) to the launch configuration above. + ## Debugging -Prerequisite: +### Prerequisites -- You must have a running GDK instance. +- You must have a running [GDK](contributing/first_contribution.md#step-1-configure-the-gitlab-development-kit) instance. To start debugging, do one of the following: diff --git a/doc/user/project/merge_requests/widgets.md b/doc/user/project/merge_requests/widgets.md index 2c87aa4e1dd..639552ca75a 100644 --- a/doc/user/project/merge_requests/widgets.md +++ b/doc/user/project/merge_requests/widgets.md @@ -65,7 +65,7 @@ faster to preview proposed modifications. ## License compliance **(ULTIMATE)** -If you have configured [License Compliance](../../compliance/license_compliance/index.md) for your project, then you can view a list of licenses that are detected for your project's dependencies. +If you have configured [License Compliance](../../compliance/license_scanning_of_cyclonedx_files/index.md) for your project, then you can view a list of licenses that are detected for your project's dependencies. ![Merge request pipeline](img/license_compliance_widget_v15_3.png) diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index c7c41ee8558..8c2bcb250a9 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -81,6 +81,31 @@ Administrators can set a default branch protection level in the Configure protected branches for all projects in a group, or just for a project. +### For a project + +Prerequisites: + +- You must have at least the Maintainer role. +- When granting a group **Allowed to merge** or **Allowed to push and merge** permissions + on a protected branch, the group must be added to the project. + +To protect a branch: + +1. On the top bar, select **Main menu > Projects** and find your project. +1. On the left sidebar, select **Settings > Repository**. +1. Expand **Protected branches**. +1. From the **Branch** dropdown list, select the branch you want to protect. +1. From the **Allowed to merge** list, select a role that can merge into this branch. +1. From the **Allowed to push and merge** list, select a role that can push to this branch. + + NOTE: + In GitLab Premium and Ultimate, you can also add groups or individual users + to **Allowed to merge** and **Allowed to push and merge**. + +1. Select **Protect**. + +The protected branch displays in the list of protected branches. + ### For all projects in a group **(PREMIUM)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/106532) in GitLab 15.9 behind a feature flag, disabled by default. @@ -108,28 +133,6 @@ To protect a branch for all the projects in a group: The protected branch is added to the list of protected branches. -### For a project - -Prerequisite: - -- You must have at least the Maintainer role. -- When granting a group **Allowed to merge** or **Allowed to push and merge** permissions - on a protected branch, the group must be added to the project. - -To protect a branch: - -1. On the top bar, select **Main menu > Projects** and find your project. -1. On the left sidebar, select **Settings > Repository**. -1. Expand **Protected branches**. -1. From the **Branch** dropdown list, select the branch you want to protect. -1. From the **Allowed to merge** list, select a role that can merge into this branch. - In GitLab Premium and Ultimate, you can also add groups or individual users. -1. From the **Allowed to push and merge** list, select a role that can push to this branch. - In GitLab Premium and Ultimate, you can also add groups or individual users. -1. Select **Protect**. - -The protected branch displays in the list of protected branches. - ## Configure multiple protected branches by using a wildcard If both a specific rule and a wildcard rule apply to the same branch, the most diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md index ac9f990a598..c8a02366e19 100644 --- a/doc/user/project/protected_tags.md +++ b/doc/user/project/protected_tags.md @@ -40,7 +40,11 @@ Prerequisites: 1. Enter the string to use for tag matching. Wildcards (`*`) are supported. 1. Select **Create wildcard**. 1. In **Allowed to create** , select roles that may create protected tags. - In GitLab Premium and Ultimate, you can also select groups or individual users. + + NOTE: + In GitLab Premium and Ultimate, you can also add groups or individual users + to **Allowed to create**. + 1. Select **Protect**. The protected tag (or wildcard) displays in the **Protected tags** list. diff --git a/doc/user/project/repository/code_suggestions.md b/doc/user/project/repository/code_suggestions.md index c3d29261f07..2c20ebbe08b 100644 --- a/doc/user/project/repository/code_suggestions.md +++ b/doc/user/project/repository/code_suggestions.md @@ -64,7 +64,7 @@ Each user can enable Code Suggestions for themselves: 1. On the top bar, in the upper-right corner, select your avatar. 1. On the left sidebar, select **Preferences**. -1. In the **Code Suggestions** section, enable the setting. +1. In the **Code Suggestions** section, select **Enable Code Suggestions**. NOTE: If Code Suggestions is [enabled for the group](../../group/manage.md#group-code-suggestions), the group setting overrides the user setting. @@ -111,6 +111,21 @@ Start typing and receive suggestions for your GitLab projects. <iframe src="https://www.youtube-nocookie.com/embed/WnxBYxN2-p4" frameborder="0" allowfullscreen> </iframe> </figure> +## Why can't I see any code suggestions? + +If Code Suggestions are not displayed, try the following troubleshooting steps. + +In GitLab, ensure Code Suggestions is enabled: + +- [For your user account](#enable-code-suggestions-for-an-individual-user). +- [For *all* top-level groups your account belongs to](../../group/manage.md#group-code-suggestions). If you don't have a role that lets you view the top-level group's settings, contact a group owner. + +In VS Code: + +- Ensure [your IDE is configured properly](#enable-code-suggestions-in-vs-code). + +To confirm that your account is enabled, go to [https://gitlab.com/api/v4/ml/ai-assist](https://gitlab.com/api/v4/ml/ai-assist). A response of `user_is_allowed` should return `true`. + ## Stability and performance This feature is currently in [Beta](/ee/policy/alpha-beta-support.md#beta). diff --git a/lib/api/v3/github.rb b/lib/api/v3/github.rb index 7d8c37cd39b..7348ed612fc 100644 --- a/lib/api/v3/github.rb +++ b/lib/api/v3/github.rb @@ -29,10 +29,9 @@ module API feature_category :integrations before do - reversible_end_of_life! - authorize_jira_user_agent!(request) authenticate! + reversible_end_of_life! end helpers do @@ -50,6 +49,13 @@ module API # TODO Make the breaking change irreversible https://gitlab.com/gitlab-org/gitlab/-/issues/408148. def reversible_end_of_life! not_found! unless Feature.enabled?(:jira_dvcs_end_of_life_amnesty) + + Gitlab::IntegrationsLogger.info( + user_id: current_user&.id, + namespace: params[:namespace], + project: params[:project], + message: 'Deprecated Jira DVCS endpoint request' + ) end def authorize_jira_user_agent!(request) diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb index eb99805e2e8..79f12ee13f7 100644 --- a/lib/gitlab/regex.rb +++ b/lib/gitlab/regex.rb @@ -548,8 +548,8 @@ module Gitlab # https://confluence.atlassian.com/adminjiraserver073/changing-the-project-key-format-861253229.html # Avoids linking CVE IDs (https://cve.mitre.org/cve/identifiers/syntaxchange.html#new) as Jira issues. # CVE IDs use the format of CVE-YYYY-NNNNNNN - def jira_issue_key_regex - @jira_issue_key_regex ||= /(?!CVE-\d+-\d+)[A-Z][A-Z_0-9]+-\d+/ + def jira_issue_key_regex(expression_escape: '\b') + /#{expression_escape}(?!CVE-\d+-\d+)[A-Z][A-Z_0-9]+-\d+/ end def jira_issue_key_project_key_extraction_regex diff --git a/lib/sidebars/groups/menus/packages_registries_menu.rb b/lib/sidebars/groups/menus/packages_registries_menu.rb index 68c8e9675a7..bd1cca6473a 100644 --- a/lib/sidebars/groups/menus/packages_registries_menu.rb +++ b/lib/sidebars/groups/menus/packages_registries_menu.rb @@ -36,7 +36,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Package Registry'), link: group_packages_path(context.group), - super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::OperationsMenu, + super_sidebar_parent: ::Sidebars::Groups::SuperSidebarMenus::DeployMenu, active_routes: { controller: 'groups/packages' }, item_id: :packages_registry ) diff --git a/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb b/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb new file mode 100644 index 00000000000..fe9cc5280c7 --- /dev/null +++ b/lib/sidebars/groups/super_sidebar_menus/deploy_menu.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Sidebars + module Groups + module SuperSidebarMenus + class DeployMenu < ::Sidebars::Menu + override :title + def title + s_('Navigation|Deploy') + end + + override :sprite_icon + def sprite_icon + 'deployments' + end + + override :configure_menu_items + def configure_menu_items + [ + :packages_registry + ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } + end + end + end + end +end diff --git a/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb b/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb index fe17ada69e4..e716801486e 100644 --- a/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb +++ b/lib/sidebars/groups/super_sidebar_menus/operations_menu.rb @@ -11,14 +11,13 @@ module Sidebars override :sprite_icon def sprite_icon - 'deployments' + 'cloud-pod' end override :configure_menu_items def configure_menu_items [ :dependency_proxy, - :packages_registry, :container_registry, :group_kubernetes_clusters ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } diff --git a/lib/sidebars/groups/super_sidebar_panel.rb b/lib/sidebars/groups/super_sidebar_panel.rb index 03af904d99d..d1000ef0868 100644 --- a/lib/sidebars/groups/super_sidebar_panel.rb +++ b/lib/sidebars/groups/super_sidebar_panel.rb @@ -17,6 +17,7 @@ module Sidebars add_menu(Sidebars::Groups::SuperSidebarMenus::PlanMenu.new(context)) add_menu(Sidebars::Groups::SuperSidebarMenus::CodeMenu.new(context)) add_menu(Sidebars::Groups::SuperSidebarMenus::BuildMenu.new(context)) + add_menu(Sidebars::Groups::SuperSidebarMenus::DeployMenu.new(context)) add_menu(Sidebars::Groups::SuperSidebarMenus::SecureMenu.new(context)) add_menu(Sidebars::Groups::SuperSidebarMenus::OperationsMenu.new(context)) add_menu(Sidebars::Groups::SuperSidebarMenus::MonitorMenu.new(context)) diff --git a/lib/sidebars/projects/menus/deployments_menu.rb b/lib/sidebars/projects/menus/deployments_menu.rb index 19612fcee85..0a411f075b7 100644 --- a/lib/sidebars/projects/menus/deployments_menu.rb +++ b/lib/sidebars/projects/menus/deployments_menu.rb @@ -49,7 +49,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: s_('FeatureFlags|Feature flags'), link: project_feature_flags_path(context.project), - super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu, + super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::DeployMenu, active_routes: { controller: :feature_flags }, container_html_options: { class: 'shortcuts-feature-flags' }, item_id: :feature_flags @@ -64,7 +64,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Environments'), link: project_environments_path(context.project), - super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu, + super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::OperationsMenu, active_routes: { controller: :environments }, container_html_options: { class: 'shortcuts-environments' }, item_id: :environments @@ -80,7 +80,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Releases'), link: project_releases_path(context.project), - super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::BuildMenu, + super_sidebar_parent: ::Sidebars::Projects::SuperSidebarMenus::DeployMenu, item_id: :releases, active_routes: { controller: :releases }, container_html_options: { class: 'shortcuts-deployments-releases' } diff --git a/lib/sidebars/projects/menus/packages_registries_menu.rb b/lib/sidebars/projects/menus/packages_registries_menu.rb index 31a1aa56ab5..39adbdce63b 100644 --- a/lib/sidebars/projects/menus/packages_registries_menu.rb +++ b/lib/sidebars/projects/menus/packages_registries_menu.rb @@ -39,7 +39,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Package Registry'), link: project_packages_path(context.project), - super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::OperationsMenu, + super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu, active_routes: { controller: :packages }, item_id: :packages_registry, container_html_options: { class: 'shortcuts-container-registry' } @@ -54,7 +54,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Container Registry'), link: project_container_registry_index_path(context.project), - super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::OperationsMenu, + super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu, active_routes: { controller: 'projects/registry/repositories' }, item_id: :container_registry ) @@ -98,7 +98,7 @@ module Sidebars ::Sidebars::MenuItem.new( title: _('Model experiments'), link: project_ml_experiments_path(context.project), - super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, + super_sidebar_parent: Sidebars::Projects::SuperSidebarMenus::DeployMenu, active_routes: { controller: %w[projects/ml/experiments projects/ml/candidates] }, item_id: :model_experiments ) diff --git a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb index 58b231a269c..2c5dc8a08e7 100644 --- a/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/analyze_menu.rb @@ -25,8 +25,7 @@ module Sidebars :code_review, :merge_request_analytics, :issues, - :insights, - :model_experiments + :insights ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } end end diff --git a/lib/sidebars/projects/super_sidebar_menus/build_menu.rb b/lib/sidebars/projects/super_sidebar_menus/build_menu.rb index 30603e1deeb..119ddf28873 100644 --- a/lib/sidebars/projects/super_sidebar_menus/build_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/build_menu.rb @@ -20,10 +20,7 @@ module Sidebars :pipelines, :jobs, :pipelines_editor, - :releases, - :environments, :pipeline_schedules, - :feature_flags, :test_cases, :artifacts ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } diff --git a/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb b/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb new file mode 100644 index 00000000000..49aa6a23a0e --- /dev/null +++ b/lib/sidebars/projects/super_sidebar_menus/deploy_menu.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Sidebars + module Projects + module SuperSidebarMenus + class DeployMenu < ::Sidebars::Menu + override :title + def title + s_('Navigation|Deploy') + end + + override :sprite_icon + def sprite_icon + 'deployments' + end + + override :configure_menu_items + def configure_menu_items + [ + :releases, + :feature_flags, + :packages_registry, + :container_registry, + :model_experiments + ].each { |id| add_item(::Sidebars::NilMenuItem.new(item_id: id)) } + end + end + end + end +end diff --git a/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb b/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb index 64cf4aee9c5..85d60c0bad3 100644 --- a/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb +++ b/lib/sidebars/projects/super_sidebar_menus/operations_menu.rb @@ -11,14 +11,13 @@ module Sidebars override :sprite_icon def sprite_icon - 'deployments' + 'cloud-pod' end override :configure_menu_items def configure_menu_items [ - :packages_registry, - :container_registry, + :environments, :kubernetes, :terraform_states, :infrastructure_registry, diff --git a/lib/sidebars/projects/super_sidebar_panel.rb b/lib/sidebars/projects/super_sidebar_panel.rb index 640666fd968..c3dfd9804af 100644 --- a/lib/sidebars/projects/super_sidebar_panel.rb +++ b/lib/sidebars/projects/super_sidebar_panel.rb @@ -17,6 +17,7 @@ module Sidebars add_menu(Sidebars::Projects::SuperSidebarMenus::PlanMenu.new(context)) add_menu(Sidebars::Projects::SuperSidebarMenus::CodeMenu.new(context)) add_menu(Sidebars::Projects::SuperSidebarMenus::BuildMenu.new(context)) + add_menu(Sidebars::Projects::SuperSidebarMenus::DeployMenu.new(context)) add_menu(Sidebars::Projects::SuperSidebarMenus::SecureMenu.new(context)) add_menu(Sidebars::Projects::SuperSidebarMenus::OperationsMenu.new(context)) add_menu(Sidebars::Projects::SuperSidebarMenus::MonitorMenu.new(context)) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7a76d7ebb42..e445147d38f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -10862,6 +10862,15 @@ msgstr "" msgid "CodeQuality|New code quality degradations on this line" msgstr "" +msgid "CodeSuggestionsAlert|Code faster and more efficiently with AI-powered code suggestions in VS Code. 13 languages are supported, including Javascript, Python, Go, Java, and Kotlin. Enable Code Suggestions in your %{preference_link_start}user profile preferences%{link_end} or %{docs_link_start}see the documentation%{link_end} to learn more." +msgstr "" + +msgid "CodeSuggestionsAlert|Enable Code Suggestions" +msgstr "" + +msgid "CodeSuggestionsAlert|Get started with Code Suggestions, available for free during the beta period." +msgstr "" + msgid "CodeSuggestions|%{link_start}What are code suggestions?%{link_end}" msgstr "" @@ -27707,6 +27716,9 @@ msgstr "" msgid "MemberRole|maximum number of Member Roles are already in use by the group hierarchy. Please delete an existing Member Role." msgstr "" +msgid "MemberRole|minimal base access level must be %{min_access_level}." +msgstr "" + msgid "MemberRole|must be top-level namespace" msgstr "" @@ -29514,6 +29526,9 @@ msgstr "" msgid "Navigation|Context navigation" msgstr "" +msgid "Navigation|Deploy" +msgstr "" + msgid "Navigation|Enter admin mode" msgstr "" @@ -32141,6 +32156,9 @@ msgstr "" msgid "PackageRegistry|Something went wrong while deleting the package." msgstr "" +msgid "PackageRegistry|Something went wrong while fetching package assets." +msgstr "" + msgid "PackageRegistry|Something went wrong while fetching the package history." msgstr "" diff --git a/spec/features/nav/pinned_nav_items_spec.rb b/spec/features/nav/pinned_nav_items_spec.rb index 308350d5166..cf53e0a322a 100644 --- a/spec/features/nav/pinned_nav_items_spec.rb +++ b/spec/features/nav/pinned_nav_items_spec.rb @@ -89,7 +89,7 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio before do within '#super-sidebar' do click_on 'Operate' - add_pin('Package Registry') + add_pin('Terraform states') add_pin('Terraform modules') wait_for_requests end @@ -97,8 +97,8 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio it 'can be unpinned from within the pinned section' do within '[data-testid="pinned-nav-items"]' do - remove_pin('Package Registry') - expect(page).not_to have_content 'Package Registry' + remove_pin('Terraform states') + expect(page).not_to have_content 'Terraform states' end end @@ -117,7 +117,7 @@ RSpec.describe 'Navigation menu item pinning', :js, feature_category: :navigatio it 'can be reordered' do within '[data-testid="pinned-nav-items"]' do pinned_items = page.find_all('a').map(&:text) - item2 = page.find('a', text: 'Package Registry') + item2 = page.find('a', text: 'Terraform states') item3 = page.find('a', text: 'Terraform modules') expect(pinned_items[1..2]).to eq [item2.text, item3.text] drag_item(item3, to: item2) diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js index c435dd57de2..88d4398aa70 100644 --- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js +++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js @@ -24,7 +24,7 @@ describe('RunnerStatusCell', () => { propsData: { runner: { runnerType: INSTANCE_TYPE, - active: true, + paused: false, status: STATUS_ONLINE, jobExecutionStatus: JOB_STATUS_IDLE, ...runner, @@ -59,7 +59,7 @@ describe('RunnerStatusCell', () => { it('Displays paused status', () => { createComponent({ runner: { - active: false, + paused: true, status: STATUS_ONLINE, }, }); diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js index 3123f2894fb..3b3f3b1770d 100644 --- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js @@ -236,7 +236,7 @@ describe('RunnerDeleteButton', () => { createComponent({ props: { runner: { - active: true, + paused: false, }, compact: true, }, diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js index 0f4ec717c3e..9da640afeb7 100644 --- a/spec/frontend/ci/runner/components/runner_list_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_spec.js @@ -18,7 +18,6 @@ import { I18N_PROJECT_TYPE, I18N_STATUS_NEVER_CONTACTED } from '~/ci/runner/cons import { allRunnersData, onlineContactTimeoutSecs, staleTimeoutSecs } from '../mock_data'; const mockRunners = allRunnersData.data.runners.nodes; -const mockActiveRunnersCount = mockRunners.length; describe('RunnerList', () => { let wrapper; @@ -44,7 +43,6 @@ describe('RunnerList', () => { apolloProvider: createMockApollo([], {}, cacheConfig), propsData: { runners: mockRunners, - activeRunnersCount: mockActiveRunnersCount, ...props, }, provide: { diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js index 350d029f3fc..1ea870e004a 100644 --- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js +++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js @@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper'; -import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql'; +import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql'; import waitForPromises from 'helpers/wait_for_promises'; import { captureException } from '~/ci/runner/sentry_utils'; import { createAlert } from '~/alert'; @@ -27,7 +27,7 @@ jest.mock('~/ci/runner/sentry_utils'); describe('RunnerPauseButton', () => { let wrapper; - let runnerToggleActiveHandler; + let runnerTogglePausedHandler; const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value; const findBtn = () => wrapper.findComponent(GlButton); @@ -39,12 +39,12 @@ describe('RunnerPauseButton', () => { propsData: { runner: { id: mockRunner.id, - active: mockRunner.active, + paused: mockRunner.paused, ...runner, }, ...propsData, }, - apolloProvider: createMockApollo([[runnerToggleActiveMutation, runnerToggleActiveHandler]]), + apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]), directives: { GlTooltip: createMockDirective('gl-tooltip'), }, @@ -57,13 +57,13 @@ describe('RunnerPauseButton', () => { }; beforeEach(() => { - runnerToggleActiveHandler = jest.fn().mockImplementation(({ input }) => { + runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => { return Promise.resolve({ data: { runnerUpdate: { runner: { id: input.id, - active: input.active, + paused: !input.paused, }, errors: [], }, @@ -76,15 +76,15 @@ describe('RunnerPauseButton', () => { describe('Pause/Resume action', () => { describe.each` - runnerState | icon | content | tooltip | isActive | newActiveValue - ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${false} | ${true} - ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${true} | ${false} - `('When the runner is $runnerState', ({ icon, content, tooltip, isActive, newActiveValue }) => { + runnerState | icon | content | tooltip | isPaused | newPausedValue + ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${true} | ${false} + ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${false} | ${true} + `('When the runner is $runnerState', ({ icon, content, tooltip, isPaused, newPausedValue }) => { beforeEach(() => { createComponent({ props: { runner: { - active: isActive, + paused: isPaused, }, }, }); @@ -106,7 +106,7 @@ describe('RunnerPauseButton', () => { describe(`Before the ${icon} button is clicked`, () => { it('The mutation has not been called', () => { - expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(0); + expect(runnerTogglePausedHandler).not.toHaveBeenCalled(); }); }); @@ -134,12 +134,12 @@ describe('RunnerPauseButton', () => { await clickAndWait(); }); - it(`The mutation to that sets active to ${newActiveValue} is called`, () => { - expect(runnerToggleActiveHandler).toHaveBeenCalledTimes(1); - expect(runnerToggleActiveHandler).toHaveBeenCalledWith({ + it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => { + expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1); + expect(runnerTogglePausedHandler).toHaveBeenCalledWith({ input: { id: mockRunner.id, - active: newActiveValue, + paused: newPausedValue, }, }); }); @@ -158,7 +158,7 @@ describe('RunnerPauseButton', () => { const mockErrorMsg = 'Update error!'; beforeEach(async () => { - runnerToggleActiveHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); + runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg)); await clickAndWait(); }); @@ -180,12 +180,12 @@ describe('RunnerPauseButton', () => { const mockErrorMsg2 = 'User not allowed!'; beforeEach(async () => { - runnerToggleActiveHandler.mockResolvedValueOnce({ + runnerTogglePausedHandler.mockResolvedValueOnce({ data: { runnerUpdate: { runner: { id: mockRunner.id, - active: isActive, + paused: isPaused, }, errors: [mockErrorMsg, mockErrorMsg2], }, @@ -215,7 +215,7 @@ describe('RunnerPauseButton', () => { createComponent({ props: { runner: { - active: true, + paused: false, }, compact: true, }, diff --git a/spec/frontend/ci/runner/components/runner_update_form_spec.js b/spec/frontend/ci/runner/components/runner_update_form_spec.js index ee37d6241b5..d1d4e38f47c 100644 --- a/spec/frontend/ci/runner/components/runner_update_form_spec.js +++ b/spec/frontend/ci/runner/components/runner_update_form_spec.js @@ -56,7 +56,7 @@ describe('RunnerUpdateForm', () => { const submitFormAndWait = () => submitForm().then(waitForPromises); const getFieldsModel = () => ({ - active: !findPausedCheckbox().element.checked, + paused: findPausedCheckbox().element.checked, accessLevel: findProtectedCheckbox().element.checked ? ACCESS_LEVEL_REF_PROTECTED : ACCESS_LEVEL_NOT_PROTECTED, @@ -179,8 +179,8 @@ describe('RunnerUpdateForm', () => { describe('On submit, runner gets updated', () => { it.each` test | initialValue | findCheckbox | checked | submitted - ${'pauses'} | ${{ active: true }} | ${findPausedCheckbox} | ${true} | ${{ active: false }} - ${'activates'} | ${{ active: false }} | ${findPausedCheckbox} | ${false} | ${{ active: true }} + ${'pauses'} | ${{ paused: false }} | ${findPausedCheckbox} | ${true} | ${{ paused: true }} + ${'activates'} | ${{ paused: true }} | ${findPausedCheckbox} | ${false} | ${{ paused: false }} ${'unprotects'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} | ${findProtectedCheckbox} | ${true} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} ${'protects'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED }} | ${findProtectedCheckbox} | ${false} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED }} ${'"runs untagged jobs"'} | ${{ runUntagged: true }} | ${findRunUntaggedCheckbox} | ${false} | ${{ runUntagged: false }} diff --git a/spec/frontend/ci/runner/runner_update_form_utils_spec.js b/spec/frontend/ci/runner/runner_update_form_utils_spec.js index b2f7bbc49a9..80c492bb431 100644 --- a/spec/frontend/ci/runner/runner_update_form_utils_spec.js +++ b/spec/frontend/ci/runner/runner_update_form_utils_spec.js @@ -12,7 +12,7 @@ const mockRunner = { description: mockDescription, maximumTimeout: 100, accessLevel: ACCESS_LEVEL_NOT_PROTECTED, - active: true, + paused: false, locked: true, runUntagged: true, tagList: ['tag-1', 'tag-2'], @@ -79,7 +79,7 @@ describe('~/ci/runner/runner_update_form_utils', () => { ${',,,,, commas'} | ${['commas']} ${'more ,,,,, commas'} | ${['more', 'commas']} ${' trimmed , trimmed2 '} | ${['trimmed', 'trimmed2']} - `('collect tags separated by commas for "$value"', ({ tagList, tagListInput }) => { + `('collect comma-separated tags "$tagList" as $tagListInput', ({ tagList, tagListInput }) => { const variables = modelToUpdateMutationVariables({ ...mockModel, tagList, diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index 1dcac017ccf..5c36dbf9c9c 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -1,22 +1,34 @@ -import { GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; -import { nextTick } from 'vue'; +import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; import { stubComponent } from 'helpers/stub_component'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; -import { packageFiles as packageFilesMock } from 'jest/packages_and_registries/package_registry/mock_data'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { s__ } from '~/locale'; +import { + packageFiles as packageFilesMock, + packageFilesQuery, +} from 'jest/packages_and_registries/package_registry/mock_data'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql'; + +Vue.use(VueApollo); + describe('Package Files', () => { let wrapper; + let apolloProvider; const findAllRows = () => wrapper.findAllByTestId('file-row'); const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected'); const findFirstRow = () => extendedWrapper(findAllRows().at(0)); const findSecondRow = () => extendedWrapper(findAllRows().at(1)); + const findPackageFilesAlert = () => wrapper.findComponent(GlAlert); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link'); - const findFirstRowCommitLink = () => findFirstRow().findByTestId('commit-link'); - const findSecondRowCommitLink = () => findSecondRow().findByTestId('commit-link'); const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon); const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip); const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown)); @@ -30,16 +42,23 @@ describe('Package Files', () => { const [file] = files; const createComponent = ({ - packageFiles = [file], + packageId = '1', + packageType = 'NPM', isLoading = false, canDelete = true, stubs, + resolver = jest.fn().mockResolvedValue(packageFilesQuery([file])), } = {}) => { + const requestHandlers = [[getPackageFiles, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + wrapper = mountExtended(PackageFiles, { + apolloProvider, propsData: { canDelete, isLoading, - packageFiles, + packageId, + packageType, }, stubs: { GlTable: false, @@ -49,35 +68,61 @@ describe('Package Files', () => { }; describe('rows', () => { - it('renders a single file for an npm package', () => { + it('do not get rendered when query is loading', () => { createComponent(); + expect(findLoadingIcon().exists()).toBe(true); + expect(findDeleteSelectedButton().props('disabled')).toBe(true); + }); + + it('renders a single file for an npm package', async () => { + createComponent(); + await waitForPromises(); + expect(findAllRows()).toHaveLength(1); + expect(findLoadingIcon().exists()).toBe(false); }); - it('renders multiple files for a package that contains more than one file', () => { - createComponent({ packageFiles: files }); + it('renders multiple files for a package that contains more than one file', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(findAllRows()).toHaveLength(2); }); + + it('does not render gl-alert', async () => { + createComponent(); + await waitForPromises(); + + expect(findPackageFilesAlert().exists()).toBe(false); + }); + + it('renders gl-alert if load fails', async () => { + createComponent({ resolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + expect(findPackageFilesAlert().exists()).toBe(true); + expect(findPackageFilesAlert().text()).toBe( + s__('PackageRegistry|Something went wrong while fetching package assets.'), + ); + }); }); describe('link', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowDownloadLink().exists()).toBe(true); }); it('has the correct attrs bound', () => { - createComponent(); - expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath); }); it('emits "download-file" event on click', () => { - createComponent(); - findFirstRowDownloadLink().vm.$emit('click'); expect(wrapper.emitted('download-file')).toEqual([[]]); @@ -85,90 +130,43 @@ describe('Package Files', () => { }); describe('file-icon', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowFileIcon().exists()).toBe(true); }); it('has the correct props bound', () => { - createComponent(); - expect(findFirstRowFileIcon().props('fileName')).toBe(file.fileName); }); }); describe('time-ago tooltip', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstRowCreatedAt().exists()).toBe(true); }); it('has the correct props bound', () => { - createComponent(); - expect(findFirstRowCreatedAt().props('time')).toBe(file.createdAt); }); }); - describe('commit', () => { - const withPipeline = { - ...file, - pipelines: [ - { - sha: 'sha', - id: 1, - commitPath: 'commitPath', - }, - ], - }; - - describe('when package file has a pipeline associated', () => { - it('exists', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().exists()).toBe(true); - }); - - it('the link points to the commit path', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().attributes('href')).toBe( - withPipeline.pipelines[0].commitPath, - ); - }); - - it('the text is the pipeline sha', () => { - createComponent({ packageFiles: [withPipeline] }); - - expect(findFirstRowCommitLink().text()).toBe(withPipeline.pipelines[0].sha); - }); - }); - - describe('when package file has no pipeline associated', () => { - it('does not exist', () => { - createComponent(); - - expect(findFirstRowCommitLink().exists()).toBe(false); - }); - }); - - describe('when only one file lacks an associated pipeline', () => { - it('renders the commit when it exists and not otherwise', () => { - createComponent({ packageFiles: [withPipeline, file] }); - - expect(findFirstRowCommitLink().exists()).toBe(true); - expect(findSecondRowCommitLink().exists()).toBe(false); - }); - }); - }); - describe('action menu', () => { describe('when the user can delete', () => { - it('exists', () => { + beforeEach(async () => { createComponent(); + await waitForPromises(); + }); + it('exists', () => { expect(findFirstActionMenu().exists()).toBe(true); expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v'); expect(findFirstActionMenu().props('textSrOnly')).toBe(true); @@ -178,14 +176,10 @@ describe('Package Files', () => { describe('menu items', () => { describe('delete file', () => { it('exists', () => { - createComponent(); - expect(findActionMenuDelete().exists()).toBe(true); }); it('emits a delete event when clicked', async () => { - createComponent(); - await findActionMenuDelete().trigger('click'); const [[items]] = wrapper.emitted('delete-files'); @@ -199,8 +193,9 @@ describe('Package Files', () => { describe('when the user can not delete', () => { const canDelete = false; - it('does not exist', () => { + it('does not exist', async () => { createComponent({ canDelete }); + await waitForPromises(); expect(findFirstActionMenu().exists()).toBe(false); }); @@ -209,22 +204,33 @@ describe('Package Files', () => { describe('multi select', () => { describe('when user can delete', () => { - it('delete selected button exists & is disabled', () => { + it('delete selected button exists & is disabled', async () => { createComponent(); + await waitForPromises(); expect(findDeleteSelectedButton().exists()).toBe(true); expect(findDeleteSelectedButton().text()).toMatchInterpolatedText('Delete selected'); expect(findDeleteSelectedButton().props('disabled')).toBe(true); }); - it('delete selected button exists & is disabled when isLoading prop is true', () => { - createComponent({ isLoading: true }); + it('delete selected button exists & is disabled when isLoading prop is true', async () => { + createComponent(); + await waitForPromises(); + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + expect(findDeleteSelectedButton().props('disabled')).toBe(false); + + await wrapper.setProps({ isLoading: true }); expect(findDeleteSelectedButton().props('disabled')).toBe(true); + expect(findLoadingIcon().exists()).toBe(true); }); - it('checkboxes to select file are visible', () => { - createComponent({ packageFiles: files }); + it('checkboxes to select file are visible', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(findCheckAllCheckbox().exists()).toBe(true); expect(findAllRowCheckboxes()).toHaveLength(2); @@ -232,6 +238,7 @@ describe('Package Files', () => { it('selecting a checkbox enables delete selected button', async () => { createComponent(); + await waitForPromises(); const first = findAllRowCheckboxes().at(0); @@ -244,7 +251,8 @@ describe('Package Files', () => { it('will toggle between selecting all and deselecting all files', async () => { const getChecked = () => findAllRowCheckboxes().filter((x) => x.element.checked === true); - createComponent({ packageFiles: files }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); expect(getChecked()).toHaveLength(0); @@ -262,9 +270,10 @@ describe('Package Files', () => { expect(findCheckAllCheckbox().props('indeterminate')).toBe(state); createComponent({ - packageFiles: files, + resolver: jest.fn().mockResolvedValue(packageFilesQuery()), stubs: { GlFormCheckbox: stubComponent(GlFormCheckbox, { props: ['indeterminate'] }) }, }); + await waitForPromises(); expectIndeterminateState(false); @@ -288,6 +297,7 @@ describe('Package Files', () => { it('emits a delete event when selected', async () => { createComponent(); + await waitForPromises(); const first = findAllRowCheckboxes().at(0); @@ -301,7 +311,8 @@ describe('Package Files', () => { }); it('emits delete event with both items when all are selected', async () => { - createComponent({ packageFiles: files }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); + await waitForPromises(); await findCheckAllCheckbox().setChecked(true); @@ -315,14 +326,16 @@ describe('Package Files', () => { describe('when user cannot delete', () => { const canDelete = false; - it('delete selected button does not exist', () => { + it('delete selected button does not exist', async () => { createComponent({ canDelete }); + await waitForPromises(); expect(findDeleteSelectedButton().exists()).toBe(false); }); - it('checkboxes to select file are not visible', () => { - createComponent({ packageFiles: files, canDelete }); + it('checkboxes to select file are not visible', async () => { + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()), canDelete }); + await waitForPromises(); expect(findCheckAllCheckbox().exists()).toBe(false); expect(findAllRowCheckboxes()).toHaveLength(0); @@ -332,24 +345,27 @@ describe('Package Files', () => { describe('additional details', () => { describe('details toggle button', () => { - it('exists', () => { + it('exists', async () => { createComponent(); + await waitForPromises(); expect(findFirstToggleDetailsButton().exists()).toBe(true); }); - it('is hidden when no details is present', () => { + it('is hidden when no details is present', async () => { const { ...noShaFile } = file; noShaFile.fileSha256 = null; noShaFile.fileMd5 = null; noShaFile.fileSha1 = null; - createComponent({ packageFiles: [noShaFile] }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([noShaFile])) }); + await waitForPromises(); expect(findFirstToggleDetailsButton().exists()).toBe(false); }); it('toggles the details row', async () => { createComponent(); + await waitForPromises(); expect(findFirstToggleDetailsButton().props('icon')).toBe('chevron-down'); @@ -380,6 +396,7 @@ describe('Package Files', () => { ${'sha-1'} | ${'SHA-1'} | ${'be93151dc23ac34a82752444556fe79b32c7a1ad'} `('has a $title row', async ({ selector, title, sha }) => { createComponent(); + await waitForPromises(); await showShaFiles(); @@ -393,7 +410,8 @@ describe('Package Files', () => { const { ...missingMd5 } = file; missingMd5.fileMd5 = null; - createComponent({ packageFiles: [missingMd5] }); + createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([missingMd5])) }); + await waitForPromises(); await showShaFiles(); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index 5fb53566d4e..fa6a69b1a1f 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -257,7 +257,7 @@ export const packageDetailsQuery = ({ pageInfo: { hasNextPage: true, }, - nodes: packageFiles(), + nodes: packageFiles().map(({ id, size }) => ({ id, size })), __typename: 'PackageFileConnection', }, versions: { @@ -285,6 +285,19 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({ }, }); +export const packageFilesQuery = (files = packageFiles()) => ({ + data: { + package: { + id: 'gid://gitlab/Packages::Package/111', + packageFiles: { + nodes: files, + __typename: 'PackageFileConnection', + }, + __typename: 'PackageDetailsType', + }, + }, +}); + export const emptyPackageDetailsQuery = () => ({ data: { package: { diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index 0962b4fa757..8b15dfd7d4a 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -328,18 +328,18 @@ describe('PackagesApp', () => { describe('package files', () => { it('renders the package files component and has the right props', async () => { - const expectedFile = { ...packageFiles()[0] }; - // eslint-disable-next-line no-underscore-dangle - delete expectedFile.__typename; createComponent(); await waitForPromises(); expect(findPackageFiles().exists()).toBe(true); - expect(findPackageFiles().props('packageFiles')[0]).toMatchObject(expectedFile); - expect(findPackageFiles().props('canDelete')).toBe(packageData().canDestroy); - expect(findPackageFiles().props('isLoading')).toEqual(false); + expect(findPackageFiles().props()).toMatchObject({ + canDelete: packageData().canDestroy, + isLoading: false, + packageId: packageData().id, + packageType: packageData().packageType, + }); }); it('does not render the package files table when the package is composer', async () => { diff --git a/spec/graphql/resolvers/users/participants_resolver_spec.rb b/spec/graphql/resolvers/users/participants_resolver_spec.rb index 224213d1521..63a14daabba 100644 --- a/spec/graphql/resolvers/users/participants_resolver_spec.rb +++ b/spec/graphql/resolvers/users/participants_resolver_spec.rb @@ -8,39 +8,54 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do describe '#resolve' do let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) } - let_it_be(:project) { create(:project, :public) } + let_it_be(:project) do + create(:project, :public).tap do |r| + r.add_developer(user) + r.add_guest(guest) + end + end + + let_it_be(:private_project) { create(:project, :private).tap { |r| r.add_developer(user) } } + let_it_be(:issue) { create(:issue, project: project) } + let_it_be(:private_issue) { create(:issue, project: private_project) } let_it_be(:public_note_author) { create(:user) } let_it_be(:public_reply_author) { create(:user) } let_it_be(:internal_note_author) { create(:user) } let_it_be(:internal_reply_author) { create(:user) } + let_it_be(:system_note_author) { create(:user) } + let_it_be(:internal_system_note_author) { create(:user) } let_it_be(:public_note) { create(:note, project: project, noteable: issue, author: public_note_author) } let_it_be(:internal_note) { create(:note, :confidential, project: project, noteable: issue, author: internal_note_author) } - let_it_be(:public_reply) { create(:note, noteable: issue, in_reply_to: public_note, project: project, author: public_reply_author) } - let_it_be(:internal_reply) { create(:note, :confidential, noteable: issue, in_reply_to: internal_note, project: project, author: internal_reply_author) } - - let_it_be(:note_metadata2) { create(:system_note_metadata, note: public_note) } + let_it_be(:public_reply) do + create(:note, noteable: issue, in_reply_to: public_note, project: project, author: public_reply_author) + end - let_it_be(:issue_emoji) { create(:award_emoji, name: 'thumbsup', awardable: issue) } - let_it_be(:note_emoji1) { create(:award_emoji, name: 'thumbsup', awardable: public_note) } - let_it_be(:note_emoji2) { create(:award_emoji, name: 'thumbsup', awardable: internal_note) } - let_it_be(:note_emoji3) { create(:award_emoji, name: 'thumbsup', awardable: public_reply) } - let_it_be(:note_emoji4) { create(:award_emoji, name: 'thumbsup', awardable: internal_reply) } + let_it_be(:internal_reply) do + create(:note, :confidential, noteable: issue, in_reply_to: internal_note, project: project, author: internal_reply_author) + end - let_it_be(:issue_emoji_author) { issue_emoji.user } - let_it_be(:public_note_emoji_author) { note_emoji1.user } - let_it_be(:internal_note_emoji_author) { note_emoji2.user } - let_it_be(:public_reply_emoji_author) { note_emoji3.user } - let_it_be(:internal_reply_emoji_author) { note_emoji4.user } + let_it_be(:issue_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: issue).user } + let_it_be(:public_note_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: public_note).user } + let_it_be(:internal_note_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: internal_note).user } + let_it_be(:public_reply_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: public_reply).user } + let_it_be(:internal_reply_emoji_author) { create(:award_emoji, name: 'thumbsup', awardable: internal_reply).user } - subject(:resolved_items) { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } + subject(:resolved_items) do + resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items + end - before do - project.add_guest(guest) - project.add_developer(user) + before_all do + create(:system_note, project: project, noteable: issue, author: system_note_author) + create( + :system_note, + note: "mentioned in issue #{private_issue.to_reference(full: true)}", + project: project, noteable: issue, author: internal_system_note_author + ) + create(:system_note_metadata, note: public_note) end context 'when current user is not set' do @@ -54,7 +69,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do public_note_author, public_note_emoji_author, public_reply_author, - public_reply_emoji_author + public_reply_emoji_author, + system_note_author ] ) end @@ -71,7 +87,8 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do public_note_author, public_note_emoji_author, public_reply_author, - public_reply_emoji_author + public_reply_emoji_author, + system_note_author ] ) end @@ -92,13 +109,17 @@ RSpec.describe Resolvers::Users::ParticipantsResolver do internal_note_emoji_author, internal_reply_author, public_reply_emoji_author, - internal_reply_emoji_author + internal_reply_emoji_author, + system_note_author, + internal_system_note_author ] ) end context 'N+1 queries' do - let(:query) { -> { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } } + let(:query) do + -> { resolve(described_class, args: {}, ctx: { current_user: current_user }, obj: issue)&.items } + end before do # warm-up diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb new file mode 100644 index 00000000000..ec3f911d8dc --- /dev/null +++ b/spec/lib/sidebars/groups/super_sidebar_menus/deploy_menu_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Groups::SuperSidebarMenus::DeployMenu, feature_category: :navigation do + subject { described_class.new({}) } + + let(:items) { subject.instance_variable_get(:@items) } + + it 'has title and sprite_icon' do + expect(subject.title).to eq(s_("Navigation|Deploy")) + expect(subject.sprite_icon).to eq("deployments") + end + + it 'defines list of NilMenuItem placeholders' do + expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) + expect(items.map(&:item_id)).to eq([ + :packages_registry + ]) + end +end diff --git a/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb b/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb index e9c2701021c..df37d5f1b0d 100644 --- a/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb +++ b/spec/lib/sidebars/groups/super_sidebar_menus/operations_menu_spec.rb @@ -9,14 +9,13 @@ RSpec.describe Sidebars::Groups::SuperSidebarMenus::OperationsMenu, feature_cate it 'has title and sprite_icon' do expect(subject.title).to eq(s_("Navigation|Operate")) - expect(subject.sprite_icon).to eq("deployments") + expect(subject.sprite_icon).to eq("cloud-pod") end it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ :dependency_proxy, - :packages_registry, :container_registry, :group_kubernetes_clusters ]) diff --git a/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb index 5035da9c488..245d1eca0a4 100644 --- a/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb +++ b/spec/lib/sidebars/groups/super_sidebar_panel_spec.rb @@ -36,6 +36,7 @@ RSpec.describe Sidebars::Groups::SuperSidebarPanel, feature_category: :navigatio Sidebars::Groups::SuperSidebarMenus::PlanMenu, Sidebars::Groups::SuperSidebarMenus::CodeMenu, Sidebars::Groups::SuperSidebarMenus::BuildMenu, + Sidebars::Groups::SuperSidebarMenus::DeployMenu, Sidebars::Groups::SuperSidebarMenus::SecureMenu, Sidebars::Groups::SuperSidebarMenus::OperationsMenu, Sidebars::Groups::SuperSidebarMenus::MonitorMenu, diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb index d459d47c31a..b7d05867d77 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/analyze_menu_spec.rb @@ -23,8 +23,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::AnalyzeMenu, feature_categ :code_review, :merge_request_analytics, :issues, - :insights, - :model_experiments + :insights ]) end end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb index 3f2a40e1c7d..06b87003d83 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/build_menu_spec.rb @@ -18,10 +18,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::BuildMenu, feature_categor :pipelines, :jobs, :pipelines_editor, - :releases, - :environments, :pipeline_schedules, - :feature_flags, :test_cases, :artifacts ]) diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb new file mode 100644 index 00000000000..50eee173d31 --- /dev/null +++ b/spec/lib/sidebars/projects/super_sidebar_menus/deploy_menu_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Projects::SuperSidebarMenus::DeployMenu, feature_category: :navigation do + subject { described_class.new({}) } + + let(:items) { subject.instance_variable_get(:@items) } + + it 'has title and sprite_icon' do + expect(subject.title).to eq(s_("Navigation|Deploy")) + expect(subject.sprite_icon).to eq("deployments") + end + + it 'defines list of NilMenuItem placeholders' do + expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) + expect(items.map(&:item_id)).to eq([ + :releases, + :feature_flags, + :packages_registry, + :container_registry, + :model_experiments + ]) + end +end diff --git a/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb index 6ab070c40ae..68ca4fe2aa0 100644 --- a/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_menus/operations_menu_spec.rb @@ -9,14 +9,13 @@ RSpec.describe Sidebars::Projects::SuperSidebarMenus::OperationsMenu, feature_ca it 'has title and sprite_icon' do expect(subject.title).to eq(s_("Navigation|Operate")) - expect(subject.sprite_icon).to eq("deployments") + expect(subject.sprite_icon).to eq("cloud-pod") end it 'defines list of NilMenuItem placeholders' do expect(items.map(&:class).uniq).to eq([Sidebars::NilMenuItem]) expect(items.map(&:item_id)).to eq([ - :packages_registry, - :container_registry, + :environments, :kubernetes, :terraform_states, :infrastructure_registry, diff --git a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb index 93f0072a111..9ed328f5090 100644 --- a/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb +++ b/spec/lib/sidebars/projects/super_sidebar_panel_spec.rb @@ -47,6 +47,7 @@ RSpec.describe Sidebars::Projects::SuperSidebarPanel, feature_category: :navigat Sidebars::Projects::SuperSidebarMenus::PlanMenu, Sidebars::Projects::SuperSidebarMenus::CodeMenu, Sidebars::Projects::SuperSidebarMenus::BuildMenu, + Sidebars::Projects::SuperSidebarMenus::DeployMenu, Sidebars::Projects::SuperSidebarMenus::SecureMenu, Sidebars::Projects::SuperSidebarMenus::OperationsMenu, Sidebars::Projects::SuperSidebarMenus::MonitorMenu, diff --git a/spec/models/integrations/jira_spec.rb b/spec/models/integrations/jira_spec.rb index d3cb386e8e0..71dd543b3ec 100644 --- a/spec/models/integrations/jira_spec.rb +++ b/spec/models/integrations/jira_spec.rb @@ -326,6 +326,18 @@ RSpec.describe Integrations::Jira, feature_category: :integrations do end end end + + context 'with long running regex' do + let(:key) { "JIRAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1\nanother line\n" } + + before do + jira_integration.jira_issue_regex = '((a|b)+|c)+$' + end + + it 'handles long inputs' do + expect(jira_integration.reference_pattern.match(key).to_s).to eq('') + end + end end describe '.valid_jira_cloud_url?' do diff --git a/spec/models/integrations/pipelines_email_spec.rb b/spec/models/integrations/pipelines_email_spec.rb index 37a3849a768..7e80defcb87 100644 --- a/spec/models/integrations/pipelines_email_spec.rb +++ b/spec/models/integrations/pipelines_email_spec.rb @@ -84,7 +84,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do end it 'sends email' do - emails = receivers.map { |r| double(notification_email_or_default: r) } + emails = receivers.map { |r| double(notification_email_or_default: r, username: r, id: r) } should_only_email(*emails) end @@ -206,10 +206,6 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do end context 'with recipients' do - context 'with failed pipeline' do - it_behaves_like 'sending email' - end - context 'with succeeded pipeline' do before do data[:object_attributes][:status] = 'success' @@ -240,10 +236,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do context 'when the pipeline failed' do context 'on default branch' do - before do - data[:object_attributes][:ref] = project.default_branch - pipeline.update!(ref: project.default_branch) - end + it_behaves_like 'sending email' context 'notifications are enabled only for default branch' do it_behaves_like 'sending email', branches_to_be_notified: "default" @@ -253,7 +246,7 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do it_behaves_like 'not sending email', branches_to_be_notified: "protected" end - context 'notifications are enabled only for default and protected branches ' do + context 'notifications are enabled only for default and protected branches' do it_behaves_like 'sending email', branches_to_be_notified: "default_and_protected" end @@ -273,11 +266,13 @@ RSpec.describe Integrations::PipelinesEmail, :mailer do it_behaves_like 'not sending email', branches_to_be_notified: "default" end - context 'notifications are enabled only for protected branch' do + context 'notifications are enabled only for protected branch', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/411331' do it_behaves_like 'sending email', branches_to_be_notified: "protected" end - context 'notifications are enabled only for default and protected branches ' do + context 'notifications are enabled only for default and protected branches', + quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/411331' do it_behaves_like 'sending email', branches_to_be_notified: "default_and_protected" end diff --git a/spec/requests/api/v3/github_spec.rb b/spec/requests/api/v3/github_spec.rb index b6fccd9b7cb..fbda291e901 100644 --- a/spec/requests/api/v3/github_spec.rb +++ b/spec/requests/api/v3/github_spec.rb @@ -13,16 +13,33 @@ RSpec.describe API::V3::Github, :aggregate_failures, feature_category: :integrat end describe 'GET /orgs/:namespace/repos' do + let_it_be(:group) { create(:group) } + it_behaves_like 'a GitHub Enterprise Jira DVCS reversible end of life endpoint' do subject do - group = create(:group) jira_get v3_api("/orgs/#{group.path}/repos", user) end end - it 'returns an empty array' do - group = create(:group) + it 'logs when the endpoint is hit and `jira_dvcs_end_of_life_amnesty` is enabled' do + expect(Gitlab::JsonLogger).to receive(:info).with( + including( + namespace: group.path, + user_id: user.id, + message: 'Deprecated Jira DVCS endpoint request' + ) + ) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + + stub_feature_flags(jira_dvcs_end_of_life_amnesty: false) + expect(Gitlab::JsonLogger).not_to receive(:info) + + jira_get v3_api("/orgs/#{group.path}/repos", user) + end + + it 'returns an empty array' do jira_get v3_api("/orgs/#{group.path}/repos", user) expect(response).to have_gitlab_http_status(:ok) diff --git a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb index ec7a9105bb2..f772cfc6bbd 100644 --- a/spec/support/shared_examples/models/concerns/participable_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/participable_shared_examples.rb @@ -10,13 +10,14 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do | allow(model).to receive(:participant_attrs).and_return([:bar]) end - shared_examples 'check for participables read ability' do |ability_name| + shared_examples 'check for participables read ability' do |ability_name, ability_source: nil| it 'receives expected ability' do instance = model.new + source = ability_source == :participable_source ? participable_source : instance allow(instance).to receive(:bar).and_return(participable_source) - expect(Ability).to receive(:allowed?).with(anything, ability_name, instance) + expect(Ability).to receive(:allowed?).with(anything, ability_name, source) expect(instance.visible_participants(user1)).to be_empty end @@ -39,4 +40,10 @@ RSpec.shared_examples 'visible participants for issuable with read ability' do | it_behaves_like 'check for participables read ability', :read_internal_note end + + context 'when source is a system note' do + let(:participable_source) { build(:system_note) } + + it_behaves_like 'check for participables read ability', :read_note, ability_source: :participable_source + end end |