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> <script>
import { GlTabs, GlTab } from '@gitlab/ui'; import { GlTabs, GlTab } from '@gitlab/ui';
import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility'; import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { InternalEvents } from '~/tracking'; import { InternalEvents } from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import PipelineCharts from './pipeline_charts.vue'; import PipelinesDashboard from './pipelines_dashboard.vue';
import PipelineChartsNew from './pipeline_charts_new.vue'; import PipelinesDashboardClickhouse from './pipelines_dashboard_clickhouse.vue';
const URL_PARAM_KEY = 'chart';
export default { export default {
components: { components: {
GlTabs, GlTabs,
GlTab, 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()], mixins: [InternalEvents.mixin(), glFeatureFlagsMixin()],
inject: { inject: {
shouldRenderDoraCharts: { shouldRenderDoraCharts: {
@ -38,50 +26,83 @@ export default {
}, },
}, },
data() { 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 { 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() { created() {
this.selectTab(); this.syncActiveTab();
window.addEventListener('popstate', this.selectTab); window.addEventListener('popstate', this.syncActiveTab);
}, },
methods: { methods: {
selectTab() { syncActiveTab() {
const [chart] = getParameterValues('chart') || this.charts; const paramValue = getParameterValues(URL_PARAM_KEY)?.[0];
const tab = this.charts.indexOf(chart); const selectedIndex = this.tabs.map((tab) => tab.key).indexOf(paramValue);
this.selectedTab = tab >= 0 ? tab : 0; this.activeTabIndex = selectedIndex >= 0 ? selectedIndex : 0;
}, },
onTabChange(index) { onTabInput(index) {
if (index !== this.selectedTab) { if (index !== this.activeTabIndex) {
this.selectedTab = index; const tab = this.tabs[index];
const path = mergeUrlParams({ chart: this.charts[index] }, window.location.pathname); 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 }); updateHistory({ url: path, title: window.title });
} }
}, },
@ -90,48 +111,17 @@ export default {
</script> </script>
<template> <template>
<div> <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 <gl-tab
:title="__('Pipelines')" v-for="tab in tabs"
data-testid="pipelines-tab" :key="tab.key"
@click="trackEvent($options.pipelinesTabEvent)" :title="tab.title"
:lazy="tab.lazy"
@click="tab.event && trackEvent(tab.event)"
> >
<component :is="pipelineChartsComponent" /> <component :is="tab.componentIs" />
</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 />
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
<component :is="pipelineChartsComponent" v-else /> <component :is="tabs[0].componentIs" v-else />
</div> </div>
</template> </template>

View File

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

View File

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

View File

@ -8,15 +8,6 @@ description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/9d6fe7bfdf9ff3f68ee73baa0e3d0aa7df13c351 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/commit/9d6fe7bfdf9ff3f68ee73baa0e3d0aa7df13c351
milestone: '10.8' milestone: '10.8'
gitlab_schema: gitlab_ci 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 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, lock_version integer DEFAULT 0 NOT NULL,
build_id bigint NOT NULL, build_id bigint NOT NULL,
partition_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 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 Use `trigger:include` to declare that a job is a "trigger job" which starts a
[child pipeline](../pipelines/downstream_pipelines.md#parent-child-pipelines). [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. **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), - 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**. 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` #### `trigger:forward`
{{< history >}} {{< history >}}

View File

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

View File

@ -42,7 +42,8 @@ module Gitlab
committer: 'MergeRequest::DiffCommitUser', committer: 'MergeRequest::DiffCommitUser',
merge_request_diff_commits: 'MergeRequestDiffCommit', merge_request_diff_commits: 'MergeRequestDiffCommit',
work_item_type: 'WorkItems::Type', 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 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 describe GraphQL::Query, type: :request do
let_it_be(:group) { create(:group, path: 'container-registry-group') } 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(:project) { create(:project, group: group, path: 'container-registry-project') }
let_it_be(:owner) { create(:user) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
before_all do
project.add_owner(owner)
end
describe 'Protected container image tags' do describe 'Protected container image tags' do
base_path = 'packages_and_registries/settings/project/graphql' base_path = 'packages_and_registries/settings/project/graphql'
project_container_protection_tag_rules_query_path = 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 = update_container_protection_tag_rule_mutation_path =
"#{base_path}/mutations/update_container_protection_tag_rule.mutation.graphql" "#{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 before do
stub_gitlab_api_client_to_support_gitlab_api(supported: true) stub_gitlab_api_client_to_support_gitlab_api(supported: true)
end end
context 'when user does not have access to the project' do context 'when user does not have access to the project' do
it "graphql/#{project_container_protection_tag_rules_query_path}.null_project.json" 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: variables)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, first: 5 })
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
end end
context 'when user has access to the project &' do context 'when user has access to the project &' do
before_all do
project.add_owner(user)
end
context 'with no tag protection rules' do context 'with no tag protection rules' do
it "graphql/#{project_container_protection_tag_rules_query_path}.empty_rules.json" 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: user, variables: variables)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
@ -63,9 +67,7 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
end end
it "graphql/#{project_container_protection_tag_rules_query_path}.json" do 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: user, variables: variables)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
@ -81,129 +83,117 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
end end
it "graphql/#{project_container_protection_tag_rules_query_path}.max_rules.json" do 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: user, variables: variables)
post_graphql(query, current_user: owner, variables: { projectPath: project.full_path, first: 5 })
expect_graphql_errors_to_be_empty expect_graphql_errors_to_be_empty
end end
end end
context 'when there are no errors deleting a rule' do describe 'deleting a rule' do
let_it_be(:container_protection_tag_rule) do let(:mutation) { get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path) }
create(:container_registry_protection_tag_rule,
project: project, context 'when there are no errors' do
minimum_access_level_for_push: Gitlab::Access::MAINTAINER, let_it_be(:container_protection_tag_rule) do
minimum_access_level_for_delete: Gitlab::Access::OWNER 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 end
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.json" do context 'when there are errors' do
mutation = get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path) it "graphql/#{delete_container_protection_tag_rule_mutation_path}.errors.json" do
post_graphql(
post_graphql( mutation,
mutation, current_user: user,
current_user: owner, variables: {
variables: { input: {
input: { id: 'gid://gitlab/ContainerRegistry::Protection::TagRule/non-existent'
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}" }
} }
} )
)
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
end end
context 'when there are errors deleting a rule' do describe 'creating a rule' do
it "graphql/#{delete_container_protection_tag_rule_mutation_path}.errors.json" do let(:mutation) { get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path) }
mutation = get_graphql_query_as_string(delete_container_protection_tag_rule_mutation_path)
post_graphql( let(:variables) do
mutation, {
current_user: owner, input: {
variables: { projectPath: project.full_path,
input: { tagNamePattern: 'v.*',
id: 'gid://gitlab/ContainerRegistry::Protection::TagRule/non-existent' 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 end
it "graphql/#{create_container_protection_tag_rule_mutation_path}.errors.json" do context 'when there are no errors' do
mutation = get_graphql_query_as_string(create_container_protection_tag_rule_mutation_path) it "graphql/#{create_container_protection_tag_rule_mutation_path}.json" do
post_graphql(
mutation,
current_user: user,
variables: variables
)
post_graphql( expect_graphql_errors_to_be_empty
mutation, end
current_user: owner, end
variables: {
input: {
project_path: project.full_path,
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
)
expect(graphql_data_at('createContainerProtectionTagRule', 'errors')) context 'when there are field errors' do
.to include('Tag name pattern has already been taken') 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
end end
@ -216,21 +206,26 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
) )
end 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 context 'when there are no errors' do
it "graphql/#{update_container_protection_tag_rule_mutation_path}.json" 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) mutation = get_graphql_query_as_string(update_container_protection_tag_rule_mutation_path)
post_graphql( post_graphql(
mutation, mutation,
current_user: owner, current_user: user,
variables: { variables: variables
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'ADMIN',
minimumAccessLevelForDelete: 'ADMIN'
}
}
) )
expect_graphql_errors_to_be_empty 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 context 'when there are field errors' do
it "graphql/#{update_container_protection_tag_rule_mutation_path}.server_errors.json" 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( post_graphql(
mutation, mutation,
current_user: owner, current_user: user,
variables: { variables: variables
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: '',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
) )
expect_graphql_errors_to_include( expect_graphql_errors_to_include(
@ -271,15 +259,8 @@ RSpec.describe 'Container registry (JavaScript fixtures)', feature_category: :co
post_graphql( post_graphql(
mutation, mutation,
current_user: owner, current_user: user,
variables: { variables: variables
input: {
id: "gid://gitlab/ContainerRegistry::Protection::TagRule/#{container_protection_tag_rule.id}",
tagNamePattern: 'v.*',
minimumAccessLevelForPush: 'MAINTAINER',
minimumAccessLevelForDelete: 'OWNER'
}
}
) )
expect(graphql_data_at('updateContainerProtectionTagRule', 'errors')) expect(graphql_data_at('updateContainerProtectionTagRule', 'errors'))

View File

@ -1,261 +1,52 @@
import { GlTabs, GlTab } from '@gitlab/ui'; import { GlTabs } from '@gitlab/ui';
import { merge } from 'lodash'; import { shallowMount } from '@vue/test-utils';
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';
jest.mock('~/lib/utils/url_utility'); import App from '~/projects/pipelines/charts/components/app.vue';
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} }; import PipelinesDashboard from '~/projects/pipelines/charts/components/pipelines_dashboard.vue';
const LeadTimeChartsStub = { name: 'LeadTimeCharts', render: () => {} }; import PipelinesDashboardClickhouse from '~/projects/pipelines/charts/components/pipelines_dashboard_clickhouse.vue';
const TimeToRestoreServiceChartsStub = { name: 'TimeToRestoreServiceCharts', render: () => {} };
const ChangeFailureRateChartsStub = { name: 'ChangeFailureRateCharts', render: () => {} };
const ProjectQualitySummaryStub = { name: 'ProjectQualitySummary', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => { describe('ProjectsPipelinesChartsApp', () => {
let wrapper; let wrapper;
function createComponent(mountOptions = {}) { const createWrapper = ({ provide, ...options } = {}) => {
wrapper = shallowMountExtended( wrapper = shallowMount(App, {
Component, provide: {
merge( ...provide,
{}, },
{ ...options,
provide: { });
shouldRenderDoraCharts: true, };
shouldRenderQualitySummary: true,
},
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
LeadTimeCharts: LeadTimeChartsStub,
TimeToRestoreServiceCharts: TimeToRestoreServiceChartsStub,
ChangeFailureRateCharts: ChangeFailureRateChartsStub,
ProjectQualitySummary: ProjectQualitySummaryStub,
},
},
mountOptions,
),
);
}
const findGlTabs = () => wrapper.findComponent(GlTabs); 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(() => { beforeEach(() => {
createComponent(); createWrapper();
});
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 },
});
}); });
it('does not render tabs', () => { it('does not render tabs', () => {
// tabs are only shown in EE
expect(findGlTabs().exists()).toBe(false); expect(findGlTabs().exists()).toBe(false);
}); });
it('renders the pipeline charts', () => { it('shows pipelines dashboard', () => {
expect(findPipelineCharts().exists()).toBe(true); expect(wrapper.findComponent(PipelinesDashboard).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);
}); });
}); });
describe('ci_improved_project_pipeline_analytics feature flag', () => { describe('ci_improved_project_pipeline_analytics feature flag', () => {
describe.each` describe.each`
status | finderFn status | finderFn
${false} | ${findPipelineCharts} ${false} | ${findPipelinesDashboard}
${true} | ${findPipelineChartsNew} ${true} | ${findPipelinesDashboardClickhouse}
`('when flag is $status', ({ status, finderFn }) => { `('when flag is $status', ({ status, finderFn }) => {
it('renders component', () => { it('renders component', () => {
createComponent({ createWrapper({
provide: { provide: {
glFeatures: { glFeatures: {
ciImprovedProjectPipelineAnalytics: status, ciImprovedProjectPipelineAnalytics: status,

View File

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

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; 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 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 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'; 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'; const projectPath = 'gitlab-org/gitlab';
Vue.use(VueApollo); Vue.use(VueApollo);
describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => { describe('PipelinesDashboard', () => {
let wrapper; let wrapper;
function createMockApolloProvider() { function createMockApolloProvider() {
@ -27,7 +27,7 @@ describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
} }
beforeEach(async () => { beforeEach(async () => {
wrapper = shallowMount(PipelineCharts, { wrapper = shallowMount(PipelinesDashboard, {
provide: { provide: {
projectPath, projectPath,
}, },

View File

@ -61,6 +61,7 @@ RSpec.describe 'new tables missing sharding_key', feature_category: :cell do
'ci_pipeline_messages.project_id', 'ci_pipeline_messages.project_id',
# LFK already present on ci_pipeline_schedules and cascade delete all ci resources. # LFK already present on ci_pipeline_schedules and cascade delete all ci resources.
'ci_pipeline_schedule_variables.project_id', '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 '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 '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 '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 - protected_tag
- group - group
- deploy_key - deploy_key
squash_option:
- protected_branch
container_repositories: container_repositories:
- project - project
- name - 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
project.ci_pipelines.notes.events.push_event_payload project.ci_pipelines.notes.events.push_event_payload
project.protected_branches.unprotect_access_levels project.protected_branches.unprotect_access_levels
project.protected_branches.squash_option
project.boards.lists.label.priorities project.boards.lists.label.priorities
project.service_desk_setting project.service_desk_setting
project.security_setting project.security_setting

View File

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