Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
88ad172d04
commit
25bfb256b3
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 }}
|
||||
|
|
|
@ -80,6 +80,7 @@ export default () => ({
|
|||
projectPath: null,
|
||||
operationsSettingsPath: '',
|
||||
logsPath: invalidUrl,
|
||||
addDashboardDocumentationPath: '',
|
||||
|
||||
// static paths
|
||||
customDashboardBasePath: '',
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -9,7 +9,7 @@ module Metrics
|
|||
DASHBOARD_NAME = 'Cluster'
|
||||
|
||||
# SHA256 hash of dashboard content
|
||||
DASHBOARD_VERSION = '9349afc1d96329c08ab478ea0b77db94ee5cc2549b8c754fba67a7f424666b22'
|
||||
DASHBOARD_VERSION = 'e1a4f8cc2c044cf32273af2cd775eb484729baac0995db687d81d92686bf588e'
|
||||
|
||||
SEQUENCE = [
|
||||
STAGES::ClusterEndpointInserter,
|
||||
|
|
|
@ -9,7 +9,7 @@ module Metrics
|
|||
DASHBOARD_NAME = N_('Overview')
|
||||
|
||||
# SHA256 hash of dashboard content
|
||||
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
|
||||
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
|
||||
|
||||
SEQUENCE = [
|
||||
STAGES::CommonMetricsInserter,
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add target_project_id to merge_request_metrics table
|
||||
merge_request: 37713
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add index to resource_milestone_events for add actions
|
||||
merge_request: 37636
|
||||
author:
|
||||
type: other
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
630029f7d90da29022404146ce8c488108a2232d2bfd0864a6f5d659f3999af8
|
|
@ -0,0 +1 @@
|
|||
7d6f3601187c98f091cb0c5449ff7c6ca53392f006435223dcc067e4a73dab11
|
|
@ -0,0 +1 @@
|
|||
b3fcb58bbeae8af800a32158a8d272ec524594391e96357fdad955f70864bc95
|
|
@ -0,0 +1 @@
|
|||
5dc4cbfc6d7e79e5909e5250f382bc3c9fa4246b8f2aed81404899aee4eef81b
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
"""
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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).
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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!';
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
|
@ -287,6 +287,7 @@ MergeRequest::Metrics:
|
|||
- first_approved_at
|
||||
- first_reassigned_at
|
||||
- added_lines
|
||||
- target_project_id
|
||||
- removed_lines
|
||||
Ci::Pipeline:
|
||||
- id
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue