Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-05 12:09:45 +00:00
parent 88ad172d04
commit 25bfb256b3
59 changed files with 1008 additions and 188 deletions

View File

@ -72,10 +72,6 @@ export default {
type: String,
required: true,
},
addDashboardDocumentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
@ -409,7 +405,6 @@ export default {
v-if="showHeader"
ref="prometheusGraphsHeader"
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
:add-dashboard-documentation-path="addDashboardDocumentationPath"
:default-branch="defaultBranch"
:rearrange-panels-available="rearrangePanelsAvailable"
:custom-metrics-available="customMetricsAvailable"

View File

@ -107,10 +107,6 @@ export default {
type: Object,
required: true,
},
addDashboardDocumentationPath: {
type: String,
required: true,
},
},
data() {
return {
@ -128,6 +124,7 @@ export default {
'canAccessOperationsSettings',
'operationsSettingsPath',
'currentDashboard',
'addDashboardDocumentationPath',
]),
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
isOutOfTheBoxDashboard() {

View File

@ -1,6 +1,14 @@
<script>
import { mapActions, mapState } from 'vuex';
import { GlCard, GlForm, GlFormGroup, GlFormTextarea, GlButton, GlAlert } from '@gitlab/ui';
import {
GlCard,
GlForm,
GlFormGroup,
GlFormTextarea,
GlButton,
GlSprintf,
GlAlert,
} from '@gitlab/ui';
import DashboardPanel from './dashboard_panel.vue';
const initialYml = `title:
@ -18,6 +26,7 @@ export default {
GlFormGroup,
GlFormTextarea,
GlButton,
GlSprintf,
GlAlert,
DashboardPanel,
},
@ -31,6 +40,8 @@ export default {
'panelPreviewIsLoading',
'panelPreviewError',
'panelPreviewGraphData',
'projectPath',
'addDashboardDocumentationPath',
]),
},
methods: {
@ -43,45 +54,91 @@ export default {
</script>
<template>
<div>
<gl-card>
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|Define and preview panel') }}</h2>
</template>
<template #default>
<gl-form @submit.prevent="onSubmit">
<gl-form-group
:label="s__('Metrics|Panel YAML')"
:description="s__('Metrics|Define panel YAML to preview panel.')"
label-for="panel-yml-input"
<div class="gl-display-flex gl-mx-n3">
<gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|1. Define and preview panel') }}</h2>
</template>
<template #default>
<p>{{ s__('Metrics|Define panel YAML below to preview panel.') }}</p>
<gl-form @submit.prevent="onSubmit">
<gl-form-group :label="s__('Metrics|Panel YAML')" label-for="panel-yml-input">
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
/>
</gl-form-group>
<div class="gl-text-right">
<gl-button
ref="clipboardCopyBtn"
variant="success"
category="secondary"
:data-clipboard-text="yml"
@click="$toast.show(s__('Metrics|Panel YAML copied'))"
>
{{ s__('Metrics|Copy YAML') }}
</gl-button>
<gl-button
type="submit"
variant="success"
:disabled="panelPreviewIsLoading"
class="js-no-auto-disable"
>
{{ s__('Metrics|Preview panel') }}
</gl-button>
</div>
</gl-form>
</template>
</gl-card>
<gl-card
class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3"
body-class="gl-display-flex gl-flex-direction-column"
>
<template #header>
<h2 class="gl-font-size-h2 gl-my-3">
{{ s__('Metrics|2. Paste panel YAML into dashboard') }}
</h2>
</template>
<template #default>
<div
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-justify-content-center"
>
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
/>
</gl-form-group>
<p>
{{ s__('Metrics|Copy and paste the panel YAML into your dashboard YAML file.') }}
<br />
<gl-sprintf
:message="
s__(
'Metrics|Dashboard files can be found in %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project.',
)
"
>
<template #code="{content}">
<code>{{ content }}</code>
</template>
</gl-sprintf>
</p>
</div>
<div class="gl-text-right">
<gl-button
ref="clipboardCopyBtn"
variant="success"
ref="viewDocumentationBtn"
category="secondary"
:data-clipboard-text="yml"
@click="$toast.show(s__('Metrics|Panel YAML copied'))"
variant="info"
target="_blank"
:href="addDashboardDocumentationPath"
>
{{ s__('Metrics|Copy YAML') }}
{{ s__('Metrics|View documentation') }}
</gl-button>
<gl-button
type="submit"
variant="success"
:disabled="panelPreviewIsLoading"
class="js-no-auto-disable"
>
{{ s__('Metrics|Preview panel') }}
<gl-button ref="openRepositoryBtn" variant="success" :href="projectPath">
{{ s__('Metrics|Open repository') }}
</gl-button>
</div>
</gl-form>
</template>
</gl-card>
</template>
</gl-card>
</div>
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
{{ panelPreviewError }}

View File

@ -80,6 +80,7 @@ export default () => ({
projectPath: null,
operationsSettingsPath: '',
logsPath: invalidUrl,
addDashboardDocumentationPath: '',
// static paths
customDashboardBasePath: '',

View File

@ -32,6 +32,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
logsPath,
currentEnvironmentName,
customDashboardBasePath,
addDashboardDocumentationPath,
...dataProps
} = dataset;
@ -54,6 +55,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
logsPath,
currentEnvironmentName,
customDashboardBasePath,
addDashboardDocumentationPath,
},
dataProps,
};

View File

@ -0,0 +1,98 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import { generateConanRecipe } from '../utils';
import { PackageType } from '../../shared/constants';
export default {
i18n: {
sourceText: s__('PackageRegistry|Source project located at %{link}'),
licenseText: s__('PackageRegistry|License information located at %{link}'),
recipeText: s__('PackageRegistry|Recipe: %{recipe}'),
appGroup: s__('PackageRegistry|App group: %{group}'),
appName: s__('PackageRegistry|App name: %{name}'),
},
components: {
DetailsRow,
GlLink,
GlSprintf,
},
props: {
packageEntity: {
type: Object,
required: true,
},
},
computed: {
conanRecipe() {
return generateConanRecipe(this.packageEntity);
},
showMetadata() {
const visibilityConditions = {
[PackageType.NUGET]: this.packageEntity.nuget_metadatum,
[PackageType.CONAN]: this.packageEntity.conan_metadatum,
[PackageType.MAVEN]: this.packageEntity.maven_metadatum,
};
return visibilityConditions[this.packageEntity.package_type];
},
},
};
</script>
<template>
<div v-if="showMetadata">
<h3 class="gl-font-lg gl-mt-5" data-testid="title">{{ __('Additional Metadata') }}</h3>
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
<template v-if="packageEntity.nuget_metadatum">
<details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{
packageEntity.nuget_metadatum.project_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
<details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
<gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{
packageEntity.nuget_metadatum.license_url
}}</gl-link>
</template>
</gl-sprintf>
</details-row>
</template>
<details-row
v-else-if="packageEntity.conan_metadatum"
icon="information-o"
padding="gl-p-4"
data-testid="conan-recipe"
>
<gl-sprintf :message="$options.i18n.recipeText">
<template #recipe>{{ conanRecipe }}</template>
</gl-sprintf>
</details-row>
<template v-else-if="packageEntity.maven_metadatum">
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
<gl-sprintf :message="$options.i18n.appName">
<template #name>
<strong>{{ packageEntity.maven_metadatum.app_name }}</strong>
</template>
</gl-sprintf>
</details-row>
<details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
<gl-sprintf :message="$options.i18n.appGroup">
<template #group>
<strong>{{ packageEntity.maven_metadatum.app_group }}</strong>
</template>
</gl-sprintf>
</details-row>
</template>
</div>
</div>
</template>

View File

@ -12,6 +12,7 @@ import {
GlTable,
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import PackageActivity from './activity.vue';
import PackageHistory from './package_history.vue';
@ -25,6 +26,7 @@ import PypiInstallation from './pypi_installation.vue';
import PackagesListLoader from '../../shared/components/packages_list_loader.vue';
import PackageListRow from '../../shared/components/package_list_row.vue';
import DependencyRow from './dependency_row.vue';
import AdditionalMetadata from './additional_metadata.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@ -32,7 +34,6 @@ import { generatePackageInfo } from '../utils';
import { __, s__ } from '~/locale';
import { PackageType, TrackingActions } from '../../shared/constants';
import { packageTypeToTrackCategory } from '../../shared/utils';
import { mapActions, mapState } from 'vuex';
export default {
name: 'PackagesApp',
@ -59,6 +60,7 @@ export default {
PackageListRow,
DependencyRow,
PackageHistory,
AdditionalMetadata,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -253,9 +255,12 @@ export default {
<package-activity />
</template>
<package-history v-else :package-entity="packageEntity" :project-name="projectName" />
<template v-else>
<package-history :package-entity="packageEntity" :project-name="projectName" />
<additional-metadata :package-entity="packageEntity" />
</template>
<h3 class="gl-font-lg">{{ __('Files') }}</h3>
<h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
<gl-table
:fields="$options.filesTableHeaderFields"
:items="filesTableRows"

View File

@ -19,7 +19,7 @@ export default {
</script>
<template>
<timeline-entry-item class="system-note note-wrapper gl-my-6!">
<timeline-entry-item class="system-note note-wrapper gl-mb-6!">
<div class="timeline-icon">
<gl-icon :name="icon" />
</div>

View File

@ -44,8 +44,8 @@ export default {
<template>
<div class="issuable-discussion">
<h3 class="gl-ml-6" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-my-4" data-testid="timeline">
<h3 class="gl-font-lg gl-my-3" data-testid="title">{{ __('History') }}</h3>
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
<history-element icon="clock" data-testid="created-on">
<gl-sprintf :message="$options.i18n.createdOn">
<template #name>

View File

@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { formatDate } from '~/lib/utils/datetime_utility';
import DeleteButton from '../delete_button.vue';
import ListItem from '../list_item.vue';
import DetailsRow from './details_row.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
DIGEST_LABEL,

View File

@ -10,13 +10,29 @@ export default {
type: String,
required: true,
},
padding: {
type: String,
default: 'gl-py-2',
required: false,
},
dashed: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
borderClass() {
return this.dashed ? 'gl-border-b-solid gl-border-gray-100 gl-border-b-1' : '';
},
},
};
</script>
<template>
<div
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-py-2 gl-word-break-all"
class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
:class="[padding, borderClass]"
>
<gl-icon :name="icon" class="gl-mr-4" />
<span>

View File

@ -51,7 +51,10 @@ class Projects::IssuesController < Projects::ApplicationController
end
before_action only: :show do
push_frontend_feature_flag(:real_time_issue_sidebar, @project)
real_time_feature_flag = :real_time_issue_sidebar
real_time_enabled = Gitlab::ActionCable::Config.in_app? || Feature.enabled?(real_time_feature_flag, @project)
gon.push({ features: { real_time_feature_flag.to_s.camelize(:lower) => real_time_enabled } }, true)
end
before_action only: :index do

View File

@ -18,6 +18,8 @@ class RegistrationsController < Devise::RegistrationsController
def new
if experiment_enabled?(:signup_flow)
track_experiment_event(:signup_flow, 'start') # We want this event to be tracked when the user is _in_ the experimental group
track_experiment_event(:terms_opt_in, 'start')
@resource = build_resource
else
redirect_to new_user_session_path(anchor: 'register-pane')
@ -26,6 +28,7 @@ class RegistrationsController < Devise::RegistrationsController
def create
track_experiment_event(:signup_flow, 'end') unless experiment_enabled?(:signup_flow) # We want this event to be tracked when the user is _in_ the control group
track_experiment_event(:terms_opt_in, 'end')
accept_pending_invitations
@ -178,6 +181,8 @@ class RegistrationsController < Devise::RegistrationsController
end
def terms_accepted?
return true if experiment_enabled?(:terms_opt_in)
Gitlab::Utils.to_boolean(params[:terms_opt_in])
end

View File

@ -65,7 +65,7 @@ module Issuable
has_many :labels, through: :label_links
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :metrics
has_one :metrics, inverse_of: model_name.singular.to_sym, autosave: true
delegate :name,
:email,

View File

@ -1607,7 +1607,12 @@ class MergeRequest < ApplicationRecord
override :ensure_metrics
def ensure_metrics
MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id).tap do |metrics_record|
# Backward compatibility: some merge request metrics records will not have target_project_id filled in.
# In that case the first `safe_find_or_create_by` will return false.
# The second finder call will be eliminated in https://gitlab.com/gitlab-org/gitlab/-/issues/233507
metrics_record = MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id, target_project_id: target_project_id) || MergeRequest::Metrics.safe_find_or_create_by(merge_request_id: id)
metrics_record.tap do |metrics_record|
# Make sure we refresh the loaded association object with the newly created/loaded item.
# This is needed in order to have the exact functionality than before.
#
@ -1617,6 +1622,8 @@ class MergeRequest < ApplicationRecord
# merge_request.ensure_metrics
# merge_request.metrics # should return the metrics record and not nil
# merge_request.metrics.merge_request # should return the same MR record
metrics_record.target_project_id = target_project_id
metrics_record.association(:merge_request).target = self
association(:metrics).target = metrics_record
end

View File

@ -1,10 +1,18 @@
# frozen_string_literal: true
class MergeRequest::Metrics < ApplicationRecord
belongs_to :merge_request
belongs_to :merge_request, inverse_of: :metrics
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
belongs_to :latest_closed_by, class_name: 'User'
belongs_to :merged_by, class_name: 'User'
before_save :ensure_target_project_id
private
def ensure_target_project_id
self.target_project_id ||= merge_request.target_project_id
end
end
MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')

View File

@ -22,7 +22,7 @@ module Issues
end
def after_update(issue)
IssuesChannel.broadcast_to(issue, event: 'updated') if Feature.enabled?(:broadcast_issue_updates, issue.project)
IssuesChannel.broadcast_to(issue, event: 'updated') if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
end
def handle_changes(issue, options)

View File

@ -9,7 +9,7 @@ module Metrics
DASHBOARD_NAME = 'Cluster'
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '9349afc1d96329c08ab478ea0b77db94ee5cc2549b8c754fba67a7f424666b22'
DASHBOARD_VERSION = 'e1a4f8cc2c044cf32273af2cd775eb484729baac0995db687d81d92686bf588e'
SEQUENCE = [
STAGES::ClusterEndpointInserter,

View File

@ -9,7 +9,7 @@ module Metrics
DASHBOARD_NAME = N_('Overview')
# SHA256 hash of dashboard content
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
SEQUENCE = [
STAGES::CommonMetricsInserter,

View File

@ -28,7 +28,7 @@
= f.label :password, class: 'label-bold'
= f.password_field :password, class: "form-control bottom", data: { qa_selector: 'new_user_password_field' }, required: true, pattern: ".{#{@minimum_password_length},}", title: _("Minimum length is %{minimum_password_length} characters.") % { minimum_password_length: @minimum_password_length }
%p.gl-field-hint.text-secondary= _('Minimum length is %{minimum_password_length} characters') % { minimum_password_length: @minimum_password_length }
- if Gitlab::CurrentSettings.current_application_settings.enforce_terms?
- if Gitlab::CurrentSettings.current_application_settings.enforce_terms? && !experiment_enabled?(:terms_opt_in)
.form-group
= check_box_tag :terms_opt_in, '1', false, required: true, data: { qa_selector: 'new_user_accept_terms_checkbox' }
= label_tag :terms_opt_in do
@ -41,5 +41,8 @@
= recaptcha_tags
.submit-container.mt-3
= f.submit _("Register"), class: "btn-register btn btn-block btn-success mb-0 p-2", data: { qa_selector: 'new_user_register_button' }
- if experiment_enabled?(:terms_opt_in)
%p.gl-text-gray-700.gl-mt-5.gl-mb-0
= html_escape(_("By clicking Register, I agree that I have read and accepted the GitLab %{linkStart}Terms of Use and Privacy Policy%{linkEnd}")) % { linkStart: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, linkEnd: '</a>'.html_safe }
- if omniauth_enabled? && button_based_providers_enabled?
= render 'devise/shared/experimental_separate_sign_up_flow_omniauth_box'

View File

@ -0,0 +1,5 @@
---
title: Add target_project_id to merge_request_metrics table
merge_request: 37713
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add index to resource_milestone_events for add actions
merge_request: 37636
author:
type: other

View File

@ -2,12 +2,10 @@ dashboard: 'Cluster health'
priority: 1
panel_groups:
- group: Cluster Health
priority: 10
panels:
- title: "CPU Usage"
type: "area-chart"
y_label: "CPU (cores)"
weight: 1
metrics:
- id: cluster_health_cpu_usage
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
@ -24,7 +22,6 @@ panel_groups:
- title: "Memory Usage"
type: "area-chart"
y_label: "Memory (GiB)"
weight: 1
metrics:
- id: cluster_health_memory_usage
query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'

View File

@ -2,12 +2,10 @@ dashboard: 'Environment metrics'
priority: 1
panel_groups:
- group: System metrics (Kubernetes)
priority: 15
panels:
- title: "Memory Usage (Total)"
type: "area-chart"
y_label: "Total Memory Used (GB)"
weight: 4
metrics:
- id: system_metrics_kubernetes_container_memory_total
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -18,7 +16,6 @@ panel_groups:
- title: "Core Usage (Total)"
type: "area-chart"
y_label: "Total Cores"
weight: 3
metrics:
- id: system_metrics_kubernetes_container_cores_total
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -29,7 +26,6 @@ panel_groups:
- title: "Memory Usage (Pod average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -40,7 +36,6 @@ panel_groups:
- title: "Canary: Memory Usage (Pod Average)"
type: "line-chart"
y_label: "Memory Used per Pod (MB)"
weight: 2
metrics:
- id: system_metrics_kubernetes_container_memory_average_canary
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -52,7 +47,6 @@ panel_groups:
- title: "Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -63,7 +57,6 @@ panel_groups:
- title: "Canary: Core Usage (Pod Average)"
type: "line-chart"
y_label: "Cores per Pod"
weight: 1
metrics:
- id: system_metrics_kubernetes_container_core_usage_canary
# Remove the second metric (after OR) when we drop support for K8s 1.13
@ -75,7 +68,6 @@ panel_groups:
- title: "Knative function invocations"
type: "area-chart"
y_label: "Invocations"
weight: 1
metrics:
- id: system_metrics_knative_function_invocation_count
query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="{{kube_namespace}}", destination_service=~"{{function_name}}.*"}[1m])*60))'
@ -83,12 +75,10 @@ panel_groups:
unit: requests
# NGINX Ingress metrics for pre-0.16.0 versions
- group: Response metrics (NGINX Ingress VTS)
priority: 10
panels:
- title: "Throughput"
type: "area-chart"
y_label: "Requests / Sec"
weight: 1
metrics:
- id: response_metrics_nginx_ingress_throughput_status_code
query_range: 'sum(rate(nginx_upstream_responses_total{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) by (status_code)'
@ -99,7 +89,6 @@ panel_groups:
y_label: "Latency (ms)"
y_axis:
format: milliseconds
weight: 1
metrics:
- id: response_metrics_nginx_ingress_latency_pod_average
query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"})'
@ -110,7 +99,6 @@ panel_groups:
y_label: "HTTP Errors (%)"
y_axis:
format: percentHundred
weight: 1
metrics:
- id: response_metrics_nginx_ingress_http_error_rate
query_range: 'sum(rate(nginx_upstream_responses_total{status_code="5xx", upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) / sum(rate(nginx_upstream_responses_total{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"}[2m])) * 100'
@ -118,12 +106,10 @@ panel_groups:
unit: "%"
# NGINX Ingress metrics for post-0.16.0 versions
- group: Response metrics (NGINX Ingress)
priority: 10
panels:
- title: "Throughput"
type: "area-chart"
y_label: "Requests / Sec"
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_throughput_status_code
query_range: 'sum(label_replace(rate(nginx_ingress_controller_requests{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m]), "status_code", "${1}xx", "status", "(.)..")) by (status_code)'
@ -132,7 +118,6 @@ panel_groups:
- title: "Latency"
type: "area-chart"
y_label: "Latency (ms)"
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_latency_pod_average
query_range: 'sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_sum{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) / sum(rate(nginx_ingress_controller_ingress_upstream_latency_seconds_count{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) * 1000'
@ -141,19 +126,16 @@ panel_groups:
- title: "HTTP Error Rate"
type: "area-chart"
y_label: "HTTP Errors (%)"
weight: 1
metrics:
- id: response_metrics_nginx_ingress_16_http_error_rate
query_range: 'sum(rate(nginx_ingress_controller_requests{status=~"5.*",namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) / sum(rate(nginx_ingress_controller_requests{namespace="{{kube_namespace}}",ingress=~".*{{ci_environment_slug}}.*"}[2m])) * 100'
label: 5xx Errors (%)
unit: "%"
- group: Response metrics (HA Proxy)
priority: 10
panels:
- title: "Throughput"
type: "area-chart"
y_label: "Requests / Sec"
weight: 1
metrics:
- id: response_metrics_ha_proxy_throughput_status_code
query_range: 'sum(rate(haproxy_frontend_http_requests_total{ {{environment_filter}} }[2m])) by (code)'
@ -162,19 +144,16 @@ panel_groups:
- title: "HTTP Error Rate"
type: "area-chart"
y_label: "Error Rate (%)"
weight: 1
metrics:
- id: response_metrics_ha_proxy_http_error_rate
query_range: 'sum(rate(haproxy_frontend_http_responses_total{code="5xx",{{environment_filter}} }[2m])) / sum(rate(haproxy_frontend_http_responses_total{ {{environment_filter}} }[2m]))'
label: HTTP Errors (%)
unit: "%"
- group: Response metrics (AWS ELB)
priority: 10
panels:
- title: "Throughput"
type: "area-chart"
y_label: "Requests / Sec"
weight: 1
metrics:
- id: response_metrics_aws_elb_throughput_requests
query_range: 'sum(aws_elb_request_count_sum{ {{environment_filter}} }) / 60'
@ -183,7 +162,6 @@ panel_groups:
- title: "Latency"
type: "area-chart"
y_label: "Latency (ms)"
weight: 1
metrics:
- id: response_metrics_aws_elb_latency_average
query_range: 'avg(aws_elb_latency_average{ {{environment_filter}} }) * 1000'
@ -192,19 +170,16 @@ panel_groups:
- title: "HTTP Error Rate"
type: "area-chart"
y_label: "Error Rate (%)"
weight: 1
metrics:
- id: response_metrics_aws_elb_http_error_rate
query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{ {{environment_filter}} }) / sum(aws_elb_request_count_sum{ {{environment_filter}} })'
label: HTTP Errors (%)
unit: "%"
- group: Response metrics (NGINX)
priority: 10
panels:
- title: "Throughput"
type: "area-chart"
y_label: "Requests / Sec"
weight: 1
metrics:
- id: response_metrics_nginx_throughput_status_code
query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", {{environment_filter}} }[2m])) by (code)'
@ -213,7 +188,6 @@ panel_groups:
- title: "Latency"
type: "area-chart"
y_label: "Latency (ms)"
weight: 1
metrics:
- id: response_metrics_nginx_latency
query_range: 'avg(nginx_server_requestMsec{ {{environment_filter}} })'
@ -224,7 +198,6 @@ panel_groups:
y_label: "HTTP 500 Errors / Sec"
y_axis:
precision: 0
weight: 1
metrics:
- id: response_metrics_nginx_http_error_rate
query_range: 'sum(rate(nginx_server_requests{code="5xx", {{environment_filter}} }[2m]))'
@ -233,7 +206,6 @@ panel_groups:
- title: "HTTP Error Rate"
type: "area-chart"
y_label: "HTTP Errors (%)"
weight: 1
metrics:
- id: response_metrics_nginx_http_error_percentage
query_range: 'sum(rate(nginx_server_requests{code=~"5.*", host="*", {{environment_filter}} }[2m])) / sum(rate(nginx_server_requests{code="total", host="*", {{environment_filter}} }[2m])) * 100'

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddTargetProjectIdToMrMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :merge_request_metrics, :target_project_id, :integer
end
end
def down
with_lock_retries do
remove_column :merge_request_metrics, :target_project_id, :integer
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class AddFkToMetricsTargetProjectId < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index(:merge_request_metrics, :target_project_id)
add_concurrent_foreign_key(:merge_request_metrics, :projects, column: :target_project_id, on_delete: :cascade)
end
def down
remove_foreign_key(:merge_request_metrics, column: :target_project_id)
remove_concurrent_index(:merge_request_metrics, :target_project_id)
end
end

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddIndexToResourceMilestoneEventsAddEvents < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_resource_milestone_events_on_milestone_id_and_add_action'
ADD_ACTION = '1'
def up
# Index add milestone events
add_concurrent_index :resource_milestone_events, :milestone_id, where: "action = #{ADD_ACTION}", name: INDEX_NAME
end
def down
remove_concurrent_index :resource_milestone_events, :milestone_id, name: INDEX_NAME
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
class ScheduleCopyOfMrTargetProjectIdToMrMetrics < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INTERVAL = 2.minutes.to_i
BATCH_SIZE = 5_000
MIGRATION = 'CopyMergeRequestTargetProjectToMergeRequestMetrics'
disable_ddl_transaction!
class MergeRequest < ActiveRecord::Base
include EachBatch
self.table_name = 'merge_requests'
end
def up
MergeRequest.reset_column_information
queue_background_migration_jobs_by_range_at_intervals(
MergeRequest,
MIGRATION,
INTERVAL,
batch_size: BATCH_SIZE
)
end
def down
# noop
end
end

View File

@ -0,0 +1 @@
630029f7d90da29022404146ce8c488108a2232d2bfd0864a6f5d659f3999af8

View File

@ -0,0 +1 @@
7d6f3601187c98f091cb0c5449ff7c6ca53392f006435223dcc067e4a73dab11

View File

@ -0,0 +1 @@
b3fcb58bbeae8af800a32158a8d272ec524594391e96357fdad955f70864bc95

View File

@ -0,0 +1 @@
5dc4cbfc6d7e79e5909e5250f382bc3c9fa4246b8f2aed81404899aee4eef81b

View File

@ -13038,7 +13038,8 @@ CREATE TABLE public.merge_request_metrics (
first_approved_at timestamp with time zone,
first_reassigned_at timestamp with time zone,
added_lines integer,
removed_lines integer
removed_lines integer,
target_project_id integer
);
CREATE SEQUENCE public.merge_request_metrics_id_seq
@ -19881,6 +19882,8 @@ CREATE INDEX index_merge_request_metrics_on_merged_by_id ON public.merge_request
CREATE INDEX index_merge_request_metrics_on_pipeline_id ON public.merge_request_metrics USING btree (pipeline_id);
CREATE INDEX index_merge_request_metrics_on_target_project_id ON public.merge_request_metrics USING btree (target_project_id);
CREATE UNIQUE INDEX index_merge_request_user_mentions_on_note_id ON public.merge_request_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_merge_requests_closing_issues_on_issue_id ON public.merge_requests_closing_issues USING btree (issue_id);
@ -20465,6 +20468,8 @@ CREATE INDEX index_resource_milestone_events_on_merge_request_id ON public.resou
CREATE INDEX index_resource_milestone_events_on_milestone_id ON public.resource_milestone_events USING btree (milestone_id);
CREATE INDEX index_resource_milestone_events_on_milestone_id_and_add_action ON public.resource_milestone_events USING btree (milestone_id) WHERE (action = 1);
CREATE INDEX index_resource_milestone_events_on_user_id ON public.resource_milestone_events USING btree (user_id);
CREATE INDEX index_resource_state_events_on_epic_id ON public.resource_state_events USING btree (epic_id);
@ -21385,6 +21390,9 @@ ALTER TABLE ONLY public.path_locks
ALTER TABLE ONLY public.clusters_applications_prometheus
ADD CONSTRAINT fk_557e773639 FOREIGN KEY (cluster_id) REFERENCES public.clusters(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.merge_request_metrics
ADD CONSTRAINT fk_56067dcb44 FOREIGN KEY (target_project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.vulnerability_feedback
ADD CONSTRAINT fk_563ff1912e FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE SET NULL;

View File

@ -96,13 +96,13 @@ Example response:
Gets a single feature flag.
```plaintext
GET /projects/:id/feature_flags/:name
GET /projects/:id/feature_flags/:feature_flag_name
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
| `feature_flag_name` | string | yes | The name of the feature flag. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
@ -201,15 +201,16 @@ Example response:
Updates a feature flag.
```plaintext
PUT /projects/:id/feature_flags/:name
PUT /projects/:id/feature_flags/:feature_flag_name
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
| `feature_flag_name` | string | yes | The current name of the feature flag. |
| `description` | string | no | The description of the feature flag. |
| `active` | boolean | no | The active state of the flag. [Supported](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38350) in GitLab 13.3 and later. |
| `name` | string | no | The new name of the feature flag. [Supported](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38350) in GitLab 13.3 and later. |
| `strategies` | JSON | no | The feature flag [strategies](../operations/feature_flags.md#feature-flag-strategies). |
| `strategies:id` | JSON | no | The feature flag strategy id. |
| `strategies:name` | JSON | no | The strategy name. |
@ -275,13 +276,13 @@ Example response:
Deletes a feature flag.
```plaintext
DELETE /projects/:id/feature_flags/:name
DELETE /projects/:id/feature_flags/:feature_flag_name
```
| Attribute | Type | Required | Description |
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
| `name` | string | yes | The name of the feature flag. |
| `feature_flag_name` | string | yes | The name of the feature flag. |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature"

View File

@ -3950,6 +3950,11 @@ type Epic implements Noteable {
"""
last: Int
"""
Filter epics by milestone title, computed from epic's issues
"""
milestoneTitle: String
"""
Search query for epic title or description
"""
@ -5247,6 +5252,11 @@ type Group {
"""
labelName: [String!]
"""
Filter epics by milestone title, computed from epic's issues
"""
milestoneTitle: String
"""
Search query for epic title or description
"""
@ -5324,6 +5334,11 @@ type Group {
"""
last: Int
"""
Filter epics by milestone title, computed from epic's issues
"""
milestoneTitle: String
"""
Search query for epic title or description
"""

View File

@ -11050,6 +11050,16 @@
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Filter epics by milestone title, computed from epic's issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
@ -14693,6 +14703,16 @@
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Filter epics by milestone title, computed from epic's issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",
@ -14822,6 +14842,16 @@
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Filter epics by milestone title, computed from epic's issues",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "iidStartsWith",
"description": "Filter epics by iid for autocomplete",

View File

@ -190,7 +190,7 @@ conversions can be viewed on Google Analytics by navigating to **Behavior > Even
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36576/) in GitLab 13.2 as GitLab Development documentation.
'Good practice' examples demonstrate encouraged ways of writing code while comparing with examples of practices to avoid.
"Good practice" examples demonstrate encouraged ways of writing code while comparing with examples of practices to avoid.
These examples are labeled as "Bad" or "Good".
In GitLab development guidelines, when presenting the cases, it is recommended
to follow a **first-bad-then-good** strategy. First demonstrate the "Bad" practice (how things _could_ be done, which is often still working code),
@ -205,3 +205,8 @@ With many examples being presented, a clear separation helps the reader to go di
Consider offering an explanation (for example, a comment, a link to a resource, etc.) on why something is bad practice.
- Better and best cases can be considered part of the good case(s) code block.
In the same code block, precede each with comments: `# Better` and `# Best`.
NOTE: **Note:**
While the bad-then-good approach is acceptable for the GitLab development guidelines, do not use it
for user documentation. For user documentation, use "Do" and "Don't." For example, see the
[Pajamas Design System](https://design.gitlab.com/content/punctuation).

View File

@ -109,6 +109,15 @@ Key actions for Issues include:
On an issue's page, you can view [all aspects of the issue](issue_data_and_actions.md),
and modify them if you have the necessary [permissions](../../permissions.md).
#### Real-time sidebar **(CORE ONLY)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/17589) in GitLab 13.3.
> - It cannot be enabled or disabled per-project.
> - It's not recommended for production use.
Assignees in the sidebar are updated in real time. This feature is **disabled by default**.
To enable, you need to enable [ActionCable in-app mode](https://docs.gitlab.com/omnibus/settings/actioncable.html).
### Issues list
![Project issues list view](img/project_issues_list_view.png)

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
class CopyMergeRequestTargetProjectToMergeRequestMetrics
extend ::Gitlab::Utils::Override
def perform(start_id, stop_id)
ActiveRecord::Base.connection.execute <<~SQL
WITH merge_requests_batch AS (
SELECT id, target_project_id
FROM merge_requests WHERE id BETWEEN #{Integer(start_id)} AND #{Integer(stop_id)}
)
UPDATE
merge_request_metrics
SET
target_project_id = merge_requests_batch.target_project_id
FROM merge_requests_batch
WHERE merge_request_metrics.merge_request_id=merge_requests_batch.id
SQL
end
end
end
end

View File

@ -53,6 +53,9 @@ module Gitlab
},
new_create_project_ui: {
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
},
terms_opt_in: {
tracking_category: 'Growth::Acquisition::Experiment::TermsOptIn'
}
}.freeze

View File

@ -1629,6 +1629,9 @@ msgstr ""
msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission"
msgstr ""
msgid "Additional Metadata"
msgstr ""
msgid "Additional minutes"
msgstr ""
@ -4109,6 +4112,9 @@ msgstr ""
msgid "By URL"
msgstr ""
msgid "By clicking Register, I agree that I have read and accepted the GitLab %{linkStart}Terms of Use and Privacy Policy%{linkEnd}"
msgstr ""
msgid "By default GitLab sends emails in HTML and plain text formats so mail clients can choose what format to use. Disable this option if you only want to send emails in plain text format."
msgstr ""
@ -15094,6 +15100,12 @@ msgstr ""
msgid "MetricsSettings|User's local timezone"
msgstr ""
msgid "Metrics|1. Define and preview panel"
msgstr ""
msgid "Metrics|2. Paste panel YAML into dashboard"
msgstr ""
msgid "Metrics|Add metric"
msgstr ""
@ -15115,6 +15127,9 @@ msgstr ""
msgid "Metrics|Copy YAML"
msgstr ""
msgid "Metrics|Copy and paste the panel YAML into your dashboard YAML file."
msgstr ""
msgid "Metrics|Create custom dashboard %{fileName}"
msgstr ""
@ -15133,10 +15148,10 @@ msgstr ""
msgid "Metrics|Current"
msgstr ""
msgid "Metrics|Define and preview panel"
msgid "Metrics|Dashboard files can be found in %{codeStart}.gitlab/dashboards%{codeEnd} at the root of this project."
msgstr ""
msgid "Metrics|Define panel YAML to preview panel."
msgid "Metrics|Define panel YAML below to preview panel."
msgstr ""
msgid "Metrics|Delete metric"
@ -16914,6 +16929,12 @@ msgstr ""
msgid "PackageRegistry|Add NuGet Source"
msgstr ""
msgid "PackageRegistry|App group: %{group}"
msgstr ""
msgid "PackageRegistry|App name: %{name}"
msgstr ""
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
msgstr ""
@ -17004,6 +17025,9 @@ msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
msgid "PackageRegistry|License information located at %{link}"
msgstr ""
msgid "PackageRegistry|Manually Published"
msgstr ""
@ -17046,6 +17070,9 @@ msgstr ""
msgid "PackageRegistry|PyPi"
msgstr ""
msgid "PackageRegistry|Recipe: %{recipe}"
msgstr ""
msgid "PackageRegistry|Registry Setup"
msgstr ""
@ -17055,6 +17082,9 @@ msgstr ""
msgid "PackageRegistry|Sorry, your filter produced no results"
msgstr ""
msgid "PackageRegistry|Source project located at %{link}"
msgstr ""
msgid "PackageRegistry|There are no %{packageType} packages yet"
msgstr ""

View File

@ -964,6 +964,33 @@ RSpec.describe Projects::IssuesController do
expect { issue.update(description: [issue.description, labels].join(' ')) }
.not_to exceed_query_limit(control_count + 2 * labels.count)
end
context 'real-time sidebar feature flag' do
using RSpec::Parameterized::TableSyntax
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, project: project) }
where(:action_cable_in_app_enabled, :feature_flag_enabled, :gon_feature_flag) do
true | true | true
true | false | true
false | true | true
false | false | false
end
with_them do
before do
expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
stub_feature_flags(real_time_issue_sidebar: feature_flag_enabled)
end
it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
go(id: issue.to_param)
expect(Gon.features).to include('realTimeIssueSidebar' => gon_feature_flag)
end
end
end
end
describe 'GET #realtime_changes' do

