Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-12-17 00:15:24 +00:00
parent 040df42a88
commit b49ce524ed
36 changed files with 606 additions and 161 deletions

View File

@ -1,8 +1,10 @@
<script> <script>
import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; import { GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
import { __, s__ } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql'; import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.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 EnvironmentFolder from './new_environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue'; import EnableReviewAppModal from './enable_review_app_modal.vue';
@ -11,6 +13,7 @@ export default {
EnvironmentFolder, EnvironmentFolder,
EnableReviewAppModal, EnableReviewAppModal,
GlBadge, GlBadge,
GlPagination,
GlTab, GlTab,
GlTabs, GlTabs,
}, },
@ -20,6 +23,7 @@ export default {
variables() { variables() {
return { return {
scope: this.scope, scope: this.scope,
page: this.page ?? 1,
}; };
}, },
pollInterval() { pollInterval() {
@ -29,6 +33,9 @@ export default {
interval: { interval: {
query: pollIntervalQuery, query: pollIntervalQuery,
}, },
pageInfo: {
query: pageInfoQuery,
},
}, },
inject: ['newEnvironmentPath', 'canCreateEnvironment'], inject: ['newEnvironmentPath', 'canCreateEnvironment'],
i18n: { i18n: {
@ -36,11 +43,21 @@ export default {
reviewAppButtonLabel: s__('Environments|Enable review app'), reviewAppButtonLabel: s__('Environments|Enable review app'),
available: __('Available'), available: __('Available'),
stopped: __('Stopped'), 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', modalId: 'enable-review-app-info',
data() { data() {
const scope = new URLSearchParams(window.location.search).get('scope') || 'available'; const { page = '1', scope = 'available' } = queryToObject(window.location.search);
return { interval: undefined, scope, isReviewAppModalVisible: false }; return {
interval: undefined,
isReviewAppModalVisible: false,
page: parseInt(page, 10),
scope,
};
}, },
computed: { computed: {
canSetupReviewApp() { canSetupReviewApp() {
@ -82,6 +99,19 @@ export default {
stoppedCount() { stoppedCount() {
return this.environmentApp?.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: { methods: {
showReviewAppModal() { showReviewAppModal() {
@ -89,12 +119,30 @@ export default {
}, },
setScope(scope) { setScope(scope) {
this.scope = 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.$apollo.queries.environmentApp.stopPolling();
this.$nextTick(() => { this.$nextTick(() => {
if (this.interval) { if (this.interval) {
this.$apollo.queries.environmentApp.startPolling(this.interval); this.$apollo.queries.environmentApp.startPolling(this.interval);
} else { } 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" class="gl-mb-3"
:nested-environment="folder" :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> </div>
</template> </template>

View File

@ -1,6 +1,7 @@
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import environmentApp from './queries/environment_app.query.graphql'; import environmentApp from './queries/environment_app.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
import { resolvers } from './resolvers'; import { resolvers } from './resolvers';
import typeDefs from './typedefs.graphql'; import typeDefs from './typedefs.graphql';
@ -19,6 +20,19 @@ export const apolloProvider = (endpoint) => {
stoppedCount: 0, stoppedCount: 0,
}, },
}); });
cache.writeQuery({
query: pageInfoQuery,
data: {
pageInfo: {
total: 0,
perPage: 20,
nextPage: 0,
previousPage: 0,
__typename: 'LocalPageInfo',
},
},
});
return new VueApollo({ return new VueApollo({
defaultClient, defaultClient,
}); });

View File

@ -1,5 +1,5 @@
query getEnvironmentApp($scope: String) { query getEnvironmentApp($page: Int, $scope: String) {
environmentApp(scope: $scope) @client { environmentApp(page: $page, scope: $scope) @client {
availableCount availableCount
stoppedCount stoppedCount
environments environments

View File

@ -0,0 +1,8 @@
query getPageInfo {
pageInfo @client {
total
perPage
nextPage
previousPage
}
}

View File

@ -1,9 +1,15 @@
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale'; 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 pollIntervalQuery from './queries/poll_interval.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql'; import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql'; import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
const buildErrors = (errors = []) => ({ const buildErrors = (errors = []) => ({
errors, errors,
@ -21,9 +27,11 @@ const mapEnvironment = (env) => ({
export const resolvers = (endpoint) => ({ export const resolvers = (endpoint) => ({
Query: { Query: {
environmentApp(_context, { scope }, { cache }) { environmentApp(_context, { page, scope }, { cache }) {
return axios.get(endpoint, { params: { nested: true, scope } }).then((res) => { return axios.get(endpoint, { params: { nested: true, page, scope } }).then((res) => {
const interval = res.headers['poll-interval']; const headers = normalizeHeaders(res.headers);
const interval = headers['POLL-INTERVAL'];
const pageInfo = { ...parseIntPagination(headers), __typename: 'LocalPageInfo' };
if (interval) { if (interval) {
cache.writeQuery({ query: pollIntervalQuery, data: { interval: parseFloat(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: pollIntervalQuery, data: { interval: undefined } });
} }
cache.writeQuery({
query: pageInfoQuery,
data: { pageInfo },
});
return { return {
availableCount: res.data.available_count, availableCount: res.data.available_count,
environments: res.data.environments.map(mapNestedEnvironment), environments: res.data.environments.map(mapNestedEnvironment),

View File

@ -55,10 +55,18 @@ type LocalErrors {
errors: [String!]! errors: [String!]!
} }
type LocalPageInfo {
total: Int!
perPage: Int!
nextPage: Int!
previousPage: Int!
}
extend type Query { extend type Query {
environmentApp: LocalEnvironmentApp environmentApp(page: Int, scope: String): LocalEnvironmentApp
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
environmentToDelete: LocalEnvironment environmentToDelete: LocalEnvironment
pageInfo: LocalPageInfo
environmentToRollback: LocalEnvironment environmentToRollback: LocalEnvironment
isLastDeployment: Boolean isLastDeployment: Boolean
} }

View File

@ -17,6 +17,7 @@ export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip'; export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show'; export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide'; export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
export const BV_COLLAPSE_STATE = 'bv::collapse::state';
export const DEFAULT_TH_CLASSES = export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!'; 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';

View File

@ -45,7 +45,7 @@
= render 'shared/user_dropdown_instance_review' = render 'shared/user_dropdown_instance_review'
- if Gitlab.com_but_not_canary? - if Gitlab.com_but_not_canary?
%li.d-md-none %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) - if current_user_menu?(:sign_out)
%li.divider %li.divider

View File

@ -15,7 +15,7 @@
%span.logo-text.d-none.d-lg-block.gl-ml-3 %span.logo-text.d-none.d-lg-block.gl-ml-3
= logo_text = logo_text
- if Gitlab.com_and_canary? - 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 %span.gl-badge.gl-bg-green-500.gl-text-white.gl-rounded-pill.gl-font-weight-bold.gl-py-1
= _('Next') = _('Next')

View File

@ -20,4 +20,4 @@
= render 'shared/user_dropdown_instance_review' = render 'shared/user_dropdown_instance_review'
- if Gitlab.com_but_not_canary? - if Gitlab.com_but_not_canary?
%li %li
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/" = link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url

View File

@ -10,9 +10,7 @@
.card-header .card-header
%strong %strong
= s_('PrometheusService|Custom metrics') = s_('PrometheusService|Custom metrics')
-# haml-lint:disable NoPlainNodes = gl_badge_tag 0, nil, class: 'js-custom-monitored-count'
%span.badge.badge-pill.js-custom-monitored-count 0
-# haml-lint:enable NoPlainNodes
= 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' } = 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 .card-body
.flash-container.hidden .flash-container.hidden

View File

@ -12,7 +12,7 @@
.card-header .card-header
%strong %strong
= s_('PrometheusService|Common metrics') = s_('PrometheusService|Common metrics')
%span.badge.badge-pill.js-monitored-count 0 = gl_badge_tag 0, nil, class: 'js-monitored-count'
.card-body .card-body
.loading-metrics.js-loading-metrics .loading-metrics.js-loading-metrics
%p.m-3 %p.m-3
@ -28,7 +28,7 @@
= sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' ) = 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' ) = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' )
= s_('PrometheusService|Missing environment variable') = 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 .card-body.hidden
.flash-container .flash-container
.flash-notice .flash-notice

View File

@ -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_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 = gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do
= _('Open') = _('Open')
%span{ class: count_badge_classes } = gl_tab_counter_badge counts[:opened], { class: count_badge_classes }
= counts[:opened]
= gl_tab_link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc'), { item_active: params[:state] == 'closed' } do = gl_tab_link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc'), { item_active: params[:state] == 'closed' } do
= _('Closed') = _('Closed')
%span{ class: count_badge_classes } = gl_tab_counter_badge counts[:closed], { class: count_badge_classes }
= counts[:closed]
= gl_tab_link_to milestones_filter_path(state: 'all', sort: 'due_date_desc'), { item_active: params[:state] == 'all' } do = gl_tab_link_to milestones_filter_path(state: 'all', sort: 'due_date_desc'), { item_active: params[:state] == 'all' } do
= _('All') = _('All')
%span{ class: count_badge_classes } = gl_tab_counter_badge counts[:all], { class: count_badge_classes }
= counts[:all]

View File

@ -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

View File

@ -0,0 +1 @@
9bbd4c3e396e0de130418e705a370ce629ca507c82fa2ff5bbf085cdf01c2ff3

View File

@ -35,9 +35,9 @@ verification methods:
| Git | Project Snippets | Geo with Gitaly | Gitaly Checksum | | Git | Project Snippets | Geo with Gitaly | Gitaly Checksum |
| Git | Personal Snippets | Geo with Gitaly | Gitaly Checksum | | Git | Personal Snippets | Geo with Gitaly | Gitaly Checksum |
| Git | Group wiki repository | Geo with Gitaly | _Not implemented_ | | 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 | 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 | 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 _(file system)_ | Geo with API | _Not implemented_ |
| Blobs | CI job artifacts _(object storage)_ | Geo with API/Managed (*2*) | _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 repository](../../../user/project/repository/) | **Yes** (10.2) | **Yes** (10.7) | No | |
|[Project wiki repository](../../../user/project/wiki/) | **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. | |[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. | |[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` 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 | | |[Personal snippets](../../../user/snippets.md) | **Yes** (10.2) | **Yes** (10.2) | No | |
|[Project 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. | |[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. |

View File

@ -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 When you are editing your `.gitlab-ci.yml` file, you can validate it with the
[CI Lint](../lint.md) tool. [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 ## Keywords
A GitLab CI/CD pipeline configuration includes: A GitLab CI/CD pipeline configuration includes:

View File

@ -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 not use **above** when referring to versions of the product. Use [**later**](#later) instead.
- Do: In GitLab 14.4 and later... Use:
- Do not: In GitLab 14.4 and above...
- Do not: In GitLab 14.4 and higher... - In GitLab 14.4 and later...
Instead of:
- In GitLab 14.4 and above...
- In GitLab 14.4 and higher...
## access level ## 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). An **administrator** is not a [role](#roles) or [permission](#permissions).
- Do: To do this thing, you must be an administrator. Use:
- Do: To do this thing, you must have the administrator access level.
- Do not: To do this thing, you must have the Admin role. - 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 ## 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 ## 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. Use:
- Do not: This feature allows you to create a pipeline.
- 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. 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). [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**. Don't use a descriptor with **button**.
- Do: Select **Run pipelines**. Use:
- Do not: Select the **Run pipelines** button.
- Select **Run pipelines**.
Instead of:
- Select the **Run pipelines** button.
## cannot, can not ## 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 - 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**. 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: - To describe a situation where the Developer role is the minimum required:
- Do: at least the Developer role - Use: at least the Developer role
- Do not: the Developer role or higher - 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. 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. Use **earlier** when talking about version numbers.
- Do: In GitLab 14.1 and earlier. Use:
- Do not: In GitLab 14.1 and lower.
- In GitLab 14.1 and earlier.
Instead of:
- In GitLab 14.1 and lower.
## easily ## easily
@ -254,8 +280,13 @@ Use lowercase for **epic board**.
Try to avoid **etc.**. Be as specific as you can. Do not use Try to avoid **etc.**. Be as specific as you can. Do not use
[**and so on**](#and-so-on) as a replacement. [**and so on**](#and-so-on) as a replacement.
- Do: You can update objects, like merge requests and issues. Use:
- Do not: You can update objects, like merge requests, issues, etc.
- You can update objects, like merge requests and issues.
Instead of:
- You can update objects, like merge requests, issues, etc.
## expand ## 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**. Use **box** instead of **field** or **text box**.
- Do: In the **Variable name** box, enter `my text`. Use:
- Do not: In the **Variable name** field, enter `my text`.
- 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 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: 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 - 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**. 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: - To describe a situation where the Guest role is the minimum required:
- Do: at least the Guest role - Use: at least the Guest role
- Do not: the Guest role or higher - 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. 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 not use **higher** when talking about version numbers.
- Do: In GitLab 14.4 and later... Use:
- Do not: In GitLab 14.4 and higher...
- Do not: In GitLab 14.4 and above... - In GitLab 14.4 and later...
Instead of:
- In GitLab 14.4 and higher...
- In GitLab 14.4 and above...
## hit ## hit
Don't use **hit** to mean **press**. Don't use **hit** to mean **press**.
- Do: Press **ENTER**. Use:
- Do not: Hit the **ENTER** button.
- Press **ENTER**.
Instead of:
- Hit the **ENTER** button.
## I ## I
@ -395,9 +441,14 @@ Do not use:
Use **later** when talking about version numbers. Use **later** when talking about version numbers.
- Do: In GitLab 14.1 and later... Use:
- Do not: In GitLab 14.1 and higher...
- Do not: In GitLab 14.1 and above... - In GitLab 14.1 and later...
Instead of:
- In GitLab 14.1 and higher...
- In GitLab 14.1 and above...
## list ## 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 not use **lower** when talking about version numbers.
- Do: In GitLab 14.1 and earlier. Use:
- Do not: In GitLab 14.1 and lower.
- In GitLab 14.1 and earlier.
Instead of:
- In GitLab 14.1 and lower.
## Maintainer ## 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 - 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**. 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: - To describe a situation where the Maintainer role is the minimum required:
- Do: at least the Maintainer role - Use: at least the Maintainer role
- Do not: the Maintainer role or higher - 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. 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**. 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. Use:
- Do not: You need to set the variable.
- 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 **Should** is acceptable for recommended actions or items, or in cases where an event may not
happen. For example: happen. For example:
@ -483,22 +545,37 @@ happen. For example:
Do not use **note that** because it's wordy. Do not use **note that** because it's wordy.
- Do: You can change the settings. Use:
- Do not: Note that you can change the settings.
- You can change the settings.
Instead of:
- Note that you can change the settings.
## on ## on
When documenting how to select high-level UI elements, use the word **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...` - Do not: `From the left sidebar...` or `In the left sidebar...`
## once ## once
The word **once** means **one time**. Don't use it to mean **after** or **when**. The word **once** means **one time**. Don't use it to mean **after** or **when**.
- Do: When the process is complete... Use:
- Do not: Once the process is complete...
- When the process is complete...
Instead of:
- Once the process is complete...
## only ## 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 - 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**. 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: - To describe a situation where the Reporter role is the minimum required:
- Do: at least the Reporter role - Use: at least the Reporter role
- Do not: the Reporter role or higher - 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. 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 not use **(s)** to make a word optionally plural. It can slow down comprehension. For example:
- Do: Select the jobs you want. Use:
- Do not: Select the job(s) you want.
- 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. 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 We often think of expandable/collapsible areas as **sections**. When you refer to expanding
or collapsing a section, don't include the word **section**. 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. - Do not: Expand the **Auto DevOps** section.
## select ## 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**. 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. Use:
- Do not: Since you have the Developer role, you can delete the widget.
- 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 ## 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 not use **that** when describing a noun. For example:
- Do: The file you save... Use:
- Do not: The file **that** you save...
- The file you save...
Instead of:
- The file **that** you save...
See also [this, these, that, those](#this-these-that-those). 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. Try to avoid **there is** and **there are**. These phrases hide the subject.
- Do: The bucket has holes. Use:
- Do not: There are holes in the bucket.
- The bucket has holes.
Instead of:
- There are holes in the bucket.
## they ## they
@ -697,17 +799,17 @@ a gender-neutral pronoun.
Always follow these words with a noun. For example: Always follow these words with a noun. For example:
- Do: **This setting** improves performance. - Use: **This setting** improves performance.
- Do not: **This** improves performance. - Instead of: **This** improves performance.
- Do: **These pants** are the best. - Use: **These pants** are the best.
- Do not: **These** are the best. - Instead of: **These** are the best.
- Do: **That droid** is the one you are looking for. - Use: **That droid** is the one you are looking for.
- Do not: **That** 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.**) - Use: **Those settings** need to be configured. (Or even better, **Configure those settings.**)
- Do not: **Those** need to be configured. - Instead of: **Those** need to be configured.
## to-do item ## 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**. When possible, address the reader directly, instead of calling them **users**.
Use the [second person](#you-your-yours), **you**, instead. Use the [second person](#you-your-yours), **you**, instead.
- Do: You can configure a pipeline. Use:
- Do not: Users can configure a pipeline.
- You can configure a pipeline.
Instead of:
- Users can configure a pipeline.
## utilize ## 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. 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. Use:
- Do not: We created a feature for you to add widgets.
- 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)) 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). 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. 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. Use:
- Do not: Users can configure a pipeline.
- You can configure a pipeline.
Instead of:
- Users can configure a pipeline.
<!-- vale on --> <!-- vale on -->
<!-- markdownlint-enable --> <!-- markdownlint-enable -->

View File

@ -492,10 +492,13 @@ image::screenshot.png[block image,800,450]
Press image:reload.svg[reload,16,opts=interactive] to reload the page. Press image:reload.svg[reload,16,opts=interactive] to reload the page.
video::movie.mp4[width=640,start=60,end=140,options=autoplay] 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 ### Breaks

View File

@ -344,6 +344,11 @@ Here is an example of milestones with no releases, one release, and two releases
![Milestones with and without Release associations](img/milestone_list_with_releases_v12_5.png) ![Milestones with and without Release associations](img/milestone_list_with_releases_v12_5.png)
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 ## Get notified when a release is created
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26001) in GitLab 12.4. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/26001) in GitLab 12.4.

View File

@ -143,7 +143,9 @@ you can choose from:
![Filter MRs by their environment](img/filtering_merge_requests_by_environment_v14_6.png) ![Filter MRs by their environment](img/filtering_merge_requests_by_environment_v14_6.png)
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 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"`): both a date and time (`"YYYY-MM-DD HH:MM"`):

View File

@ -14,11 +14,11 @@ module Gitlab
@result_dir = result_dir @result_dir = result_dir
end end
def observe(version:, name:, &block) def observe(version:, name:, connection:, &block)
observation = Observation.new(version, name) observation = Observation.new(version, name)
observation.success = true 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 exception = nil

View File

@ -7,8 +7,8 @@ module Gitlab
class MigrationObserver class MigrationObserver
attr_reader :connection, :observation, :output_dir attr_reader :connection, :observation, :output_dir
def initialize(observation, output_dir) def initialize(observation, output_dir, connection)
@connection = ActiveRecord::Base.connection @connection = connection
@observation = observation @observation = observation
@output_dir = output_dir @output_dir = output_dir
end end

View File

@ -69,7 +69,7 @@ module Gitlab
instrumentation = Instrumentation.new(result_dir: result_dir) instrumentation = Instrumentation.new(result_dir: result_dir)
sorted_migrations.each do |migration| 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 ActiveRecord::Migrator.new(direction, migration_context.migrations, migration_context.schema_migration, migration.version).run
end end
end end

View File

@ -13,6 +13,10 @@ module Gitlab
'https://staging.gitlab.com' 'https://staging.gitlab.com'
end end
def self.canary_toggle_com_url
'https://next.gitlab.com'
end
def self.subdomain_regex def self.subdomain_regex
%r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}.freeze
end end

View File

@ -16406,6 +16406,9 @@ msgstr ""
msgid "Go to next page" msgid "Go to next page"
msgstr "" msgstr ""
msgid "Go to page %{page}"
msgstr ""
msgid "Go to parent" msgid "Go to parent"
msgstr "" msgstr ""

View File

@ -5,6 +5,7 @@ import environmentToRollback from '~/environments/graphql/queries/environment_to
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql'; import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.query.graphql'; 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 { TEST_HOST } from 'helpers/test_constants';
import { import {
environmentsApp, environmentsApp,
@ -37,9 +38,11 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should fetch environments and map them to frontend data', async () => { it('should fetch environments and map them to frontend data', async () => {
const cache = { writeQuery: jest.fn() }; const cache = { writeQuery: jest.fn() };
const scope = 'available'; 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(app).toEqual(resolvedEnvironmentsApp);
expect(cache.writeQuery).toHaveBeenCalledWith({ expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery, query: pollIntervalQuery,
@ -49,14 +52,70 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should set the poll interval when there is one', async () => { it('should set the poll interval when there is one', async () => {
const cache = { writeQuery: jest.fn() }; const cache = { writeQuery: jest.fn() };
const scope = 'stopped'; const scope = 'stopped';
const interval = 3000;
mock mock
.onGet(ENDPOINT, { params: { nested: true, scope } }) .onGet(ENDPOINT, { params: { nested: true, scope, page: 1 } })
.reply(200, environmentsApp, { 'poll-interval': 3000 }); .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({ expect(cache.writeQuery).toHaveBeenCalledWith({
query: pollIntervalQuery, 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,
},
},
}); });
}); });
}); });

View File

@ -1,9 +1,11 @@
import Vue, { nextTick } from 'vue'; import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlPagination } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; 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 EnvironmentsApp from '~/environments/components/new_environments_app.vue';
import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue'; import EnvironmentsFolder from '~/environments/components/new_environment_folder.vue';
import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data'; import { resolvedEnvironmentsApp, resolvedFolder } from './graphql/mock_data';
@ -14,12 +16,14 @@ describe('~/environments/components/new_environments_app.vue', () => {
let wrapper; let wrapper;
let environmentAppMock; let environmentAppMock;
let environmentFolderMock; let environmentFolderMock;
let paginationMock;
const createApolloProvider = () => { const createApolloProvider = () => {
const mockResolvers = { const mockResolvers = {
Query: { Query: {
environmentApp: environmentAppMock, environmentApp: environmentAppMock,
folder: environmentFolderMock, folder: environmentFolderMock,
pageInfo: paginationMock,
}, },
}; };
@ -37,9 +41,23 @@ describe('~/environments/components/new_environments_app.vue', () => {
apolloProvider, 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); environmentAppMock.mockReturnValue(environmentsApp);
environmentFolderMock.mockReturnValue(folder); environmentFolderMock.mockReturnValue(folder);
paginationMock.mockReturnValue(pageInfo);
const apolloProvider = createApolloProvider(); const apolloProvider = createApolloProvider();
wrapper = createWrapper({ apolloProvider, provide }); wrapper = createWrapper({ apolloProvider, provide });
@ -50,6 +68,7 @@ describe('~/environments/components/new_environments_app.vue', () => {
beforeEach(() => { beforeEach(() => {
environmentAppMock = jest.fn(); environmentAppMock = jest.fn();
environmentFolderMock = jest.fn(); environmentFolderMock = jest.fn();
paginationMock = jest.fn();
}); });
afterEach(() => { afterEach(() => {
@ -118,42 +137,135 @@ describe('~/environments/components/new_environments_app.vue', () => {
expect(button.exists()).toBe(false); expect(button.exists()).toBe(false);
}); });
it('should show tabs for available and stopped environmets', async () => { describe('tabs', () => {
await createWrapperWithMocked({ it('should show tabs for available and stopped environmets', async () => {
environmentsApp: resolvedEnvironmentsApp, await createWrapperWithMocked({
folder: resolvedFolder, 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')); stopped.trigger('click');
expect(available.text()).toContain(resolvedEnvironmentsApp.availableCount);
expect(stopped.text()).toContain(__('Stopped')); await nextTick();
expect(stopped.text()).toContain(resolvedEnvironmentsApp.stoppedCount); 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 () => { describe('pagination', () => {
environmentAppMock.mockReturnValue(resolvedEnvironmentsApp); it('should sync page from query params on load', async () => {
environmentFolderMock.mockReturnValue(resolvedFolder); await createWrapperWithMocked({
const apolloProvider = createApolloProvider(); environmentsApp: resolvedEnvironmentsApp,
wrapper = createWrapper({ apolloProvider }); folder: resolvedFolder,
});
await waitForPromises(); expect(wrapper.findComponent(GlPagination).props('value')).toBe(2);
await nextTick();
const stopped = wrapper.findByRole('tab', {
name: `${__('Stopped')} ${resolvedEnvironmentsApp.stoppedCount}`,
}); });
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(); next.trigger('click');
await waitForPromises();
expect(environmentAppMock).toHaveBeenCalledWith( await nextTick();
expect.anything(), await waitForPromises();
{ scope: 'stopped' },
expect.anything(), expect(environmentAppMock).toHaveBeenCalledWith(
expect.anything(), 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');
});
}); });
}); });

View File

@ -3,6 +3,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Instrumentation do RSpec.describe Gitlab::Database::Migrations::Instrumentation do
let(:result_dir) { Dir.mktmpdir } let(:result_dir) { Dir.mktmpdir }
let(:connection) { ActiveRecord::Migration.connection }
after do after do
FileUtils.rm_rf(result_dir) FileUtils.rm_rf(result_dir)
@ -14,11 +15,11 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
let(:migration_version) { '12345' } let(:migration_version) { '12345' }
it 'executes the given block' do 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 end
context 'behavior with observers' do 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) } 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 it 'instantiates observer with observation' do
expect(Gitlab::Database::Migrations::Observers::MigrationObserver) expect(Gitlab::Database::Migrations::Observers::MigrationObserver)
.to receive(:new) .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) .and_return(observer)
subject subject
@ -63,7 +64,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
end end
context 'on successful execution' do 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 it 'records walltime' do
expect(subject.walltime).not_to be_nil expect(subject.walltime).not_to be_nil
@ -83,7 +84,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
end end
context 'upon failure' do 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 it 'raises the exception' do
expect { subject }.to raise_error(/something went wrong/) expect { subject }.to raise_error(/something went wrong/)
@ -93,7 +94,7 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
subject { instance.observations.first } subject { instance.observations.first }
before do 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 rescue StandardError
# ignore # ignore
end end
@ -125,8 +126,8 @@ RSpec.describe Gitlab::Database::Migrations::Instrumentation do
let(:migration2) { double('migration2', call: nil) } let(:migration2) { double('migration2', call: nil) }
it 'records observations for all migrations' do it 'records observations for all migrations' do
subject.observe(version: migration_version, name: migration_name) {} subject.observe(version: migration_version, name: migration_name, connection: connection) {}
subject.observe(version: migration_version, name: migration_name) { raise 'something went wrong' } rescue nil subject.observe(version: migration_version, name: migration_name, connection: connection) { raise 'something went wrong' } rescue nil
expect(subject.observations.size).to eq(2) expect(subject.observations.size).to eq(2)
end end

View File

@ -2,10 +2,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryDetails do 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(: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) { "select date_trunc('day', $1::timestamptz) + $2 * (interval '1 hour')" }
let(:query_binds) { [Time.current, 3] } let(:query_binds) { [Time.current, 3] }
let(:directory_path) { Dir.mktmpdir } let(:directory_path) { Dir.mktmpdir }

View File

@ -2,10 +2,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryLog do 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(: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(:query) { 'select 1' }
let(:directory_path) { Dir.mktmpdir } let(:directory_path) { Dir.mktmpdir }
let(:migration_version) { 20210422152437 } let(:migration_version) { 20210422152437 }

View File

@ -2,10 +2,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::QueryStatistics do 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(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:connection) { ActiveRecord::Base.connection } let(:connection) { ActiveRecord::Migration.connection }
def mock_pgss(enabled: true) def mock_pgss(enabled: true)
if enabled if enabled

View File

@ -2,10 +2,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::TotalDatabaseSizeChange do 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(:observation) { Gitlab::Database::Migrations::Observation.new }
let(:connection) { ActiveRecord::Base.connection } let(:connection) { ActiveRecord::Migration.connection }
let(:query) { 'select pg_database_size(current_database())' } let(:query) { 'select pg_database_size(current_database())' }
it 'records the size change' do it 'records the size change' do

View File

@ -2,8 +2,9 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::Database::Migrations::Observers::TransactionDuration do 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(:observation) { Gitlab::Database::Migrations::Observation.new(migration_version, migration_name) }
let(:directory_path) { Dir.mktmpdir } let(:directory_path) { Dir.mktmpdir }
let(:log_file) { "#{directory_path}/#{migration_version}_#{migration_name}-transaction-duration.json" } 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 end
def run_real_transactions def run_real_transactions
ActiveRecord::Base.transaction do ApplicationRecord.transaction do
end end
end end
def run_sub_transactions def run_sub_transactions
ActiveRecord::Base.transaction(requires_new: true) do ApplicationRecord.transaction(requires_new: true) do
end end
end end
def run_transaction 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.using_connection(connection) do
Gitlab::Database::SharedModel.transaction do Gitlab::Database::SharedModel.transaction do
Gitlab::Database::SharedModel.transaction(requires_new: true) do Gitlab::Database::SharedModel.transaction(requires_new: true) do

View File

@ -76,7 +76,7 @@ RSpec.describe Gitlab::Database::Migrations::Runner do
it 'runs the unapplied migrations in version order', :aggregate_failures do it 'runs the unapplied migrations in version order', :aggregate_failures do
up.run 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)) expect(migration_runs.map(&:version_to_migrate)).to eq(pending_migrations.map(&:version))
end end
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 it 'runs the applied migrations for the current branch in reverse order', :aggregate_failures do
down.run 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)) expect(migration_runs.map(&:version_to_migrate)).to eq(applied_migrations_this_branch.reverse.map(&:version))
end end
end end

View File

@ -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