diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 12:07:12 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 12:07:12 +0000 |
commit | 43771438e9ccf20d1b6cf12b690e63844d7c3d49 (patch) | |
tree | 147aefba22d99be62ff3c112f50e205e486e58c7 | |
parent | eeb25534bae1021f5b7940138ee56dea8fc79949 (diff) | |
download | gitlab-ce-43771438e9ccf20d1b6cf12b690e63844d7c3d49.tar.gz |
Add latest changes from gitlab-org/gitlab@master
56 files changed, 1237 insertions, 230 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 9ceb8466270..c6d2d48db9b 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1012,3 +1012,8 @@ Graphql/ResourceNotAvailableError: Exclude: # Definition of `raise_resource_not_available_error!` - 'lib/gitlab/graphql/authorize/authorize_resource.rb' + +RSpec/FactoryBot/LocalStaticAssignment: + Include: + - spec/factories/**/*.rb + - ee/spec/factories/**/*.rb diff --git a/.rubocop_todo/layout/empty_line_after_magic_comment.yml b/.rubocop_todo/layout/empty_line_after_magic_comment.yml index 536f13e3919..f8f4f1d3b8a 100644 --- a/.rubocop_todo/layout/empty_line_after_magic_comment.yml +++ b/.rubocop_todo/layout/empty_line_after_magic_comment.yml @@ -271,11 +271,6 @@ Layout/EmptyLineAfterMagicComment: - 'ee/lib/gitlab/cidr.rb' - 'ee/lib/quality/seeders/vulnerabilities.rb' - 'ee/spec/components/billing/plan_component_spec.rb' - - 'ee/spec/components/namespaces/storage/limit_alert_component_spec.rb' - - 'ee/spec/components/namespaces/storage/pre_enforcement_alert_component_spec.rb' - - 'ee/spec/components/namespaces/storage/project_pre_enforcement_alert_component_spec.rb' - - 'ee/spec/components/namespaces/storage/subgroup_pre_enforcement_alert_component_spec.rb' - - 'ee/spec/components/namespaces/storage/user_pre_enforcement_alert_component_spec.rb' - 'ee/spec/controllers/admin/geo/nodes_controller_spec.rb' - 'ee/spec/controllers/ee/projects/autocomplete_sources_controller_spec.rb' - 'ee/spec/controllers/ee/projects/protected_branches_controller_spec.rb' diff --git a/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml b/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml new file mode 100644 index 00000000000..7a4201ca027 --- /dev/null +++ b/.rubocop_todo/rspec/factory_bot/local_static_assignment.yml @@ -0,0 +1,3 @@ +--- +RSpec/FactoryBot/LocalStaticAssignment: + Details: grace period diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 84cd2d63778..43c6a2652b9 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -05463be9a1df998a5a02f8b4063bad83040bc649 +da80ab3efcbf3dc0289aacf698ffeabc3c381275 diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue index 3f6ea56382f..035e26d09e6 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue @@ -9,6 +9,7 @@ import { GlIcon, GlPagination, GlFormCheckbox, + GlTooltipDirective, } from '@gitlab/ui'; import { createAlert } from '~/alert'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; @@ -43,6 +44,7 @@ import { I18N_BULK_DELETE_PARTIAL_ERROR, I18N_BULK_DELETE_CONFIRMATION_TOAST, SELECTED_ARTIFACTS_MAX_COUNT, + I18N_BULK_DELETE_MAX_SELECTED, } from '../constants'; import JobCheckbox from './job_checkbox.vue'; import ArtifactsBulkDelete from './artifacts_bulk_delete.vue'; @@ -78,6 +80,9 @@ export default { ArtifactsTableRowDetails, FeedbackBanner, }, + directives: { + GlTooltip: GlTooltipDirective, + }, mixins: [glFeatureFlagsMixin()], inject: ['projectId', 'projectPath', 'canDestroyArtifacts'], apollo: { @@ -164,6 +169,25 @@ export default { artifactsToDelete() { return this.isDeletingArtifactsForJob ? this.jobArtifactsToDelete : this.selectedArtifacts; }, + isAnyVisibleArtifactSelected() { + return this.jobArtifacts.some((job) => + job.artifacts.nodes.some((artifactNode) => + this.selectedArtifacts.includes(artifactNode.id), + ), + ); + }, + areAllVisibleArtifactsSelected() { + return this.jobArtifacts.every((job) => + job.artifacts.nodes.every((artifactNode) => + this.selectedArtifacts.includes(artifactNode.id), + ), + ); + }, + selectAllTooltipText() { + return this.isSelectedArtifactsLimitReached && !this.isAnyVisibleArtifactSelected + ? I18N_BULK_DELETE_MAX_SELECTED + : ''; + }, }, methods: { refetchArtifacts() { @@ -205,11 +229,11 @@ export default { } }, selectArtifact(artifactNode, checked) { - if (checked) { - if (!this.isSelectedArtifactsLimitReached) { - this.selectedArtifacts.push(artifactNode.id); - } - } else { + const isSelected = this.selectedArtifacts.includes(artifactNode.id); + + if (checked && !isSelected && !this.isSelectedArtifactsLimitReached) { + this.selectedArtifacts.push(artifactNode.id); + } else if (isSelected) { this.selectedArtifacts.splice(this.selectedArtifacts.indexOf(artifactNode.id), 1); } }, @@ -274,6 +298,11 @@ export default { this.isBulkDeleteModalVisible = false; this.jobArtifactsToDelete = []; }, + handleSelectAllChecked(checked) { + this.jobArtifacts.map((job) => + job.artifacts.nodes.map((artifactNode) => this.selectArtifact(artifactNode, checked)), + ); + }, clearSelectedArtifacts() { this.selectedArtifacts = []; }, @@ -369,10 +398,12 @@ export default { </template> <template v-if="canBulkDestroyArtifacts" #head(checkbox)> <gl-form-checkbox - :disabled="!anyArtifactsSelected" - :checked="anyArtifactsSelected" - :indeterminate="anyArtifactsSelected" - @change="clearSelectedArtifacts" + v-gl-tooltip.right + :title="selectAllTooltipText" + :checked="isAnyVisibleArtifactSelected" + :indeterminate="isAnyVisibleArtifactSelected && !areAllVisibleArtifactsSelected" + :disabled="isSelectedArtifactsLimitReached && !isAnyVisibleArtifactSelected" + @change="handleSelectAllChecked" /> </template> <template diff --git a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue index 91296bd507e..861278147e9 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_checkbox.vue @@ -48,7 +48,7 @@ export default { }, }, methods: { - handleInput(checked) { + handleChange(checked) { if (checked) { this.unselectedArtifacts.forEach((node) => this.$emit('selectArtifact', node, true)); } else { @@ -65,6 +65,6 @@ export default { :disabled="disabled" :checked="checked" :indeterminate="indeterminate" - @input="handleInput" + @change="handleChange" /> </template> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue index 3ef73e7c874..b7a612a9688 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue @@ -17,6 +17,10 @@ import { DELETE_CANDIDATE_PRIMARY_ACTION_LABEL, DELETE_CANDIDATE_MODAL_TITLE, MLFLOW_ID_LABEL, + CI_SECTION_LABEL, + JOB_LABEL, + CI_USER_LABEL, + CI_MR_LABEL, } from './translations'; export default { @@ -43,11 +47,18 @@ export default { DELETE_CANDIDATE_PRIMARY_ACTION_LABEL, DELETE_CANDIDATE_MODAL_TITLE, MLFLOW_ID_LABEL, + CI_SECTION_LABEL, + JOB_LABEL, + CI_USER_LABEL, + CI_MR_LABEL, }, computed: { info() { return Object.freeze(this.candidate.info); }, + ciJob() { + return Object.freeze(this.info.ci_job); + }, sections() { return [ { @@ -106,6 +117,31 @@ export default { :text="$options.i18n.ARTIFACTS_LABEL" /> + <template v-if="ciJob"> + <tr class="divider"></tr> + + <detail-row + :label="$options.i18n.JOB_LABEL" + :text="ciJob.name" + :href="ciJob.path" + :section-label="$options.i18n.CI_SECTION_LABEL" + /> + + <detail-row + v-if="ciJob.user" + :label="$options.i18n.CI_USER_LABEL" + :href="ciJob.user.path" + :text="ciJob.user.username" + /> + + <detail-row + v-if="ciJob.merge_request" + :label="$options.i18n.CI_MR_LABEL" + :text="ciJob.merge_request.title" + :href="ciJob.merge_request.path" + /> + </template> + <template v-for="{ sectionName, sectionValues } in sections"> <tr v-if="sectionValues" :key="sectionName" class="divider"></tr> diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js index 66ee84adb4e..fa9518f3e27 100644 --- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js +++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js @@ -1,4 +1,4 @@ -import { s__ } from '~/locale'; +import { __, s__ } from '~/locale'; export const TITLE_LABEL = s__('MlExperimentTracking|Model candidate details'); export const INFO_LABEL = s__('MlExperimentTracking|Info'); @@ -15,3 +15,7 @@ export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__( ); export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate'); export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?'); +export const CI_SECTION_LABEL = __('CI'); +export const JOB_LABEL = __('Job'); +export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by'); +export const CI_MR_LABEL = __('Merge request'); diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue new file mode 100644 index 00000000000..936938f3032 --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue @@ -0,0 +1,121 @@ +<script> +import { createAlert } from '~/alert'; +import commitReferencesQuery from '../graphql/queries/commit_references.query.graphql'; +import containingBranchesQuery from '../graphql/queries/commit_containing_branches.query.graphql'; +import containingTagsQuery from '../graphql/queries/commit_containing_tags.query.graphql'; +import { + BRANCHES, + TAGS, + FETCH_CONTAINING_REFS_EVENT, + FETCH_COMMIT_REFERENCES_ERROR, +} from '../constants'; +import RefsList from './refs_list.vue'; + +export default { + name: 'CommitRefs', + components: { + RefsList, + }, + inject: ['fullPath', 'commitSha'], + apollo: { + project: { + query: commitReferencesQuery, + variables() { + return this.queryVariables; + }, + update({ + project: { + commitReferences: { tippingTags, tippingBranches, containingBranches, containingTags }, + }, + }) { + this.tippingTags = tippingTags.names; + this.tippingBranches = tippingBranches.names; + this.hasContainingBranches = Boolean(containingBranches.names.length); + this.hasContainingTags = Boolean(containingTags.names.length); + }, + error() { + createAlert({ + message: this.$options.i18n.errorMessage, + captureError: true, + }); + }, + }, + }, + data() { + return { + containingTags: [], + containingBranches: [], + tippingTags: [], + tippingBranches: [], + hasContainingBranches: false, + hasContainingTags: false, + }; + }, + computed: { + hasBranches() { + return this.tippingBranches.length || this.hasContainingBranches; + }, + hasTags() { + return this.tippingTags.length || this.hasContainingTags; + }, + queryVariables() { + return { + fullPath: this.fullPath, + commitSha: this.commitSha, + }; + }, + }, + methods: { + async fetchContainingRefs({ query, namespace }) { + try { + const { data } = await this.$apollo.query({ + query, + variables: this.queryVariables, + }); + this[namespace] = data.project.commitReferences[namespace].names; + return data.project.commitReferences[namespace].names; + } catch { + return createAlert({ + message: this.$options.i18n.errorMessage, + captureError: true, + }); + } + }, + fetchContainingBranches() { + this.fetchContainingRefs({ query: containingBranchesQuery, namespace: 'containingBranches' }); + }, + fetchContainingTags() { + this.fetchContainingRefs({ query: containingTagsQuery, namespace: 'containingTags' }); + }, + }, + i18n: { + branches: BRANCHES, + tags: TAGS, + errorMessage: FETCH_COMMIT_REFERENCES_ERROR, + }, + fetchContainingRefsEvent: FETCH_CONTAINING_REFS_EVENT, +}; +</script> + +<template> + <div class="gl-ml-7"> + <refs-list + v-if="hasBranches" + :has-containing-refs="hasContainingBranches" + :is-loading="$apollo.queries.project.loading" + :tipping-refs="tippingBranches" + :containing-refs="containingBranches" + :namespace="$options.i18n.branches" + @[$options.fetchContainingRefsEvent]="fetchContainingBranches" + /> + <refs-list + v-if="hasTags" + :has-containing-refs="hasContainingTags" + :is-loading="$apollo.queries.project.loading" + :tipping-refs="tippingTags" + :containing-refs="containingTags" + :namespace="$options.i18n.tags" + @[$options.fetchContainingRefsEvent]="fetchContainingTags" + /> + </div> +</template> diff --git a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue new file mode 100644 index 00000000000..602fa26efa7 --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue @@ -0,0 +1,91 @@ +<script> +import { GlCollapse, GlBadge, GlButton, GlIcon, GlSkeletonLoader } from '@gitlab/ui'; +import { CONTAINING_COMMIT, FETCH_CONTAINING_REFS_EVENT } from '../constants'; + +export default { + name: 'RefsList', + components: { + GlCollapse, + GlSkeletonLoader, + GlBadge, + GlButton, + GlIcon, + }, + props: { + containingRefs: { + type: Array, + required: false, + default: () => [], + }, + tippingRefs: { + type: Array, + required: false, + default: () => [], + }, + namespace: { + type: String, + required: true, + }, + hasContainingRefs: { + type: Boolean, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + }, + data() { + return { + isContainingRefsVisible: false, + }; + }, + computed: { + collapseIcon() { + return this.isContainingRefsVisible ? 'chevron-down' : 'chevron-right'; + }, + isLoadingRefs() { + return this.isLoading && !this.containingRefs.length; + }, + }, + methods: { + toggleCollapse() { + this.isContainingRefsVisible = !this.isContainingRefsVisible; + }, + showRefs() { + this.toggleCollapse(); + this.$emit(FETCH_CONTAINING_REFS_EVENT); + }, + }, + i18n: { + containingCommit: CONTAINING_COMMIT, + }, +}; +</script> + +<template> + <div class="gl-pt-4"> + <span data-testid="title" class="gl-mr-2">{{ namespace }}</span> + <gl-badge v-for="ref in tippingRefs" :key="ref" class="gl-mt-2 gl-mr-2" size="sm">{{ + ref + }}</gl-badge> + <gl-button + v-if="hasContainingRefs" + class="gl-mr-2 gl-font-sm!" + variant="link" + size="small" + @click="showRefs" + > + <gl-icon :name="collapseIcon" :size="14" /> + {{ namespace }} {{ $options.i18n.containingCommit }} + </gl-button> + <gl-collapse :visible="isContainingRefsVisible"> + <gl-skeleton-loader v-if="isLoadingRefs" :lines="1" /> + <template v-else> + <gl-badge v-for="ref in containingRefs" :key="ref" class="gl-mt-3 gl-mr-2" size="sm">{{ + ref + }}</gl-badge> + </template> + </gl-collapse> + </div> +</template> diff --git a/app/assets/javascripts/projects/commit_box/info/constants.js b/app/assets/javascripts/projects/commit_box/info/constants.js index be0bf715314..f255d6c3877 100644 --- a/app/assets/javascripts/projects/commit_box/info/constants.js +++ b/app/assets/javascripts/projects/commit_box/info/constants.js @@ -1,7 +1,19 @@ -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; export const COMMIT_BOX_POLL_INTERVAL = 10000; export const PIPELINE_STATUS_FETCH_ERROR = __( 'There was a problem fetching the latest pipeline status.', ); + +export const BRANCHES = s__('Commit|Branches'); + +export const TAGS = s__('Commit|Tags'); + +export const CONTAINING_COMMIT = s__('Commit|containing commit'); + +export const FETCH_CONTAINING_REFS_EVENT = 'fetch-containing-refs'; + +export const FETCH_COMMIT_REFERENCES_ERROR = s__( + 'Commit|There was an error fetching the commit references. Please try again later.', +); diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql new file mode 100644 index 00000000000..ea74efdbc46 --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql @@ -0,0 +1,10 @@ +query CommitContainingBranches($fullPath: ID!, $commitSha: String!) { + project(fullPath: $fullPath) { + id + commitReferences(commitSha: $commitSha) { + containingBranches(excludeTipped: true) { + names + } + } + } +} diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql new file mode 100644 index 00000000000..d736dc3ab66 --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_containing_tags.query.graphql @@ -0,0 +1,10 @@ +query CommitContainingTags($fullPath: ID!, $commitSha: String!) { + project(fullPath: $fullPath) { + id + commitReferences(commitSha: $commitSha) { + containingTags(excludeTipped: true) { + names + } + } + } +} diff --git a/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql new file mode 100644 index 00000000000..71d911c2acc --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/graphql/queries/commit_references.query.graphql @@ -0,0 +1,19 @@ +query CommitReferences($fullPath: ID!, $commitSha: String!) { + project(fullPath: $fullPath) { + id + commitReferences(commitSha: $commitSha) { + containingBranches(excludeTipped: true, limit: 1) { + names + } + containingTags(excludeTipped: true, limit: 1) { + names + } + tippingBranches { + names + } + tippingTags { + names + } + } + } +} diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js index 7c4b76fd62f..8f09c8e1e11 100644 --- a/app/assets/javascripts/projects/commit_box/info/index.js +++ b/app/assets/javascripts/projects/commit_box/info/index.js @@ -1,12 +1,10 @@ import { fetchCommitMergeRequests } from '~/commit_merge_requests'; import { initCommitPipelineMiniGraph } from './init_commit_pipeline_mini_graph'; -import { loadBranches } from './load_branches'; import initCommitPipelineStatus from './init_commit_pipeline_status'; +import initCommitReferences from './init_commit_references'; export const initCommitBoxInfo = () => { // Display commit related branches - loadBranches(); - // Related merge requests to this commit fetchCommitMergeRequests(); @@ -14,4 +12,6 @@ export const initCommitBoxInfo = () => { initCommitPipelineMiniGraph(); initCommitPipelineStatus(); + + initCommitReferences(); }; diff --git a/app/assets/javascripts/projects/commit_box/info/init_commit_references.js b/app/assets/javascripts/projects/commit_box/info/init_commit_references.js new file mode 100644 index 00000000000..c8497187211 --- /dev/null +++ b/app/assets/javascripts/projects/commit_box/info/init_commit_references.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import CommitBranches from './components/commit_refs.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default (selector = 'js-commit-branches-and-tags') => { + const el = document.getElementById(selector); + + if (!el) { + return false; + } + + const { fullPath, commitSha } = el.dataset; + + return new Vue({ + el, + apolloProvider, + provide: { + fullPath, + commitSha, + }, + render(createElement) { + return createElement(CommitBranches); + }, + }); +}; diff --git a/app/assets/javascripts/projects/commit_box/info/load_branches.js b/app/assets/javascripts/projects/commit_box/info/load_branches.js deleted file mode 100644 index 8333e70b951..00000000000 --- a/app/assets/javascripts/projects/commit_box/info/load_branches.js +++ /dev/null @@ -1,24 +0,0 @@ -import axios from 'axios'; -import { sanitize } from '~/lib/dompurify'; -import { __ } from '~/locale'; -import { initDetailsButton } from './init_details_button'; - -export const loadBranches = (containerSelector = '.js-commit-box-info') => { - const containerEl = document.querySelector(containerSelector); - if (!containerEl) { - return; - } - - const { commitPath } = containerEl.dataset; - const branchesEl = containerEl.querySelector('.commit-info.branches'); - axios - .get(commitPath) - .then(({ data }) => { - branchesEl.innerHTML = sanitize(data); - - initDetailsButton(); - }) - .catch(() => { - branchesEl.textContent = __('Failed to load branches. Please try again.'); - }); -}; diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb index e0594247975..556f04e8786 100644 --- a/app/services/alert_management/process_prometheus_alert_service.rb +++ b/app/services/alert_management/process_prometheus_alert_service.rb @@ -6,9 +6,10 @@ module AlertManagement include ::AlertManagement::AlertProcessing include ::AlertManagement::Responses - def initialize(project, payload) + def initialize(project, payload, integration: nil) @project = project @payload = payload + @integration = integration end def execute @@ -24,7 +25,7 @@ module AlertManagement private - attr_reader :project, :payload + attr_reader :project, :payload, :integration override :incoming_payload def incoming_payload @@ -32,6 +33,7 @@ module AlertManagement Gitlab::AlertManagement::Payload.parse( project, payload, + integration: integration, monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] ) end diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb index 1d24a113e05..f1c093c89b7 100644 --- a/app/services/projects/prometheus/alerts/notify_service.rb +++ b/app/services/projects/prometheus/alerts/notify_service.rb @@ -36,7 +36,7 @@ module Projects truncate_alerts! if max_alerts_exceeded? - process_prometheus_alerts + process_prometheus_alerts(integration) created end @@ -151,10 +151,10 @@ module Projects ActiveSupport::SecurityUtils.secure_compare(expected, actual) end - def process_prometheus_alerts + def process_prometheus_alerts(integration) alerts.map do |alert| AlertManagement::ProcessPrometheusAlertService - .new(project, alert) + .new(project, alert, integration: integration) .execute end end diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml index 079e24c6389..c161e1c9d2a 100644 --- a/app/views/projects/commit/_commit_box.html.haml +++ b/app/views/projects/commit/_commit_box.html.haml @@ -29,15 +29,14 @@ %pre.commit-description< = preserve(markdown_field(@commit, :description)) -.info-well.js-commit-box-info{ 'data-commit-path' => branches_project_commit_path(@project, @commit.id) } +.info-well .well-segment .icon-container.commit-icon = custom_icon("icon_commit") %span.cgray= n_('parent', 'parents', @commit.parents.count) - @commit.parents.each do |parent| = link_to parent.short_id, project_commit_path(@project, parent), class: "commit-sha" - .commit-info.branches - = gl_loading_icon(inline: true, css_class: 'gl-vertical-align-middle') + #js-commit-branches-and-tags{ data: { full_path: @project.full_path, commit_sha: @commit.short_id } } .well-segment.merge-request-info .icon-container diff --git a/config/feature_flags/development/password_reset_any_verified_email.yml b/config/feature_flags/development/password_reset_any_verified_email.yml index 9438c6ef414..2b8ee79b7e8 100644 --- a/config/feature_flags/development/password_reset_any_verified_email.yml +++ b/config/feature_flags/development/password_reset_any_verified_email.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/410038 milestone: '16.0' type: development group: group::authentication and authorization -default_enabled: false +default_enabled: true diff --git a/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb b/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb new file mode 100644 index 00000000000..ed4245694d4 --- /dev/null +++ b/db/migrate/20230512141931_add_group_id_to_dependency_list_exports.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddGroupIdToDependencyListExports < Gitlab::Database::Migration[2.1] + def change + add_column :dependency_list_exports, :group_id, :bigint + end +end diff --git a/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb b/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb new file mode 100644 index 00000000000..d7077e9dfdd --- /dev/null +++ b/db/post_migrate/20230512143000_remove_dependency_list_exports_project_id_not_null_constraint.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RemoveDependencyListExportsProjectIdNotNullConstraint < Gitlab::Database::Migration[2.1] + def up + change_column_null :dependency_list_exports, :project_id, true + end + + def down + # no-op as there can be null values after the migration + end +end diff --git a/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb b/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb new file mode 100644 index 00000000000..6019074b1f7 --- /dev/null +++ b/db/post_migrate/20230515101208_index_group_id_on_dependency_list_exports.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class IndexGroupIdOnDependencyListExports < Gitlab::Database::Migration[2.1] + INDEX_NAME = 'index_dependency_list_exports_on_group_id' + + disable_ddl_transaction! + + def up + add_concurrent_index :dependency_list_exports, :group_id, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :dependency_list_exports, INDEX_NAME + end +end diff --git a/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb b/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb new file mode 100644 index 00000000000..2780c551b2d --- /dev/null +++ b/db/post_migrate/20230515102353_add_foreign_key_to_group_id_on_dependency_list_exports.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddForeignKeyToGroupIdOnDependencyListExports < Gitlab::Database::Migration[2.1] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :dependency_list_exports, + :namespaces, + column: :group_id, + on_delete: :cascade, + reverse_lock_order: true + end + + def down + remove_foreign_key_if_exists :dependency_list_exports, column: :group_id + end +end diff --git a/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb b/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb new file mode 100644 index 00000000000..55b46369870 --- /dev/null +++ b/db/post_migrate/20230517001535_prepare_async_index_for_ci_pipeline_variables_bigint_id.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class PrepareAsyncIndexForCiPipelineVariablesBigintId < Gitlab::Database::Migration[2.1] + TABLE_NAME = :ci_pipeline_variables + INDEX_NAME = "index_#{TABLE_NAME}_on_id_convert_to_bigint" + + # TODO: Index to be created synchronously in https://gitlab.com/gitlab-org/gitlab/-/issues/408936 + def up + prepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME + end + + def down + unprepare_async_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: INDEX_NAME + end +end diff --git a/db/schema_migrations/20230512141931 b/db/schema_migrations/20230512141931 new file mode 100644 index 00000000000..7652e281eb7 --- /dev/null +++ b/db/schema_migrations/20230512141931 @@ -0,0 +1 @@ +2acead22b097b6873efa3108fedb374eeed793aef1519d9825685d97d844d2fe
\ No newline at end of file diff --git a/db/schema_migrations/20230512143000 b/db/schema_migrations/20230512143000 new file mode 100644 index 00000000000..6534634133d --- /dev/null +++ b/db/schema_migrations/20230512143000 @@ -0,0 +1 @@ +3a29402e93ec0239bf6012f29b31613f2ea91def3096673ee0b44bfb8624a532
\ No newline at end of file diff --git a/db/schema_migrations/20230515101208 b/db/schema_migrations/20230515101208 new file mode 100644 index 00000000000..5cd9727331f --- /dev/null +++ b/db/schema_migrations/20230515101208 @@ -0,0 +1 @@ +ddd627c22bc925cb186c54d1df2897cba93027c2919dbe279063ac82a496b812
\ No newline at end of file diff --git a/db/schema_migrations/20230515102353 b/db/schema_migrations/20230515102353 new file mode 100644 index 00000000000..17eee8f1d9f --- /dev/null +++ b/db/schema_migrations/20230515102353 @@ -0,0 +1 @@ +c7cad916d89ef08e0c7c184b2c68a09aa5fa7ea39de11e2bff3aa190e74ca986
\ No newline at end of file diff --git a/db/schema_migrations/20230517001535 b/db/schema_migrations/20230517001535 new file mode 100644 index 00000000000..1511ebfbccb --- /dev/null +++ b/db/schema_migrations/20230517001535 @@ -0,0 +1 @@ +5d23a9be6e9ba6424208c3e14fc708e45b3a68e14de7be87e294c4e3d926a31f
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 013b0e67c9a..5631d206fbb 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -15022,11 +15022,12 @@ CREATE TABLE dependency_list_exports ( id bigint NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, - project_id bigint NOT NULL, + project_id bigint, user_id bigint, file_store integer, status smallint DEFAULT 0 NOT NULL, file text, + group_id bigint, CONSTRAINT check_fff6fc9b2f CHECK ((char_length(file) <= 255)) ); @@ -30463,6 +30464,8 @@ CREATE UNIQUE INDEX index_dast_sites_on_project_id_and_url ON dast_sites USING b CREATE UNIQUE INDEX index_dep_prox_manifests_on_group_id_file_name_and_status ON dependency_proxy_manifests USING btree (group_id, file_name, status); +CREATE INDEX index_dependency_list_exports_on_group_id ON dependency_list_exports USING btree (group_id); + CREATE INDEX index_dependency_list_exports_on_project_id ON dependency_list_exports USING btree (project_id); CREATE INDEX index_dependency_list_exports_on_user_id ON dependency_list_exports USING btree (user_id); @@ -35515,6 +35518,9 @@ ALTER TABLE ONLY protected_branches ALTER TABLE ONLY issues ADD CONSTRAINT fk_df75a7c8b8 FOREIGN KEY (promoted_to_epic_id) REFERENCES epics(id) ON DELETE SET NULL; +ALTER TABLE ONLY dependency_list_exports + ADD CONSTRAINT fk_e133f6725e FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; + ALTER TABLE ONLY approval_project_rules ADD CONSTRAINT fk_e1372c912e FOREIGN KEY (scan_result_policy_id) REFERENCES scan_result_policies(id) ON DELETE CASCADE; diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md index dbbd348556c..ef1f4e06595 100644 --- a/doc/administration/gitaly/troubleshooting.md +++ b/doc/administration/gitaly/troubleshooting.md @@ -400,6 +400,10 @@ To resolve this, remove the `noexec` option from the file system mount. An alter 1. Add `gitaly['runtime_dir'] = '<PATH_WITH_EXEC_PERM>'` to `/etc/gitlab/gitlab.rb` and specify a location without `noexec` set. 1. Run `sudo gitlab-ctl reconfigure`. +### Commit signing fails with `invalid argument: signing key is encrypted` or `invalid data: tag byte does not have MSB set.` + +Because Gitaly commit signing is headless and not associated with a specific user, the GPG signing key must be created without a passphrase, or the passphrase must be removed before export. + ## Troubleshoot Praefect (Gitaly Cluster) The following sections provide possible solutions to Gitaly Cluster errors. diff --git a/doc/integration/azure.md b/doc/integration/azure.md index 0d8c830c016..8d993dfbb63 100644 --- a/doc/integration/azure.md +++ b/doc/integration/azure.md @@ -29,9 +29,13 @@ You must set the `uid_field`, which differs across the providers: | [`omniauth_openid_connect`](https://github.com/omniauth/omniauth_openid_connect/) | `sub` | Specify `uid_field` to use another field | To migrate from `omniauth-azure-oauth2` to `omniauth_openid_connect` you -must change the configuration: +must change the configuration. -- **For Omnibus installations** +::Tabs + +:::TabTitle Linux package (Omnibus) + +Remove some of the existing configuration and add new configuration as shown. ```diff gitlab_rails['omniauth_providers'] = [ @@ -60,7 +64,9 @@ gitlab_rails['omniauth_providers'] = [ ] ``` -- **For installations from source** +:::TabTitle Self-compiled (source) + +Remove some of the existing configuration and add new configuration as shown. ```diff - { name: 'azure_oauth2', @@ -88,10 +94,16 @@ gitlab_rails['omniauth_providers'] = [ } ``` +::EndTabs + To migrate for example from `omniauth-azure-activedirectory-v2` to `omniauth_openid_connect` you -must change the configuration: +must change the configuration. -- **For Omnibus installations** +::Tabs + +:::TabTitle Linux package (Omnibus) + +Remove some of the existing configuration and add new configuration as shown. ```diff gitlab_rails['omniauth_providers'] = [ @@ -120,7 +132,9 @@ gitlab_rails['omniauth_providers'] = [ ] ``` -- **For installations from source** +:::TabTitle Self-compiled (source) + +Remove some of the existing configuration and add new configuration as shown. ```diff - { name: 'azure_activedirectory_v2', @@ -148,6 +162,8 @@ gitlab_rails['omniauth_providers'] = [ } ``` +::EndTabs + For more information on other customizations, see [`gitlab_username_claim`](index.md#authentication-sources). ## Register an Azure application diff --git a/doc/user/product_analytics/index.md b/doc/user/product_analytics/index.md index b5f936e7848..1c6a241d5f4 100644 --- a/doc/user/product_analytics/index.md +++ b/doc/user/product_analytics/index.md @@ -23,9 +23,9 @@ For more information, see the [group direction page](https://about.gitlab.com/di Product analytics uses several tools: -- [**Jitsu**](https://jitsu.com/docs) - A web and app event collection platform that provides a consistent API to collect user data and pass it through to ClickHouse. +- [**Snowplow**](https://docs.snowplow.io/docs) - A developer-first engine for collecting behavioral data, and passing it through to ClickHouse. - [**ClickHouse**](https://clickhouse.com/docs) - A database suited to store, query, and retrieve analytical data. -- [**Cube.js**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse. +- [**Cube**](https://cube.dev/docs/) - An analytical graphing library that provides an API to run queries against the data stored in Clickhouse. The following diagram illustrates the product analytics flow: @@ -34,19 +34,21 @@ The following diagram illustrates the product analytics flow: title: Product Analytics flow --- flowchart TB - subgraph Adding data - A([SDK]) --Send user data--> B[Analytics Proxy] - B --Transform data and pass it through--> C[Snowplow] - C --Pass the data to the associated database--> D([Clickhouse]) + subgraph Event collection + A([SDK]) --Send user data--> B[Snowplow Collector] + B --Pass data through--> C[Snowplow Enricher] end - subgraph Showing dashboards - E([Dashboards]) --Generated from the YAML definition--> F[Dashboard] + subgraph Data warehouse + C --Transform and enrich data--> D([Clickhouse]) + end + subgraph Data visualization with dashboards + E([Dashboards]) --Generated from the YAML definition--> F[Panels/Visualizations] F --Request data--> G[Product Analytics API] - G --Run Cube queries with pre-aggregations--> H[Cube.js] + G --Run Cube queries with pre-aggregations--> H[Cube] H --Get data from database--> D D --Return results--> H - H --> G - G --Transform data to be rendered--> F + H --Transform data to be rendered--> G + G --Return data--> F end ``` @@ -73,20 +75,6 @@ Prerequisite: 1. On the left sidebar, select **Settings > General**. 1. Expand the **Analytics** tab and find the **Product analytics** section. 1. Select **Enable product analytics** and enter the configuration values. - The following table shows the required configuration parameters and example values: - - | Name | Value | - |--------------------------------|------------------------------------------------------------| - | Configurator connection string | `https://test:test@configurator.gitlab.com` | - | Jitsu host | `https://jitsu.gitlab.com` | - | Jitsu project ID | `g0maofw84gx5sjxgse2k` | - | Jitsu administrator email | `jitsu.admin@gitlab.com` | - | Jitsu administrator password | `<your_password>` | - | Collector host | `https://collector.gitlab.com` | - | ClickHouse URL | `https://<username>:<password>@clickhouse.gitlab.com:8123` | - | Cube API URL | `https://cube.gitlab.com` | - | Cube API key | `25718201b3e9...ae6bbdc62dbb` | - 1. Select **Save changes**. ## Product analytics dashboards diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb index 01dcb95eab5..5b136431ce7 100644 --- a/lib/gitlab/alert_management/payload/base.rb +++ b/lib/gitlab/alert_management/payload/base.rb @@ -181,7 +181,6 @@ module Gitlab end end - # Overriden in EE::Gitlab::AlertManagement::Payload::Generic def value_for_paths(paths) target_path = paths.find { |path| payload&.dig(*path) } diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb index 4c36ebbf3aa..76f3da8366b 100644 --- a/lib/gitlab/alert_management/payload/prometheus.rb +++ b/lib/gitlab/alert_management/payload/prometheus.rb @@ -94,6 +94,10 @@ module Gitlab project && title && starts_at_raw end + def source + integration&.name || monitoring_tool + end + private override :severity_mapping @@ -131,3 +135,5 @@ module Gitlab end end end + +Gitlab::AlertManagement::Payload::Prometheus.prepend_mod diff --git a/lib/sidebars/user_profile/panel.rb b/lib/sidebars/user_profile/panel.rb index 9a595fdf64c..1852ef928f4 100644 --- a/lib/sidebars/user_profile/panel.rb +++ b/lib/sidebars/user_profile/panel.rb @@ -4,6 +4,9 @@ module Sidebars module UserProfile class Panel < ::Sidebars::Panel include UsersHelper + include Gitlab::Allowable + + delegate :current_user, to: :@context override :configure_menus def configure_menus diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 61bb794f7d9..6f2d5b8456a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8383,6 +8383,9 @@ msgstr "" msgid "CHANGELOG" msgstr "" +msgid "CI" +msgstr "" + msgid "CI Lint" msgstr "" @@ -11098,6 +11101,18 @@ msgstr "" msgid "Committed-before" msgstr "" +msgid "Commit|Branches" +msgstr "" + +msgid "Commit|Tags" +msgstr "" + +msgid "Commit|There was an error fetching the commit references. Please try again later." +msgstr "" + +msgid "Commit|containing commit" +msgstr "" + msgid "Community forum" msgstr "" @@ -28981,6 +28996,9 @@ msgstr "" msgid "MlExperimentTracking|Status" msgstr "" +msgid "MlExperimentTracking|Triggered by" +msgstr "" + msgid "Modal updated" msgstr "" diff --git a/rubocop/cop/rspec/factory_bot/local_static_assignment.rb b/rubocop/cop/rspec/factory_bot/local_static_assignment.rb new file mode 100644 index 00000000000..1a26bc31c78 --- /dev/null +++ b/rubocop/cop/rspec/factory_bot/local_static_assignment.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + module FactoryBot + # Flags local assignments during factory "load time". This leads to + # static data definitions. + # + # Move these definitions into attribute block or + # `transient` block to ensure that the data is evaluated during + # "runtime" and remains dynamic. + # + # @example + # # bad + # factory :foo do + # random = rand(23) + # baz { "baz-#{random}" } + # + # trait :a_trait do + # random = rand(23) + # baz { "baz-#{random}" } + # end + # + # transient do + # random = rand(23) + # baz { "baz-#{random}" } + # end + # end + # + # # good + # factory :foo do + # baz { "baz-#{random}" } + # + # trait :a_trait do + # baz { "baz-#{random}" } + # end + # + # transient do + # random { rand(23) } + # end + # end + class LocalStaticAssignment < RuboCop::Cop::Base + MSG = 'Avoid local static assignemnts in factories which lead to static data definitions.' + + RESTRICT_ON_SEND = %i[factory transient trait].freeze + + def_node_search :local_assignment, <<~PATTERN + (begin $(lvasgn ...)) + PATTERN + + def on_send(node) + return unless node.parent&.block_type? + + node.parent.each_child_node(:begin) do |begin_node| + begin_node.each_child_node(:lvasgn) do |lvasgn_node| + add_offense(lvasgn_node) + end + end + end + end + end + end + end +end diff --git a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb index 926cd7ea681..428ce5b5607 100644 --- a/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/conflicts_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::MergeRequests::ConflictsController do +RSpec.describe Projects::MergeRequests::ConflictsController, feature_category: :code_review_workflow do let(:project) { create(:project, :repository) } let(:user) { project.first_owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } diff --git a/spec/controllers/projects/merge_requests/content_controller_spec.rb b/spec/controllers/projects/merge_requests/content_controller_spec.rb index 0116071bddf..69edb47fe71 100644 --- a/spec/controllers/projects/merge_requests/content_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/content_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Projects::MergeRequests::ContentController do +RSpec.describe Projects::MergeRequests::ContentController, feature_category: :code_review_workflow do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:merge_request) { create(:merge_request, target_project: project, source_project: project) } diff --git a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb index 6632473a85c..c3a5255b584 100644 --- a/spec/controllers/projects/merge_requests/drafts_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/drafts_controller_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe Projects::MergeRequests::DraftsController do +RSpec.describe Projects::MergeRequests::DraftsController, feature_category: :code_review_workflow do include RepoHelpers let(:project) { create(:project, :repository) } diff --git a/spec/factories/design_management/designs.rb b/spec/factories/design_management/designs.rb index d16fd0c297b..b284c7f5737 100644 --- a/spec/factories/design_management/designs.rb +++ b/spec/factories/design_management/designs.rb @@ -26,7 +26,7 @@ FactoryBot.define do sequence(:relative_position) { |n| n * 1000 } end - create_versions = ->(design, evaluator, commit_version) do + create_versions = ->(design, evaluator, commit_version) do # rubocop:disable RSpec/FactoryBot/LocalStaticAssignment unless evaluator.versions_count == 0 project = design.project issue = design.issue diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js index 514644a92f2..046302d07c1 100644 --- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js +++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js @@ -79,6 +79,16 @@ describe('JobArtifactsTable component', () => { const findDeleteButton = () => wrapper.findByTestId('job-artifacts-delete-button'); const findArtifactDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button'); + // first checkbox is the "select all" checkbox in the table header + const findSelectAllCheckbox = () => wrapper.findComponent(GlFormCheckbox); + const findSelectAllCheckboxChecked = () => findSelectAllCheckbox().find('input').element.checked; + const findSelectAllCheckboxIndeterminate = () => + findSelectAllCheckbox().find('input').element.indeterminate; + const findSelectAllCheckboxDisabled = () => + findSelectAllCheckbox().find('input').element.disabled; + const toggleSelectAllCheckbox = () => + findSelectAllCheckbox().vm.$emit('change', !findSelectAllCheckboxChecked()); + // first checkbox is a "select all", this finder should get the first job checkbox const findJobCheckbox = (i = 1) => wrapper.findAllComponents(GlFormCheckbox).at(i); const findAnyCheckbox = () => wrapper.findComponent(GlFormCheckbox); @@ -125,7 +135,15 @@ describe('JobArtifactsTable component', () => { }, }); - const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill({}); + const allArtifacts = getJobArtifactsResponse.data.project.jobs.nodes + .map((jobNode) => jobNode.artifacts.nodes.map((artifactNode) => artifactNode.id)) + .reduce((artifacts, jobArtifacts) => artifacts.concat(jobArtifacts)); + + const maxSelectedArtifacts = new Array(SELECTED_ARTIFACTS_MAX_COUNT).fill('artifact-id'); + const maxSelectedArtifactsIncludingCurrentPage = [ + ...allArtifacts, + ...new Array(SELECTED_ARTIFACTS_MAX_COUNT - allArtifacts.length).fill('artifact-id'), + ]; const createComponent = ({ handlers = { @@ -394,7 +412,7 @@ describe('JobArtifactsTable component', () => { it('does not clear selected artifacts on success', async () => { // select job 2 via checkbox - findJobCheckbox(2).vm.$emit('input', true); + findJobCheckbox(2).vm.$emit('change', true); // click delete button job 1 findDeleteButton().vm.$emit('click'); @@ -434,7 +452,7 @@ describe('JobArtifactsTable component', () => { await waitForPromises(); // select job 2 via checkbox - findJobCheckbox(2).vm.$emit('input', true); + findJobCheckbox(2).vm.$emit('change', true); // click delete button job 1 findDeleteButton().vm.$emit('click'); @@ -494,14 +512,14 @@ describe('JobArtifactsTable component', () => { it('shows selected artifacts when a job is checked', async () => { expect(findBulkDeleteContainer().exists()).toBe(false); - await findJobCheckbox().vm.$emit('input', true); + await findJobCheckbox().vm.$emit('change', true); expect(findBulkDeleteContainer().exists()).toBe(true); expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(selectedArtifacts); }); it('disappears when selected artifacts are cleared', async () => { - await findJobCheckbox().vm.$emit('input', true); + await findJobCheckbox().vm.$emit('change', true); expect(findBulkDeleteContainer().exists()).toBe(true); @@ -511,7 +529,7 @@ describe('JobArtifactsTable component', () => { }); it('shows a modal to confirm bulk delete', async () => { - findJobCheckbox().vm.$emit('input', true); + findJobCheckbox().vm.$emit('change', true); findBulkDelete().vm.$emit('showBulkDeleteModal'); await nextTick(); @@ -520,7 +538,7 @@ describe('JobArtifactsTable component', () => { }); it('deletes the selected artifacts and shows a toast', async () => { - findJobCheckbox().vm.$emit('input', true); + findJobCheckbox().vm.$emit('change', true); findBulkDelete().vm.$emit('showBulkDeleteModal'); findBulkDeleteModal().vm.$emit('primary'); @@ -537,7 +555,7 @@ describe('JobArtifactsTable component', () => { }); it('clears selected artifacts on success', async () => { - findJobCheckbox().vm.$emit('input', true); + findJobCheckbox().vm.$emit('change', true); findBulkDelete().vm.$emit('showBulkDeleteModal'); findBulkDeleteModal().vm.$emit('primary'); @@ -545,34 +563,216 @@ describe('JobArtifactsTable component', () => { expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]); }); - }); - describe('when the selected artifacts limit is reached', () => { - beforeEach(async () => { - createComponent({ - canDestroyArtifacts: true, - glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, - data: { selectedArtifacts: maxSelectedArtifacts }, + describe('select all checkbox', () => { + describe('when no artifacts are selected', () => { + it('is not checked', () => { + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findSelectAllCheckboxIndeterminate()).toBe(false); + }); + + it('selects all artifacts when toggled', async () => { + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findSelectAllCheckboxIndeterminate()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual(allArtifacts); + }); }); - await nextTick(); + describe('when some artifacts are selected', () => { + beforeEach(async () => { + findJobCheckbox().vm.$emit('change', true); + + await nextTick(); + }); + + it('is indeterminate', () => { + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findSelectAllCheckboxIndeterminate()).toBe(true); + }); + + it('deselects all artifacts when toggled', async () => { + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]); + }); + }); + + describe('when all artifacts are selected', () => { + beforeEach(async () => { + findJobCheckbox(1).vm.$emit('change', true); + findJobCheckbox(2).vm.$emit('change', true); + + await nextTick(); + }); + + it('is checked', () => { + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findSelectAllCheckboxIndeterminate()).toBe(false); + }); + + it('deselects all artifacts when toggled', async () => { + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([]); + }); + }); + + describe('when an artifact is selected on another page', () => { + const otherPageArtifact = { id: 'gid://gitlab/Ci::JobArtifact/some/other/id' }; + + beforeEach(async () => { + // expand the first job row to access the details component + findCount().trigger('click'); + + await nextTick(); + + // mock the selection of an artifact on another page by emitting a select event + findDetailsInRow(1).vm.$emit('selectArtifact', otherPageArtifact, true); + }); + + it('is not checked even though an artifact is selected', () => { + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([ + otherPageArtifact.id, + ]); + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findSelectAllCheckboxIndeterminate()).toBe(false); + }); + + it('only toggles selection of visible artifacts, leaving the other artifact selected', async () => { + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([ + otherPageArtifact.id, + ...allArtifacts, + ]); + + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toStrictEqual([ + otherPageArtifact.id, + ]); + }); + }); }); + }); + + describe('select all checkbox respects selected artifacts limit', () => { + describe('when selecting all visible artifacts would exceed the limit', () => { + const selectedArtifactsLength = SELECTED_ARTIFACTS_MAX_COUNT - 1; + + beforeEach(async () => { + createComponent({ + canDestroyArtifacts: true, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, + data: { + selectedArtifacts: new Array(selectedArtifactsLength).fill('artifact-id'), + }, + }); + + await nextTick(); + }); + + it('selects only up to the limit', async () => { + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toHaveLength(selectedArtifactsLength); - it('passes isSelectedArtifactsLimitReached to bulk delete', () => { - expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true); + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findBulkDelete().props('selectedArtifacts')).toHaveLength( + SELECTED_ARTIFACTS_MAX_COUNT, + ); + expect(findBulkDelete().props('selectedArtifacts')).not.toContain( + allArtifacts[allArtifacts.length - 1], + ); + }); }); - it('passes isSelectedArtifactsLimitReached to job checkbox', () => { - expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe( - true, - ); + describe('when limit has been reached without artifacts on the current page', () => { + beforeEach(async () => { + createComponent({ + canDestroyArtifacts: true, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, + data: { selectedArtifacts: maxSelectedArtifacts }, + }); + + await nextTick(); + }); + + it('passes isSelectedArtifactsLimitReached to bulk delete', () => { + expect(findBulkDelete().props('isSelectedArtifactsLimitReached')).toBe(true); + }); + + it('passes isSelectedArtifactsLimitReached to job checkbox', () => { + expect(wrapper.findComponent(JobCheckbox).props('isSelectedArtifactsLimitReached')).toBe( + true, + ); + }); + + it('passes isSelectedArtifactsLimitReached to table row details', async () => { + findCount().trigger('click'); + await nextTick(); + + expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true); + }); + + it('disables the select all checkbox', () => { + expect(findSelectAllCheckboxDisabled()).toBe(true); + }); }); - it('passes isSelectedArtifactsLimitReached to table row details', async () => { - findCount().trigger('click'); - await nextTick(); + describe('when limit has been reached including artifacts on the current page', () => { + beforeEach(async () => { + createComponent({ + canDestroyArtifacts: true, + glFeatures: { [BULK_DELETE_FEATURE_FLAG]: true }, + data: { + selectedArtifacts: maxSelectedArtifactsIncludingCurrentPage, + }, + }); + + await nextTick(); + }); + + describe('the select all checkbox', () => { + it('is checked', () => { + expect(findSelectAllCheckboxChecked()).toBe(true); + expect(findSelectAllCheckboxIndeterminate()).toBe(false); + }); - expect(findDetailsInRow(1).props('isSelectedArtifactsLimitReached')).toBe(true); + it('deselects all artifacts when toggled', async () => { + expect(findBulkDelete().props('selectedArtifacts')).toHaveLength( + SELECTED_ARTIFACTS_MAX_COUNT, + ); + + toggleSelectAllCheckbox(); + + await nextTick(); + + expect(findSelectAllCheckboxChecked()).toBe(false); + expect(findBulkDelete().props('selectedArtifacts')).toHaveLength( + SELECTED_ARTIFACTS_MAX_COUNT - allArtifacts.length, + ); + }); + }); }); }); @@ -588,7 +788,7 @@ describe('JobArtifactsTable component', () => { await waitForPromises(); - findJobCheckbox().vm.$emit('input', true); + findJobCheckbox().vm.$emit('change', true); findBulkDelete().vm.$emit('showBulkDeleteModal'); findBulkDeleteModal().vm.$emit('primary'); diff --git a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js index 8b47571239c..73a49506564 100644 --- a/spec/frontend/ci/artifacts/components/job_checkbox_spec.js +++ b/spec/frontend/ci/artifacts/components/job_checkbox_spec.js @@ -48,7 +48,7 @@ describe('JobCheckbox component', () => { }); it('selects the unselected artifacts on click', () => { - findCheckbox().vm.$emit('input', true); + findCheckbox().vm.$emit('change', true); expect(wrapper.emitted('selectArtifact')).toMatchObject([ [mockUnselectedArtifacts[0], true], @@ -83,7 +83,7 @@ describe('JobCheckbox component', () => { }); it('deselects the selected artifacts on click', () => { - findCheckbox().vm.$emit('input', false); + findCheckbox().vm.$emit('change', false); expect(wrapper.emitted('selectArtifact')).toMatchObject([ [mockSelectedArtifacts[0], false], @@ -105,7 +105,7 @@ describe('JobCheckbox component', () => { }); it('selects the artifacts on click', () => { - findCheckbox().vm.$emit('input', true); + findCheckbox().vm.$emit('change', true); expect(wrapper.emitted('selectArtifact')).toMatchObject([ [mockUnselectedArtifacts[0], true], diff --git a/spec/frontend/commit/components/commit_refs_spec.js b/spec/frontend/commit/components/commit_refs_spec.js new file mode 100644 index 00000000000..7c35ff1969c --- /dev/null +++ b/spec/frontend/commit/components/commit_refs_spec.js @@ -0,0 +1,82 @@ +import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { createAlert } from '~/alert'; +import commitReferences from '~/projects/commit_box/info/graphql/queries/commit_references.query.graphql'; +import containingBranchesQuery from '~/projects/commit_box/info/graphql/queries/commit_containing_branches.query.graphql'; +import RefsList from '~/projects/commit_box/info/components/refs_list.vue'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { + FETCH_CONTAINING_REFS_EVENT, + FETCH_COMMIT_REFERENCES_ERROR, +} from '~/projects/commit_box/info/constants'; +import CommitRefs from '~/projects/commit_box/info/components/commit_refs.vue'; + +import { + mockCommitReferencesResponse, + mockOnlyBranchesResponse, + mockContainingBranchesResponse, + refsListPropsMock, +} from '../mock_data'; + +Vue.use(VueApollo); + +jest.mock('~/alert'); + +describe('Commit references component', () => { + let wrapper; + + const successQueryHandler = (mockResponse) => jest.fn().mockResolvedValue(mockResponse); + const failedQueryHandler = jest.fn().mockRejectedValue(new Error('GraphQL error')); + const containingBranchesQueryHandler = successQueryHandler(mockContainingBranchesResponse); + const findRefsLists = () => wrapper.findAllComponents(RefsList); + const branchesList = () => findRefsLists().at(0); + + const createComponent = async ( + commitReferencesQueryHandler = successQueryHandler(mockCommitReferencesResponse), + ) => { + wrapper = shallowMount(CommitRefs, { + apolloProvider: createMockApollo([ + [commitReferences, commitReferencesQueryHandler], + [containingBranchesQuery, containingBranchesQueryHandler], + ]), + provide: { + fullPath: '/some/project', + commitSha: 'xxx', + }, + }); + + await waitForPromises(); + }; + + it('renders component correcrly', async () => { + await createComponent(); + expect(findRefsLists()).toHaveLength(2); + }); + + it('passes props to refs list', async () => { + await createComponent(); + expect(branchesList().props()).toEqual(refsListPropsMock); + }); + + it('shows alert when response fails', async () => { + await createComponent(failedQueryHandler); + expect(createAlert).toHaveBeenCalledWith({ + message: FETCH_COMMIT_REFERENCES_ERROR, + captureError: true, + }); + }); + + it('fetches containing refs on the fetch event', async () => { + await createComponent(); + branchesList().vm.$emit(FETCH_CONTAINING_REFS_EVENT); + await waitForPromises(); + expect(containingBranchesQueryHandler).toHaveBeenCalledTimes(1); + }); + + it('does not render list when there is no branches or tags', async () => { + await createComponent(successQueryHandler(mockOnlyBranchesResponse)); + expect(findRefsLists()).toHaveLength(1); + }); +}); diff --git a/spec/frontend/commit/components/refs_list_spec.js b/spec/frontend/commit/components/refs_list_spec.js new file mode 100644 index 00000000000..d124f4a41b8 --- /dev/null +++ b/spec/frontend/commit/components/refs_list_spec.js @@ -0,0 +1,70 @@ +import { GlCollapse, GlButton, GlBadge, GlSkeletonLoader } from '@gitlab/ui'; +import RefsList from '~/projects/commit_box/info/components/refs_list.vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { + CONTAINING_COMMIT, + FETCH_CONTAINING_REFS_EVENT, +} from '~/projects/commit_box/info/constants'; +import { refsListPropsMock, containingBranchesMock } from '../mock_data'; + +describe('Commit references component', () => { + let wrapper; + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(RefsList, { + propsData: { + ...refsListPropsMock, + ...props, + }, + }); + }; + + const findTitle = () => wrapper.findByTestId('title'); + const findCollapseButton = () => wrapper.findComponent(GlButton); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findTippingRefs = () => wrapper.findAllComponents(GlBadge); + const findContainingRefs = () => wrapper.findComponent(GlCollapse); + + beforeEach(() => { + createComponent(); + }); + + it('renders the namespace passed', () => { + expect(findTitle().text()).toEqual(refsListPropsMock.namespace); + }); + + it('renders list of tipping branches or tags', () => { + expect(findTippingRefs()).toHaveLength(refsListPropsMock.tippingRefs.length); + }); + + it('does not render collapse with containing branches ot tags when there is no data', () => { + createComponent({ hasContainingRefs: false }); + expect(findCollapseButton().exists()).toBe(false); + }); + + it('renders collapse component if commit has containing branches', () => { + expect(findCollapseButton().text()).toContain(CONTAINING_COMMIT); + }); + + it('Emits event when collapse button is clicked', () => { + findCollapseButton().vm.$emit('click'); + expect(wrapper.emitted()[FETCH_CONTAINING_REFS_EVENT]).toHaveLength(1); + }); + + it('Renders the list of containing branches or tags when collapse is expanded', () => { + createComponent({ containingRefs: containingBranchesMock }); + const containingRefsList = findContainingRefs(); + expect(containingRefsList.findAllComponents(GlBadge)).toHaveLength( + containingBranchesMock.length, + ); + }); + + it('Does not reneder list of tipping branches or tags if there is no data', () => { + createComponent({ tippingRefs: [] }); + expect(findTippingRefs().exists()).toBe(false); + }); + + it('Renders skeleton loader when isLoading prop has true value', () => { + createComponent({ isLoading: true, containingRefs: [] }); + expect(findSkeletonLoader().exists()).toBe(true); + }); +}); diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js index 3b6971d9607..1c2bcb17663 100644 --- a/spec/frontend/commit/mock_data.js +++ b/spec/frontend/commit/mock_data.js @@ -232,3 +232,60 @@ export const x509CertificateDetailsProp = { subject: 'CN=gitlab@example.org,OU=Example,O=World', subjectKeyIdentifier: 'BC BC BC BC BC BC BC BC', }; + +export const tippingBranchesMock = ['main', 'development']; + +export const containingBranchesMock = ['branch-1', 'branch-2', 'branch-3']; + +export const mockCommitReferencesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + commitReferences: { + containingBranches: { names: ['branch-1'], __typename: 'CommitParentNames' }, + containingTags: { names: ['tag-1'], __typename: 'CommitParentNames' }, + tippingBranches: { names: tippingBranchesMock, __typename: 'CommitParentNames' }, + tippingTags: { names: ['tag-latest'], __typename: 'CommitParentNames' }, + __typename: 'CommitReferences', + }, + __typename: 'Project', + }, + }, +}; + +export const mockOnlyBranchesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + commitReferences: { + containingBranches: { names: ['branch-1'], __typename: 'CommitParentNames' }, + containingTags: { names: [], __typename: 'CommitParentNames' }, + tippingBranches: { names: tippingBranchesMock, __typename: 'CommitParentNames' }, + tippingTags: { names: [], __typename: 'CommitParentNames' }, + __typename: 'CommitReferences', + }, + __typename: 'Project', + }, + }, +}; + +export const mockContainingBranchesResponse = { + data: { + project: { + id: 'gid://gitlab/Project/1', + commitReferences: { + containingBranches: { names: containingBranchesMock, __typename: 'CommitParentNames' }, + __typename: 'CommitReferences', + }, + __typename: 'Project', + }, + }, +}; + +export const refsListPropsMock = { + hasContainingRefs: true, + containingRefs: [], + namespace: 'Branches', + tippingRefs: tippingBranchesMock, + isLoading: false, +}; diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js index 9d1c22faa8f..07d501c4e44 100644 --- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js @@ -48,6 +48,14 @@ describe('MlCandidatesShow', () => { ['', 'Status', CANDIDATE.info.status, ''], ['', 'Experiment', CANDIDATE.info.experiment_name, CANDIDATE.info.path_to_experiment], ['', 'Artifacts', 'Artifacts', CANDIDATE.info.path_to_artifact], + ['CI', 'Job', CANDIDATE.info.ci_job.name, CANDIDATE.info.ci_job.path], + ['', 'Triggered by', CANDIDATE.info.ci_job.user.username, CANDIDATE.info.ci_job.user.path], + [ + '', + 'Merge request', + CANDIDATE.info.ci_job.merge_request.title, + CANDIDATE.info.ci_job.merge_request.path, + ], ['Parameters', CANDIDATE.params[0].name, CANDIDATE.params[0].value, ''], ['', CANDIDATE.params[1].name, CANDIDATE.params[1].value, ''], ['Metrics', CANDIDATE.metrics[0].name, CANDIDATE.metrics[0].value, ''], @@ -75,6 +83,9 @@ describe('MlCandidatesShow', () => { expect(findSectionLabel('Parameters').exists()).toBe(true); expect(findSectionLabel('Metadata').exists()).toBe(true); expect(findSectionLabel('Metrics').exists()).toBe(true); + expect(findSectionLabel('CI').exists()).toBe(true); + expect(findLabel('Merge request').exists()).toBe(true); + expect(findLabel('Triggered by').exists()).toBe(true); }); }); @@ -99,6 +110,7 @@ describe('MlCandidatesShow', () => { delete candidate.params; delete candidate.metrics; delete candidate.metadata; + delete candidate.info.ci_job; return candidate; }), ); @@ -114,6 +126,29 @@ describe('MlCandidatesShow', () => { it('does not render metrics', () => { expect(findSectionLabel('Metrics').exists()).toBe(false); }); + + it('does not render CI info', () => { + expect(findSectionLabel('CI').exists()).toBe(false); + }); + }); + + describe('Has CI, but no user or mr', () => { + beforeEach(() => + createWrapper(() => { + const candidate = newCandidate(); + delete candidate.info.ci_job.user; + delete candidate.info.ci_job.merge_request; + return candidate; + }), + ); + + it('does not render MR info', () => { + expect(findLabel('Merge request').exists()).toBe(false); + }); + + it('does not render CI user info', () => { + expect(findLabel('Triggered by').exists()).toBe(false); + }); }); }); }); diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js index cad2c03fc93..16c1b29f7bc 100644 --- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js +++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js @@ -19,5 +19,17 @@ export const newCandidate = () => ({ path_to_experiment: 'path/to/experiment', status: 'SUCCESS', path: 'path_to_candidate', + ci_job: { + name: 'test', + path: 'path/to/job', + merge_request: { + path: 'path/to/mr', + title: 'Some MR', + }, + user: { + path: 'path/to/ci/user', + username: 'ciuser', + }, + }, }, }); diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js deleted file mode 100644 index b00a6378e07..00000000000 --- a/spec/frontend/projects/commit_box/info/load_branches_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import { setHTMLFixture } from 'helpers/fixtures'; -import waitForPromises from 'helpers/wait_for_promises'; -import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; -import { loadBranches } from '~/projects/commit_box/info/load_branches'; -import { initDetailsButton } from '~/projects/commit_box/info/init_details_button'; - -jest.mock('~/projects/commit_box/info/init_details_button'); - -const mockCommitPath = '/commit/abcd/branches'; -const mockBranchesRes = - '<a href="/-/commits/main">main</a><span><a href="/-/commits/my-branch">my-branch</a></span>'; - -describe('~/projects/commit_box/info/load_branches', () => { - let mock; - - const getElInnerHtml = () => document.querySelector('.js-commit-box-info').innerHTML; - - beforeEach(() => { - setHTMLFixture(` - <div class="js-commit-box-info" data-commit-path="${mockCommitPath}"> - <div class="commit-info branches"> - <span class="spinner"/> - </div> - </div>`); - - mock = new MockAdapter(axios); - mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes); - }); - - it('initializes the details button', async () => { - loadBranches(); - await waitForPromises(); - - expect(initDetailsButton).toHaveBeenCalled(); - }); - - it('loads and renders branches info', async () => { - loadBranches(); - await waitForPromises(); - - expect(getElInnerHtml()).toMatchInterpolatedText( - `<div class="commit-info branches">${mockBranchesRes}</div>`, - ); - }); - - it('does not load when no container is provided', async () => { - loadBranches('.js-another-class'); - await waitForPromises(); - - expect(mock.history.get).toHaveLength(0); - }); - - describe('when branches request returns unsafe content', () => { - beforeEach(() => { - mock - .onGet(mockCommitPath) - .reply(HTTP_STATUS_OK, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>'); - }); - - it('displays sanitized html', async () => { - loadBranches(); - await waitForPromises(); - - expect(getElInnerHtml()).toMatchInterpolatedText( - '<div class="commit-info branches"><a href="/-/commits/main">main</a></div>', - ); - }); - }); - - describe('when branches request fails', () => { - beforeEach(() => { - mock.onGet(mockCommitPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Error!'); - }); - - it('attempts to load and renders an error', async () => { - loadBranches(); - await waitForPromises(); - - expect(getElInnerHtml()).toMatchInterpolatedText( - '<div class="commit-info branches">Failed to load branches. Please try again.</div>', - ); - }); - }); -}); diff --git a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb index 6a4f35c01e3..8ead292c27a 100644 --- a/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb +++ b/spec/lib/gitlab/alert_management/payload/prometheus_spec.rb @@ -297,4 +297,18 @@ RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do it { is_expected.to be_nil } end end + + describe '#source' do + subject { parsed_payload.source } + + it { is_expected.to eq('Prometheus') } + + context 'with alerting integration provided' do + before do + parsed_payload.integration = instance_double('::AlertManagement::HttpIntegration', name: 'INTEGRATION') + end + + it { is_expected.to eq('INTEGRATION') } + end + end end diff --git a/spec/requests/api/internal/base_spec.rb b/spec/requests/api/internal/base_spec.rb index 6414b1efe6a..d1a0ad57a84 100644 --- a/spec/requests/api/internal/base_spec.rb +++ b/spec/requests/api/internal/base_spec.rb @@ -513,24 +513,63 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do project.add_developer(user) end + shared_context 'with env passed as a JSON' do + let(:obj_dir_relative) { './objects' } + let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] } + let(:env) do + { + GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, + GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative + } + end + end + shared_examples 'sets hook env' do - context 'with env passed as a JSON' do - let(:obj_dir_relative) { './objects' } - let(:alt_obj_dirs_relative) { ['./alt-objects-1', './alt-objects-2'] } - let(:env) do - { - GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative, - GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative - } - end + include_context 'with env passed as a JSON' - it 'sets env in RequestStore' do - expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys) + it 'sets env in RequestStore' do + expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys) - subject + subject - expect(response).to have_gitlab_http_status(:ok) - end + expect(response).to have_gitlab_http_status(:ok) + end + end + + shared_examples 'sets hook env and routes to primary' do + include_context 'with env passed as a JSON' + + let(:interceptor) do + Class.new(::GRPC::ClientInterceptor) do + def route_to_primary_received? + @route_to_primary_count.to_i > 0 + end + + def request_response(request:, call:, method:, metadata:) # rubocop:disable Lint/UnusedMethodArgument + @route_to_primary_count ||= 0 + @route_to_primary_count += 1 if metadata['gitaly-route-repository-accessor-policy'] == 'primary-only' + + yield + end + end.new + end + + before do + Gitlab::GitalyClient.clear_stubs! + allow(::Gitlab::GitalyClient).to receive(:interceptors).and_return([interceptor]) + end + + after do + Gitlab::GitalyClient.clear_stubs! + end + + it 'sets env in RequestStore and routes gRPC messages to primary', :request_store do + expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, env.stringify_keys).and_call_original + + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(interceptor.route_to_primary_received?).to be_truthy end end @@ -549,6 +588,8 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end + # Wiki repositories don't invoke any Gitaly RPCs to check for changes, so we can only test for the + # hook environment being set. it_behaves_like 'sets hook env' do let(:gl_repository) { Gitlab::GlRepository::WIKI.identifier_for_container(project.wiki) } end @@ -588,7 +629,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end - it_behaves_like 'sets hook env' do + it_behaves_like 'sets hook env and routes to primary' do let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(personal_snippet) } end end @@ -620,7 +661,7 @@ RSpec.describe API::Internal::Base, feature_category: :system_access do expect(user.reload.last_activity_on).to eql(Date.today) end - it_behaves_like 'sets hook env' do + it_behaves_like 'sets hook env and routes to primary' do let(:gl_repository) { Gitlab::GlRepository::SNIPPET.identifier_for_container(project_snippet) } end end diff --git a/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb b/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb new file mode 100644 index 00000000000..de86435616c --- /dev/null +++ b/spec/rubocop/cop/rspec/factory_bot/local_static_assignment_spec.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../../../rubocop/cop/rspec/factory_bot/local_static_assignment' + +RSpec.describe RuboCop::Cop::RSpec::FactoryBot::LocalStaticAssignment, feature_category: :tooling do + shared_examples 'local static assignment' do |block| + it "flags static local assignment in `#{block}`" do + expect_offense(<<~RUBY, block: block) + %{block} do + age + name + + random_number = rand(23) + ^^^^^^^^^^^^^^^^^^^^^^^^ Avoid local static assignemnts in factories which lead to static data definitions. + + random_string = SecureRandom.uuid + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Avoid local static assignemnts in factories which lead to static data definitions. + + project + end + RUBY + end + + it 'does not flag correct use' do + expect_no_offenses(<<~RUBY) + #{block} do + age do + random_number = rand(23) + random_number + 1 + end + end + RUBY + end + end + + it_behaves_like 'local static assignment', 'factory :project' + it_behaves_like 'local static assignment', 'transient' + it_behaves_like 'local static assignment', 'trait :closed' + + it 'does not flag local assignments in unrelated blocks' do + expect_no_offenses(<<~RUBY) + factory :project do + sequence(:number) do |n| + random_number = rand(23) + random_number * n + end + + name do + random_string = SecureRandom.uuid + random_string + "-name" + end + + initialize_with do + random_string = SecureRandom.uuid + new(name: random_string) + end + end + RUBY + end +end diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index 24affa45aa5..cc1f83ddc2b 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -224,12 +224,9 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :i end context 'process Alert Management alerts' do - let(:process_service) { instance_double(AlertManagement::ProcessPrometheusAlertService) } + let(:integration) { build_stubbed(:alert_management_http_integration, project: project, token: token) } - before do - create(:prometheus_integration, project: project) - create(:project_alerting_setting, project: project, token: token) - end + subject { service.execute(token_input, integration) } context 'with multiple firing alerts and resolving alerts' do let(:payload_raw) do @@ -239,7 +236,7 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService, feature_category: :i it 'processes Prometheus alerts' do expect(AlertManagement::ProcessPrometheusAlertService) .to receive(:new) - .with(project, kind_of(Hash)) + .with(project, kind_of(Hash), integration: integration) .exactly(3).times .and_call_original |