View File

@ -52,6 +52,53 @@ RSpec.describe RegistrationsController do
expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
end
end
context 'with sign up flow and terms_opt_in experiment being enabled' do
before do
stub_experiment(signup_flow: true, terms_opt_in: true)
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Acquisition::Experiment::SignUpFlow',
'start',
label: anything,
property: 'experimental_group'
)
end
context 'when user is not part of the experiment' do
before do
stub_experiment_for_user(signup_flow: true, terms_opt_in: false)
end
it 'tracks event with right parameters' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Acquisition::Experiment::TermsOptIn',
'start',
label: anything,
property: 'control_group'
)
subject
end
end
context 'when user is part of the experiment' do
before do
stub_experiment_for_user(signup_flow: true, terms_opt_in: true)
end
it 'tracks event with right parameters' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Acquisition::Experiment::TermsOptIn',
'start',
label: anything,
property: 'experimental_group'
)
subject
end
end
end
end
describe '#create' do
@ -250,6 +297,37 @@ RSpec.describe RegistrationsController do
expect(subject.current_user).to be_present
expect(subject.current_user.terms_accepted?).to be(true)
end
context 'when experiment terms_opt_in is enabled' do
before do
stub_experiment(terms_opt_in: true)
end
context 'when user is part of the experiment' do
before do
stub_experiment_for_user(terms_opt_in: true)
end
it 'creates the user with accepted terms' do
post :create, params: user_params
expect(subject.current_user).to be_present
expect(subject.current_user.terms_accepted?).to be(true)
end
end
context 'when user is not part of the experiment' do
before do
stub_experiment_for_user(terms_opt_in: false)
end
it 'creates the user without accepted terms' do
post :create, params: user_params
expect(flash[:alert]).to eq(_('You must accept our Terms of Service and privacy policy in order to register an account'))
end
end
end
end
describe 'tracking data' do
@ -281,6 +359,48 @@ RSpec.describe RegistrationsController do
post :create, params: user_params
end
end
context 'with sign up flow and terms_opt_in experiment being enabled' do
subject { post :create, params: user_params }
before do
stub_experiment(signup_flow: true, terms_opt_in: true)
end
context 'when user is not part of the experiment' do
before do
stub_experiment_for_user(signup_flow: true, terms_opt_in: false)
end
it 'tracks event with right parameters' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Acquisition::Experiment::TermsOptIn',
'end',
label: anything,
property: 'control_group'
)
subject
end
end
context 'when user is part of the experiment' do
before do
stub_experiment_for_user(signup_flow: true, terms_opt_in: true)
end
it 'tracks event with right parameters' do
expect(Gitlab::Tracking).to receive(:event).with(
'Growth::Acquisition::Experiment::TermsOptIn',
'end',
label: anything,
property: 'experimental_group'
)
subject
end
end
end
end
it "logs a 'User Created' message" do

