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,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
addDashboardDocumentationPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
settingsPath: {
|
settingsPath: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -409,7 +405,6 @@ export default {
|
||||||
v-if="showHeader"
|
v-if="showHeader"
|
||||||
ref="prometheusGraphsHeader"
|
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"
|
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"
|
:default-branch="defaultBranch"
|
||||||
:rearrange-panels-available="rearrangePanelsAvailable"
|
:rearrange-panels-available="rearrangePanelsAvailable"
|
||||||
:custom-metrics-available="customMetricsAvailable"
|
:custom-metrics-available="customMetricsAvailable"
|
||||||
|
|
|
@ -107,10 +107,6 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
addDashboardDocumentationPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -128,6 +124,7 @@ export default {
|
||||||
'canAccessOperationsSettings',
|
'canAccessOperationsSettings',
|
||||||
'operationsSettingsPath',
|
'operationsSettingsPath',
|
||||||
'currentDashboard',
|
'currentDashboard',
|
||||||
|
'addDashboardDocumentationPath',
|
||||||
]),
|
]),
|
||||||
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
|
...mapGetters('monitoringDashboard', ['selectedDashboard', 'filteredEnvironments']),
|
||||||
isOutOfTheBoxDashboard() {
|
isOutOfTheBoxDashboard() {
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapState } from 'vuex';
|
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';
|
import DashboardPanel from './dashboard_panel.vue';
|
||||||
|
|
||||||
const initialYml = `title:
|
const initialYml = `title:
|
||||||
|
@ -18,6 +26,7 @@ export default {
|
||||||
GlFormGroup,
|
GlFormGroup,
|
||||||
GlFormTextarea,
|
GlFormTextarea,
|
||||||
GlButton,
|
GlButton,
|
||||||
|
GlSprintf,
|
||||||
GlAlert,
|
GlAlert,
|
||||||
DashboardPanel,
|
DashboardPanel,
|
||||||
},
|
},
|
||||||
|
@ -31,6 +40,8 @@ export default {
|
||||||
'panelPreviewIsLoading',
|
'panelPreviewIsLoading',
|
||||||
'panelPreviewError',
|
'panelPreviewError',
|
||||||
'panelPreviewGraphData',
|
'panelPreviewGraphData',
|
||||||
|
'projectPath',
|
||||||
|
'addDashboardDocumentationPath',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -43,45 +54,91 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<gl-card>
|
<div class="gl-display-flex gl-mx-n3">
|
||||||
<template #header>
|
<gl-card class="gl-flex-grow-1 gl-flex-basis-0 gl-mx-3">
|
||||||
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|Define and preview panel') }}</h2>
|
<template #header>
|
||||||
</template>
|
<h2 class="gl-font-size-h2 gl-my-3">{{ s__('Metrics|1. Define and preview panel') }}</h2>
|
||||||
<template #default>
|
</template>
|
||||||
<gl-form @submit.prevent="onSubmit">
|
<template #default>
|
||||||
<gl-form-group
|
<p>{{ s__('Metrics|Define panel YAML below to preview panel.') }}</p>
|
||||||
:label="s__('Metrics|Panel YAML')"
|
<gl-form @submit.prevent="onSubmit">
|
||||||
:description="s__('Metrics|Define panel YAML to preview panel.')"
|
<gl-form-group :label="s__('Metrics|Panel YAML')" label-for="panel-yml-input">
|
||||||
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
|
<p>
|
||||||
id="panel-yml-input"
|
{{ s__('Metrics|Copy and paste the panel YAML into your dashboard YAML file.') }}
|
||||||
v-model="yml"
|
<br />
|
||||||
class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
|
<gl-sprintf
|
||||||
/>
|
:message="
|
||||||
</gl-form-group>
|
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">
|
<div class="gl-text-right">
|
||||||
<gl-button
|
<gl-button
|
||||||
ref="clipboardCopyBtn"
|
ref="viewDocumentationBtn"
|
||||||
variant="success"
|
|
||||||
category="secondary"
|
category="secondary"
|
||||||
:data-clipboard-text="yml"
|
variant="info"
|
||||||
@click="$toast.show(s__('Metrics|Panel YAML copied'))"
|
target="_blank"
|
||||||
|
:href="addDashboardDocumentationPath"
|
||||||
>
|
>
|
||||||
{{ s__('Metrics|Copy YAML') }}
|
{{ s__('Metrics|View documentation') }}
|
||||||
</gl-button>
|
</gl-button>
|
||||||
<gl-button
|
<gl-button ref="openRepositoryBtn" variant="success" :href="projectPath">
|
||||||
type="submit"
|
{{ s__('Metrics|Open repository') }}
|
||||||
variant="success"
|
|
||||||
:disabled="panelPreviewIsLoading"
|
|
||||||
class="js-no-auto-disable"
|
|
||||||
>
|
|
||||||
{{ s__('Metrics|Preview panel') }}
|
|
||||||
</gl-button>
|
</gl-button>
|
||||||
</div>
|
</div>
|
||||||
</gl-form>
|
</template>
|
||||||
</template>
|
</gl-card>
|
||||||
</gl-card>
|
</div>
|
||||||
|
|
||||||
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
|
<gl-alert v-if="panelPreviewError" variant="warning" :dismissible="false">
|
||||||
{{ panelPreviewError }}
|
{{ panelPreviewError }}
|
||||||
|
|
|
@ -80,6 +80,7 @@ export default () => ({
|
||||||
projectPath: null,
|
projectPath: null,
|
||||||
operationsSettingsPath: '',
|
operationsSettingsPath: '',
|
||||||
logsPath: invalidUrl,
|
logsPath: invalidUrl,
|
||||||
|
addDashboardDocumentationPath: '',
|
||||||
|
|
||||||
// static paths
|
// static paths
|
||||||
customDashboardBasePath: '',
|
customDashboardBasePath: '',
|
||||||
|
|
|
@ -32,6 +32,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
|
||||||
logsPath,
|
logsPath,
|
||||||
currentEnvironmentName,
|
currentEnvironmentName,
|
||||||
customDashboardBasePath,
|
customDashboardBasePath,
|
||||||
|
addDashboardDocumentationPath,
|
||||||
...dataProps
|
...dataProps
|
||||||
} = dataset;
|
} = dataset;
|
||||||
|
|
||||||
|
@ -54,6 +55,7 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
|
||||||
logsPath,
|
logsPath,
|
||||||
currentEnvironmentName,
|
currentEnvironmentName,
|
||||||
customDashboardBasePath,
|
customDashboardBasePath,
|
||||||
|
addDashboardDocumentationPath,
|
||||||
},
|
},
|
||||||
dataProps,
|
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,
|
GlTable,
|
||||||
GlSprintf,
|
GlSprintf,
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
|
import { mapActions, mapState } from 'vuex';
|
||||||
import Tracking from '~/tracking';
|
import Tracking from '~/tracking';
|
||||||
import PackageActivity from './activity.vue';
|
import PackageActivity from './activity.vue';
|
||||||
import PackageHistory from './package_history.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 PackagesListLoader from '../../shared/components/packages_list_loader.vue';
|
||||||
import PackageListRow from '../../shared/components/package_list_row.vue';
|
import PackageListRow from '../../shared/components/package_list_row.vue';
|
||||||
import DependencyRow from './dependency_row.vue';
|
import DependencyRow from './dependency_row.vue';
|
||||||
|
import AdditionalMetadata from './additional_metadata.vue';
|
||||||
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
import { numberToHumanSize } from '~/lib/utils/number_utils';
|
||||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||||
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
import FileIcon from '~/vue_shared/components/file_icon.vue';
|
||||||
|
@ -32,7 +34,6 @@ import { generatePackageInfo } from '../utils';
|
||||||
import { __, s__ } from '~/locale';
|
import { __, s__ } from '~/locale';
|
||||||
import { PackageType, TrackingActions } from '../../shared/constants';
|
import { PackageType, TrackingActions } from '../../shared/constants';
|
||||||
import { packageTypeToTrackCategory } from '../../shared/utils';
|
import { packageTypeToTrackCategory } from '../../shared/utils';
|
||||||
import { mapActions, mapState } from 'vuex';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PackagesApp',
|
name: 'PackagesApp',
|
||||||
|
@ -59,6 +60,7 @@ export default {
|
||||||
PackageListRow,
|
PackageListRow,
|
||||||
DependencyRow,
|
DependencyRow,
|
||||||
PackageHistory,
|
PackageHistory,
|
||||||
|
AdditionalMetadata,
|
||||||
},
|
},
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
|
@ -253,9 +255,12 @@ export default {
|
||||||
<package-activity />
|
<package-activity />
|
||||||
</template>
|
</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
|
<gl-table
|
||||||
:fields="$options.filesTableHeaderFields"
|
:fields="$options.filesTableHeaderFields"
|
||||||
:items="filesTableRows"
|
:items="filesTableRows"
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<div class="timeline-icon">
|
||||||
<gl-icon :name="icon" />
|
<gl-icon :name="icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,8 +44,8 @@ export default {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="issuable-discussion">
|
<div class="issuable-discussion">
|
||||||
<h3 class="gl-ml-6" data-testid="title">{{ __('History') }}</h3>
|
<h3 class="gl-font-lg gl-my-3" data-testid="title">{{ __('History') }}</h3>
|
||||||
<ul class="timeline main-notes-list notes gl-my-4" data-testid="timeline">
|
<ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline">
|
||||||
<history-element icon="clock" data-testid="created-on">
|
<history-element icon="clock" data-testid="created-on">
|
||||||
<gl-sprintf :message="$options.i18n.createdOn">
|
<gl-sprintf :message="$options.i18n.createdOn">
|
||||||
<template #name>
|
<template #name>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import { formatDate } from '~/lib/utils/datetime_utility';
|
import { formatDate } from '~/lib/utils/datetime_utility';
|
||||||
import DeleteButton from '../delete_button.vue';
|
import DeleteButton from '../delete_button.vue';
|
||||||
import ListItem from '../list_item.vue';
|
import ListItem from '../list_item.vue';
|
||||||
import DetailsRow from './details_row.vue';
|
import DetailsRow from '~/registry/shared/components/details_row.vue';
|
||||||
import {
|
import {
|
||||||
REMOVE_TAG_BUTTON_TITLE,
|
REMOVE_TAG_BUTTON_TITLE,
|
||||||
DIGEST_LABEL,
|
DIGEST_LABEL,
|
||||||
|
|
|
@ -10,13 +10,29 @@ export default {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<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" />
|
<gl-icon :name="icon" class="gl-mr-4" />
|
||||||
<span>
|
<span>
|
|
@ -51,7 +51,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
before_action only: :show do
|
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
|
end
|
||||||
|
|
||||||
before_action only: :index do
|
before_action only: :index do
|
||||||
|
|
|
@ -18,6 +18,8 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
def new
|
def new
|
||||||
if experiment_enabled?(:signup_flow)
|
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(: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
|
@resource = build_resource
|
||||||
else
|
else
|
||||||
redirect_to new_user_session_path(anchor: 'register-pane')
|
redirect_to new_user_session_path(anchor: 'register-pane')
|
||||||
|
@ -26,6 +28,7 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
|
|
||||||
def create
|
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(: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
|
accept_pending_invitations
|
||||||
|
|
||||||
|
@ -178,6 +181,8 @@ class RegistrationsController < Devise::RegistrationsController
|
||||||
end
|
end
|
||||||
|
|
||||||
def terms_accepted?
|
def terms_accepted?
|
||||||
|
return true if experiment_enabled?(:terms_opt_in)
|
||||||
|
|
||||||
Gitlab::Utils.to_boolean(params[:terms_opt_in])
|
Gitlab::Utils.to_boolean(params[:terms_opt_in])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ module Issuable
|
||||||
has_many :labels, through: :label_links
|
has_many :labels, through: :label_links
|
||||||
has_many :todos, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
|
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,
|
delegate :name,
|
||||||
:email,
|
:email,
|
||||||
|
|
|
@ -1607,7 +1607,12 @@ class MergeRequest < ApplicationRecord
|
||||||
|
|
||||||
override :ensure_metrics
|
override :ensure_metrics
|
||||||
def 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.
|
# 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.
|
# 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.ensure_metrics
|
||||||
# merge_request.metrics # should return the metrics record and not nil
|
# merge_request.metrics # should return the metrics record and not nil
|
||||||
# merge_request.metrics.merge_request # should return the same MR record
|
# 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
|
metrics_record.association(:merge_request).target = self
|
||||||
association(:metrics).target = metrics_record
|
association(:metrics).target = metrics_record
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class MergeRequest::Metrics < ApplicationRecord
|
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 :pipeline, class_name: 'Ci::Pipeline', foreign_key: :pipeline_id
|
||||||
belongs_to :latest_closed_by, class_name: 'User'
|
belongs_to :latest_closed_by, class_name: 'User'
|
||||||
belongs_to :merged_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
|
end
|
||||||
|
|
||||||
MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
|
MergeRequest::Metrics.prepend_if_ee('EE::MergeRequest::Metrics')
|
||||||
|
|
|
@ -22,7 +22,7 @@ module Issues
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_update(issue)
|
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
|
end
|
||||||
|
|
||||||
def handle_changes(issue, options)
|
def handle_changes(issue, options)
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Metrics
|
||||||
DASHBOARD_NAME = 'Cluster'
|
DASHBOARD_NAME = 'Cluster'
|
||||||
|
|
||||||
# SHA256 hash of dashboard content
|
# SHA256 hash of dashboard content
|
||||||
DASHBOARD_VERSION = '9349afc1d96329c08ab478ea0b77db94ee5cc2549b8c754fba67a7f424666b22'
|
DASHBOARD_VERSION = 'e1a4f8cc2c044cf32273af2cd775eb484729baac0995db687d81d92686bf588e'
|
||||||
|
|
||||||
SEQUENCE = [
|
SEQUENCE = [
|
||||||
STAGES::ClusterEndpointInserter,
|
STAGES::ClusterEndpointInserter,
|
||||||
|
|
|
@ -9,7 +9,7 @@ module Metrics
|
||||||
DASHBOARD_NAME = N_('Overview')
|
DASHBOARD_NAME = N_('Overview')
|
||||||
|
|
||||||
# SHA256 hash of dashboard content
|
# SHA256 hash of dashboard content
|
||||||
DASHBOARD_VERSION = '4685fe386c25b1a786b3be18f79bb2ee9828019003e003816284cdb634fa3e13'
|
DASHBOARD_VERSION = 'ce9ae27d2913f637de851d61099bc4151583eae68b1386a2176339ef6e653223'
|
||||||
|
|
||||||
SEQUENCE = [
|
SEQUENCE = [
|
||||||
STAGES::CommonMetricsInserter,
|
STAGES::CommonMetricsInserter,
|
||||||
|
|
|
@ -28,7 +28,7 @@
|
||||||
= f.label :password, class: 'label-bold'
|
= 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 }
|
= 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 }
|
%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
|
.form-group
|
||||||
= check_box_tag :terms_opt_in, '1', false, required: true, data: { qa_selector: 'new_user_accept_terms_checkbox' }
|
= check_box_tag :terms_opt_in, '1', false, required: true, data: { qa_selector: 'new_user_accept_terms_checkbox' }
|
||||||
= label_tag :terms_opt_in do
|
= label_tag :terms_opt_in do
|
||||||
|
@ -41,5 +41,8 @@
|
||||||
= recaptcha_tags
|
= recaptcha_tags
|
||||||
.submit-container.mt-3
|
.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' }
|
= 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?
|
- if omniauth_enabled? && button_based_providers_enabled?
|
||||||
= render 'devise/shared/experimental_separate_sign_up_flow_omniauth_box'
|
= 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
|
priority: 1
|
||||||
panel_groups:
|
panel_groups:
|
||||||
- group: Cluster Health
|
- group: Cluster Health
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "CPU Usage"
|
- title: "CPU Usage"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "CPU (cores)"
|
y_label: "CPU (cores)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: cluster_health_cpu_usage
|
- id: cluster_health_cpu_usage
|
||||||
query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{id="/"}[15m])) by (job)) without (job)'
|
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"
|
- title: "Memory Usage"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Memory (GiB)"
|
y_label: "Memory (GiB)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: cluster_health_memory_usage
|
- id: cluster_health_memory_usage
|
||||||
query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
|
query_range: 'avg(sum(container_memory_usage_bytes{id="/"}) by (job)) without (job) / 2^30'
|
||||||
|
|
|
@ -2,12 +2,10 @@ dashboard: 'Environment metrics'
|
||||||
priority: 1
|
priority: 1
|
||||||
panel_groups:
|
panel_groups:
|
||||||
- group: System metrics (Kubernetes)
|
- group: System metrics (Kubernetes)
|
||||||
priority: 15
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Memory Usage (Total)"
|
- title: "Memory Usage (Total)"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Total Memory Used (GB)"
|
y_label: "Total Memory Used (GB)"
|
||||||
weight: 4
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_memory_total
|
- id: system_metrics_kubernetes_container_memory_total
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
||||||
|
@ -18,7 +16,6 @@ panel_groups:
|
||||||
- title: "Core Usage (Total)"
|
- title: "Core Usage (Total)"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Total Cores"
|
y_label: "Total Cores"
|
||||||
weight: 3
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_cores_total
|
- id: system_metrics_kubernetes_container_cores_total
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# 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)"
|
- title: "Memory Usage (Pod average)"
|
||||||
type: "line-chart"
|
type: "line-chart"
|
||||||
y_label: "Memory Used per Pod (MB)"
|
y_label: "Memory Used per Pod (MB)"
|
||||||
weight: 2
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_memory_average
|
- id: system_metrics_kubernetes_container_memory_average
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# 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)"
|
- title: "Canary: Memory Usage (Pod Average)"
|
||||||
type: "line-chart"
|
type: "line-chart"
|
||||||
y_label: "Memory Used per Pod (MB)"
|
y_label: "Memory Used per Pod (MB)"
|
||||||
weight: 2
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_memory_average_canary
|
- id: system_metrics_kubernetes_container_memory_average_canary
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# 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)"
|
- title: "Core Usage (Pod Average)"
|
||||||
type: "line-chart"
|
type: "line-chart"
|
||||||
y_label: "Cores per Pod"
|
y_label: "Cores per Pod"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_core_usage
|
- id: system_metrics_kubernetes_container_core_usage
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# 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)"
|
- title: "Canary: Core Usage (Pod Average)"
|
||||||
type: "line-chart"
|
type: "line-chart"
|
||||||
y_label: "Cores per Pod"
|
y_label: "Cores per Pod"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_kubernetes_container_core_usage_canary
|
- id: system_metrics_kubernetes_container_core_usage_canary
|
||||||
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
# Remove the second metric (after OR) when we drop support for K8s 1.13
|
||||||
|
@ -75,7 +68,6 @@ panel_groups:
|
||||||
- title: "Knative function invocations"
|
- title: "Knative function invocations"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Invocations"
|
y_label: "Invocations"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: system_metrics_knative_function_invocation_count
|
- 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))'
|
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
|
unit: requests
|
||||||
# NGINX Ingress metrics for pre-0.16.0 versions
|
# NGINX Ingress metrics for pre-0.16.0 versions
|
||||||
- group: Response metrics (NGINX Ingress VTS)
|
- group: Response metrics (NGINX Ingress VTS)
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Throughput"
|
- title: "Throughput"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Requests / Sec"
|
y_label: "Requests / Sec"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_throughput_status_code
|
- 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)'
|
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_label: "Latency (ms)"
|
||||||
y_axis:
|
y_axis:
|
||||||
format: milliseconds
|
format: milliseconds
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_latency_pod_average
|
- id: response_metrics_nginx_ingress_latency_pod_average
|
||||||
query_range: 'avg(nginx_upstream_response_msecs_avg{upstream=~"{{kube_namespace}}-{{ci_environment_slug}}-.*"})'
|
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_label: "HTTP Errors (%)"
|
||||||
y_axis:
|
y_axis:
|
||||||
format: percentHundred
|
format: percentHundred
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_http_error_rate
|
- 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'
|
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: "%"
|
unit: "%"
|
||||||
# NGINX Ingress metrics for post-0.16.0 versions
|
# NGINX Ingress metrics for post-0.16.0 versions
|
||||||
- group: Response metrics (NGINX Ingress)
|
- group: Response metrics (NGINX Ingress)
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Throughput"
|
- title: "Throughput"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Requests / Sec"
|
y_label: "Requests / Sec"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_16_throughput_status_code
|
- 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)'
|
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"
|
- title: "Latency"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Latency (ms)"
|
y_label: "Latency (ms)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_16_latency_pod_average
|
- 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'
|
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"
|
- title: "HTTP Error Rate"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "HTTP Errors (%)"
|
y_label: "HTTP Errors (%)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_ingress_16_http_error_rate
|
- 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'
|
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 (%)
|
label: 5xx Errors (%)
|
||||||
unit: "%"
|
unit: "%"
|
||||||
- group: Response metrics (HA Proxy)
|
- group: Response metrics (HA Proxy)
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Throughput"
|
- title: "Throughput"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Requests / Sec"
|
y_label: "Requests / Sec"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_ha_proxy_throughput_status_code
|
- id: response_metrics_ha_proxy_throughput_status_code
|
||||||
query_range: 'sum(rate(haproxy_frontend_http_requests_total{ {{environment_filter}} }[2m])) by (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"
|
- title: "HTTP Error Rate"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Error Rate (%)"
|
y_label: "Error Rate (%)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_ha_proxy_http_error_rate
|
- 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]))'
|
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 (%)
|
label: HTTP Errors (%)
|
||||||
unit: "%"
|
unit: "%"
|
||||||
- group: Response metrics (AWS ELB)
|
- group: Response metrics (AWS ELB)
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Throughput"
|
- title: "Throughput"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Requests / Sec"
|
y_label: "Requests / Sec"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_aws_elb_throughput_requests
|
- id: response_metrics_aws_elb_throughput_requests
|
||||||
query_range: 'sum(aws_elb_request_count_sum{ {{environment_filter}} }) / 60'
|
query_range: 'sum(aws_elb_request_count_sum{ {{environment_filter}} }) / 60'
|
||||||
|
@ -183,7 +162,6 @@ panel_groups:
|
||||||
- title: "Latency"
|
- title: "Latency"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Latency (ms)"
|
y_label: "Latency (ms)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_aws_elb_latency_average
|
- id: response_metrics_aws_elb_latency_average
|
||||||
query_range: 'avg(aws_elb_latency_average{ {{environment_filter}} }) * 1000'
|
query_range: 'avg(aws_elb_latency_average{ {{environment_filter}} }) * 1000'
|
||||||
|
@ -192,19 +170,16 @@ panel_groups:
|
||||||
- title: "HTTP Error Rate"
|
- title: "HTTP Error Rate"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Error Rate (%)"
|
y_label: "Error Rate (%)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_aws_elb_http_error_rate
|
- 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}} })'
|
query_range: 'sum(aws_elb_httpcode_backend_5_xx_sum{ {{environment_filter}} }) / sum(aws_elb_request_count_sum{ {{environment_filter}} })'
|
||||||
label: HTTP Errors (%)
|
label: HTTP Errors (%)
|
||||||
unit: "%"
|
unit: "%"
|
||||||
- group: Response metrics (NGINX)
|
- group: Response metrics (NGINX)
|
||||||
priority: 10
|
|
||||||
panels:
|
panels:
|
||||||
- title: "Throughput"
|
- title: "Throughput"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Requests / Sec"
|
y_label: "Requests / Sec"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_throughput_status_code
|
- id: response_metrics_nginx_throughput_status_code
|
||||||
query_range: 'sum(rate(nginx_server_requests{server_zone!="*", server_zone!="_", {{environment_filter}} }[2m])) by (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"
|
- title: "Latency"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "Latency (ms)"
|
y_label: "Latency (ms)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_latency
|
- id: response_metrics_nginx_latency
|
||||||
query_range: 'avg(nginx_server_requestMsec{ {{environment_filter}} })'
|
query_range: 'avg(nginx_server_requestMsec{ {{environment_filter}} })'
|
||||||
|
@ -224,7 +198,6 @@ panel_groups:
|
||||||
y_label: "HTTP 500 Errors / Sec"
|
y_label: "HTTP 500 Errors / Sec"
|
||||||
y_axis:
|
y_axis:
|
||||||
precision: 0
|
precision: 0
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_http_error_rate
|
- id: response_metrics_nginx_http_error_rate
|
||||||
query_range: 'sum(rate(nginx_server_requests{code="5xx", {{environment_filter}} }[2m]))'
|
query_range: 'sum(rate(nginx_server_requests{code="5xx", {{environment_filter}} }[2m]))'
|
||||||
|
@ -233,7 +206,6 @@ panel_groups:
|
||||||
- title: "HTTP Error Rate"
|
- title: "HTTP Error Rate"
|
||||||
type: "area-chart"
|
type: "area-chart"
|
||||||
y_label: "HTTP Errors (%)"
|
y_label: "HTTP Errors (%)"
|
||||||
weight: 1
|
|
||||||
metrics:
|
metrics:
|
||||||
- id: response_metrics_nginx_http_error_percentage
|
- 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'
|
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_approved_at timestamp with time zone,
|
||||||
first_reassigned_at timestamp with time zone,
|
first_reassigned_at timestamp with time zone,
|
||||||
added_lines integer,
|
added_lines integer,
|
||||||
removed_lines integer
|
removed_lines integer,
|
||||||
|
target_project_id integer
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE SEQUENCE public.merge_request_metrics_id_seq
|
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_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 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);
|
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 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_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);
|
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
|
ALTER TABLE ONLY public.clusters_applications_prometheus
|
||||||
ADD CONSTRAINT fk_557e773639 FOREIGN KEY (cluster_id) REFERENCES public.clusters(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY public.vulnerability_feedback
|
||||||
ADD CONSTRAINT fk_563ff1912e FOREIGN KEY (merge_request_id) REFERENCES public.merge_requests(id) ON DELETE SET NULL;
|
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.
|
Gets a single feature flag.
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
GET /projects/:id/feature_flags/:name
|
GET /projects/:id/feature_flags/:feature_flag_name
|
||||||
```
|
```
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
| `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
|
```shell
|
||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature
|
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.
|
Updates a feature flag.
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
PUT /projects/:id/feature_flags/:name
|
PUT /projects/:id/feature_flags/:feature_flag_name
|
||||||
```
|
```
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
| `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. |
|
| `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. |
|
| `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` | JSON | no | The feature flag [strategies](../operations/feature_flags.md#feature-flag-strategies). |
|
||||||
| `strategies:id` | JSON | no | The feature flag strategy id. |
|
| `strategies:id` | JSON | no | The feature flag strategy id. |
|
||||||
| `strategies:name` | JSON | no | The strategy name. |
|
| `strategies:name` | JSON | no | The strategy name. |
|
||||||
|
@ -275,13 +276,13 @@ Example response:
|
||||||
Deletes a feature flag.
|
Deletes a feature flag.
|
||||||
|
|
||||||
```plaintext
|
```plaintext
|
||||||
DELETE /projects/:id/feature_flags/:name
|
DELETE /projects/:id/feature_flags/:feature_flag_name
|
||||||
```
|
```
|
||||||
|
|
||||||
| Attribute | Type | Required | Description |
|
| Attribute | Type | Required | Description |
|
||||||
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
| ------------------- | ---------------- | ---------- | ---------------------------------------------------------------------------------------|
|
||||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding). |
|
| `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
|
```shell
|
||||||
curl --header "PRIVATE-TOKEN: <your_access_token>" --request DELETE "https://gitlab.example.com/api/v4/projects/1/feature_flags/awesome_feature"
|
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
|
last: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter epics by milestone title, computed from epic's issues
|
||||||
|
"""
|
||||||
|
milestoneTitle: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Search query for epic title or description
|
Search query for epic title or description
|
||||||
"""
|
"""
|
||||||
|
@ -5247,6 +5252,11 @@ type Group {
|
||||||
"""
|
"""
|
||||||
labelName: [String!]
|
labelName: [String!]
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter epics by milestone title, computed from epic's issues
|
||||||
|
"""
|
||||||
|
milestoneTitle: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Search query for epic title or description
|
Search query for epic title or description
|
||||||
"""
|
"""
|
||||||
|
@ -5324,6 +5334,11 @@ type Group {
|
||||||
"""
|
"""
|
||||||
last: Int
|
last: Int
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filter epics by milestone title, computed from epic's issues
|
||||||
|
"""
|
||||||
|
milestoneTitle: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Search query for epic title or description
|
Search query for epic title or description
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11050,6 +11050,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"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",
|
"name": "iidStartsWith",
|
||||||
"description": "Filter epics by iid for autocomplete",
|
"description": "Filter epics by iid for autocomplete",
|
||||||
|
@ -14693,6 +14703,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"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",
|
"name": "iidStartsWith",
|
||||||
"description": "Filter epics by iid for autocomplete",
|
"description": "Filter epics by iid for autocomplete",
|
||||||
|
@ -14822,6 +14842,16 @@
|
||||||
},
|
},
|
||||||
"defaultValue": null
|
"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",
|
"name": "iidStartsWith",
|
||||||
"description": "Filter epics by iid for autocomplete",
|
"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.
|
> [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".
|
These examples are labeled as "Bad" or "Good".
|
||||||
In GitLab development guidelines, when presenting the cases, it is recommended
|
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),
|
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.
|
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.
|
- 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`.
|
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),
|
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).
|
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
|
### Issues list
|
||||||
|
|
||||||
![Project issues list view](img/project_issues_list_view.png)
|
![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: {
|
new_create_project_ui: {
|
||||||
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
|
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
|
||||||
|
},
|
||||||
|
terms_opt_in: {
|
||||||
|
tracking_category: 'Growth::Acquisition::Experiment::TermsOptIn'
|
||||||
}
|
}
|
||||||
}.freeze
|
}.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"
|
msgid "Adding new applications is disabled in your GitLab instance. Please contact your GitLab administrator to get the permission"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Additional Metadata"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Additional minutes"
|
msgid "Additional minutes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -4109,6 +4112,9 @@ msgstr ""
|
||||||
msgid "By URL"
|
msgid "By URL"
|
||||||
msgstr ""
|
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."
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15094,6 +15100,12 @@ msgstr ""
|
||||||
msgid "MetricsSettings|User's local timezone"
|
msgid "MetricsSettings|User's local timezone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Metrics|1. Define and preview panel"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Metrics|2. Paste panel YAML into dashboard"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|Add metric"
|
msgid "Metrics|Add metric"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15115,6 +15127,9 @@ msgstr ""
|
||||||
msgid "Metrics|Copy YAML"
|
msgid "Metrics|Copy YAML"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Metrics|Copy and paste the panel YAML into your dashboard YAML file."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|Create custom dashboard %{fileName}"
|
msgid "Metrics|Create custom dashboard %{fileName}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15133,10 +15148,10 @@ msgstr ""
|
||||||
msgid "Metrics|Current"
|
msgid "Metrics|Current"
|
||||||
msgstr ""
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|Define panel YAML to preview panel."
|
msgid "Metrics|Define panel YAML below to preview panel."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Metrics|Delete metric"
|
msgid "Metrics|Delete metric"
|
||||||
|
@ -16914,6 +16929,12 @@ msgstr ""
|
||||||
msgid "PackageRegistry|Add NuGet Source"
|
msgid "PackageRegistry|Add NuGet Source"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|App group: %{group}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|App name: %{name}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
|
msgid "PackageRegistry|Commit %{link} on branch %{branch}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17004,6 +17025,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
|
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|License information located at %{link}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Manually Published"
|
msgid "PackageRegistry|Manually Published"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17046,6 +17070,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|PyPi"
|
msgid "PackageRegistry|PyPi"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|Recipe: %{recipe}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|Registry Setup"
|
msgid "PackageRegistry|Registry Setup"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -17055,6 +17082,9 @@ msgstr ""
|
||||||
msgid "PackageRegistry|Sorry, your filter produced no results"
|
msgid "PackageRegistry|Sorry, your filter produced no results"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "PackageRegistry|Source project located at %{link}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "PackageRegistry|There are no %{packageType} packages yet"
|
msgid "PackageRegistry|There are no %{packageType} packages yet"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -964,6 +964,33 @@ RSpec.describe Projects::IssuesController do
|
||||||
expect { issue.update(description: [issue.description, labels].join(' ')) }
|
expect { issue.update(description: [issue.description, labels].join(' ')) }
|
||||||
.not_to exceed_query_limit(control_count + 2 * labels.count)
|
.not_to exceed_query_limit(control_count + 2 * labels.count)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe 'GET #realtime_changes' do
|
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'))
|
expect(response).to redirect_to(new_user_session_path(anchor: 'register-pane'))
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
describe '#create' do
|
describe '#create' do
|
||||||
|
@ -250,6 +297,37 @@ RSpec.describe RegistrationsController do
|
||||||
expect(subject.current_user).to be_present
|
expect(subject.current_user).to be_present
|
||||||
expect(subject.current_user.terms_accepted?).to be(true)
|
expect(subject.current_user.terms_accepted?).to be(true)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe 'tracking data' do
|
describe 'tracking data' do
|
||||||
|
@ -281,6 +359,48 @@ RSpec.describe RegistrationsController do
|
||||||
post :create, params: user_params
|
post :create, params: user_params
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
it "logs a 'User Created' message" do
|
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)
|
expect(page).to have_current_path(new_project_path)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
|
@ -18,6 +18,7 @@ import {
|
||||||
import { redirectTo } from '~/lib/utils/url_utility';
|
import { redirectTo } from '~/lib/utils/url_utility';
|
||||||
|
|
||||||
const mockProjectPath = 'https://path/to/project';
|
const mockProjectPath = 'https://path/to/project';
|
||||||
|
const mockAddDashboardDocPath = '/doc/add-dashboard';
|
||||||
|
|
||||||
jest.mock('~/lib/utils/url_utility', () => ({
|
jest.mock('~/lib/utils/url_utility', () => ({
|
||||||
redirectTo: jest.fn(),
|
redirectTo: jest.fn(),
|
||||||
|
@ -362,6 +363,7 @@ describe('Dashboard header', () => {
|
||||||
describe('actions menu modals', () => {
|
describe('actions menu modals', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
store.state.monitoringDashboard.projectPath = mockProjectPath;
|
||||||
|
store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
|
||||||
setupAllDashboards(store);
|
setupAllDashboards(store);
|
||||||
|
|
||||||
createShallowWrapper();
|
createShallowWrapper();
|
||||||
|
@ -381,6 +383,9 @@ describe('Dashboard header', () => {
|
||||||
|
|
||||||
it('"Create new dashboard" modal contains correct buttons', () => {
|
it('"Create new dashboard" modal contains correct buttons', () => {
|
||||||
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
|
expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
|
||||||
|
expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
|
||||||
|
mockAddDashboardDocPath,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('"Duplicate Dashboard" opens up a modal', () => {
|
it('"Duplicate Dashboard" opens up a modal', () => {
|
||||||
|
|
|
@ -34,6 +34,8 @@ describe('dashboard invalid url parameters', () => {
|
||||||
const findTxtArea = () => findForm().find(GlFormTextarea);
|
const findTxtArea = () => findForm().find(GlFormTextarea);
|
||||||
const findSubmitBtn = () => findForm().find('[type="submit"]');
|
const findSubmitBtn = () => findForm().find('[type="submit"]');
|
||||||
const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
|
const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
|
||||||
|
const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
|
||||||
|
const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
|
||||||
const findPanel = () => wrapper.find(DashboardPanel);
|
const findPanel = () => wrapper.find(DashboardPanel);
|
||||||
|
|
||||||
beforeEach(() => {
|
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', () => {
|
describe('when there is an error', () => {
|
||||||
const mockError = 'an error ocurred!';
|
const mockError = 'an error ocurred!';
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,6 @@ const datasetState = stateAndPropsFromDataset(
|
||||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
|
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
|
||||||
export const dashboardProps = {
|
export const dashboardProps = {
|
||||||
...datasetState.dataProps,
|
...datasetState.dataProps,
|
||||||
addDashboardDocumentationPath: 'https://path/to/docs',
|
|
||||||
alertsEndpoint: null,
|
alertsEndpoint: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -611,7 +611,6 @@ export const storeVariables = [
|
||||||
|
|
||||||
export const dashboardHeaderProps = {
|
export const dashboardHeaderProps = {
|
||||||
defaultBranch: 'master',
|
defaultBranch: 'master',
|
||||||
addDashboardDocumentationPath: 'https://path/to/docs',
|
|
||||||
isRearrangingPanels: false,
|
isRearrangingPanels: false,
|
||||||
selectedTimeRange: {
|
selectedTimeRange: {
|
||||||
start: '2020-01-01T00:00:00.000Z',
|
start: '2020-01-01T00:00:00.000Z',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
exports[`History Element renders the correct markup 1`] = `
|
exports[`History Element renders the correct markup 1`] = `
|
||||||
<li
|
<li
|
||||||
class="timeline-entry system-note note-wrapper gl-my-6!"
|
class="timeline-entry system-note note-wrapper gl-mb-6!"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="timeline-entry-inner"
|
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 PypiInstallation from '~/packages/details/components/pypi_installation.vue';
|
||||||
import DependencyRow from '~/packages/details/components/dependency_row.vue';
|
import DependencyRow from '~/packages/details/components/dependency_row.vue';
|
||||||
import PackageHistory from '~/packages/details/components/package_history.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 PackageActivity from '~/packages/details/components/activity.vue';
|
||||||
import {
|
import {
|
||||||
conanPackage,
|
conanPackage,
|
||||||
|
@ -99,6 +100,7 @@ describe('PackagesApp', () => {
|
||||||
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
|
const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
|
||||||
const dependencyRows = () => wrapper.findAll(DependencyRow);
|
const dependencyRows = () => wrapper.findAll(DependencyRow);
|
||||||
const findPackageHistory = () => wrapper.find(PackageHistory);
|
const findPackageHistory = () => wrapper.find(PackageHistory);
|
||||||
|
const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
|
||||||
const findPackageActivity = () => wrapper.find(PackageActivity);
|
const findPackageActivity = () => wrapper.find(PackageActivity);
|
||||||
const findOldPackageInfo = () => wrapper.find('[data-testid="old-package-info"]');
|
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('one column layout feature flag', () => {
|
||||||
describe.each`
|
describe.each([true, false])('with oneColumnView set to %s', oneColumnView => {
|
||||||
oneColumnView | history | oldInfo | activity
|
beforeEach(() => {
|
||||||
${true} | ${true} | ${false} | ${false}
|
createComponent({ oneColumnView });
|
||||||
${false} | ${false} | ${true} | ${true}
|
});
|
||||||
`(
|
|
||||||
'with oneColumnView set to $oneColumnView',
|
|
||||||
({ oneColumnView, history, oldInfo, activity }) => {
|
|
||||||
beforeEach(() => {
|
|
||||||
createComponent({ oneColumnView });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('package history', () => {
|
it(`is ${oneColumnView} that package history is visible`, () => {
|
||||||
expect(findPackageHistory().exists()).toBe(history);
|
expect(findPackageHistory().exists()).toBe(oneColumnView);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('old info block', () => {
|
it(`is ${oneColumnView} that additional metadata is visible`, () => {
|
||||||
expect(findOldPackageInfo().exists()).toBe(oldInfo);
|
expect(findAdditionalMetadata().exists()).toBe(oneColumnView);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('package activity', () => {
|
it(`is ${!oneColumnView} that old info block is visible`, () => {
|
||||||
expect(findPackageActivity().exists()).toBe(activity);
|
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 TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
|
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
|
||||||
import DeleteButton from '~/registry/explorer/components/delete_button.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 {
|
import {
|
||||||
REMOVE_TAG_BUTTON_TITLE,
|
REMOVE_TAG_BUTTON_TITLE,
|
||||||
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
|
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_approved_at
|
||||||
- first_reassigned_at
|
- first_reassigned_at
|
||||||
- added_lines
|
- added_lines
|
||||||
|
- target_project_id
|
||||||
- removed_lines
|
- removed_lines
|
||||||
Ci::Pipeline:
|
Ci::Pipeline:
|
||||||
- id
|
- 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(:latest_closed_by).class_name('User') }
|
||||||
it { is_expected.to belong_to(:merged_by).class_name('User') }
|
it { is_expected.to belong_to(:merged_by).class_name('User') }
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -247,24 +247,20 @@ RSpec.describe MergeRequest do
|
||||||
|
|
||||||
describe 'callbacks' do
|
describe 'callbacks' do
|
||||||
describe '#ensure_merge_request_metrics' do
|
describe '#ensure_merge_request_metrics' do
|
||||||
it 'creates metrics after saving' do
|
let(:merge_request) { create(:merge_request) }
|
||||||
merge_request = create(:merge_request)
|
|
||||||
|
|
||||||
|
it 'creates metrics after saving' do
|
||||||
expect(merge_request.metrics).to be_persisted
|
expect(merge_request.metrics).to be_persisted
|
||||||
expect(MergeRequest::Metrics.count).to eq(1)
|
expect(MergeRequest::Metrics.count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not duplicate metrics for a merge request' do
|
it 'does not duplicate metrics for a merge request' do
|
||||||
merge_request = create(:merge_request)
|
|
||||||
|
|
||||||
merge_request.mark_as_merged!
|
merge_request.mark_as_merged!
|
||||||
|
|
||||||
expect(MergeRequest::Metrics.count).to eq(1)
|
expect(MergeRequest::Metrics.count).to eq(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not create duplicated metrics records when MR is concurrently updated' do
|
it 'does not create duplicated metrics records when MR is concurrently updated' do
|
||||||
merge_request = create(:merge_request)
|
|
||||||
|
|
||||||
merge_request.metrics.destroy
|
merge_request.metrics.destroy
|
||||||
|
|
||||||
instance1 = MergeRequest.find(merge_request.id)
|
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)
|
metrics_records = MergeRequest::Metrics.where(merge_request_id: merge_request.id)
|
||||||
expect(metrics_records.size).to eq(1)
|
expect(metrics_records.size).to eq(1)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -840,27 +840,27 @@ RSpec.describe Issues::UpdateService, :mailer do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'real-time updates' do
|
context 'real-time updates' do
|
||||||
|
using RSpec::Parameterized::TableSyntax
|
||||||
|
|
||||||
let(:update_params) { { assignee_ids: [user2.id] } }
|
let(:update_params) { { assignee_ids: [user2.id] } }
|
||||||
|
|
||||||
context 'when broadcast_issue_updates is enabled' do
|
where(:action_cable_in_app_enabled, :feature_flag_enabled, :should_broadcast) do
|
||||||
before do
|
true | true | true
|
||||||
stub_feature_flags(broadcast_issue_updates: true)
|
true | false | true
|
||||||
end
|
false | true | true
|
||||||
|
false | false | false
|
||||||
it 'broadcasts to the issues channel' do
|
|
||||||
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
|
|
||||||
|
|
||||||
update_issue(update_params)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when broadcast_issue_updates is disabled' do
|
with_them do
|
||||||
before do
|
it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
|
||||||
stub_feature_flags(broadcast_issue_updates: false)
|
expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
|
||||||
end
|
stub_feature_flags(broadcast_issue_updates: feature_flag_enabled)
|
||||||
|
|
||||||
it 'does not broadcast to the issues channel' do
|
if should_broadcast
|
||||||
expect(IssuesChannel).not_to receive(:broadcast_to)
|
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
|
||||||
|
else
|
||||||
|
expect(IssuesChannel).not_to receive(:broadcast_to)
|
||||||
|
end
|
||||||
|
|
||||||
update_issue(update_params)
|
update_issue(update_params)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue