提交 846a84f2 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 8e42824b
import Tracking from '~/tracking'; import Tracking from '~/tracking';
function assembleDesignPayload(payloadArr) {
return {
value: {
'internal-object-refrerer': payloadArr[0],
'design-collection-owner': payloadArr[1],
'design-version-number': payloadArr[2],
'design-is-current-version': payloadArr[3],
},
};
}
// Tracking Constants // Tracking Constants
const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0';
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
const DESIGN_TRACKING_EVENT_NAME = 'view_design';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView( export function trackDesignDetailView(
...@@ -21,8 +12,16 @@ export function trackDesignDetailView( ...@@ -21,8 +12,16 @@ export function trackDesignDetailView(
designVersion = 1, designVersion = 1,
latestVersion = false, latestVersion = false,
) { ) {
Tracking.event(DESIGN_TRACKING_PAGE_NAME, 'design_viewed', { Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, {
label: 'design_viewed', label: DESIGN_TRACKING_EVENT_NAME,
...assembleDesignPayload([referer, owner, designVersion, latestVersion]), context: {
schema: DESIGN_TRACKING_CONTEXT_SCHEMA,
data: {
'design-version-number': designVersion,
'design-is-current-version': latestVersion,
'internal-object-referrer': referer,
'design-collection-owner': owner,
},
},
}); });
} }
import Tracking from '~/tracking'; import Tracking from '~/tracking';
function assembleDesignPayload(payloadArr) {
return {
value: {
'internal-object-refrerer': payloadArr[0],
'design-collection-owner': payloadArr[1],
'design-version-number': payloadArr[2],
'design-is-current-version': payloadArr[3],
},
};
}
// Tracking Constants // Tracking Constants
const DESIGN_TRACKING_CONTEXT_SCHEMA = 'iglu:com.gitlab/design_management_context/jsonschema/1-0-0';
const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design'; const DESIGN_TRACKING_PAGE_NAME = 'projects:issues:design';
const DESIGN_TRACKING_EVENT_NAME = 'view_design';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function trackDesignDetailView( export function trackDesignDetailView(
...@@ -21,8 +12,16 @@ export function trackDesignDetailView( ...@@ -21,8 +12,16 @@ export function trackDesignDetailView(
designVersion = 1, designVersion = 1,
latestVersion = false, latestVersion = false,
) { ) {
Tracking.event(DESIGN_TRACKING_PAGE_NAME, 'design_viewed', { Tracking.event(DESIGN_TRACKING_PAGE_NAME, DESIGN_TRACKING_EVENT_NAME, {
label: 'design_viewed', label: DESIGN_TRACKING_EVENT_NAME,
...assembleDesignPayload([referer, owner, designVersion, latestVersion]), context: {
schema: DESIGN_TRACKING_CONTEXT_SCHEMA,
data: {
'design-version-number': designVersion,
'design-is-current-version': latestVersion,
'internal-object-referrer': referer,
'design-collection-owner': owner,
},
},
}); });
} }
<script>
import { GlNewDropdownHeader, GlNewDropdownItem, GlBadge, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
export default {
name: 'RefResultsSection',
components: {
GlNewDropdownHeader,
GlNewDropdownItem,
GlBadge,
GlIcon,
},
props: {
sectionTitle: {
type: String,
required: true,
},
totalCount: {
type: Number,
required: true,
},
/**
* An array of object that have the following properties:
*
* - name (String, required): The name of the ref that will be displayed
* - value (String, optional): The value that will be selected when the ref
* is selected. If not provided, `name` will be used as the value.
* For example, commits use the short SHA for `name`
* and long SHA for `value`.
* - subtitle (String, optional): Text to render underneath the name.
* For example, used to render the commit's title underneath its SHA.
* - default (Boolean, optional): Whether or not to render a "default"
* indicator next to the item. Used to indicate
* the project's default branch.
*
*/
items: {
type: Array,
required: true,
validator: items => Array.isArray(items) && items.every(item => item.name),
},
/**
* The currently selected ref.
* Used to render a check mark by the selected item.
* */
selectedRef: {
type: String,
required: false,
default: '',
},
/**
* An error object that indicates that an error
* occurred while fetching items for this section
*/
error: {
type: Error,
required: false,
default: null,
},
/** The message to display if an error occurs */
errorMessage: {
type: String,
required: false,
default: '',
},
},
computed: {
totalCountText() {
return this.totalCount > 999 ? s__('TotalRefCountIndicator|1000+') : `${this.totalCount}`;
},
},
methods: {
showCheck(item) {
return item.name === this.selectedRef || item.value === this.selectedRef;
},
},
};
</script>
<template>
<div>
<gl-new-dropdown-header>
<div class="gl-display-flex align-items-center" data-testid="section-header">
<span class="gl-mr-2 gl-mb-1">{{ sectionTitle }}</span>
<gl-badge variant="neutral">{{ totalCountText }}</gl-badge>
</div>
</gl-new-dropdown-header>
<template v-if="error">
<div class="gl-display-flex align-items-start text-danger gl-ml-4 gl-mr-4 gl-mb-3">
<gl-icon name="error" class="gl-mr-2 gl-mt-2 gl-flex-shrink-0" />
<span>{{ errorMessage }}</span>
</div>
</template>
<template v-else>
<gl-new-dropdown-item
v-for="item in items"
:key="item.name"
@click="$emit('selected', item.value || item.name)"
>
<div class="gl-display-flex align-items-start">
<gl-icon
name="mobile-issue-close"
class="gl-mr-2 gl-flex-shrink-0"
:class="{ 'gl-visibility-hidden': !showCheck(item) }"
/>
<div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column">
<span class="gl-font-monospace">{{ item.name }}</span>
<span class="gl-text-gray-600">{{ item.subtitle }}</span>
</div>
<gl-badge v-if="item.default" size="sm" variant="info">{{
s__('DefaultBranchLabel|default')
}}</gl-badge>
</div>
</gl-new-dropdown-item>
</template>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createStore from '../stores';
import { SEARCH_DEBOUNCE_MS, DEFAULT_I18N } from '../constants';
import RefResultsSection from './ref_results_section.vue';
export default {
name: 'RefSelector',
store: createStore(),
components: {
GlNewDropdown,
GlNewDropdownDivider,
GlNewDropdownHeader,
GlSearchBoxByType,
GlSprintf,
GlIcon,
GlLoadingIcon,
RefResultsSection,
},
props: {
value: {
type: String,
required: false,
default: '',
},
projectId: {
type: String,
required: true,
},
translations: {
type: Object,
required: false,
default: () => ({}),
},
},
data() {
return {
query: '',
};
},
computed: {
...mapState({
matches: state => state.matches,
lastQuery: state => state.query,
selectedRef: state => state.selectedRef,
}),
...mapGetters(['isLoading', 'isQueryPossiblyASha']),
i18n() {
return {
...DEFAULT_I18N,
...this.translations,
};
},
showBranchesSection() {
return Boolean(this.matches.branches.totalCount > 0 || this.matches.branches.error);
},
showTagsSection() {
return Boolean(this.matches.tags.totalCount > 0 || this.matches.tags.error);
},
showCommitsSection() {
return Boolean(this.matches.commits.totalCount > 0 || this.matches.commits.error);
},
showNoResults() {
return !this.showBranchesSection && !this.showTagsSection && !this.showCommitsSection;
},
},
created() {
this.setProjectId(this.projectId);
this.search(this.query);
},
methods: {
...mapActions(['setProjectId', 'setSelectedRef', 'search']),
focusSearchBox() {
this.$refs.searchBox.$el.querySelector('input').focus();
},
onSearchBoxInput: debounce(function search() {
this.search(this.query);
}, SEARCH_DEBOUNCE_MS),
selectRef(ref) {
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
},
},
};
</script>
<template>
<gl-new-dropdown class="ref-selector" @shown="focusSearchBox">
<template slot="button-content">
<span class="gl-flex-grow-1 gl-ml-2 gl-text-gray-600" data-testid="button-content">
<span v-if="selectedRef" class="gl-font-monospace">{{ selectedRef }}</span>
<span v-else>{{ i18n.noRefSelected }}</span>
</span>
<gl-icon name="chevron-down" />
</template>
<div class="gl-display-flex gl-flex-direction-column ref-selector-dropdown-content">
<gl-new-dropdown-header>
<span class="gl-text-center gl-display-block">{{ i18n.dropdownHeader }}</span>
</gl-new-dropdown-header>
<gl-new-dropdown-divider />
<gl-search-box-by-type
ref="searchBox"
v-model.trim="query"
class="gl-m-3"
:placeholder="i18n.searchPlaceholder"
@input="onSearchBoxInput"
/>
<div class="gl-flex-grow-1 gl-overflow-y-auto">
<gl-loading-icon v-if="isLoading" size="lg" class="gl-my-3" />
<div
v-else-if="showNoResults"
class="gl-text-center gl-mx-3 gl-py-3"
data-testid="no-results"
>
<gl-sprintf v-if="lastQuery" :message="i18n.noResultsWithQuery">
<template #query>
<b class="gl-word-break-all">{{ lastQuery }}</b>
</template>
</gl-sprintf>
<span v-else>{{ i18n.noResults }}</span>
</div>
<template v-else>
<template v-if="showBranchesSection">
<ref-results-section
:section-title="i18n.branches"
:total-count="matches.branches.totalCount"
:items="matches.branches.list"
:selected-ref="selectedRef"
:error="matches.branches.error"
:error-message="i18n.branchesErrorMessage"
data-testid="branches-section"
@selected="selectRef($event)"
/>
<gl-new-dropdown-divider v-if="showTagsSection || showCommitsSection" />
</template>
<template v-if="showTagsSection">
<ref-results-section
:section-title="i18n.tags"
:total-count="matches.tags.totalCount"
:items="matches.tags.list"
:selected-ref="selectedRef"
:error="matches.tags.error"
:error-message="i18n.tagsErrorMessage"
data-testid="tags-section"
@selected="selectRef($event)"
/>
<gl-new-dropdown-divider v-if="showCommitsSection" />
</template>
<template v-if="showCommitsSection">
<ref-results-section
:section-title="i18n.commits"
:total-count="matches.commits.totalCount"
:items="matches.commits.list"
:selected-ref="selectedRef"
:error="matches.commits.error"
:error-message="i18n.commitsErrorMessage"
data-testid="commits-section"
@selected="selectRef($event)"
/>
</template>
</template>
</div>
</div>
</gl-new-dropdown>
</template>
// This eslint-disable can be removed once a second import { __ } from '~/locale';
// value is added to this file.
/* eslint-disable import/prefer-default-export */
export const X_TOTAL_HEADER = 'x-total'; export const X_TOTAL_HEADER = 'x-total';
export const SEARCH_DEBOUNCE_MS = 250;
export const DEFAULT_I18N = Object.freeze({
dropdownHeader: __('Select Git revision'),
searchPlaceholder: __('Search by Git revision'),
noResultsWithQuery: __('No matching results for "%{query}"'),
noResults: __('No matching results'),
branchesErrorMessage: __('An error occurred while fetching branches. Retry the search.'),
tagsErrorMessage: __('An error occurred while fetching tags. Retry the search.'),
commitsErrorMessage: __('An error occurred while fetching commits. Retry the search.'),
branches: __('Branches'),
tags: __('Tags'),
commits: __('Commits'),
noRefSelected: __('No ref selected'),
});
.ref-selector {
& &-dropdown-content {
// Setting a max height is necessary to allow the dropdown's content
// to control where and how scrollbars appear.
// This content is limited to the max-height of the dropdown
// ($dropdown-max-height-lg) minus the additional padding
// on the top and bottom (2 * $gl-padding-8)
max-height: $dropdown-max-height-lg - 2 * $gl-padding-8;
}
.dropdown-menu.show {
// Make the dropdown a little wider and longer than usual
// since it contains quite a bit of content.
width: 20rem;
max-height: $dropdown-max-height-lg;
}
}
...@@ -17,6 +17,9 @@ module Clusters ...@@ -17,6 +17,9 @@ module Clusters
default_value_for :version, VERSION default_value_for :version, VERSION
scope :preload_cluster_platform, -> { preload(cluster: [:platform_kubernetes]) }
scope :with_clusters_with_cilium, -> { joins(:cluster).merge(Clusters::Cluster.with_available_cilium) }
attr_encrypted :alert_manager_token, attr_encrypted :alert_manager_token,
mode: :per_attribute_iv, mode: :per_attribute_iv,
key: Settings.attr_encrypted_db_key_base_truncated, key: Settings.attr_encrypted_db_key_base_truncated,
......
...@@ -133,6 +133,7 @@ module Clusters ...@@ -133,6 +133,7 @@ module Clusters
scope :with_enabled_modsecurity, -> { joins(:application_ingress).merge(::Clusters::Applications::Ingress.modsecurity_enabled) } scope :with_enabled_modsecurity, -> { joins(:application_ingress).merge(::Clusters::Applications::Ingress.modsecurity_enabled) }
scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) } scope :with_available_elasticstack, -> { joins(:application_elastic_stack).merge(::Clusters::Applications::ElasticStack.available) }
scope :with_available_cilium, -> { joins(:application_cilium).merge(::Clusters::Applications::Cilium.available) }
scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct } scope :distinct_with_deployed_environments, -> { joins(:environments).merge(::Deployment.success).distinct }
scope :preload_elasticstack, -> { preload(:application_elastic_stack) } scope :preload_elasticstack, -> { preload(:application_elastic_stack) }
scope :preload_environments, -> { preload(:environments) } scope :preload_environments, -> { preload(:environments) }
......
# frozen_string_literal: true
#
# A Namespace::TraversalHierarchy is the collection of namespaces that descend
# from a root Namespace as defined by the Namespace#traversal_ids attributes.
#
# This class provides operations to be performed on the hierarchy itself,
# rather than individual namespaces.
#
# This includes methods for synchronizing traversal_ids attributes to a correct
# state. We use recursive methods to determine the correct state so we don't
# have to depend on the integrity of the traversal_ids attribute values
# themselves.
#
class Namespace
class TraversalHierarchy
attr_accessor :root
def self.for_namespace(namespace)
new(recursive_root_ancestor(namespace))
end
def initialize(root)
raise StandardError.new('Must specify a root node') if root.parent_id
@root = root
end
# Update all traversal_ids in the current namespace hierarchy.
def sync_traversal_ids!
# An issue in Rails since 2013 prevents this kind of join based update in
# ActiveRecord. https://github.com/rails/rails/issues/13496
# Ideally it would be:
# `incorrect_traversal_ids.update_all('traversal_ids = cte.traversal_ids')`
sql = """
UPDATE namespaces
SET traversal_ids = cte.traversal_ids
FROM (#{recursive_traversal_ids}) as cte
WHERE namespaces.id = cte.id
AND namespaces.traversal_ids <> cte.traversal_ids
"""
Namespace.connection.exec_query(sql)
end
# Identify all incorrect traversal_ids in the current namespace hierarchy.
def incorrect_traversal_ids
Namespace
.joins("INNER JOIN (#{recursive_traversal_ids}) as cte ON namespaces.id = cte.id")
.where('namespaces.traversal_ids <> cte.traversal_ids')
end
private
# Determine traversal_ids for the namespace hierarchy using recursive methods.
# Generate a collection of [id, traversal_ids] rows.
#
# Note that the traversal_ids represent a calculated traversal path for the
# namespace and not the value stored within the traversal_ids attribute.
def recursive_traversal_ids
root_id = Integer(@root.id)
"""
WITH RECURSIVE cte(id, traversal_ids, cycle) AS (
VALUES(#{root_id}, ARRAY[#{root_id}], false)
UNION ALL
SELECT n.id, cte.traversal_ids || n.id, n.id = ANY(cte.traversal_ids)
FROM namespaces n, cte
WHERE n.parent_id = cte.id AND NOT cycle
)
SELECT id, traversal_ids FROM cte
"""
end
# This is essentially Namespace#root_ancestor which will soon be rewritten
# to use traversal_ids. We replicate here as a reliable way to find the
# root using recursive methods.
def self.recursive_root_ancestor(namespace)
Gitlab::ObjectHierarchy
.new(Namespace.where(id: namespace))
.base_and_ancestors
.reorder(nil)
.find_by(parent_id: nil)
end
end
end
...@@ -28,6 +28,9 @@ class PrometheusService < MonitoringService ...@@ -28,6 +28,9 @@ class PrometheusService < MonitoringService
after_create_commit :create_default_alerts after_create_commit :create_default_alerts
scope :preload_project, -> { preload(:project) }
scope :with_clusters_with_cilium, -> { joins(project: [:clusters]).merge(Clusters::Cluster.with_available_cilium) }
def initialize_properties def initialize_properties
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
......
---
title: Define a namespace traversal cache
merge_request: 35713
author:
type: performance
...@@ -574,6 +574,12 @@ Gitlab.ee do ...@@ -574,6 +574,12 @@ Gitlab.ee do
Settings.cron_jobs['web_application_firewall_metrics_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['web_application_firewall_metrics_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['web_application_firewall_metrics_worker']['cron'] ||= '0 1 * * 0' Settings.cron_jobs['web_application_firewall_metrics_worker']['cron'] ||= '0 1 * * 0'
Settings.cron_jobs['web_application_firewall_metrics_worker']['job_class'] = 'IngressModsecurityCounterMetricsWorker' Settings.cron_jobs['web_application_firewall_metrics_worker']['job_class'] = 'IngressModsecurityCounterMetricsWorker'
Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *'
Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker'
Settings.cron_jobs['network_policy_metrics_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['network_policy_metrics_worker']['cron'] ||= '0 3 * * 0'
Settings.cron_jobs['network_policy_metrics_worker']['job_class'] = 'NetworkPolicyMetricsWorker'
end end
# #
......
...@@ -81,7 +81,7 @@ if changes.any? ...@@ -81,7 +81,7 @@ if changes.any?
roulette_spins = roulette.spin(project, categories, branch_name, timezone_experiment: TIMEZONE_EXPERIMENT) roulette_spins = roulette.spin(project, categories, branch_name, timezone_experiment: TIMEZONE_EXPERIMENT)
rows = roulette_spins.map do |spin| rows = roulette_spins.map do |spin|
# MR includes QA changes, but also other changes, and author isn't an SET # MR includes QA changes, but also other changes, and author isn't an SET
if spin.category == :qa && categories.size > 1 && !mr_author.reviewer?(project, spin.category, []) if spin.category == :qa && categories.size > 1 && mr_author && !mr_author.reviewer?(project, spin.category, [])
spin.optional_role = :maintainer spin.optional_role = :maintainer
end end
......
# frozen_string_literal: true
class AddTraversalIdsToNamespaces < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :namespaces, :traversal_ids, :integer, array: true, default: [], null: false
end
end
def down
with_lock_retries do
remove_column :namespaces, :traversal_ids
end
end
end
...@@ -13134,7 +13134,8 @@ CREATE TABLE public.namespaces ( ...@@ -13134,7 +13134,8 @@ CREATE TABLE public.namespaces (
max_personal_access_token_lifetime integer, max_personal_access_token_lifetime integer,
push_rule_id bigint, push_rule_id bigint,
shared_runners_enabled boolean DEFAULT true NOT NULL, shared_runners_enabled boolean DEFAULT true NOT NULL,
allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL allow_descendants_override_disabled_shared_runners boolean DEFAULT false NOT NULL,
traversal_ids integer[] DEFAULT '{}'::integer[] NOT NULL
); );
CREATE SEQUENCE public.namespaces_id_seq CREATE SEQUENCE public.namespaces_id_seq
...@@ -23658,6 +23659,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -23658,6 +23659,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200608075553 20200608075553
20200608214008 20200608214008
20200609002841 20200609002841
20200609012539
20200609142506 20200609142506
20200609142507 20200609142507
20200609142508 20200609142508
......
...@@ -208,7 +208,7 @@ attribute. As a prerequisite, you must use an LDAP server that: ...@@ -208,7 +208,7 @@ attribute. As a prerequisite, you must use an LDAP server that:
client_certificate_required_port: 3443 client_certificate_required_port: 3443
``` ```
NOTE: **Note** NOTE: **Note:**
Assign a value to at least one of the following variables: Assign a value to at least one of the following variables:
`client_certificate_required_host` or `client_certificate_required_port`. `client_certificate_required_host` or `client_certificate_required_port`.
......
...@@ -274,7 +274,7 @@ secondary domain, like changing Git remotes and API URLs. ...@@ -274,7 +274,7 @@ secondary domain, like changing Git remotes and API URLs.
external_url 'https://<new_external_url>' external_url 'https://<new_external_url>'
``` ```
NOTE: **Note** NOTE: **Note:**
Changing `external_url` won't prevent access via the old secondary URL, as Changing `external_url` won't prevent access via the old secondary URL, as
long as the secondary DNS records are still intact. long as the secondary DNS records are still intact.
......
...@@ -18,7 +18,7 @@ Though these instructions use [AWS Route53](https://aws.amazon.com/route53/), ...@@ -18,7 +18,7 @@ Though these instructions use [AWS Route53](https://aws.amazon.com/route53/),
other services such as [Cloudflare](https://www.cloudflare.com/) could be used other services such as [Cloudflare](https://www.cloudflare.com/) could be used
as well. as well.
NOTE: **Note** NOTE: **Note:**
You can also use a load balancer to distribute web UI or API traffic to You can also use a load balancer to distribute web UI or API traffic to
[multiple Geo **secondary** nodes](../../../user/admin_area/geo_nodes.md#multiple-secondary-nodes-behind-a-load-balancer). [multiple Geo **secondary** nodes](../../../user/admin_area/geo_nodes.md#multiple-secondary-nodes-behind-a-load-balancer).
Importantly, the **primary** node cannot yet be included. See the feature request Importantly, the **primary** node cannot yet be included. See the feature request
......
...@@ -966,7 +966,7 @@ Virtual storage: default ...@@ -966,7 +966,7 @@ Virtual storage: default
Currently `dataloss` only considers a repository up to date if it has been directly replicated to from the previous write-enabled primary. While reconciling from an up to date secondary can recover the data, this is not visible in the data loss report. This is due for improvement via [Gitaly#2866](https://gitlab.com/gitlab-org/gitaly/-/issues/2866). Currently `dataloss` only considers a repository up to date if it has been directly replicated to from the previous write-enabled primary. While reconciling from an up to date secondary can recover the data, this is not visible in the data loss report. This is due for improvement via [Gitaly#2866](https://gitlab.com/gitlab-org/gitaly/-/issues/2866).
NOTE: **NOTE** `dataloss` is still in beta and the output format is subject to change. NOTE: **Note:** `dataloss` is still in beta and the output format is subject to change.
### Checking repository checksums ### Checking repository checksums
......
...@@ -106,7 +106,7 @@ If you configure GitLab to store CI logs and artifacts on object storage, you mu ...@@ -106,7 +106,7 @@ If you configure GitLab to store CI logs and artifacts on object storage, you mu
#### Object Storage Settings #### Object Storage Settings
NOTE: **Note** In GitLab 13.2 and later, we recommend using the NOTE: **Note:** In GitLab 13.2 and later, we recommend using the
[consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration). [consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration).
This section describes the earlier configuration format. This section describes the earlier configuration format.
......
...@@ -63,7 +63,7 @@ GitLab provides two different options for the uploading mechanism: "Direct uploa ...@@ -63,7 +63,7 @@ GitLab provides two different options for the uploading mechanism: "Direct uploa
[Read more about using object storage with GitLab](../object_storage.md). [Read more about using object storage with GitLab](../object_storage.md).
NOTE: **Note** In GitLab 13.2 and later, we recommend using the NOTE: **Note:** In GitLab 13.2 and later, we recommend using the
[consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration). [consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration).
This section describes the earlier configuration format. This section describes the earlier configuration format.
......
...@@ -72,7 +72,7 @@ be configured already. ...@@ -72,7 +72,7 @@ be configured already.
## Object Storage Settings ## Object Storage Settings
NOTE: **Note** In GitLab 13.2 and later, we recommend using the NOTE: **Note:** In GitLab 13.2 and later, we recommend using the
[consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration). [consolidated object storage settings](object_storage.md#consolidated-object-storage-configuration).
This section describes the earlier configuration format. This section describes the earlier configuration format.
......
...@@ -253,7 +253,7 @@ gitlab_rails['object_store']['connection'] = { ...@@ -253,7 +253,7 @@ gitlab_rails['object_store']['connection'] = {
#### OpenStack-compatible connection settings #### OpenStack-compatible connection settings
NOTE: **Note** This is not compatible with the consolidated object storage form. NOTE: **Note:** This is not compatible with the consolidated object storage form.
OpenStack Swift is only supported with the storage-specific form. See the OpenStack Swift is only supported with the storage-specific form. See the
[S3 settings](#s3-compatible-connection-settings) if you want to use the consolidated form. [S3 settings](#s3-compatible-connection-settings) if you want to use the consolidated form.
...@@ -274,7 +274,7 @@ Here are the valid connection settings below for the Swift API, provided by ...@@ -274,7 +274,7 @@ Here are the valid connection settings below for the Swift API, provided by
#### Rackspace Cloud Files #### Rackspace Cloud Files
NOTE: **Note** This is not compatible with the consolidated object NOTE: **Note:** This is not compatible with the consolidated object
storage form. Rackspace Cloud is only supported with the storage-specific form. storage form. Rackspace Cloud is only supported with the storage-specific form.
Here are the valid connection parameters for Rackspace Cloud, provided by Here are the valid connection parameters for Rackspace Cloud, provided by
...@@ -408,7 +408,7 @@ additional complexity and unnecessary redundancy. Since both GitLab ...@@ -408,7 +408,7 @@ additional complexity and unnecessary redundancy. Since both GitLab
Rails and Workhorse components need access to object storage, the Rails and Workhorse components need access to object storage, the
consolidated form avoids excessive duplication of credentials. consolidated form avoids excessive duplication of credentials.
NOTE: **Note** The consolidated object storage configuration is **only** used if all NOTE: **Note:** The consolidated object storage configuration is **only** used if all
lines from the original form is omitted. To move to the consolidated form, remove the original configuration (for example, `artifacts_object_store_enabled`, `uploads_object_store_connection`, and so on.) lines from the original form is omitted. To move to the consolidated form, remove the original configuration (for example, `artifacts_object_store_enabled`, `uploads_object_store_connection`, and so on.)
## Storage-specific configuration ## Storage-specific configuration
......
...@@ -87,7 +87,7 @@ store the blobs of the dependency proxy. ...@@ -87,7 +87,7 @@ store the blobs of the dependency proxy.
[Read more about using object storage with GitLab](../object_storage.md). [Read more about using object storage with GitLab](../object_storage.md).
NOTE: **Note** In GitLab 13.2 and later, we recommend using the NOTE: **Note:** In GitLab 13.2 and later, we recommend using the
[consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration). [consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration).
This section describes the earlier configuration format. This section describes the earlier configuration format.
......
...@@ -99,7 +99,7 @@ store packages. ...@@ -99,7 +99,7 @@ store packages.
[Read more about using object storage with GitLab](../object_storage.md). [Read more about using object storage with GitLab](../object_storage.md).
NOTE: **Note** We recommend using the [consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration). The following instructions apply to the original config format. NOTE: **Note:** We recommend using the [consolidated object storage settings](../object_storage.md#consolidated-object-storage-configuration). The following instructions apply to the original config format.
**Omnibus GitLab installations** **Omnibus GitLab installations**
......
...@@ -5099,6 +5099,11 @@ type Group { ...@@ -5099,6 +5099,11 @@ type Group {
""" """
iid: ID iid: ID
"""
Whether to include ancestor Iterations. Defaults to true
"""
includeAncestors: Boolean
""" """
Returns the last _n_ elements from the list. Returns the last _n_ elements from the list.
""" """
......
...@@ -14110,6 +14110,16 @@ ...@@ -14110,6 +14110,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "includeAncestors",
"description": "Whether to include ancestor Iterations. Defaults to true",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "after", "name": "after",
"description": "Returns the elements in the list that come after the specified cursor.", "description": "Returns the elements in the list that come after the specified cursor.",
...@@ -730,6 +730,8 @@ appear to be associated to any of the services running, since they all appear to ...@@ -730,6 +730,8 @@ appear to be associated to any of the services running, since they all appear to
| `process_memory_uss` | `topology > nodes > node_services` | `enablement` | | | The average Unique Set Size of a service process | | `process_memory_uss` | `topology > nodes > node_services` | `enablement` | | | The average Unique Set Size of a service process |
| `process_memory_pss` | `topology > nodes > node_services` | `enablement` | | | The average Proportional Set Size of a service process | | `process_memory_pss` | `topology > nodes > node_services` | `enablement` | | | The average Proportional Set Size of a service process |
| `server` | `topology > nodes > node_services` | `enablement` | | | The type of web server used (Unicorn or Puma) | | `server` | `topology > nodes > node_services` | `enablement` | | | The type of web server used (Unicorn or Puma) |
| `network_policy_forwards` | `counts` | `defend` | | EE | Cumulative count of forwarded packets by Container Network |
| `network_policy_drops` | `counts` | `defend` | | EE | Cumulative count of dropped packets by Container Network |
## Example Usage Ping payload ## Example Usage Ping payload
......
...@@ -170,7 +170,7 @@ You can filter the selection dropdown by writing part of the namespace or projec ...@@ -170,7 +170,7 @@ You can filter the selection dropdown by writing part of the namespace or projec
![limit namespace filter](img/limit_namespace_filter.png) ![limit namespace filter](img/limit_namespace_filter.png)
NOTE: **Note**: NOTE: **Note:**
If no namespaces or projects are selected, no Elasticsearch indexing will take place. If no namespaces or projects are selected, no Elasticsearch indexing will take place.
CAUTION: **Warning**: CAUTION: **Warning**:
......
...@@ -2480,6 +2480,12 @@ msgstr "" ...@@ -2480,6 +2480,12 @@ msgstr ""
msgid "An error occurred while enabling Service Desk." msgid "An error occurred while enabling Service Desk."
msgstr "" msgstr ""
msgid "An error occurred while fetching branches. Retry the search."
msgstr ""
msgid "An error occurred while fetching commits. Retry the search."
msgstr ""
msgid "An error occurred while fetching coverage reports." msgid "An error occurred while fetching coverage reports."
msgstr "" msgstr ""
...@@ -2510,6 +2516,9 @@ msgstr "" ...@@ -2510,6 +2516,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data" msgid "An error occurred while fetching sidebar data"
msgstr "" msgstr ""
msgid "An error occurred while fetching tags. Retry the search."
msgstr ""
msgid "An error occurred while fetching terraform reports." msgid "An error occurred while fetching terraform reports."
msgstr "" msgstr ""
...@@ -7458,6 +7467,9 @@ msgstr "" ...@@ -7458,6 +7467,9 @@ msgstr ""
msgid "Default: Map a FogBugz account ID to a full name" msgid "Default: Map a FogBugz account ID to a full name"
msgstr "" msgstr ""
msgid "DefaultBranchLabel|default"
msgstr ""
msgid "Define a custom pattern with cron syntax" msgid "Define a custom pattern with cron syntax"
msgstr "" msgstr ""
...@@ -13628,6 +13640,11 @@ msgstr "" ...@@ -13628,6 +13640,11 @@ msgstr ""
msgid "LicenseCompliance|License Approvals" msgid "LicenseCompliance|License Approvals"
msgstr "" msgstr ""
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required" msgid "LicenseCompliance|License Compliance detected %d license and policy violation for the source branch only; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required" msgid_plural "LicenseCompliance|License Compliance detected %d licenses and policy violations for the source branch only; approval required"
msgstr[0] "" msgstr[0] ""
...@@ -13643,6 +13660,11 @@ msgid_plural "LicenseCompliance|License Compliance detected %d new licenses" ...@@ -13643,6 +13660,11 @@ msgid_plural "LicenseCompliance|License Compliance detected %d new licenses"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d new license and policy violation"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations"
msgstr[0] ""
msgstr[1] ""
msgid "LicenseCompliance|License Compliance detected %d new license and policy violation; approval required" msgid "LicenseCompliance|License Compliance detected %d new license and policy violation; approval required"
msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required" msgid_plural "LicenseCompliance|License Compliance detected %d new licenses and policy violations; approval required"
msgstr[0] "" msgstr[0] ""
...@@ -15618,6 +15640,9 @@ msgstr "" ...@@ -15618,6 +15640,9 @@ msgstr ""
msgid "No matching results" msgid "No matching results"
msgstr "" msgstr ""
msgid "No matching results for \"%{query}\""
msgstr ""
msgid "No merge requests found" msgid "No merge requests found"
msgstr "" msgstr ""
...@@ -15654,6 +15679,9 @@ msgstr "" ...@@ -15654,6 +15679,9 @@ msgstr ""
msgid "No public groups" msgid "No public groups"
msgstr "" msgstr ""
msgid "No ref selected"
msgstr ""
msgid "No related merge requests found." msgid "No related merge requests found."
msgstr "" msgstr ""
...@@ -20270,6 +20298,9 @@ msgstr "" ...@@ -20270,6 +20298,9 @@ msgstr ""
msgid "Search branches and tags" msgid "Search branches and tags"
msgstr "" msgstr ""
msgid "Search by Git revision"
msgstr ""
msgid "Search by author" msgid "Search by author"
msgstr "" msgstr ""
...@@ -24652,6 +24683,9 @@ msgstr "" ...@@ -24652,6 +24683,9 @@ msgstr ""
msgid "Total: %{total}" msgid "Total: %{total}"
msgstr "" msgstr ""
msgid "TotalRefCountIndicator|1000+"
msgstr ""
msgid "Trace" msgid "Trace"
msgstr "" msgstr ""
...@@ -28149,6 +28183,9 @@ msgstr "" ...@@ -28149,6 +28183,9 @@ msgstr ""
msgid "mrWidget|You can merge this merge request manually using the" msgid "mrWidget|You can merge this merge request manually using the"
msgstr "" msgstr ""
msgid "mrWidget|You can only merge once the denied license is removed"
msgstr ""
msgid "mrWidget|Your password" msgid "mrWidget|Your password"
msgstr "" msgstr ""
......
...@@ -29,5 +29,35 @@ FactoryBot.define do ...@@ -29,5 +29,35 @@ FactoryBot.define do
trait :with_root_storage_statistics do trait :with_root_storage_statistics do
association :root_storage_statistics, factory: :namespace_root_storage_statistics association :root_storage_statistics, factory: :namespace_root_storage_statistics
end end
# Construct a hierarchy underneath the namespace.
# Each namespace will have `children` amount of children,
# and `depth` levels of descendants.
trait :with_hierarchy do
transient do
children { 4 }
depth { 4 }
end
after(:create) do |namespace, evaluator|
def create_graph(parent: nil, children: 4, depth: 4)
return unless depth > 1
children.times do
factory_name = parent.model_name.singular
child = FactoryBot.create(factory_name, parent: parent)
create_graph(parent: child, children: children, depth: depth - 1)
end
parent
end
create_graph(
parent: namespace,
children: evaluator.children,
depth: evaluator.depth
)
end
end
end end
end end
...@@ -8,7 +8,7 @@ function getTrackingSpy(key) { ...@@ -8,7 +8,7 @@ function getTrackingSpy(key) {
describe('Tracking Events', () => { describe('Tracking Events', () => {
describe('trackDesignDetailView', () => { describe('trackDesignDetailView', () => {
const eventKey = 'projects:issues:design'; const eventKey = 'projects:issues:design';
const eventName = 'design_viewed'; const eventName = 'view_design';
it('trackDesignDetailView fires a tracking event when called', () => { it('trackDesignDetailView fires a tracking event when called', () => {
const trackingSpy = getTrackingSpy(eventKey); const trackingSpy = getTrackingSpy(eventKey);
...@@ -20,11 +20,14 @@ describe('Tracking Events', () => { ...@@ -20,11 +20,14 @@ describe('Tracking Events', () => {
eventName, eventName,
expect.objectContaining({ expect.objectContaining({
label: eventName, label: eventName,
value: { context: {
'internal-object-refrerer': '', schema: expect.any(String),
'design-collection-owner': '', data: {
'design-version-number': 1, 'design-version-number': 1,
'design-is-current-version': false, 'design-is-current-version': false,
'internal-object-referrer': '',
'design-collection-owner': '',
},
}, },
}), }),
); );
...@@ -40,11 +43,14 @@ describe('Tracking Events', () => { ...@@ -40,11 +43,14 @@ describe('Tracking Events', () => {
eventName, eventName,
expect.objectContaining({ expect.objectContaining({
label: eventName, label: eventName,
value: { context: {
'internal-object-refrerer': 'from-a-test', schema: expect.any(String),
'design-collection-owner': 'test', data: {
'design-version-number': 100, 'design-version-number': 100,
'design-is-current-version': true, 'design-is-current-version': true,
'internal-object-referrer': 'from-a-test',
'design-collection-owner': 'test',
},
}, },
}), }),
); );
......
...@@ -8,7 +8,7 @@ function getTrackingSpy(key) { ...@@ -8,7 +8,7 @@ function getTrackingSpy(key) {
describe('Tracking Events', () => { describe('Tracking Events', () => {
describe('trackDesignDetailView', () => { describe('trackDesignDetailView', () => {
const eventKey = 'projects:issues:design'; const eventKey = 'projects:issues:design';
const eventName = 'design_viewed'; const eventName = 'view_design';
it('trackDesignDetailView fires a tracking event when called', () => { it('trackDesignDetailView fires a tracking event when called', () => {
const trackingSpy = getTrackingSpy(eventKey); const trackingSpy = getTrackingSpy(eventKey);
...@@ -20,11 +20,14 @@ describe('Tracking Events', () => { ...@@ -20,11 +20,14 @@ describe('Tracking Events', () => {
eventName, eventName,
expect.objectContaining({ expect.objectContaining({
label: eventName, label: eventName,
value: { context: {
'internal-object-refrerer': '', schema: expect.any(String),
'design-collection-owner': '', data: {
'design-version-number': 1, 'design-version-number': 1,
'design-is-current-version': false, 'design-is-current-version': false,
'internal-object-referrer': '',
'design-collection-owner': '',
},
}, },
}), }),
); );
...@@ -40,11 +43,14 @@ describe('Tracking Events', () => { ...@@ -40,11 +43,14 @@ describe('Tracking Events', () => {
eventName, eventName,
expect.objectContaining({ expect.objectContaining({
label: eventName, label: eventName,
value: { context: {
'internal-object-refrerer': 'from-a-test', schema: expect.any(String),
'design-collection-owner': 'test', data: {
'design-version-number': 100, 'design-version-number': 100,
'design-is-current-version': true, 'design-is-current-version': true,
'internal-object-referrer': 'from-a-test',
'design-collection-owner': 'test',
},
}, },
}), }),
); );
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlLoadingIcon, GlSearchBoxByType, GlNewDropdownItem, GlIcon } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { X_TOTAL_HEADER, DEFAULT_I18N } from '~/ref/constants';
import createStore from '~/ref/stores/';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Ref selector component', () => {
const fixtures = {
branches: getJSONFixture('api/branches/branches.json'),
tags: getJSONFixture('api/tags/tags.json'),
commit: getJSONFixture('api/commits/commit.json'),
};
const projectId = '8';
let wrapper;
let branchesApiCallSpy;
let tagsApiCallSpy;
let commitApiCallSpy;
const createComponent = () => {
wrapper = mount(RefSelector, {
propsData: {
projectId,
value: '',
},
listeners: {
// simulate a parent component v-model binding
input: selectedRef => {
wrapper.setProps({ value: selectedRef });
},
},
stubs: {
GlSearchBoxByType: true,
},
localVue,
store: createStore(),
});
};
beforeEach(() => {
const mock = new MockAdapter(axios);
gon.api_version = 'v4';
branchesApiCallSpy = jest
.fn()
.mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
mock
.onGet(`/api/v4/projects/${projectId}/repository/branches`)
.reply(config => branchesApiCallSpy(config));
mock
.onGet(`/api/v4/projects/${projectId}/repository/tags`)
.reply(config => tagsApiCallSpy(config));
mock
.onGet(new RegExp(`/api/v4/projects/${projectId}/repository/commits/.*`))
.reply(config => commitApiCallSpy(config));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
//
// Finders
//
const findButtonContent = () => wrapper.find('[data-testid="button-content"]');
const findNoResults = () => wrapper.find('[data-testid="no-results"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
const findBranchDropdownItems = () => findBranchesSection().findAll(GlNewDropdownItem);
const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
const findTagDropdownItems = () => findTagsSection().findAll(GlNewDropdownItem);
const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
const findCommitDropdownItems = () => findCommitsSection().findAll(GlNewDropdownItem);
const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
//
// Expecters
//
const branchesSectionContainsErrorMessage = () => {
const branchesSection = findBranchesSection();
return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
};
const tagsSectionContainsErrorMessage = () => {
const tagsSection = findTagsSection();
return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
};
const commitsSectionContainsErrorMessage = () => {
const commitsSection = findCommitsSection();
return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
};
//
// Convenience methods
//
const updateQuery = newQuery => {
wrapper.find(GlSearchBoxByType).vm.$emit('input', newQuery);
};
const selectFirstBranch = () => {
findFirstBranchDropdownItem().vm.$emit('click');
};
const selectFirstTag = () => {
findFirstTagDropdownItem().vm.$emit('click');
};
const selectFirstCommit = () => {
findFirstCommitDropdownItem().vm.$emit('click');
};
const waitForRequests = ({ andClearMocks } = { andClearMocks: false }) =>
axios.waitForAll().then(() => {
if (andClearMocks) {
branchesApiCallSpy.mockClear();
tagsApiCallSpy.mockClear();
commitApiCallSpy.mockClear();
}
});
describe('initialization behavior', () => {
beforeEach(createComponent);
it('initializes the dropdown with branches and tags when mounted', () => {
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
expect(commitApiCallSpy).not.toHaveBeenCalled();
});
});
it('shows a spinner while network requests are in progress', () => {
expect(findLoadingIcon().exists()).toBe(true);
return waitForRequests().then(() => {
expect(findLoadingIcon().exists()).toBe(false);
});
});
});
describe('post-initialization behavior', () => {
describe('when the search query is updated', () => {
beforeEach(() => {
createComponent();
return waitForRequests({ andClearMocks: true });
});
it('requeries the endpoints when the search query is updated', () => {
updateQuery('v1.2.3');
return waitForRequests().then(() => {
expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
});
it("does not make a call to the commit endpoint if the query doesn't look like a SHA", () => {
updateQuery('not a sha');
return waitForRequests().then(() => {
expect(commitApiCallSpy).not.toHaveBeenCalled();
});
});
it('searches for a commit if the query could potentially be a SHA', () => {
updateQuery('abcdef');
return waitForRequests().then(() => {
expect(commitApiCallSpy).toHaveBeenCalled();
});
});
});
describe('when no results are found', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
commitApiCallSpy = jest.fn().mockReturnValue([404]);
createComponent();
return waitForRequests();
});
describe('when the search query is empty', () => {
it('renders a "no results" message', () => {
expect(findNoResults().text()).toBe(DEFAULT_I18N.noResults);
});
});
describe('when the search query is not empty', () => {
const query = 'hello';
beforeEach(() => {
updateQuery(query);
return waitForRequests();
});
it('renders a "no results" message that includes the search query', () => {
expect(findNoResults().text()).toBe(sprintf(DEFAULT_I18N.noResultsWithQuery, { query }));
});
});
});
describe('branches', () => {
describe('when the branches search returns results', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
});
it('renders the "Branches" heading with a total number indicator', () => {
expect(
findBranchesSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Branches 123');
});
it("does not render an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(false);
});
it('renders each non-default branch as a selectable item', () => {
const dropdownItems = findBranchDropdownItems();
fixtures.branches.forEach((b, i) => {
if (!b.default) {
expect(dropdownItems.at(i).text()).toBe(b.name);
}
});
});
it('renders the default branch as a selectable item with a "default" badge', () => {
const dropdownItems = findBranchDropdownItems();
const defaultBranch = fixtures.branches.find(b => b.default);
const defaultBranchIndex = fixtures.branches.indexOf(defaultBranch);
expect(trimText(dropdownItems.at(defaultBranchIndex).text())).toBe(
`${defaultBranch.name} default`,
);
});
});
describe('when the branches search returns no results', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
return waitForRequests();
});
it('does not render the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(false);
});
});
describe('when the branches search returns an error', () => {
beforeEach(() => {
branchesApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
return waitForRequests();
});
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
});
it("renders an error message in the branches section's body", () => {
expect(branchesSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('tags', () => {
describe('when the tags search returns results', () => {
beforeEach(() => {
createComponent();
return waitForRequests();
});
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
expect(
findTagsSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Tags 456');
});
it("does not render an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(false);
});
it('renders each tag as a selectable item', () => {
const dropdownItems = findTagDropdownItems();
fixtures.tags.forEach((t, i) => {
expect(dropdownItems.at(i).text()).toBe(t.name);
});
});
});
describe('when the tags search returns no results', () => {
beforeEach(() => {
tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
return waitForRequests();
});
it('does not render the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(false);
});
});
describe('when the tags search returns an error', () => {
beforeEach(() => {
tagsApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
return waitForRequests();
});
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
});
it("renders an error message in the tags section's body", () => {
expect(tagsSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('commits', () => {
describe('when the commit search returns results', () => {
beforeEach(() => {
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('renders the commit section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(true);
});
it('renders the "Commits" heading with a total number indicator', () => {
expect(
findCommitsSection()
.find('[data-testid="section-header"]')
.text(),
).toBe('Commits 1');
});
it("does not render an error message in the comits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(false);
});
it('renders each commit as a selectable item with the short SHA and commit title', () => {
const dropdownItems = findCommitDropdownItems();
const { commit } = fixtures;
expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
});
});
describe('when the commit search returns no results (i.e. a 404)', () => {
beforeEach(() => {
commitApiCallSpy = jest.fn().mockReturnValue([404]);
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('does not render the commits section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(false);
});
});
describe('when the commit search returns an error (other than a 404)', () => {
beforeEach(() => {
commitApiCallSpy = jest.fn().mockReturnValue([500]);
createComponent();
updateQuery('abcd1234');
return waitForRequests();
});
it('renders the commits section in the dropdown', () => {
expect(findCommitsSection().exists()).toBe(true);
});
it("renders an error message in the commits section's body", () => {
expect(commitsSectionContainsErrorMessage()).toBe(true);
});
});
});
describe('selection', () => {
beforeEach(() => {
createComponent();
updateQuery(fixtures.commit.short_id);
return waitForRequests();
});
it('renders a checkmark by the selected item', () => {
expect(findFirstBranchDropdownItem().find(GlIcon).element).toHaveClass(
'gl-visibility-hidden',
);
selectFirstBranch();
return localVue.nextTick().then(() => {
expect(findFirstBranchDropdownItem().find(GlIcon).element).not.toHaveClass(
'gl-visibility-hidden',
);
});
});
describe('when a branch is seleceted', () => {
it("displays the branch name in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstBranch();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
});
});
it("updates the v-model binding with the branch's name", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstBranch();
expect(wrapper.vm.value).toEqual(fixtures.branches[0].name);
});
});
describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstTag();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
});
});
it("updates the v-model binding with the tag's name", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstTag();
expect(wrapper.vm.value).toEqual(fixtures.tags[0].name);
});
});
describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", () => {
expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
selectFirstCommit();
return localVue.nextTick().then(() => {
expect(findButtonContent().text()).toBe(fixtures.commit.id);
});
});
it("updates the v-model binding with the commit's full SHA", () => {
expect(wrapper.vm.value).toEqual('');
selectFirstCommit();
expect(wrapper.vm.value).toEqual(fixtures.commit.id);
});
});
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Namespace::TraversalHierarchy, type: :model do
let_it_be(:root, reload: true) { create(:namespace, :with_hierarchy) }
describe '.for_namespace' do
let(:hierarchy) { described_class.for_namespace(namespace) }
context 'with root group' do
let(:namespace) { root }
it { expect(hierarchy.root).to eq root }
end
context 'with child group' do
let(:namespace) { root.children.first.children.first }
it { expect(hierarchy.root).to eq root }
end
context 'with group outside of hierarchy' do
let(:namespace) { create(:namespace) }
it { expect(hierarchy.root).not_to eq root }
end
end
describe '.new' do
let(:hierarchy) { described_class.new(namespace) }
context 'with root group' do
let(:namespace) { root }
it { expect(hierarchy.root).to eq root }
end
context 'with child group' do
let(:namespace) { root.children.first }
it { expect { hierarchy }.to raise_error(StandardError, 'Must specify a root node') }
end
end
describe '#incorrect_traversal_ids' do
subject { described_class.new(root).incorrect_traversal_ids }
it { is_expected.to match_array Namespace.all }
end
describe '#sync_traversal_ids!' do
let(:hierarchy) { described_class.new(root) }
before do
hierarchy.sync_traversal_ids!
root.reload
end
it_behaves_like 'hierarchy with traversal_ids'
it { expect(hierarchy.incorrect_traversal_ids).to be_empty }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'hierarchy with traversal_ids' do
# A convenient null node to represent the parent of root.
let(:null_node) { double(traversal_ids: []) }
# Walk the tree to assert that the current_node's traversal_id is always
# present and equal to it's parent's traversal_ids plus it's own ID.
def validate_traversal_ids(current_node, parent = null_node)
expect(current_node.traversal_ids).to be_present
expect(current_node.traversal_ids).to eq parent.traversal_ids + [current_node.id]
current_node.children.each do |child|
validate_traversal_ids(child, current_node)
end
end
it 'will be valid' do
validate_traversal_ids(root)
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册