Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2025-03-17 06:12:12 +00:00
parent 5bc7b9357e
commit 1b8bee4713
19 changed files with 302 additions and 495 deletions

View File

@ -1 +1 @@
2d62ba38be14d081d99e91d4c44e6371f9deddde
7d35879381a67a26332ecdd66392a2ad30bf64ca

View File

@ -1,31 +1,19 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { InternalEvents } from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineCharts from './pipeline_charts.vue';
import PipelineChartsNew from './pipeline_charts_new.vue';
import PipelinesDashboard from './pipelines_dashboard.vue';
import PipelinesDashboardClickhouse from './pipelines_dashboard_clickhouse.vue';
const URL_PARAM_KEY = 'chart';
export default {
components: {
GlTabs,
GlTab,
PipelineCharts,
PipelineChartsNew,
DeploymentFrequencyCharts: () =>
import('ee_component/dora/components/deployment_frequency_charts.vue'),
LeadTimeCharts: () => import('ee_component/dora/components/lead_time_charts.vue'),
TimeToRestoreServiceCharts: () =>
import('ee_component/dora/components/time_to_restore_service_charts.vue'),
ChangeFailureRateCharts: () =>
import('ee_component/dora/components/change_failure_rate_charts.vue'),
ProjectQualitySummary: () => import('ee_component/project_quality_summary/app.vue'),
},
pipelinesTabEvent: 'p_analytics_ci_cd_pipelines',
deploymentFrequencyTabEvent: 'p_analytics_ci_cd_deployment_frequency',
leadTimeTabEvent: 'p_analytics_ci_cd_lead_time',
timeToRestoreServiceTabEvent: 'visit_ci_cd_time_to_restore_service_tab',
changeFailureRateTabEvent: 'visit_ci_cd_failure_rate_tab',
mixins: [InternalEvents.mixin(), glFeatureFlagsMixin()],
inject: {
shouldRenderDoraCharts: {
@ -38,50 +26,83 @@ export default {
},
},
data() {
const tabs = [
{
key: 'pipelines',
event: 'p_analytics_ci_cd_pipelines',
title: __('Pipelines'),
componentIs: this.glFeatures?.ciImprovedProjectPipelineAnalytics
? PipelinesDashboardClickhouse
: PipelinesDashboard,
lazy: true,
},
];
if (this.shouldRenderDoraCharts) {
tabs.push(
{
key: 'deployment-frequency',
event: 'p_analytics_ci_cd_deployment_frequency',
title: __('Deployment frequency'),
componentIs: () => import('ee_component/dora/components/deployment_frequency_charts.vue'),
lazy: true,
},
{
key: 'lead-time',
event: 'p_analytics_ci_cd_lead_time',
title: __('Lead time'),
componentIs: () => import('ee_component/dora/components/lead_time_charts.vue'),
lazy: true,
},
{
key: 'time-to-restore-service',
event: 'visit_ci_cd_time_to_restore_service_tab',
title: s__('DORA4Metrics|Time to restore service'),
componentIs: () =>
import('ee_component/dora/components/time_to_restore_service_charts.vue'),
lazy: true,
},
{
key: 'change-failure-rate',
event: 'visit_ci_cd_failure_rate_tab',
title: s__('DORA4Metrics|Change failure rate'),
componentIs: () => import('ee_component/dora/components/change_failure_rate_charts.vue'),
lazy: true,
},
);
}
if (this.shouldRenderQualitySummary) {
tabs.push({
key: 'project-quality',
title: s__('QualitySummary|Project quality'),
componentIs: () => import('ee_component/project_quality_summary/app.vue'),
lazy: true,
});
}
return {
selectedTab: 0,
activeTabIndex: 0,
tabs,
};
},
computed: {
charts() {
const chartsToShow = ['pipelines'];
if (this.shouldRenderDoraCharts) {
chartsToShow.push(
'deployment-frequency',
'lead-time',
'time-to-restore-service',
'change-failure-rate',
);
}
if (this.shouldRenderQualitySummary) {
chartsToShow.push('project-quality');
}
return chartsToShow;
},
pipelineChartsComponent() {
if (this.glFeatures?.ciImprovedProjectPipelineAnalytics) {
return PipelineChartsNew;
}
return PipelineCharts;
},
},
created() {
this.selectTab();
window.addEventListener('popstate', this.selectTab);
this.syncActiveTab();
window.addEventListener('popstate', this.syncActiveTab);
},
methods: {
selectTab() {
const [chart] = getParameterValues('chart') || this.charts;
const tab = this.charts.indexOf(chart);
this.selectedTab = tab >= 0 ? tab : 0;
syncActiveTab() {
const paramValue = getParameterValues(URL_PARAM_KEY)?.[0];
const selectedIndex = this.tabs.map((tab) => tab.key).indexOf(paramValue);
this.activeTabIndex = selectedIndex >= 0 ? selectedIndex : 0;
},
onTabChange(index) {
if (index !== this.selectedTab) {
this.selectedTab = index;
const path = mergeUrlParams({ chart: this.charts[index] }, window.location.pathname);
onTabInput(index) {
if (index !== this.activeTabIndex) {
const tab = this.tabs[index];
const path = mergeUrlParams({ [URL_PARAM_KEY]: tab.key }, window.location.pathname);
this.activeTabIndex = index;
tab.lazy = false; // mount the tab permanently after it is shown
updateHistory({ url: path, title: window.title });
}
},
@ -90,48 +111,17 @@ export default {
</script>
<template>
<div>
<gl-tabs v-if="charts.length > 1" :value="selectedTab" @input="onTabChange">
<gl-tabs v-if="tabs.length > 1" :value="activeTabIndex" @input="onTabInput">
<gl-tab
:title="__('Pipelines')"
data-testid="pipelines-tab"
@click="trackEvent($options.pipelinesTabEvent)"
v-for="tab in tabs"
:key="tab.key"
:title="tab.title"
:lazy="tab.lazy"
@click="tab.event && trackEvent(tab.event)"
>
<component :is="pipelineChartsComponent" />
</gl-tab>
<template v-if="shouldRenderDoraCharts">
<gl-tab
:title="__('Deployment frequency')"
data-testid="deployment-frequency-tab"
@click="trackEvent($options.deploymentFrequencyTabEvent)"
>
<deployment-frequency-charts />
</gl-tab>
<gl-tab
:title="__('Lead time')"
data-testid="lead-time-tab"
@click="trackEvent($options.leadTimeTabEvent)"
>
<lead-time-charts />
</gl-tab>
<gl-tab
:title="s__('DORA4Metrics|Time to restore service')"
data-testid="time-to-restore-service-tab"
@click="trackEvent($options.timeToRestoreServiceTabEvent)"
>
<time-to-restore-service-charts />
</gl-tab>
<gl-tab
:title="s__('DORA4Metrics|Change failure rate')"
data-testid="change-failure-rate-tab"
@click="trackEvent($options.changeFailureRateTabEvent)"
>
<change-failure-rate-charts />
</gl-tab>
</template>
<gl-tab v-if="shouldRenderQualitySummary" :title="s__('QualitySummary|Project quality')">
<project-quality-summary />
<component :is="tab.componentIs" />
</gl-tab>
</gl-tabs>
<component :is="pipelineChartsComponent" v-else />
<component :is="tabs[0].componentIs" v-else />
</div>
</template>

View File

@ -47,6 +47,7 @@ const defaultCountValues = {
};
export default {
name: 'PipelinesDashboard',
components: {
GlAlert,
GlColumnChart,

View File

@ -16,6 +16,7 @@ import PipelineDurationChart from './pipeline_duration_chart.vue';
import PipelineStatusChart from './pipeline_status_chart.vue';
export default {
name: 'PipelinesDashboardClickhouse',
components: {
GlCollapsibleListbox,
GlFormGroup,

View File

@ -8,15 +8,6 @@ description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/9d6fe7bfdf9ff3f68ee73baa0e3d0aa7df13c351
milestone: '10.8'
gitlab_schema: gitlab_ci
desired_sharding_key:
project_id:
references: projects
backfill_via:
parent:
foreign_key: build_id
table: p_ci_builds
sharding_key: project_id
belongs_to: build
foreign_key_name: fk_89e29fa5ee_p
desired_sharding_key_migration_job_name: BackfillCiBuildTraceChunksProjectId
table_size: small
sharding_key:
project_id: projects

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
class AddCiBuildTraceChunksProjectIdNotNull < Gitlab::Database::Migration[2.2]
milestone '17.11'
disable_ddl_transaction!
def up
add_not_null_constraint :ci_build_trace_chunks, :project_id
end
def down
remove_not_null_constraint :ci_build_trace_chunks, :project_id
end
end

View File

@ -0,0 +1 @@
739b48db5ebc3b2e03dff587bd1e4661764e06dc4025300a16a364271e7313e6

View File

@ -10460,7 +10460,8 @@ CREATE TABLE ci_build_trace_chunks (
lock_version integer DEFAULT 0 NOT NULL,
build_id bigint NOT NULL,
partition_id bigint NOT NULL,
project_id bigint
project_id bigint,
CONSTRAINT check_b374316678 CHECK ((project_id IS NOT NULL))
);
CREATE SEQUENCE ci_build_trace_chunks_id_seq

View File

@ -5650,7 +5650,11 @@ trigger-multi-project-pipeline:
Use `trigger:include` to declare that a job is a "trigger job" which starts a
[child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines).
Use `trigger:include:artifact` to trigger a [dynamic child pipeline](../pipelines/downstream_pipelines.md#dynamic-child-pipelines).
Additionally, use:
- `trigger:include:artifact` to trigger a [dynamic child pipeline](../pipelines/downstream_pipelines.md#dynamic-child-pipelines).
- `trigger:include:inputs` to set the [inputs](inputs.md) when the downstream pipeline configuration
uses [`spec:inputs`](#specinputs).
**Keyword type**: Job keyword. You can use it only as part of a job.
@ -5744,6 +5748,26 @@ successfully complete before starting.
- If the downstream pipeline has a failed job, but the job uses [`allow_failure: true`](#allow_failure),
the downstream pipeline is considered successful and the trigger job shows **success**.
#### `trigger:inputs`
{{< history >}}
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/519963) in GitLab 17.11 [with a flag](../../administration/feature_flags.md) named `ci_inputs_for_pipelines`. Disabled by default.
{{</history >}}
Use `trigger:inputs` to set the [inputs](inputs.md) when the downstream pipeline configuration
uses [`spec:inputs`](#specinputs).
**Example of `trigger:inputs`**:
```yaml
trigger:
- project: 'my-group/my-project'
inputs:
website: "My website"
```
#### `trigger:forward`
{{< history >}}

View File

@ -1213,6 +1213,7 @@ ee:
- :iterations_cadence
- protected_branches:
- :unprotect_access_levels
- :squash_option
- protected_environments:
- :deploy_access_levels
- :security_setting
@ -1357,6 +1358,9 @@ ee:
- :iid
vulnerability_read:
- :project_id
squash_option:
- :squash_option
- :project_id
excluded_attributes:
project:
- :vulnerability_hooks_integrations

View File

@ -42,7 +42,8 @@ module Gitlab
committer: 'MergeRequest::DiffCommitUser',
merge_request_diff_commits: 'MergeRequestDiffCommit',
work_item_type: 'WorkItems::Type',
user_contributions: 'User' }.freeze
user_contributions: 'User',
squash_option: 'Projects::BranchRules::SquashOption' }.freeze
BUILD_MODELS = %i[Ci::Build Ci::Bridge commit_status generic_commit_status].freeze

View File

@ -10,13 +10,8 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
describe GraphQL::Query, type: :request do
let_it_be(:group) { create(:group, path: 'container-registry-group') }
let_it_be(:project) { create(:project, group: group, path: 'container-registry-project') }
let_it_be(:owner) { create(:user) }
let_it_be(:user) { create(:user) }
before_all do
project.add_owner(owner)
end
describe 'Protected container image tags' do
base_path = 'packages_and_registries/settings/project/graphql'
project_container_protection_tag_rules_query_path =
@ -28,26 +23,35 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
update_container_protection_tag_rule_mutation_path =
"#{base_path}/mutations/update_container_protection_tag_rule.mutation.graphql"
let(:query) { get_graphql_query_as_string(project_container_protection_tag_rules_query_path) }
let(:variables) do
{
projectPath: project.full_path,
first: 5
}
end
before do
stub_gitlab_api_client_to_support_gitlab_api(supported: true)
end
context 'when user does not have access to the project' do
it "graphql/#{project_container_protection_tag_rules_query_path}.null_project.json" do
query = get_graphql_query_as_string(project_container_protection_tag_rules_query_path)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, first: 5 })
post_graphql(query, current_user: user, variables: variables)
expect_graphql_errors_to_be_empty
end
end
context 'when user has access to the project &' do
before_all do
project.add_owner(user)
end
context 'with no tag protection rules' do
it "graphql/#{project_container_protection_tag_rules_query_path}.empty_rules.json" do
query = get_graphql_query_as_string(project_container_protection_tag_rules_query_path)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
post_graphql(query, current_user: user, variables: variables)
expect_graphql_errors_to_be_empty
end
@ -63,9 +67,7 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
end
it "graphql/#{project_container_protection_tag_rules_query_path}.json" do
query = get_graphql_query_as_string(project_container_protection_tag_rules_query_path)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
post_graphql(query, current_user: user, variables: variables)
expect_graphql_errors_to_be_empty
end
@ -81,129 +83,117 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
end
it "graphql/#{project_container_protection_tag_rules_query_path}.max_rules.json" do
query = get_graphql_query_as_string(project_container_protection_tag_rules_query_path)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
post_graphql(query, current_user: user, variables: variables)
expect_graphql_errors_to_be_empty
end
end
context 'when there are no errors deleting a rule' do
let_it_be(:container_protection_tag_rule) do
create(:container_registry_protection_tag_rule,
project: project,
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
minimum_access_level_for_delete: Gitlab::Access::OWNER
)
describe 'deleting a rule' do
let(:mutation) { get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path) }
context 'when there are no errors' do
let_it_be(:container_protection_tag_rule) do
create(:container_registry_protection_tag_rule,
project: project,
minimum_access_level_for_push: Gitlab::Access::MAINTAINER,
minimum_access_level_for_delete: Gitlab::Access::OWNER
)
end
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.json" do
post_graphql(
mutation,
current_user: user,
variables: {
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}"
}
}
)
expect_graphql_errors_to_be_empty
end
end
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.json" do
mutation = get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path)
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}"
context 'when there are errors' do
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.errors.json" do
post_graphql(
mutation,
current_user: user,
variables: {
input: {
id: 'gid://gitlab/ContainerRegistry::Protection::TagRule/non-existent'
}
}
}
)
)
expect_graphql_errors_to_be_empty
expect_graphql_errors_to_include(
"The resource that you are attempting to access does not exist or " \
"you don't have permission to perform this action"
)
end
end
end
context 'when there are errors deleting a rule' do
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.errors.json" do
mutation = get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path)
describe 'creating a rule' do
let(:mutation) { get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path) }
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
id: 'gid://gitlab/ContainerRegistry::Protection::TagRule/non-existent'
}
let(:variables) do
{
input: {
projectPath: project.full_path,
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
)
expect_graphql_errors_to_include(
"The resource that you are attempting to access does not exist or " \
"you don't have permission to perform this action"
)
end
end
context 'when there are no errors creating a rule' do
it "graphql/#{create_container_protection_tag_rule_mutation_path}.json" do
mutation = get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path)
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
projectPath: project.full_path,
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
)
expect_graphql_errors_to_be_empty
end
end
context 'when there are field errors creating a rule' do
it "graphql/#{create_container_protection_tag_rule_mutation_path}.server_errors.json" do
mutation = get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path)
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
project_path: project.full_path,
tagNamePattern: '',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
)
expect_graphql_errors_to_include(
"tagNamePattern can't be blank"
)
end
end
context 'when there are errors creating a rule' do
before do
create(:container_registry_protection_tag_rule, project: project,
tag_name_pattern: "v.*")
}
end
it "graphql/#{create_container_protection_tag_rule_mutation_path}.errors.json" do
mutation = get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path)
context 'when there are no errors' do
it "graphql/#{create_container_protection_tag_rule_mutation_path}.json" do
post_graphql(
mutation,
current_user: user,
variables: variables
)
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
project_path: project.full_path,
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
)
expect_graphql_errors_to_be_empty
end
end
expect(graphql_data_at('createContainerProtectionTagRule', 'errors'))
.to include('Tag name pattern has already been taken')
context 'when there are field errors' do
it "graphql/#{create_container_protection_tag_rule_mutation_path}.server_errors.json" do
variables[:input][:tagNamePattern] = ''
post_graphql(
mutation,
current_user: user,
variables: variables
)
expect_graphql_errors_to_include(
"tagNamePattern can't be blank"
)
end
end
context 'when there are errors' do
before do
create(:container_registry_protection_tag_rule, project: project,
tag_name_pattern: "v.*")
end
it "graphql/#{create_container_protection_tag_rule_mutation_path}.errors.json" do
post_graphql(
mutation,
current_user: user,
variables: variables
)
expect(graphql_data_at('createContainerProtectionTagRule', 'errors'))
.to include('Tag name pattern has already been taken')
end
end
end
@ -216,21 +206,26 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
)
end
let(:mutation) { get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path) }
let(:variables) do
{
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
end
context 'when there are no errors' do
it "graphql/#{update_container_protection_tag_rule_mutation_path}.json" do
mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'ADMIN',
minimumAccessLevelForDelete: 'ADMIN'
}
}
current_user: user,
variables: variables
)
expect_graphql_errors_to_be_empty
@ -239,19 +234,12 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
context 'when there are field errors' do
it "graphql/#{update_container_protection_tag_rule_mutation_path}.server_errors.json" do
mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
variables[:input][:tagNamePattern] = ''
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: '',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
current_user: user,
variables: variables
)
expect_graphql_errors_to_include(
@ -271,15 +259,8 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
post_graphql(
mutation,
current_user: owner,
variables: {
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
current_user: user,
variables: variables
)
expect(graphql_data_at('updateContainerProtectionTagRule', 'errors'))

View File

@ -1,261 +1,52 @@
import { GlTabs, GlTab } from '@gitlab/ui';
import { merge } from 'lodash';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import PipelineChartsNew from '~/projects/pipelines/charts/components/pipeline_charts_new.vue';
import API from '~/api';
import { mockTracking } from 'helpers/tracking_helper';
import { SNOWPLOW_DATA_SOURCE, SNOWPLOW_SCHEMA } from '~/projects/pipelines/charts/constants';
import { GlTabs } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
jest.mock('~/lib/utils/url_utility');
import App from '~/projects/pipelines/charts/components/app.vue';
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} };
const TimeToRestoreServiceChartsStub = { name: 'TimeToRestoreServiceCharts', render: () => {} };
const ChangeFailureRateChartsStub = { name: 'ChangeFailureRateCharts', render: () => {} };
const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
import PipelinesDashboard from '~/projects/pipelines/charts/components/pipelines_dashboard.vue';
import PipelinesDashboardClickhouse from '~/projects/pipelines/charts/components/pipelines_dashboard_clickhouse.vue';
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
function createComponent(mountOptions = {}) {
wrapper = shallowMountExtended(
Component,
merge(
{},
{
provide: {
shouldRenderDoraCharts: true,
shouldRenderQualitySummary: true,
},
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
LeadTimeCharts: LeadTimeChartsStub,
TimeToRestoreServiceCharts: TimeToRestoreServiceChartsStub,
ChangeFailureRateCharts: ChangeFailureRateChartsStub,
ProjectQualitySummary: ProjectQualitySummaryStub,
},
},
mountOptions,
),
);
}
const createWrapper = ({ provide, ...options } = {}) => {
wrapper = shallowMount(App, {
provide: {
...provide,
},
...options,
});
};
const findGlTabs = () => wrapper.findComponent(GlTabs);
const findAllGlTabs = () => wrapper.findAllComponents(GlTab);
const findGlTabAtIndex = (index) => findAllGlTabs().at(index);
const findLeadTimeCharts = () => wrapper.findComponent(LeadTimeChartsStub);
const findTimeToRestoreServiceCharts = () =>
wrapper.findComponent(TimeToRestoreServiceChartsStub);
const findChangeFailureRateCharts = () => wrapper.findComponent(ChangeFailureRateChartsStub);
const findDeploymentFrequencyCharts = () => wrapper.findComponent(DeploymentFrequencyChartsStub);
const findPipelineCharts = () => wrapper.findComponent(PipelineCharts);
const findPipelineChartsNew = () => wrapper.findComponent(PipelineChartsNew);
const findProjectQualitySummary = () => wrapper.findComponent(ProjectQualitySummaryStub);
describe('when all charts are available', () => {
const findPipelinesDashboard = () => wrapper.findComponent(PipelinesDashboard);
const findPipelinesDashboardClickhouse = () =>
wrapper.findComponent(PipelinesDashboardClickhouse);
describe('when showing only pipelines dashboard', () => {
beforeEach(() => {
createComponent();
});
describe.each`
title | finderFn | index
${'Pipelines'} | ${findPipelineCharts} | ${0}
${'Deployment frequency'} | ${findDeploymentFrequencyCharts} | ${1}
${'Lead time'} | ${findLeadTimeCharts} | ${2}
${'Time to restore service'} | ${findTimeToRestoreServiceCharts} | ${3}
${'Change failure rate'} | ${findChangeFailureRateCharts} | ${4}
${'Project quality'} | ${findProjectQualitySummary} | ${5}
`('Tabs', ({ title, finderFn, index }) => {
it(`renders tab with a title ${title} at index ${index}`, () => {
expect(findGlTabAtIndex(index).attributes('title')).toBe(title);
});
it(`renders the ${title} chart`, () => {
expect(finderFn().exists()).toBe(true);
});
it(`updates the current tab and url when the ${title} tab is clicked`, async () => {
let chartsPath;
const tabName = title.toLowerCase().replace(/\s/g, '-');
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
mergeUrlParams.mockImplementation(({ chart }, path) => {
expect(chart).toBe(tabName);
expect(path).toBe(window.location.pathname);
chartsPath = `${path}?chart=${chart}`;
return chartsPath;
});
updateHistory.mockImplementation(({ url }) => {
expect(url).toBe(chartsPath);
});
const tabs = findGlTabs();
expect(tabs.attributes('value')).toBe('0');
tabs.vm.$emit('input', index);
await nextTick();
expect(tabs.attributes('value')).toBe(index.toString());
});
});
it('should not try to push history if the tab does not change', async () => {
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
mergeUrlParams.mockImplementation(({ chart }, path) => `${path}?chart=${chart}`);
const tabs = findGlTabs();
expect(tabs.attributes('value')).toBe('0');
tabs.vm.$emit('input', 0);
await nextTick();
expect(updateHistory).not.toHaveBeenCalled();
});
describe('event tracking', () => {
describe('Internal Events RedisHLL events', () => {
it.each`
testId | event
${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
${'time-to-restore-service-tab'} | ${'visit_ci_cd_time_to_restore_service_tab'}
${'change-failure-rate-tab'} | ${'visit_ci_cd_failure_rate_tab'}
`('tracks the $event event when clicked', ({ testId, event }) => {
const trackApiSpy = jest.spyOn(API, 'trackInternalEvent');
expect(trackApiSpy).not.toHaveBeenCalled();
wrapper.findByTestId(testId).vm.$emit('click');
expect(trackApiSpy).toHaveBeenCalledWith(event, {});
});
});
describe('Snowplow events', () => {
it.each`
testId | event
${'pipelines-tab'} | ${'p_analytics_ci_cd_pipelines'}
${'deployment-frequency-tab'} | ${'p_analytics_ci_cd_deployment_frequency'}
${'lead-time-tab'} | ${'p_analytics_ci_cd_lead_time'}
`('tracks the $event event when clicked', ({ testId, event }) => {
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.findByTestId(testId).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, event, {
context: {
schema: SNOWPLOW_SCHEMA,
data: {
event_name: event,
data_source: SNOWPLOW_DATA_SOURCE,
},
},
});
});
});
});
});
describe('when provided with a query param', () => {
it.each`
chart | tab
${'change-failure-rate'} | ${'4'}
${'time-to-restore-service'} | ${'3'}
${'lead-time'} | ${'2'}
${'deployment-frequency'} | ${'1'}
${'pipelines'} | ${'0'}
${'fake'} | ${'0'}
${''} | ${'0'}
`('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
getParameterValues.mockImplementation((name) => {
expect(name).toBe('chart');
return chart ? [chart] : [];
});
createComponent();
expect(findGlTabs().attributes('value')).toBe(tab);
});
it('should set the tab when the back button is clicked', async () => {
let popstateHandler;
window.addEventListener = jest.fn();
window.addEventListener.mockImplementation((event, handler) => {
if (event === 'popstate') {
popstateHandler = handler;
}
});
getParameterValues.mockImplementation((name) => {
expect(name).toBe('chart');
return [];
});
createComponent();
expect(findGlTabs().attributes('value')).toBe('0');
getParameterValues.mockImplementationOnce((name) => {
expect(name).toBe('chart');
return ['deployment-frequency'];
});
popstateHandler();
await nextTick();
expect(findGlTabs().attributes('value')).toBe('1');
});
});
describe('when the dora charts are not available and project quality summary is not available', () => {
beforeEach(() => {
createComponent({
provide: { shouldRenderDoraCharts: false, shouldRenderQualitySummary: false },
});
createWrapper();
});
it('does not render tabs', () => {
// tabs are only shown in EE
expect(findGlTabs().exists()).toBe(false);
});
it('renders the pipeline charts', () => {
expect(findPipelineCharts().exists()).toBe(true);
});
});
describe('when the project quality summary is not available', () => {
beforeEach(() => {
createComponent({ provide: { shouldRenderQualitySummary: false } });
});
it('does not render the tab', () => {
expect(findProjectQualitySummary().exists()).toBe(false);
it('shows pipelines dashboard', () => {
expect(wrapper.findComponent(PipelinesDashboard).exists()).toBe(true);
});
});
describe('ci_improved_project_pipeline_analytics feature flag', () => {
describe.each`
status | finderFn
${false} | ${findPipelineCharts}
${true} | ${findPipelineChartsNew}
${false} | ${findPipelinesDashboard}
${true} | ${findPipelinesDashboardClickhouse}
`('when flag is $status', ({ status, finderFn }) => {
it('renders component', () => {
createComponent({
createWrapper({
provide: {
glFeatures: {
ciImprovedProjectPipelineAnalytics: status,

View File

@ -5,7 +5,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineChartsNew from '~/projects/pipelines/charts/components/pipeline_charts_new.vue';
import PipelinesDashboardClickhouse from '~/projects/pipelines/charts/components/pipelines_dashboard_clickhouse.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import PipelineDurationChart from '~/projects/pipelines/charts/components/pipeline_duration_chart.vue';
import PipelineStatusChart from '~/projects/pipelines/charts/components/pipeline_status_chart.vue';
@ -19,7 +19,7 @@ jest.mock('~/alert');
const projectPath = 'gitlab-org/gitlab';
describe('~/projects/pipelines/charts/components/pipeline_charts_new.vue', () => {
describe('PipelinesDashboardClickhouse', () => {
useFakeDate('2022-02-15T08:30'); // a date with a time
let wrapper;
@ -32,7 +32,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts_new.vue', () =>
const findAllSingleStats = () => wrapper.findAllComponents(GlSingleStat);
const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = mountFn(PipelineChartsNew, {
wrapper = mountFn(PipelinesDashboardClickhouse, {
provide: {
projectPath,
},

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
import PipelinesDashboard from '~/projects/pipelines/charts/components/pipelines_dashboard.vue';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
@ -14,7 +14,7 @@ import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo);
describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
describe('PipelinesDashboard', () => {
let wrapper;
function createMockApolloProvider() {
@ -27,7 +27,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
}
beforeEach(async () => {
wrapper = shallowMount(PipelineCharts, {
wrapper = shallowMount(PipelinesDashboard, {
provide: {
projectPath,
},

View File

@ -61,6 +61,7 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
'ci_pipeline_messages.project_id',
# LFK already present on ci_pipeline_schedules and cascade delete all ci resources.
'ci_pipeline_schedule_variables.project_id',
'ci_build_trace_chunks.project_id', # LFK already present on p_ci_builds and cascade delete all ci resources
'p_ci_job_annotations.project_id', # LFK already present on p_ci_builds and cascade delete all ci resources
'ci_builds_runner_session.project_id', # LFK already present on p_ci_builds and cascade delete all ci resources
'p_ci_pipelines_config.project_id', # LFK already present on p_ci_pipelines and cascade delete all ci resources

View File

@ -569,6 +569,8 @@ create_access_levels:
- protected_tag
- group
- deploy_key
squash_option:
- protected_branch
container_repositories:
- project
- name

View File

@ -47,6 +47,7 @@ RSpec.describe 'Test coverage of the Project Import', feature_category: :importe
project.ci_pipelines.notes.events
project.ci_pipelines.notes.events.push_event_payload
project.protected_branches.unprotect_access_levels
project.protected_branches.squash_option
project.boards.lists.label.priorities
project.service_desk_setting
project.security_setting

View File

@ -1129,3 +1129,6 @@ Vulnerabilities::Identifier:
- url
Vulnerabilities::Read:
- project_id
Projects::BranchRules::SquashOption:
- squash_option
- project_id