Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									040df42a88
								
							
						
					
					
						commit
						b49ce524ed
					
				|  | @ -1,8 +1,10 @@ | |||
| <script> | ||||
| import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; | ||||
| import { __, s__ } from '~/locale'; | ||||
| import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui'; | ||||
| import { s__, __, sprintf } from '~/locale'; | ||||
| import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility'; | ||||
| import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; | ||||
| import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql'; | ||||
| import pageInfoQuery from '../graphql/queries/page_info.query.graphql'; | ||||
| import EnvironmentFolder from './new_environment_folder.vue'; | ||||
| import EnableReviewAppModal from './enable_review_app_modal.vue'; | ||||
| 
 | ||||
|  | @ -11,6 +13,7 @@ export default { | |||
|     EnvironmentFolder, | ||||
|     EnableReviewAppModal, | ||||
|     GlBadge, | ||||
|     GlPagination, | ||||
|     GlTab, | ||||
|     GlTabs, | ||||
|   }, | ||||
|  | @ -20,6 +23,7 @@ export default { | |||
|       variables() { | ||||
|         return { | ||||
|           scope: this.scope, | ||||
|           page: this.page ?? 1, | ||||
|         }; | ||||
|       }, | ||||
|       pollInterval() { | ||||
|  | @ -29,6 +33,9 @@ export default { | |||
|     interval: { | ||||
|       query: pollIntervalQuery, | ||||
|     }, | ||||
|     pageInfo: { | ||||
|       query: pageInfoQuery, | ||||
|     }, | ||||
|   }, | ||||
|   inject: ['newEnvironmentPath', 'canCreateEnvironment'], | ||||
|   i18n: { | ||||
|  | @ -36,11 +43,21 @@ export default { | |||
|     reviewAppButtonLabel: s__('Environments|Enable review app'), | ||||
|     available: __('Available'), | ||||
|     stopped: __('Stopped'), | ||||
|     prevPage: __('Go to previous page'), | ||||
|     nextPage: __('Go to next page'), | ||||
|     next: __('Next'), | ||||
|     prev: __('Prev'), | ||||
|     goto: (page) => sprintf(__('Go to page %{page}'), { page }), | ||||
|   }, | ||||
|   modalId: 'enable-review-app-info', | ||||
|   data() { | ||||
|     const scope = new URLSearchParams(window.location.search).get('scope') || 'available'; | ||||
|     return { interval: undefined, scope, isReviewAppModalVisible: false }; | ||||
|     const { page = '1', scope = 'available' } = queryToObject(window.location.search); | ||||
|     return { | ||||
|       interval: undefined, | ||||
|       isReviewAppModalVisible: false, | ||||
|       page: parseInt(page, 10), | ||||
|       scope, | ||||
|     }; | ||||
|   }, | ||||
|   computed: { | ||||
|     canSetupReviewApp() { | ||||
|  | @ -82,6 +99,19 @@ export default { | |||
|     stoppedCount() { | ||||
|       return this.environmentApp?.stoppedCount; | ||||
|     }, | ||||
|     totalItems() { | ||||
|       return this.pageInfo?.total; | ||||
|     }, | ||||
|     itemsPerPage() { | ||||
|       return this.pageInfo?.perPage; | ||||
|     }, | ||||
|   }, | ||||
|   mounted() { | ||||
|     window.addEventListener('popstate', this.syncPageFromQueryParams); | ||||
|   }, | ||||
|   destroyed() { | ||||
|     window.removeEventListener('popstate', this.syncPageFromQueryParams); | ||||
|     this.$apollo.queries.environmentApp.stopPolling(); | ||||
|   }, | ||||
|   methods: { | ||||
|     showReviewAppModal() { | ||||
|  | @ -89,12 +119,30 @@ export default { | |||
|     }, | ||||
|     setScope(scope) { | ||||
|       this.scope = scope; | ||||
|       this.resetPolling(); | ||||
|     }, | ||||
|     movePage(direction) { | ||||
|       this.moveToPage(this.pageInfo[`${direction}Page`]); | ||||
|     }, | ||||
|     moveToPage(page) { | ||||
|       this.page = page; | ||||
|       updateHistory({ | ||||
|         url: setUrlParams({ page: this.page }), | ||||
|         title: document.title, | ||||
|       }); | ||||
|       this.resetPolling(); | ||||
|     }, | ||||
|     syncPageFromQueryParams() { | ||||
|       const { page = '1' } = queryToObject(window.location.search); | ||||
|       this.page = parseInt(page, 10); | ||||
|     }, | ||||
|     resetPolling() { | ||||
|       this.$apollo.queries.environmentApp.stopPolling(); | ||||
|       this.$nextTick(() => { | ||||
|         if (this.interval) { | ||||
|           this.$apollo.queries.environmentApp.startPolling(this.interval); | ||||
|         } else { | ||||
|           this.$apollo.queries.environmentApp.refetch({ scope }); | ||||
|           this.$apollo.queries.environmentApp.refetch({ scope: this.scope, page: this.page }); | ||||
|         } | ||||
|       }); | ||||
|     }, | ||||
|  | @ -139,5 +187,19 @@ export default { | |||
|       class="gl-mb-3" | ||||
|       :nested-environment="folder" | ||||
|     /> | ||||
|     <gl-pagination | ||||
|       align="center" | ||||
|       :total-items="totalItems" | ||||
|       :per-page="itemsPerPage" | ||||
|       :value="page" | ||||
|       :next="$options.i18n.next" | ||||
|       :prev="$options.i18n.prev" | ||||
|       :label-previous-page="$options.prevPage" | ||||
|       :label-next-page="$options.nextPage" | ||||
|       :label-page="$options.goto" | ||||
|       @next="movePage('next')" | ||||
|       @previous="movePage('previous')" | ||||
|       @input="moveToPage" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import VueApollo from 'vue-apollo'; | ||||
| import createDefaultClient from '~/lib/graphql'; | ||||
| import environmentApp from './queries/environment_app.query.graphql'; | ||||
| import pageInfoQuery from './queries/page_info.query.graphql'; | ||||
| import { resolvers } from './resolvers'; | ||||
| import typeDefs from './typedefs.graphql'; | ||||
| 
 | ||||
|  | @ -19,6 +20,19 @@ export const apolloProvider = (endpoint) => { | |||
|       stoppedCount: 0, | ||||
|     }, | ||||
|   }); | ||||
| 
 | ||||
|   cache.writeQuery({ | ||||
|     query: pageInfoQuery, | ||||
|     data: { | ||||
|       pageInfo: { | ||||
|         total: 0, | ||||
|         perPage: 20, | ||||
|         nextPage: 0, | ||||
|         previousPage: 0, | ||||
|         __typename: 'LocalPageInfo', | ||||
|       }, | ||||
|     }, | ||||
|   }); | ||||
|   return new VueApollo({ | ||||
|     defaultClient, | ||||
|   }); | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| query getEnvironmentApp($scope: String) { | ||||
|   environmentApp(scope: $scope) @client { | ||||
| query getEnvironmentApp($page: Int, $scope: String) { | ||||
|   environmentApp(page: $page, scope: $scope) @client { | ||||
|     availableCount | ||||
|     stoppedCount | ||||
|     environments | ||||
|  |  | |||
|  | @ -0,0 +1,8 @@ | |||
| query getPageInfo { | ||||
|   pageInfo @client { | ||||
|     total | ||||
|     perPage | ||||
|     nextPage | ||||
|     previousPage | ||||
|   } | ||||
| } | ||||
|  | @ -1,9 +1,15 @@ | |||
| import axios from '~/lib/utils/axios_utils'; | ||||
| import { s__ } from '~/locale'; | ||||
| import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; | ||||
| import { | ||||
|   convertObjectPropsToCamelCase, | ||||
|   parseIntPagination, | ||||
|   normalizeHeaders, | ||||
| } from '~/lib/utils/common_utils'; | ||||
| 
 | ||||
| import pollIntervalQuery from './queries/poll_interval.query.graphql'; | ||||
| import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; | ||||
| import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql'; | ||||
| import pageInfoQuery from './queries/page_info.query.graphql'; | ||||
| 
 | ||||
| const buildErrors = (errors = []) => ({ | ||||
|   errors, | ||||
|  | @ -21,9 +27,11 @@ const mapEnvironment = (env) => ({ | |||
| 
 | ||||
| export const resolvers = (endpoint) => ({ | ||||
|   Query: { | ||||
|     environmentApp(_context, { scope }, { cache }) { | ||||
|       return axios.get(endpoint, { params: { nested: true, scope } }).then((res) => { | ||||
|         const interval = res.headers['poll-interval']; | ||||
|     environmentApp(_context, { page, scope }, { cache }) { | ||||
|       return axios.get(endpoint, { params: { nested: true, page, scope } }).then((res) => { | ||||
|         const headers = normalizeHeaders(res.headers); | ||||
|         const interval = headers['POLL-INTERVAL']; | ||||
|         const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' }; | ||||
| 
 | ||||
|         if (interval) { | ||||
|           cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(interval) } }); | ||||
|  | @ -31,6 +39,11 @@ export const resolvers = (endpoint) => ({ | |||
|           cache.writeQuery({ query: pollIntervalQuery, data: { interval: undefined } }); | ||||
|         } | ||||
| 
 | ||||
|         cache.writeQuery({ | ||||
|           query: pageInfoQuery, | ||||
|           data: { pageInfo }, | ||||
|         }); | ||||
| 
 | ||||
|         return { | ||||
|           availableCount: res.data.available_count, | ||||
|           environments: res.data.environments.map(mapNestedEnvironment), | ||||
|  |  | |||
|  | @ -55,10 +55,18 @@ type LocalErrors { | |||
|   errors: [String!]! | ||||
| } | ||||
| 
 | ||||
| type LocalPageInfo { | ||||
|   total: Int! | ||||
|   perPage: Int! | ||||
|   nextPage: Int! | ||||
|   previousPage: Int! | ||||
| } | ||||
| 
 | ||||
| extend type Query { | ||||
|   environmentApp: LocalEnvironmentApp | ||||
|   environmentApp(page: Int, scope: String): LocalEnvironmentApp | ||||
|   folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder | ||||
|   environmentToDelete: LocalEnvironment | ||||
|   pageInfo: LocalPageInfo | ||||
|   environmentToRollback: LocalEnvironment | ||||
|   isLastDeployment: Boolean | ||||
| } | ||||
|  |  | |||
|  | @ -17,6 +17,7 @@ export const BV_HIDE_MODAL = 'bv::hide::modal'; | |||
| export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip'; | ||||
| export const BV_DROPDOWN_SHOW = 'bv::dropdown::show'; | ||||
| export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide'; | ||||
| export const BV_COLLAPSE_STATE = 'bv::collapse::state'; | ||||
| 
 | ||||
| export const DEFAULT_TH_CLASSES = | ||||
|   'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!'; | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ | |||
|   = render 'shared/user_dropdown_instance_review' | ||||
|   - if Gitlab.com_but_not_canary? | ||||
|     %li.d-md-none | ||||
|       = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" | ||||
|       = link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url | ||||
| 
 | ||||
|   - if current_user_menu?(:sign_out) | ||||
|     %li.divider | ||||
|  |  | |||
|  | @ -15,7 +15,7 @@ | |||
|               %span.logo-text.d-none.d-lg-block.gl-ml-3 | ||||
|                 = logo_text | ||||
|           - if Gitlab.com_and_canary? | ||||
|             = link_to 'https://next.gitlab.com', class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: :_noopener do | ||||
|             = link_to Gitlab::Saas.canary_toggle_com_url, class: 'canary-badge bg-transparent', data: { qa_selector: 'canary_badge_link' }, target: :_blank, rel: 'noopener noreferrer' do | ||||
|               %span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1 | ||||
|                 = _('Next') | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,4 +20,4 @@ | |||
|   = render 'shared/user_dropdown_instance_review' | ||||
|   - if Gitlab.com_but_not_canary? | ||||
|     %li | ||||
|       = link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" | ||||
|       = link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url | ||||
|  |  | |||
|  | @ -10,9 +10,7 @@ | |||
|     .card-header | ||||
|       %strong | ||||
|         = s_('PrometheusService|Custom metrics') | ||||
|       -# haml-lint:disable NoPlainNodes | ||||
|       %span.badge.badge-pill.js-custom-monitored-count 0 | ||||
|       -# haml-lint:enable NoPlainNodes | ||||
|       = gl_badge_tag 0, nil, class: 'js-custom-monitored-count' | ||||
|       = link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn gl-button btn-confirm gl-ml-auto js-new-metric-button hidden', data: { qa_selector: 'new_metric_button' } | ||||
|     .card-body | ||||
|       .flash-container.hidden | ||||
|  |  | |||
|  | @ -12,7 +12,7 @@ | |||
|     .card-header | ||||
|       %strong | ||||
|         = s_('PrometheusService|Common metrics') | ||||
|       %span.badge.badge-pill.js-monitored-count 0 | ||||
|       = gl_badge_tag 0, nil, class: 'js-monitored-count' | ||||
|     .card-body | ||||
|       .loading-metrics.js-loading-metrics | ||||
|         %p.m-3 | ||||
|  | @ -28,7 +28,7 @@ | |||
|       = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' ) | ||||
|       = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' ) | ||||
|       = s_('PrometheusService|Missing environment variable') | ||||
|       %span.badge.badge-pill.js-env-var-count 0 | ||||
|       = gl_badge_tag 0, nil, class: 'js-env-var-count' | ||||
|     .card-body.hidden | ||||
|       .flash-container | ||||
|         .flash-notice | ||||
|  |  | |||
|  | @ -1,15 +1,12 @@ | |||
| -  count_badge_classes = 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex' | ||||
| -  count_badge_classes = 'gl-display-none gl-sm-display-inline-flex' | ||||
| 
 | ||||
| = gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } } ) do | ||||
|   = gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do | ||||
|     = _('Open') | ||||
|     %span{ class: count_badge_classes } | ||||
|       = counts[:opened] | ||||
|     = gl_tab_counter_badge counts[:opened], { class: count_badge_classes } | ||||
|   = gl_tab_link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc'), { item_active: params[:state] == 'closed' } do | ||||
|     = _('Closed') | ||||
|     %span{ class: count_badge_classes } | ||||
|       = counts[:closed] | ||||
|     = gl_tab_counter_badge counts[:closed], { class: count_badge_classes } | ||||
|   = gl_tab_link_to milestones_filter_path(state: 'all', sort: 'due_date_desc'), { item_active: params[:state] == 'all' } do | ||||
|     = _('All') | ||||
|     %span{ class: count_badge_classes } | ||||
|       = counts[:all] | ||||
|     = gl_tab_counter_badge counts[:all], { class: count_badge_classes } | ||||
|  |  | |||
|  | @ -0,0 +1,21 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| class RemoveVulnerabilityFindingLinksAgain < Gitlab::Database::Migration[1.0] | ||||
|   BATCH_SIZE = 50_000 | ||||
|   MIGRATION = 'RemoveVulnerabilityFindingLinks' | ||||
| 
 | ||||
|   disable_ddl_transaction! | ||||
| 
 | ||||
|   def up | ||||
|     queue_background_migration_jobs_by_range_at_intervals( | ||||
|       define_batchable_model('vulnerability_finding_links'), | ||||
|       MIGRATION, | ||||
|       2.minutes, | ||||
|       batch_size: BATCH_SIZE | ||||
|     ) | ||||
|   end | ||||
| 
 | ||||
|   def down | ||||
|     # no ops | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1 @@ | |||
| 9bbd4c3e396e0de130418e705a370ce629ca507c82fa2ff5bbf085cdf01c2ff3 | ||||
|  | @ -35,9 +35,9 @@ verification methods: | |||
| | Git      | Project Snippets                                | Geo with Gitaly                       | Gitaly Checksum        | | ||||
| | Git      | Personal Snippets                               | Geo with Gitaly                       | Gitaly Checksum        | | ||||
| | Git      | Group wiki repository                           | Geo with Gitaly                       | _Not implemented_      | | ||||
| | Blobs    | User uploads _(file system)_                    | Geo with API                          | _Not implemented_      | | ||||
| | Blobs    | User uploads _(file system)_                    | Geo with API                          | SHA256 checksum        | | ||||
| | Blobs    | User uploads _(object storage)_                 | Geo with API/Managed (*2*)            | _Not implemented_      | | ||||
| | Blobs    | LFS objects _(file system)_                     | Geo with API                          | SHA256 checksum      | | ||||
| | Blobs    | LFS objects _(file system)_                     | Geo with API                          | SHA256 checksum        | | ||||
| | Blobs    | LFS objects _(object storage)_                  | Geo with API/Managed (*2*)            | _Not implemented_      | | ||||
| | Blobs    | CI job artifacts _(file system)_                | Geo with API                          | _Not implemented_      | | ||||
| | Blobs    | CI job artifacts _(object storage)_             | Geo with API/Managed (*2*)            | _Not implemented_      | | ||||
|  | @ -188,8 +188,8 @@ successfully, you must replicate their data using some other means. | |||
| |[Project repository](../../../user/project/repository/)                                                        | **Yes** (10.2)                                                          | **Yes** (10.7)                                                             | No                                                                            |       | | ||||
| |[Project wiki repository](../../../user/project/wiki/)                                                         | **Yes** (10.2)                                                          | **Yes** (10.7)                                                             | No                                                                            |       | | ||||
| |[Group wiki repository](../../../user/project/wiki/group.md)                                                   | [**Yes** (13.10)](https://gitlab.com/gitlab-org/gitlab/-/issues/208147) | No                                                                         | No                                                                            | Behind feature flag `geo_group_wiki_repository_replication`, enabled by default. | | ||||
| |[Uploads](../../uploads.md)                                                                                    | **Yes** (10.2)                                                          | [No](https://gitlab.com/groups/gitlab-org/-/epics/1817)                    | No                                                                            | Verified only on transfer or manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. | | ||||
| |[LFS objects](../../lfs/index.md)                                                                              | **Yes** (10.2)                                                          | **Yes**(14.6)                   | Via Object Storage provider if supported. Native Geo support (Beta).          | GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is behind the feature flag `geo_lfs_object_verification` enabled by default in 14.6. | | ||||
| |[Uploads](../../uploads.md)                                                                                    | **Yes** (10.2)                                                          | **Yes** (14.6)                                                             | Via Object Storage provider if supported. Native Geo support (Beta).          | Replication is behind the feature flag `geo_upload_replication`, enabled by default. Verification is behind the feature flag `geo_upload_verification` introduced and enabled by default in 14.6. | | ||||
| |[LFS objects](../../lfs/index.md)                                                                              | **Yes** (10.2)                                                          | **Yes** (14.6)                                                             | Via Object Storage provider if supported. Native Geo support (Beta).          | GitLab versions 11.11.x and 12.0.x are affected by [a bug that prevents any new LFS objects from replicating](https://gitlab.com/gitlab-org/gitlab/-/issues/32696).<br /><br />Replication is behind the feature flag `geo_lfs_object_replication`, enabled by default. Verification is behind the feature flag `geo_lfs_object_verification` introduced and enabled by default in 14.6. | | ||||
| |[Personal snippets](../../../user/snippets.md)                                                                 | **Yes** (10.2)                                                          | **Yes** (10.2)                                                             | No                                                                            |       | | ||||
| |[Project snippets](../../../user/snippets.md)                                                                  | **Yes** (10.2)                                                          | **Yes** (10.2)                                                             | No                                                                            |       | | ||||
| |[CI job artifacts](../../../ci/pipelines/job_artifacts.md)                                                     | **Yes** (10.4)                                                          | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/8923)                   | Via Object Storage provider if supported. Native Geo support (Beta).          | Verified only manually using [Integrity Check Rake Task](../../raketasks/check.md) on both sites and comparing the output between them. Job logs also verified on transfer. | | ||||
|  |  | |||
|  | @ -16,6 +16,8 @@ This document lists the configuration options for your GitLab `.gitlab-ci.yml` f | |||
| When you are editing your `.gitlab-ci.yml` file, you can validate it with the | ||||
| [CI Lint](../lint.md) tool. | ||||
| 
 | ||||
| If you are editing this page, make sure you follow the [CI/CD YAML reference style guide](../../development/cicd/cicd_reference_documentation_guide.md). | ||||
| 
 | ||||
| ## Keywords | ||||
| 
 | ||||
| A GitLab CI/CD pipeline configuration includes: | ||||
|  |  | |||
|  | @ -33,9 +33,14 @@ Try to avoid using **above** when referring to an example or table in a document | |||
| 
 | ||||
| Do not use **above** when referring to versions of the product. Use [**later**](#later) instead. | ||||
| 
 | ||||
| - Do: In GitLab 14.4 and later... | ||||
| - Do not: In GitLab 14.4 and above... | ||||
| - Do not: In GitLab 14.4 and higher... | ||||
| Use: | ||||
| 
 | ||||
| - In GitLab 14.4 and later... | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In GitLab 14.4 and above... | ||||
| - In GitLab 14.4 and higher... | ||||
| 
 | ||||
| ## access level | ||||
| 
 | ||||
|  | @ -56,9 +61,14 @@ To view the administrator access level, in the GitLab UI, go to the Admin Area a | |||
| 
 | ||||
| An **administrator** is not a [role](#roles) or [permission](#permissions). | ||||
| 
 | ||||
| - Do: To do this thing, you must be an administrator. | ||||
| - Do: To do this thing, you must have the administrator access level. | ||||
| - Do not: To do this thing, you must have the Admin role. | ||||
| Use: | ||||
| 
 | ||||
| - To do this thing, you must be an administrator. | ||||
| - To do this thing, you must have the administrator access level. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - To do this thing, you must have the Admin role. | ||||
| 
 | ||||
| ## Admin Area | ||||
| 
 | ||||
|  | @ -67,10 +77,16 @@ This area of the UI says **Admin Area** at the top of the page and on the menu. | |||
| 
 | ||||
| ## allow, enable | ||||
| 
 | ||||
| Try to avoid **allow** and **enable**, unless you are talking about security-related features. For example: | ||||
| Try to avoid **allow** and **enable**, unless you are talking about security-related features. | ||||
| 
 | ||||
| - Do: Use this feature to create a pipeline. | ||||
| - Do not: This feature allows you to create a pipeline. | ||||
| Use: | ||||
| 
 | ||||
| - You can add a file to your repository. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - This feature allows you to add a file to your repository. | ||||
| - This feature enables users to add files to their repository. | ||||
| 
 | ||||
| This phrasing is more active and is from the user perspective, rather than the person who implemented the feature. | ||||
| [View details in the Microsoft style guide](https://docs.microsoft.com/en-us/style-guide/a-z-word-list-term-collections/a/allow-allows). | ||||
|  | @ -126,8 +142,13 @@ Use **text box** to refer to the UI field. Do not use **field** or **box**. For | |||
| 
 | ||||
| Don't use a descriptor with **button**. | ||||
| 
 | ||||
| - Do: Select **Run pipelines**. | ||||
| - Do not: Select the **Run pipelines** button. | ||||
| Use: | ||||
| 
 | ||||
| - Select **Run pipelines**. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Select the **Run pipelines** button. | ||||
| 
 | ||||
| ## cannot, can not | ||||
| 
 | ||||
|  | @ -194,8 +215,8 @@ When writing about the Developer role: | |||
| - Do not use the phrase, **if you are a developer** to mean someone who is assigned the Developer | ||||
|   role. Instead, write it out. For example, **if you are assigned the Developer role**. | ||||
| - To describe a situation where the Developer role is the minimum required: | ||||
|   - Do: at least the Developer role | ||||
|   - Do not: the Developer role or higher | ||||
|   - Use: at least the Developer role | ||||
|   - Instead of: the Developer role or higher | ||||
| 
 | ||||
| Do not use **Developer permissions**. A user who is assigned the Developer role has a set of associated permissions. | ||||
| 
 | ||||
|  | @ -217,8 +238,13 @@ For example: | |||
| 
 | ||||
| Use **earlier** when talking about version numbers. | ||||
| 
 | ||||
| - Do: In GitLab 14.1 and earlier. | ||||
| - Do not: In GitLab 14.1 and lower. | ||||
| Use: | ||||
| 
 | ||||
| - In GitLab 14.1 and earlier. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In GitLab 14.1 and lower. | ||||
| 
 | ||||
| ## easily | ||||
| 
 | ||||
|  | @ -254,8 +280,13 @@ Use lowercase for **epic board**. | |||
| Try to avoid **etc.**. Be as specific as you can. Do not use | ||||
| [**and so on**](#and-so-on) as a replacement. | ||||
| 
 | ||||
| - Do: You can update objects, like merge requests and issues. | ||||
| - Do not: You can update objects, like merge requests, issues, etc. | ||||
| Use: | ||||
| 
 | ||||
| - You can update objects, like merge requests and issues. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - You can update objects, like merge requests, issues, etc. | ||||
| 
 | ||||
| ## expand | ||||
| 
 | ||||
|  | @ -265,8 +296,13 @@ Use **expand** instead of **open** when you are talking about expanding or colla | |||
| 
 | ||||
| Use **box** instead of **field** or **text box**. | ||||
| 
 | ||||
| - Do: In the **Variable name** box, enter `my text`. | ||||
| - Do not: In the **Variable name** field, enter `my text`. | ||||
| Use: | ||||
| 
 | ||||
| - In the **Variable name** box, enter `my text`. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In the **Variable name** field, enter `my text`. | ||||
| 
 | ||||
| However, you can make an exception when you are writing a task and you need to refer to all | ||||
| of the fields at once. For example: | ||||
|  | @ -320,8 +356,8 @@ When writing about the Guest role: | |||
| - Do not use the phrase, **if you are a guest** to mean someone who is assigned the Guest | ||||
|   role. Instead, write it out. For example, **if you are assigned the Guest role**. | ||||
| - To describe a situation where the Guest role is the minimum required: | ||||
|   - Do: at least the Guest role | ||||
|   - Do not: the Guest role or higher | ||||
|   - Use: at least the Guest role | ||||
|   - Instead of: the Guest role or higher | ||||
| 
 | ||||
| Do not use **Guest permissions**. A user who is assigned the Guest role has a set of associated permissions. | ||||
| 
 | ||||
|  | @ -337,16 +373,26 @@ Do not use **high availability** or **HA**. Instead, direct readers to the GitLa | |||
| 
 | ||||
| Do not use **higher** when talking about version numbers. | ||||
| 
 | ||||
| - Do: In GitLab 14.4 and later... | ||||
| - Do not: In GitLab 14.4 and higher... | ||||
| - Do not: In GitLab 14.4 and above... | ||||
| Use: | ||||
| 
 | ||||
| - In GitLab 14.4 and later... | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In GitLab 14.4 and higher... | ||||
| - In GitLab 14.4 and above... | ||||
| 
 | ||||
| ## hit | ||||
| 
 | ||||
| Don't use **hit** to mean **press**. | ||||
| 
 | ||||
| - Do: Press **ENTER**. | ||||
| - Do not: Hit the **ENTER** button. | ||||
| Use: | ||||
| 
 | ||||
| - Press **ENTER**. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Hit the **ENTER** button. | ||||
| 
 | ||||
| ## I | ||||
| 
 | ||||
|  | @ -395,9 +441,14 @@ Do not use: | |||
| 
 | ||||
| Use **later** when talking about version numbers. | ||||
| 
 | ||||
| - Do: In GitLab 14.1 and later... | ||||
| - Do not: In GitLab 14.1 and higher... | ||||
| - Do not: In GitLab 14.1 and above... | ||||
| Use: | ||||
| 
 | ||||
| - In GitLab 14.1 and later... | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In GitLab 14.1 and higher... | ||||
| - In GitLab 14.1 and above... | ||||
| 
 | ||||
| ## list | ||||
| 
 | ||||
|  | @ -412,8 +463,13 @@ Do not use **log in** or **log on**. Use [sign in](#sign-in) instead. If the use | |||
| 
 | ||||
| Do not use **lower** when talking about version numbers. | ||||
| 
 | ||||
| - Do: In GitLab 14.1 and earlier. | ||||
| - Do not: In GitLab 14.1 and lower. | ||||
| Use: | ||||
| 
 | ||||
| - In GitLab 14.1 and earlier. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - In GitLab 14.1 and lower. | ||||
| 
 | ||||
| ## Maintainer | ||||
| 
 | ||||
|  | @ -424,8 +480,8 @@ When writing about the Maintainer role: | |||
| - Do not use the phrase, **if you are a maintainer** to mean someone who is assigned the Maintainer | ||||
|   role. Instead, write it out. For example, **if you are assigned the Maintainer role**. | ||||
| - To describe a situation where the Maintainer role is the minimum required: | ||||
|   - Do: at least the Maintainer role | ||||
|   - Do not: the Maintainer role or higher | ||||
|   - Use: at least the Maintainer role | ||||
|   - Instead of: the Maintainer role or higher | ||||
| 
 | ||||
| Do not use **Maintainer permissions**. A user who is assigned the Maintainer role has a set of associated permissions. | ||||
| 
 | ||||
|  | @ -468,8 +524,14 @@ Do not use **navigate**. Use **go** instead. For example: | |||
| 
 | ||||
| Try to avoid **needs to**, because it's wordy. Avoid **should** when you can be more specific. If something is required, use **must**. | ||||
| 
 | ||||
| - Do: You must set the variable. Or: Set the variable. | ||||
| - Do not: You need to set the variable. | ||||
| Use: | ||||
| 
 | ||||
| - You must set the variable. | ||||
| - Set the variable. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - You need to set the variable. | ||||
| 
 | ||||
| **Should** is acceptable for recommended actions or items, or in cases where an event may not | ||||
| happen. For example: | ||||
|  | @ -483,22 +545,37 @@ happen. For example: | |||
| 
 | ||||
| Do not use **note that** because it's wordy. | ||||
| 
 | ||||
| - Do: You can change the settings. | ||||
| - Do not: Note that you can change the settings. | ||||
| Use: | ||||
| 
 | ||||
| - You can change the settings. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Note that you can change the settings. | ||||
| 
 | ||||
| ## on | ||||
| 
 | ||||
| When documenting how to select high-level UI elements, use the word **on**. | ||||
| 
 | ||||
| - Do: `On the left sidebar...` | ||||
| Use: | ||||
| 
 | ||||
| - `On the left sidebar...` | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Do not: `From the left sidebar...` or `In the left sidebar...` | ||||
| 
 | ||||
| ## once | ||||
| 
 | ||||
| The word **once** means **one time**. Don't use it to mean **after** or **when**. | ||||
| 
 | ||||
| - Do: When the process is complete... | ||||
| - Do not: Once the process is complete... | ||||
| Use: | ||||
| 
 | ||||
| - When the process is complete... | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Once the process is complete... | ||||
| 
 | ||||
| ## only | ||||
| 
 | ||||
|  | @ -566,8 +643,8 @@ When writing about the Reporter role: | |||
| - Do not use the phrase, **if you are a reporter** to mean someone who is assigned the Reporter | ||||
|   role. Instead, write it out. For example, **if you are assigned the Reporter role**. | ||||
| - To describe a situation where the Reporter role is the minimum required: | ||||
|   - Do: at least the Reporter role | ||||
|   - Do not: the Reporter role or higher | ||||
|   - Use: at least the Reporter role | ||||
|   - Instead of: the Reporter role or higher | ||||
| 
 | ||||
| Do not use **Reporter permissions**. A user who is assigned the Reporter role has a set of associated permissions. | ||||
| 
 | ||||
|  | @ -589,8 +666,13 @@ Use lowercase for **runners**. These are the agents that run CI/CD jobs. See als | |||
| 
 | ||||
| Do not use **(s)** to make a word optionally plural. It can slow down comprehension. For example: | ||||
| 
 | ||||
| - Do: Select the jobs you want. | ||||
| - Do not: Select the job(s) you want. | ||||
| Use: | ||||
| 
 | ||||
| - Select the jobs you want. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Select the job(s) you want. | ||||
| 
 | ||||
| If you can select multiples of something, then write the word as plural. | ||||
| 
 | ||||
|  | @ -612,7 +694,12 @@ into separate areas, refer to these areas as sections. | |||
| We often think of expandable/collapsible areas as **sections**. When you refer to expanding | ||||
| or collapsing a section, don't include the word **section**. | ||||
| 
 | ||||
| - Do: Expand **Auto DevOps**. | ||||
| Use: | ||||
| 
 | ||||
| - Expand **Auto DevOps**. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Do not: Expand the **Auto DevOps** section. | ||||
| 
 | ||||
| ## select | ||||
|  | @ -645,8 +732,13 @@ Do not use **simply** or **simple**. If the user doesn't find the process to be | |||
| 
 | ||||
| The word **since** indicates a timeframe. For example, **Since 1984, Bon Jovi has existed**. Don't use **since** to mean **because**. | ||||
| 
 | ||||
| - Do: Because you have the Developer role, you can delete the widget. | ||||
| - Do not: Since you have the Developer role, you can delete the widget. | ||||
| Use: | ||||
| 
 | ||||
| - Because you have the Developer role, you can delete the widget. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Since you have the Developer role, you can delete the widget. | ||||
| 
 | ||||
| ## slashes | ||||
| 
 | ||||
|  | @ -664,8 +756,13 @@ Use **subgroup** (no hyphen) instead of **sub-group**. ([Vale](../testing.md#val | |||
| 
 | ||||
| Do not use **that** when describing a noun. For example: | ||||
| 
 | ||||
| - Do: The file you save... | ||||
| - Do not: The file **that** you save... | ||||
| Use: | ||||
| 
 | ||||
| - The file you save... | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - The file **that** you save... | ||||
| 
 | ||||
| See also [this, these, that, those](#this-these-that-those). | ||||
| 
 | ||||
|  | @ -684,8 +781,13 @@ Use **text box** instead of **field** or **box** when referring to the UI elemen | |||
| 
 | ||||
| Try to avoid **there is** and **there are**. These phrases hide the subject. | ||||
| 
 | ||||
| - Do: The bucket has holes. | ||||
| - Do not: There are holes in the bucket. | ||||
| Use: | ||||
| 
 | ||||
| - The bucket has holes. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - There are holes in the bucket. | ||||
| 
 | ||||
| ## they | ||||
| 
 | ||||
|  | @ -697,17 +799,17 @@ a gender-neutral pronoun. | |||
| 
 | ||||
| Always follow these words with a noun. For example: | ||||
| 
 | ||||
| - Do: **This setting** improves performance. | ||||
| - Do not: **This** improves performance. | ||||
| - Use: **This setting** improves performance. | ||||
| - Instead of: **This** improves performance. | ||||
| 
 | ||||
| - Do: **These pants** are the best. | ||||
| - Do not: **These** are the best. | ||||
| - Use: **These pants** are the best. | ||||
| - Instead of: **These** are the best. | ||||
| 
 | ||||
| - Do: **That droid** is the one you are looking for. | ||||
| - Do not: **That** is the one you are looking for. | ||||
| - Use: **That droid** is the one you are looking for. | ||||
| - Instead of: **That** is the one you are looking for. | ||||
| 
 | ||||
| - Do: **Those settings** need to be configured. (Or even better, **Configure those settings.**) | ||||
| - Do not: **Those** need to be configured. | ||||
| - Use: **Those settings** need to be configured. (Or even better, **Configure those settings.**) | ||||
| - Instead of: **Those** need to be configured. | ||||
| 
 | ||||
| ## to-do item | ||||
| 
 | ||||
|  | @ -736,8 +838,13 @@ Do not use **useful**. If the user doesn't find the process to be useful, we los | |||
| When possible, address the reader directly, instead of calling them **users**. | ||||
| Use the [second person](#you-your-yours), **you**, instead. | ||||
| 
 | ||||
| - Do: You can configure a pipeline. | ||||
| - Do not: Users can configure a pipeline. | ||||
| Use: | ||||
| 
 | ||||
| - You can configure a pipeline. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Users can configure a pipeline. | ||||
| 
 | ||||
| ## utilize | ||||
| 
 | ||||
|  | @ -756,8 +863,13 @@ Do not use Latin abbreviations. Use **with**, **through**, or **by using** inste | |||
| 
 | ||||
| Try to avoid **we** and focus instead on how the user can accomplish something in GitLab. | ||||
| 
 | ||||
| - Do: Use widgets when you have work you want to organize. | ||||
| - Do not: We created a feature for you to add widgets. | ||||
| Use: | ||||
| 
 | ||||
| - Use widgets when you have work you want to organize. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - We created a feature for you to add widgets. | ||||
| 
 | ||||
| One exception: You can use **we recommend** instead of **it is recommended** or **GitLab recommends**. ([Vale](../testing.md#vale) rule: [`SubstitutionSuggestions.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SubstitutionSuggestions.yml)) | ||||
| 
 | ||||
|  | @ -770,8 +882,13 @@ Do not use **whitelist**. Another option is **allowlist**. ([Vale](../testing.md | |||
| Use **you**, **your**, and **yours** instead of [**the user** and **the user's**](#user-users). | ||||
| Documentation should be from the [point of view](https://design.gitlab.com/content/voice-tone#point-of-view) of the reader. | ||||
| 
 | ||||
| - Do: You can configure a pipeline. | ||||
| - Do not: Users can configure a pipeline. | ||||
| Use: | ||||
| 
 | ||||
| - You can configure a pipeline. | ||||
| 
 | ||||
| Instead of: | ||||
| 
 | ||||
| - Users can configure a pipeline. | ||||
| 
 | ||||
| <!-- vale on --> | ||||
| <!-- markdownlint-enable --> | ||||
|  |  | |||
|  | @ -492,10 +492,13 @@ image::screenshot.png[block image,800,450] | |||
| Press image:reload.svg[reload,16,opts=interactive] to reload the page. | ||||
| 
 | ||||
| video::movie.mp4[width=640,start=60,end=140,options=autoplay] | ||||
| ``` | ||||
| 
 | ||||
| video::aHjpOzsQ9YI[youtube] | ||||
| GitLab does not support embedding YouTube and Vimeo videos in AsciiDoc content. | ||||
| Use a standard AsciiDoc link: | ||||
| 
 | ||||
| video::300817511[vimeo] | ||||
| ```plaintext | ||||
| https://www.youtube.com/watch?v=BlaZ65-b7y0[Link text for the video] | ||||
| ``` | ||||
| 
 | ||||
| ### Breaks | ||||
|  |  | |||
|  | @ -344,6 +344,11 @@ Here is an example of milestones with no releases, one release, and two releases | |||
| 
 | ||||
|  | ||||
| 
 | ||||
| NOTE: | ||||
| A subgroup's project releases cannot be associated with a supergroup's milestone. To learn | ||||
| more, read issue #328054, | ||||
| [Releases cannot be associated with a supergroup milestone](https://gitlab.com/gitlab-org/gitlab/-/issues/328054). | ||||
| 
 | ||||
| ## Get notified when a release is created | ||||
| 
 | ||||
| > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26001) in GitLab 12.4. | ||||
|  |  | |||
|  | @ -143,7 +143,9 @@ you can choose from: | |||
| 
 | ||||
|  | ||||
| 
 | ||||
| When filtering by a deploy date, you must enter the date manually. Deploy dates | ||||
| When filtering by `Deployed-before` or `Deployed-after`, the date refers to when  | ||||
| the deployment to an environment (triggered by the merge commit) completed successfully. | ||||
| You must enter the deploy date manually. Deploy dates | ||||
| use the format `YYYY-MM-DD`, and must be quoted if you wish to specify | ||||
| both a date and time (`"YYYY-MM-DD HH:MM"`): | ||||
| 
 | ||||
|  |  | |||
|  | @ -14,11 +14,11 @@ module Gitlab | |||
|           @result_dir = result_dir | ||||
|         end | ||||
| 
 | ||||
|         def observe(version:, name:, &block) | ||||
|         def observe(version:, name:, connection:, &block) | ||||
|           observation = Observation.new(version, name) | ||||
|           observation.success = true | ||||
| 
 | ||||
|           observers = observer_classes.map { |c| c.new(observation, @result_dir) } | ||||
|           observers = observer_classes.map { |c| c.new(observation, @result_dir, connection) } | ||||
| 
 | ||||
|           exception = nil | ||||
| 
 | ||||
|  |  | |||
|  | @ -7,8 +7,8 @@ module Gitlab | |||
|         class MigrationObserver | ||||
|           attr_reader :connection, :observation, :output_dir | ||||
| 
 | ||||
|           def initialize(observation, output_dir) | ||||
|             @connection = ActiveRecord::Base.connection | ||||
|           def initialize(observation, output_dir, connection) | ||||
|             @connection = connection | ||||
|             @observation = observation | ||||
|             @output_dir = output_dir | ||||
|           end | ||||
|  |  | |||
|  | @ -69,7 +69,7 @@ module Gitlab | |||
|           instrumentation = Instrumentation.new(result_dir: result_dir) | ||||
| 
 | ||||
|           sorted_migrations.each do |migration| | ||||
|             instrumentation.observe(version: migration.version, name: migration.name) do | ||||
|             instrumentation.observe(version: migration.version, name: migration.name, connection: ActiveRecord::Migration.connection) do | ||||
|               ActiveRecord::Migrator.new(direction, migration_context.migrations, migration_context.schema_migration, migration.version).run | ||||
|             end | ||||
|           end | ||||
|  |  | |||
|  | @ -13,6 +13,10 @@ module Gitlab | |||
|       'https://staging.gitlab.com' | ||||
|     end | ||||
| 
 | ||||
|     def self.canary_toggle_com_url | ||||
|       'https://next.gitlab.com' | ||||
|     end | ||||
| 
 | ||||
|     def self.subdomain_regex | ||||
|       %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze | ||||
|     end | ||||
|  |  | |||
|  | @ -16406,6 +16406,9 @@ msgstr "" | |||
| msgid "Go to next page" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Go to page %{page}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Go to parent" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import environmentToRollback from '~/environments/graphql/queries/environment_to | |||
| import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql'; | ||||
| import createMockApollo from 'helpers/mock_apollo_helper'; | ||||
| import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; | ||||
| import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql'; | ||||
| import { TEST_HOST } from 'helpers/test_constants'; | ||||
| import { | ||||
|   environmentsApp, | ||||
|  | @ -37,9 +38,11 @@ describe('~/frontend/environments/graphql/resolvers', () => { | |||
|     it('should fetch environments and map them to frontend data', async () => { | ||||
|       const cache = { writeQuery: jest.fn() }; | ||||
|       const scope = 'available'; | ||||
|       mock.onGet(ENDPOINT, { params: { nested: true, scope } }).reply(200, environmentsApp, {}); | ||||
|       mock | ||||
|         .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } }) | ||||
|         .reply(200, environmentsApp, {}); | ||||
| 
 | ||||
|       const app = await mockResolvers.Query.environmentApp(null, { scope }, { cache }); | ||||
|       const app = await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache }); | ||||
|       expect(app).toEqual(resolvedEnvironmentsApp); | ||||
|       expect(cache.writeQuery).toHaveBeenCalledWith({ | ||||
|         query: pollIntervalQuery, | ||||
|  | @ -49,14 +52,70 @@ describe('~/frontend/environments/graphql/resolvers', () => { | |||
|     it('should set the poll interval when there is one', async () => { | ||||
|       const cache = { writeQuery: jest.fn() }; | ||||
|       const scope = 'stopped'; | ||||
|       const interval = 3000; | ||||
|       mock | ||||
|         .onGet(ENDPOINT, { params: { nested: true, scope } }) | ||||
|         .reply(200, environmentsApp, { 'poll-interval': 3000 }); | ||||
|         .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } }) | ||||
|         .reply(200, environmentsApp, { | ||||
|           'poll-interval': interval, | ||||
|         }); | ||||
| 
 | ||||
|       await mockResolvers.Query.environmentApp(null, { scope }, { cache }); | ||||
|       await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache }); | ||||
|       expect(cache.writeQuery).toHaveBeenCalledWith({ | ||||
|         query: pollIntervalQuery, | ||||
|         data: { interval: 3000 }, | ||||
|         data: { interval }, | ||||
|       }); | ||||
|     }); | ||||
|     it('should set page info if there is any', async () => { | ||||
|       const cache = { writeQuery: jest.fn() }; | ||||
|       const scope = 'stopped'; | ||||
|       mock | ||||
|         .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } }) | ||||
|         .reply(200, environmentsApp, { | ||||
|           'x-next-page': '2', | ||||
|           'x-page': '1', | ||||
|           'X-Per-Page': '2', | ||||
|           'X-Prev-Page': '', | ||||
|           'X-TOTAL': '37', | ||||
|           'X-Total-Pages': '5', | ||||
|         }); | ||||
| 
 | ||||
|       await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache }); | ||||
|       expect(cache.writeQuery).toHaveBeenCalledWith({ | ||||
|         query: pageInfoQuery, | ||||
|         data: { | ||||
|           pageInfo: { | ||||
|             total: 37, | ||||
|             perPage: 2, | ||||
|             previousPage: NaN, | ||||
|             totalPages: 5, | ||||
|             nextPage: 2, | ||||
|             page: 1, | ||||
|             __typename: 'LocalPageInfo', | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|     it('should not set page info if there is none', async () => { | ||||
|       const cache = { writeQuery: jest.fn() }; | ||||
|       const scope = 'stopped'; | ||||
|       mock | ||||
|         .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } }) | ||||
|         .reply(200, environmentsApp, {}); | ||||
| 
 | ||||
|       await mockResolvers.Query.environmentApp(null, { scope, page: 1 }, { cache }); | ||||
|       expect(cache.writeQuery).toHaveBeenCalledWith({ | ||||
|         query: pageInfoQuery, | ||||
|         data: { | ||||
|           pageInfo: { | ||||
|             __typename: 'LocalPageInfo', | ||||
|             nextPage: NaN, | ||||
|             page: NaN, | ||||
|             perPage: NaN, | ||||
|             previousPage: NaN, | ||||
|             total: NaN, | ||||
|             totalPages: NaN, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
|  |  | |||
|  | @ -1,9 +1,11 @@ | |||
| import Vue, { nextTick } from 'vue'; | ||||
| import VueApollo from 'vue-apollo'; | ||||
| import { GlPagination } from '@gitlab/ui'; | ||||
| import { mountExtended } from 'helpers/vue_test_utils_helper'; | ||||
| import createMockApollo from 'helpers/mock_apollo_helper'; | ||||
| import waitForPromises from 'helpers/wait_for_promises'; | ||||
| import { __, s__ } from '~/locale'; | ||||
| import setWindowLocation from 'helpers/set_window_location_helper'; | ||||
| import { sprintf, __, s__ } from '~/locale'; | ||||
| import EnvironmentsApp from '~/environments/components/new_environments_app.vue'; | ||||
| import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; | ||||
| import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; | ||||
|  | @ -14,12 +16,14 @@ describe('~/environments/components/new_environments_app.vue', () => { | |||
|   let wrapper; | ||||
|   let environmentAppMock; | ||||
|   let environmentFolderMock; | ||||
|   let paginationMock; | ||||
| 
 | ||||
|   const createApolloProvider = () => { | ||||
|     const mockResolvers = { | ||||
|       Query: { | ||||
|         environmentApp: environmentAppMock, | ||||
|         folder: environmentFolderMock, | ||||
|         pageInfo: paginationMock, | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -37,9 +41,23 @@ describe('~/environments/components/new_environments_app.vue', () => { | |||
|       apolloProvider, | ||||
|     }); | ||||
| 
 | ||||
|   const createWrapperWithMocked = async ({ provide = {}, environmentsApp, folder }) => { | ||||
|   const createWrapperWithMocked = async ({ | ||||
|     provide = {}, | ||||
|     environmentsApp, | ||||
|     folder, | ||||
|     pageInfo = { | ||||
|       total: 20, | ||||
|       perPage: 5, | ||||
|       nextPage: 3, | ||||
|       page: 2, | ||||
|       previousPage: 1, | ||||
|       __typename: 'LocalPageInfo', | ||||
|     }, | ||||
|   }) => { | ||||
|     setWindowLocation('?scope=available&page=2'); | ||||
|     environmentAppMock.mockReturnValue(environmentsApp); | ||||
|     environmentFolderMock.mockReturnValue(folder); | ||||
|     paginationMock.mockReturnValue(pageInfo); | ||||
|     const apolloProvider = createApolloProvider(); | ||||
|     wrapper = createWrapper({ apolloProvider, provide }); | ||||
| 
 | ||||
|  | @ -50,6 +68,7 @@ describe('~/environments/components/new_environments_app.vue', () => { | |||
|   beforeEach(() => { | ||||
|     environmentAppMock = jest.fn(); | ||||
|     environmentFolderMock = jest.fn(); | ||||
|     paginationMock = jest.fn(); | ||||
|   }); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|  | @ -118,42 +137,135 @@ describe('~/environments/components/new_environments_app.vue', () => { | |||
|     expect(button.exists()).toBe(false); | ||||
|   }); | ||||
| 
 | ||||
|   it('should show tabs for available and stopped environmets', async () => { | ||||
|     await createWrapperWithMocked({ | ||||
|       environmentsApp: resolvedEnvironmentsApp, | ||||
|       folder: resolvedFolder, | ||||
|   describe('tabs', () => { | ||||
|     it('should show tabs for available and stopped environmets', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
| 
 | ||||
|       const [available, stopped] = wrapper.findAllByRole('tab').wrappers; | ||||
| 
 | ||||
|       expect(available.text()).toContain(__('Available')); | ||||
|       expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount); | ||||
|       expect(stopped.text()).toContain(__('Stopped')); | ||||
|       expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount); | ||||
|     }); | ||||
| 
 | ||||
|     const [available, stopped] = wrapper.findAllByRole('tab').wrappers; | ||||
|     it('should change the requested scope on tab change', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
|       const stopped = wrapper.findByRole('tab', { | ||||
|         name: `${__('Stopped')} ${resolvedEnvironmentsApp.stoppedCount}`, | ||||
|       }); | ||||
| 
 | ||||
|     expect(available.text()).toContain(__('Available')); | ||||
|     expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount); | ||||
|     expect(stopped.text()).toContain(__('Stopped')); | ||||
|     expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount); | ||||
|       stopped.trigger('click'); | ||||
| 
 | ||||
|       await nextTick(); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(environmentAppMock).toHaveBeenCalledWith( | ||||
|         expect.anything(), | ||||
|         expect.objectContaining({ scope: 'stopped' }), | ||||
|         expect.anything(), | ||||
|         expect.anything(), | ||||
|       ); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('should change the requested scope on tab change', async () => { | ||||
|     environmentAppMock.mockReturnValue(resolvedEnvironmentsApp); | ||||
|     environmentFolderMock.mockReturnValue(resolvedFolder); | ||||
|     const apolloProvider = createApolloProvider(); | ||||
|     wrapper = createWrapper({ apolloProvider }); | ||||
|   describe('pagination', () => { | ||||
|     it('should sync page from query params on load', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
| 
 | ||||
|     await waitForPromises(); | ||||
|     await nextTick(); | ||||
|     const stopped = wrapper.findByRole('tab', { | ||||
|       name: `${__('Stopped')} ${resolvedEnvironmentsApp.stoppedCount}`, | ||||
|       expect(wrapper.findComponent(GlPagination).props('value')).toBe(2); | ||||
|     }); | ||||
| 
 | ||||
|     stopped.trigger('click'); | ||||
|     it('should change the requested page on next page click', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
|       const next = wrapper.findByRole('link', { | ||||
|         name: __('Go to next page'), | ||||
|       }); | ||||
| 
 | ||||
|     await nextTick(); | ||||
|     await waitForPromises(); | ||||
|       next.trigger('click'); | ||||
| 
 | ||||
|     expect(environmentAppMock).toHaveBeenCalledWith( | ||||
|       expect.anything(), | ||||
|       { scope: 'stopped' }, | ||||
|       expect.anything(), | ||||
|       expect.anything(), | ||||
|     ); | ||||
|       await nextTick(); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(environmentAppMock).toHaveBeenCalledWith( | ||||
|         expect.anything(), | ||||
|         expect.objectContaining({ page: 3 }), | ||||
|         expect.anything(), | ||||
|         expect.anything(), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should change the requested page on previous page click', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
|       const prev = wrapper.findByRole('link', { | ||||
|         name: __('Go to previous page'), | ||||
|       }); | ||||
| 
 | ||||
|       prev.trigger('click'); | ||||
| 
 | ||||
|       await nextTick(); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(environmentAppMock).toHaveBeenCalledWith( | ||||
|         expect.anything(), | ||||
|         expect.objectContaining({ page: 1 }), | ||||
|         expect.anything(), | ||||
|         expect.anything(), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should change the requested page on specific page click', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
| 
 | ||||
|       const page = 1; | ||||
|       const pageButton = wrapper.findByRole('link', { | ||||
|         name: sprintf(__('Go to page %{page}'), { page }), | ||||
|       }); | ||||
| 
 | ||||
|       pageButton.trigger('click'); | ||||
| 
 | ||||
|       await nextTick(); | ||||
|       await waitForPromises(); | ||||
| 
 | ||||
|       expect(environmentAppMock).toHaveBeenCalledWith( | ||||
|         expect.anything(), | ||||
|         expect.objectContaining({ page }), | ||||
|         expect.anything(), | ||||
|         expect.anything(), | ||||
|       ); | ||||
|     }); | ||||
| 
 | ||||
|     it('should sync the query params to the new page', async () => { | ||||
|       await createWrapperWithMocked({ | ||||
|         environmentsApp: resolvedEnvironmentsApp, | ||||
|         folder: resolvedFolder, | ||||
|       }); | ||||
|       const next = wrapper.findByRole('link', { | ||||
|         name: __('Go to next page'), | ||||
|       }); | ||||
| 
 | ||||
|       next.trigger('click'); | ||||
| 
 | ||||
|       await nextTick(); | ||||
|       expect(window.location.search).toBe('?scope=available&page=3'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ require 'spec_helper' | |||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Instrumentation do | ||||
|   let(:result_dir) { Dir.mktmpdir } | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
| 
 | ||||
|   after do | ||||
|     FileUtils.rm_rf(result_dir) | ||||
|  | @ -14,11 +15,11 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|     let(:migration_version) { '12345' } | ||||
| 
 | ||||
|     it 'executes the given block' do | ||||
|       expect { |b| subject.observe(version: migration_version, name: migration_name, &b) }.to yield_control | ||||
|       expect { |b| subject.observe(version: migration_version, name: migration_name, connection: connection, &b) }.to yield_control | ||||
|     end | ||||
| 
 | ||||
|     context 'behavior with observers' do | ||||
|       subject { described_class.new(observer_classes: [Gitlab::Database::Migrations::Observers::MigrationObserver], result_dir: result_dir).observe(version: migration_version, name: migration_name) {} } | ||||
|       subject { described_class.new(observer_classes: [Gitlab::Database::Migrations::Observers::MigrationObserver], result_dir: result_dir).observe(version: migration_version, name: migration_name, connection: connection) {} } | ||||
| 
 | ||||
|       let(:observer) { instance_double('Gitlab::Database::Migrations::Observers::MigrationObserver', before: nil, after: nil, record: nil) } | ||||
| 
 | ||||
|  | @ -29,7 +30,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|       it 'instantiates observer with observation' do | ||||
|         expect(Gitlab::Database::Migrations::Observers::MigrationObserver) | ||||
|           .to receive(:new) | ||||
|           .with(instance_of(Gitlab::Database::Migrations::Observation), anything) { |observation| expect(observation.version).to eq(migration_version) } | ||||
|           .with(instance_of(Gitlab::Database::Migrations::Observation), anything, connection) { |observation| expect(observation.version).to eq(migration_version) } | ||||
|           .and_return(observer) | ||||
| 
 | ||||
|         subject | ||||
|  | @ -63,7 +64,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|     end | ||||
| 
 | ||||
|     context 'on successful execution' do | ||||
|       subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name) {} } | ||||
|       subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name, connection: connection) {} } | ||||
| 
 | ||||
|       it 'records walltime' do | ||||
|         expect(subject.walltime).not_to be_nil | ||||
|  | @ -83,7 +84,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|     end | ||||
| 
 | ||||
|     context 'upon failure' do | ||||
|       subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name) { raise 'something went wrong' } } | ||||
|       subject { described_class.new(result_dir: result_dir).observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' } } | ||||
| 
 | ||||
|       it 'raises the exception' do | ||||
|         expect { subject }.to raise_error(/something went wrong/) | ||||
|  | @ -93,7 +94,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|         subject { instance.observations.first } | ||||
| 
 | ||||
|         before do | ||||
|           instance.observe(version: migration_version, name: migration_name) { raise 'something went wrong' } | ||||
|           instance.observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' } | ||||
|         rescue StandardError | ||||
|           # ignore | ||||
|         end | ||||
|  | @ -125,8 +126,8 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do | |||
|       let(:migration2) { double('migration2', call: nil) } | ||||
| 
 | ||||
|       it 'records observations for all migrations' do | ||||
|         subject.observe(version: migration_version, name: migration_name) {} | ||||
|         subject.observe(version: migration_version, name: migration_name) { raise 'something went wrong' } rescue nil | ||||
|         subject.observe(version: migration_version, name: migration_name, connection: connection) {} | ||||
|         subject.observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' } rescue nil | ||||
| 
 | ||||
|         expect(subject.observations.size).to eq(2) | ||||
|       end | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Observers::QueryDetails do | ||||
|   subject { described_class.new(observation, directory_path) } | ||||
|   subject { described_class.new(observation, directory_path, connection) } | ||||
| 
 | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
|   let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) } | ||||
|   let(:connection) { ActiveRecord::Base.connection } | ||||
|   let(:query) { "select date_trunc('day', $1::timestamptz) + $2 * (interval '1 hour')" } | ||||
|   let(:query_binds) { [Time.current, 3] } | ||||
|   let(:directory_path) { Dir.mktmpdir } | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do | ||||
|   subject { described_class.new(observation, directory_path) } | ||||
|   subject { described_class.new(observation, directory_path, connection) } | ||||
| 
 | ||||
|   let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) } | ||||
|   let(:connection) { ActiveRecord::Base.connection } | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
|   let(:query) { 'select 1' } | ||||
|   let(:directory_path) { Dir.mktmpdir } | ||||
|   let(:migration_version) { 20210422152437 } | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do | ||||
|   subject { described_class.new(observation, double("unused path")) } | ||||
|   subject { described_class.new(observation, double("unused path"), connection) } | ||||
| 
 | ||||
|   let(:observation) { Gitlab::Database::Migrations::Observation.new } | ||||
|   let(:connection) { ActiveRecord::Base.connection } | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
| 
 | ||||
|   def mock_pgss(enabled: true) | ||||
|     if enabled | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do | ||||
|   subject { described_class.new(observation, double('unused path')) } | ||||
|   subject { described_class.new(observation, double('unused path'), connection) } | ||||
| 
 | ||||
|   let(:observation) { Gitlab::Database::Migrations::Observation.new } | ||||
|   let(:connection) { ActiveRecord::Base.connection } | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
|   let(:query) { 'select pg_database_size(current_database())' } | ||||
| 
 | ||||
|   it 'records the size change' do | ||||
|  |  | |||
|  | @ -2,8 +2,9 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do | ||||
|   subject(:transaction_duration_observer) { described_class.new(observation, directory_path) } | ||||
|   subject(:transaction_duration_observer) { described_class.new(observation, directory_path, connection) } | ||||
| 
 | ||||
|   let(:connection) { ActiveRecord::Migration.connection } | ||||
|   let(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) } | ||||
|   let(:directory_path) { Dir.mktmpdir } | ||||
|   let(:log_file) { "#{directory_path}/#{migration_version}_#{migration_name}-transaction-duration.json" } | ||||
|  | @ -78,17 +79,17 @@ RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do | |||
|   end | ||||
| 
 | ||||
|   def run_real_transactions | ||||
|     ActiveRecord::Base.transaction do | ||||
|     ApplicationRecord.transaction do | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def run_sub_transactions | ||||
|     ActiveRecord::Base.transaction(requires_new: true) do | ||||
|     ApplicationRecord.transaction(requires_new: true) do | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def run_transaction | ||||
|     ActiveRecord::Base.connection_pool.with_connection do |connection| | ||||
|     ApplicationRecord.connection_pool.with_connection do |connection| | ||||
|       Gitlab::Database::SharedModel.using_connection(connection) do | ||||
|         Gitlab::Database::SharedModel.transaction do | ||||
|           Gitlab::Database::SharedModel.transaction(requires_new: true) do | ||||
|  |  | |||
|  | @ -76,7 +76,7 @@ RSpec.describe Gitlab::Database::Migrations::Runner do | |||
|       it 'runs the unapplied migrations in version order', :aggregate_failures do | ||||
|         up.run | ||||
| 
 | ||||
|         expect(migration_runs.map(&:dir)).to eq([:up, :up]) | ||||
|         expect(migration_runs.map(&:dir)).to match_array([:up, :up]) | ||||
|         expect(migration_runs.map(&:version_to_migrate)).to eq(pending_migrations.map(&:version)) | ||||
|       end | ||||
|     end | ||||
|  | @ -101,7 +101,7 @@ RSpec.describe Gitlab::Database::Migrations::Runner do | |||
|       it 'runs the applied migrations for the current branch in reverse order', :aggregate_failures do | ||||
|         down.run | ||||
| 
 | ||||
|         expect(migration_runs.map(&:dir)).to eq([:down, :down]) | ||||
|         expect(migration_runs.map(&:dir)).to match_array([:down, :down]) | ||||
|         expect(migration_runs.map(&:version_to_migrate)).to eq(applied_migrations_this_branch.reverse.map(&:version)) | ||||
|       end | ||||
|     end | ||||
|  |  | |||
|  | @ -0,0 +1,13 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe Gitlab::Saas do | ||||
|   describe '.canary_toggle_com_url' do | ||||
|     subject { described_class.canary_toggle_com_url } | ||||
| 
 | ||||
|     let(:next_url) { 'https://next.gitlab.com' } | ||||
| 
 | ||||
|     it { is_expected.to eq(next_url) } | ||||
|   end | ||||
| end | ||||
		Loading…
	
		Reference in New Issue