View File

@ -509,4 +509,29 @@ RSpec.describe 'With experimental flow' do
expect(page).to have_current_path(new_project_path)
end
end
context 'when terms_opt_in experimental is enabled' do
include TermsHelper
before do
enforce_terms
stub_experiment(signup_flow: true, terms_opt_in: true)
stub_experiment_for_user(signup_flow: true, terms_opt_in: true)
end
it 'terms are checked by default' do
new_user = build_stubbed(:user)
visit new_user_registration_path
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
fill_in 'new_user_password', with: new_user.password
click_button 'Register'
expect(current_path).to eq users_sign_up_welcome_path
end
end
end

View File

@ -18,6 +18,7 @@ import {
import { redirectTo } from '~/lib/utils/url_utility';
const mockProjectPath = 'https://path/to/project';
const mockAddDashboardDocPath = '/doc/add-dashboard';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
@ -362,6 +363,7 @@ describe('Dashboard header', () => {
describe('actions menu modals', () => {
beforeEach(() => {
store.state.monitoringDashboard.projectPath = mockProjectPath;
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
setupAllDashboards(store);
createShallowWrapper();
@ -381,6 +383,9 @@ describe('Dashboard header', () => {
it('"Create new dashboard" modal contains correct buttons', () => {
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
mockAddDashboardDocPath,
);
});
it('"Duplicate Dashboard" opens up a modal', () => {

View File

@ -34,6 +34,8 @@ describe('dashboard invalid url parameters', () => {
const findTxtArea = () => findForm().find(GlFormTextarea);
const findSubmitBtn = () => findForm().find('[type="submit"]');
const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
const findPanel = () => wrapper.find(DashboardPanel);
beforeEach(() => {
@ -108,6 +110,26 @@ describe('dashboard invalid url parameters', () => {
});
});
describe('instructions card', () => {
const mockDocsPath = '/docs-path';
const mockProjectPath = '/project-path';
beforeEach(() => {
store.state.monitoringDashboard.addDashboardDocumentationPath = mockDocsPath;
store.state.monitoringDashboard.projectPath = mockProjectPath;
createComponent();
});
it('displays next actions for the user', () => {
expect(findViewDocumentationBtn().exists()).toBe(true);
expect(findViewDocumentationBtn().attributes('href')).toBe(mockDocsPath);
expect(findOpenRepositoryBtn().exists()).toBe(true);
expect(findOpenRepositoryBtn().attributes('href')).toBe(mockProjectPath);
});
});
describe('when there is an error', () => {
const mockError = 'an error ocurred!';

View File

@ -29,7 +29,6 @@ const datasetState = stateAndPropsFromDataset(
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
export const dashboardProps = {
...datasetState.dataProps,
addDashboardDocumentationPath: 'https://path/to/docs',
alertsEndpoint: null,
};

View File

@ -611,7 +611,6 @@ export const storeVariables = [
export const dashboardHeaderProps = {
defaultBranch: 'master',
addDashboardDocumentationPath: 'https://path/to/docs',
isRearrangingPanels: false,
selectedTimeRange: {
start: '2020-01-01T00:00:00.000Z',

View File

@ -2,7 +2,7 @@
exports[`History Element renders the correct markup 1`] = `
<li
class="timeline-entry system-note note-wrapper gl-my-6!"
class="timeline-entry system-note note-wrapper gl-mb-6!"
>
<div
class="timeline-entry-inner"

View File

@ -0,0 +1,119 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import component from '~/packages/details/components/additional_metadata.vue';
import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
describe('Package Additional Metadata', () => {
let wrapper;
const defaultProps = {
packageEntity: { ...mavenPackage },
};
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: { ...defaultProps, ...props },
stubs: {
DetailsRow,
GlSprintf,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findTitle = () => wrapper.find('[data-testid="title"]');
const findMainArea = () => wrapper.find('[data-testid="main"]');
const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
const findElementLink = container => container.find(GlLink);
it('has the correct title', () => {
mountComponent();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe('Additional Metadata');
});
describe.each`
packageEntity | visible | metadata
${mavenPackage} | ${true} | ${'maven_metadatum'}
${conanPackage} | ${true} | ${'conan_metadatum'}
${nugetPackage} | ${true} | ${'nuget_metadatum'}
${npmPackage} | ${false} | ${null}
`('Component visibility', ({ packageEntity, visible, metadata }) => {
it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
mountComponent({ packageEntity });
expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible);
});
it(`The component is hidden if ${metadata} is missing`, () => {
mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
expect(findTitle().exists()).toBe(false);
expect(findMainArea().exists()).toBe(false);
});
});
describe('nuget metadata', () => {
beforeEach(() => {
mountComponent({ packageEntity: nugetPackage });
});
it.each`
name | finderFunction | text | link | icon
${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'}
`('$name element', ({ finderFunction, text, link, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]);
});
});
describe('conan metadata', () => {
beforeEach(() => {
mountComponent({ packageEntity: conanPackage });
});
it.each`
name | finderFunction | text | icon
${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
});
});
describe('maven metadata', () => {
beforeEach(() => {
mountComponent();
});
it.each`
name | finderFunction | text | icon
${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'}
${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
});
});
});

View File

@ -17,6 +17,7 @@ import NugetInstallation from '~/packages/details/components/nuget_installation.
import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
import DependencyRow from '~/packages/details/components/dependency_row.vue';
import PackageHistory from '~/packages/details/components/package_history.vue';
import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
import PackageActivity from '~/packages/details/components/activity.vue';
import {
conanPackage,
@ -99,6 +100,7 @@ describe('PackagesApp', () => {
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
const dependencyRows = () => wrapper.findAll(DependencyRow);
const findPackageHistory = () => wrapper.find(PackageHistory);
const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
const findPackageActivity = () => wrapper.find(PackageActivity);
const findOldPackageInfo = () => wrapper.find('[data-testid="old-package-info"]');
@ -295,30 +297,38 @@ describe('PackagesApp', () => {
});
});
it('package history has the right props', () => {
createComponent({ oneColumnView: true });
expect(findPackageHistory().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
expect(findPackageHistory().props('projectName')).toEqual(wrapper.vm.projectName);
});
it('additional metadata has the right props', () => {
createComponent({ oneColumnView: true });
expect(findAdditionalMetadata().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
});
describe('one column layout feature flag', () => {
describe.each`
oneColumnView | history | oldInfo | activity
${true} | ${true} | ${false} | ${false}
${false} | ${false} | ${true} | ${true}
`(
'with oneColumnView set to $oneColumnView',
({ oneColumnView, history, oldInfo, activity }) => {
beforeEach(() => {
createComponent({ oneColumnView });
});
describe.each([true, false])('with oneColumnView set to %s', oneColumnView => {
beforeEach(() => {
createComponent({ oneColumnView });
});
it('package history', () => {
expect(findPackageHistory().exists()).toBe(history);
});
it(`is ${oneColumnView} that package history is visible`, () => {
expect(findPackageHistory().exists()).toBe(oneColumnView);
});
it('old info block', () => {
expect(findOldPackageInfo().exists()).toBe(oldInfo);
});
it(`is ${oneColumnView} that additional metadata is visible`, () => {
expect(findAdditionalMetadata().exists()).toBe(oneColumnView);
});
it('package activity', () => {
expect(findPackageActivity().exists()).toBe(activity);
});
},
);
it(`is ${!oneColumnView} that old info block is visible`, () => {
expect(findOldPackageInfo().exists()).toBe(!oneColumnView);
});
it(`is ${!oneColumnView} that package activity is visible`, () => {
expect(findPackageActivity().exists()).toBe(!oneColumnView);
});
});
});
});

View File

@ -1,43 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/details_row.vue';
describe('DetailsRow', () => {
let wrapper;
const findIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const mountComponent = () => {
wrapper = shallowMount(component, {
propsData: {
icon: 'clock',
},
slots: {
default: '<div data-testid="default-slot"></div>',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('contains an icon', () => {
mountComponent();
expect(findIcon().exists()).toBe(true);
});
it('icon has the correct props', () => {
mountComponent();
expect(findIcon().props()).toMatchObject({
name: 'clock',
});
});
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
});
});

View File

@ -5,7 +5,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import DetailsRow from '~/registry/explorer/components/details_page/details_row.vue';
import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,

View File

@ -0,0 +1,71 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import component from '~/registry/shared/components/details_row.vue';
describe('DetailsRow', () => {
let wrapper;
const findIcon = () => wrapper.find(GlIcon);
const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
const mountComponent = props => {
wrapper = shallowMount(component, {
propsData: {
icon: 'clock',
...props,
},
slots: {
default: '<div data-testid="default-slot"></div>',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('has a default slot', () => {
mountComponent();
expect(findDefaultSlot().exists()).toBe(true);
});
describe('icon prop', () => {
it('contains an icon', () => {
mountComponent();
expect(findIcon().exists()).toBe(true);
});
it('icon has the correct props', () => {
mountComponent();
expect(findIcon().props()).toMatchObject({
name: 'clock',
});
});
});
describe('padding prop', () => {
it('padding has a default', () => {
mountComponent();
expect(wrapper.classes('gl-py-2')).toBe(true);
});
it('is reflected in the template', () => {
mountComponent({ padding: 'gl-py-4' });
expect(wrapper.classes('gl-py-4')).toBe(true);
});
});
describe('dashed prop', () => {
const borderClasses = ['gl-border-b-solid', 'gl-border-gray-100', 'gl-border-b-1'];
it('by default component has no border', () => {
mountComponent();
expect(wrapper.classes).not.toEqual(expect.arrayContaining(borderClasses));
});
it('has a border when dashed is true', () => {
mountComponent({ dashed: true });
expect(wrapper.classes()).toEqual(expect.arrayContaining(borderClasses));
});
});
});

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::CopyMergeRequestTargetProjectToMergeRequestMetrics, :migration, schema: 20200723125205 do
let(:migration) { described_class.new }
let_it_be(:namespaces) { table(:namespaces) }
let_it_be(:projects) { table(:projects) }
let_it_be(:merge_requests) { table(:merge_requests) }
let_it_be(:metrics) { table(:merge_request_metrics) }
let!(:namespace) { namespaces.create!(name: 'namespace', path: 'namespace') }
let!(:project_1) { projects.create!(namespace_id: namespace.id) }
let!(:project_2) { projects.create!(namespace_id: namespace.id) }
let!(:merge_request_to_migrate_1) { merge_requests.create!(source_branch: 'a', target_branch: 'b', target_project_id: project_1.id) }
let!(:merge_request_to_migrate_2) { merge_requests.create!(source_branch: 'c', target_branch: 'd', target_project_id: project_2.id) }
let!(:merge_request_without_metrics) { merge_requests.create!(source_branch: 'e', target_branch: 'f', target_project_id: project_2.id) }
let!(:metrics_1) { metrics.create!(merge_request_id: merge_request_to_migrate_1.id) }
let!(:metrics_2) { metrics.create!(merge_request_id: merge_request_to_migrate_2.id) }
let(:merge_request_ids) { [merge_request_to_migrate_1.id, merge_request_to_migrate_2.id, merge_request_without_metrics.id] }
subject { migration.perform(merge_request_ids.min, merge_request_ids.max) }
it 'copies `target_project_id` to the associated `merge_request_metrics` record' do
subject
expect(metrics_1.reload.target_project_id).to eq(project_1.id)
expect(metrics_2.reload.target_project_id).to eq(project_2.id)
end
it 'does not create metrics record when it is missing' do
subject
expect(metrics.find_by_merge_request_id(merge_request_without_metrics.id)).to be_nil
end
end

View File

@ -287,6 +287,7 @@ MergeRequest::Metrics:
- first_approved_at
- first_reassigned_at
- added_lines
- target_project_id
- removed_lines
Ci::Pipeline:
- id

View File

@ -8,4 +8,15 @@ RSpec.describe MergeRequest::Metrics do
it { is_expected.to belong_to(:latest_closed_by).class_name('User') }
it { is_expected.to belong_to(:merged_by).class_name('User') }
end
it 'sets `target_project_id` before save' do
merge_request = create(:merge_request)
metrics = merge_request.metrics
metrics.update_column(:target_project_id, nil)
metrics.save!
expect(metrics.target_project_id).to eq(merge_request.target_project_id)
end
end

View File

@ -247,24 +247,20 @@ RSpec.describe MergeRequest do
describe 'callbacks' do
describe '#ensure_merge_request_metrics' do
it 'creates metrics after saving' do
merge_request = create(:merge_request)
let(:merge_request) { create(:merge_request) }
it 'creates metrics after saving' do
expect(merge_request.metrics).to be_persisted
expect(MergeRequest::Metrics.count).to eq(1)
end
it 'does not duplicate metrics for a merge request' do
merge_request = create(:merge_request)
merge_request.mark_as_merged!
expect(MergeRequest::Metrics.count).to eq(1)
end
it 'does not create duplicated metrics records when MR is concurrently updated' do
merge_request = create(:merge_request)
merge_request.metrics.destroy
instance1 = MergeRequest.find(merge_request.id)
@ -276,6 +272,27 @@ RSpec.describe MergeRequest do
metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
expect(metrics_records.size).to eq(1)
end
it 'syncs the `target_project_id` to the metrics record' do
project = create(:project)
merge_request.update!(target_project: project, state: :closed)
expect(merge_request.target_project_id).to eq(project.id)
expect(merge_request.target_project_id).to eq(merge_request.metrics.target_project_id)
end
context 'when metrics record already exists with NULL target_project_id' do
before do
merge_request.metrics.update_column(:target_project_id, nil)
end
it 'returns the metrics record' do
metrics_record = merge_request.ensure_metrics
expect(metrics_record).to be_persisted
end
end
end
end

View File

@ -840,27 +840,27 @@ RSpec.describe Issues::UpdateService, :mailer do
end
context 'real-time updates' do
using RSpec::Parameterized::TableSyntax
let(:update_params) { { assignee_ids: [user2.id] } }
context 'when broadcast_issue_updates is enabled' do
before do
stub_feature_flags(broadcast_issue_updates: true)
end
it 'broadcasts to the issues channel' do
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
update_issue(update_params)
end
where(:action_cable_in_app_enabled, :feature_flag_enabled, :should_broadcast) do
true | true | true
true | false | true
false | true | true
false | false | false
end
context 'when broadcast_issue_updates is disabled' do
before do
stub_feature_flags(broadcast_issue_updates: false)
end
with_them do
it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
stub_feature_flags(broadcast_issue_updates: feature_flag_enabled)
it 'does not broadcast to the issues channel' do
expect(IssuesChannel).not_to receive(:broadcast_to)
if should_broadcast
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
else
expect(IssuesChannel).not_to receive(:broadcast_to)
end
update_issue(update_params)
end