From 4ee038e918628b44749e1d2812fcb76e736c492c Mon Sep 17 00:00:00 2001 From: Payton Burdette Date: Thu, 18 Sep 2025 12:17:55 -0400 Subject: [PATCH 1/2] Job artifacts subscription --- .../components/job_artifacts_table.vue | 41 +++++++++++++++++++ .../ci_job_created.subscription.graphql | 33 +++++++++++++++ .../projects/artifacts_controller.rb | 5 +++ 3 files changed, 79 insertions(+) create mode 100644 app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql 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 8bd5b69ec169f6..bf5b2bf274046c 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue @@ -20,11 +20,13 @@ import { updateHistory, getParameterByName, setUrlParams } from '~/lib/utils/url import { scrollToElement } from '~/lib/utils/common_utils'; import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils'; import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { TYPENAME_PROJECT } from '~/graphql_shared/constants'; import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.graphql'; import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils'; import bulkDestroyJobArtifactsMutation from '../graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql'; import { removeArtifactFromStore } from '../graphql/cache_update'; +import jobWithArtifactsUpdatedSubscription from '../graphql/subscriptions/ci_job_created.subscription.graphql'; import { I18N_DOWNLOAD, I18N_BROWSE, @@ -85,6 +87,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + mixins: [glFeatureFlagMixin()], inject: ['projectId', 'projectPath', 'canDestroyArtifacts', 'jobArtifactsCountLimit'], apollo: { jobArtifacts: { @@ -113,6 +116,40 @@ export default { return jobNodes; }, + result({ data }) { + if ( + data?.project?.jobs?.nodes?.length > 0 && + this.shouldUseRealtimeStatus && + !this.isSubscribed + ) { + // Prevent duplicate subscriptions on refetch + this.isSubscribed = true; + + this.$apollo.queries.jobArtifacts.subscribeToMore({ + document: jobWithArtifactsUpdatedSubscription, + variables: { + projectId: convertToGraphQLId(TYPENAME_PROJECT, this.projectId), + withArtifacts: true, + }, + updateQuery(previousData, { subscriptionData: { data: ciJobCreated } }) { + if (ciJobCreated) { + const existingJobs = previousData?.project?.jobs?.nodes || []; + + return { + project: { + ...previousData.project, + jobs: { + ...previousData.project.jobs, + nodes: [ciJobCreated, ...existingJobs], + }, + }, + }; + } + return previousData; + }, + }); + } + }, error() { createAlert({ message: I18N_FETCH_ERROR, @@ -131,6 +168,7 @@ export default { jobArtifactsToDelete: [], isBulkDeleting: false, page: INITIAL_CURRENT_PAGE, + isSubscribed: false, }; }, computed: { @@ -200,6 +238,9 @@ export default { ? I18N_BULK_DELETE_MAX_SELECTED : ''; }, + shouldUseRealtimeStatus() { + return this.glFeatures?.ciJobCreatedSubscription; + }, }, created() { this.updateQueryParamsFromUrl(); diff --git a/app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql new file mode 100644 index 00000000000000..e90fc51c10660f --- /dev/null +++ b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql @@ -0,0 +1,33 @@ +#import "~/graphql_shared/fragments/ci_icon.fragment.graphql" + +subscription jobWithArtifactsUpdated($projectId: ProjectID!, $withArtifacts: Boolean) { + ciJobCreated(projectId: $projectId, withArtifacts: $withArtifacts) { + id + name + webPath + detailedStatus { + ...CiIcon + } + pipeline { + id + iid + path + } + refName + refPath + shortSha + commitPath + finishedAt + browseArtifactsPath + artifacts { + nodes { + id + name + fileType + downloadPath + size + expireAt + } + } + } +} diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb index 4d41dc006733e7..3abe7496a60333 100644 --- a/app/controllers/projects/artifacts_controller.rb +++ b/app/controllers/projects/artifacts_controller.rb @@ -17,6 +17,7 @@ class Projects::ArtifactsController < Projects::ApplicationController before_action :extract_ref_name_and_path before_action :validate_artifacts!, except: [:index, :download, :raw, :destroy] before_action :entry, only: [:external_file, :file] + before_action :push_ci_job_created_subscription_feature_flag, only: [:index] MAX_PER_PAGE = 20 @@ -192,6 +193,10 @@ def authorize_read_build_trace! def authorize_read_job_artifacts! access_denied! unless can?(current_user, :read_job_artifacts, job_artifact) end + + def push_ci_job_created_subscription_feature_flag + push_frontend_feature_flag(:ci_job_created_subscription, @project) + end end Projects::ArtifactsController.prepend_mod -- GitLab From b23436f09210d7870e2e38775e4b7d526519fed2 Mon Sep 17 00:00:00 2001 From: Payton Burdette Date: Wed, 1 Oct 2025 11:16:41 -0400 Subject: [PATCH 2/2] Use new subscription field --- .../artifacts/components/job_artifacts_table.vue | 15 +++++++++++---- ...b_with_artifacts_updated.subscription.graphql} | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) rename app/assets/javascripts/ci/artifacts/graphql/subscriptions/{ci_job_created.subscription.graphql => job_with_artifacts_updated.subscription.graphql} (87%) 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 bf5b2bf274046c..852cc44ca0632f 100644 --- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue +++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue @@ -26,7 +26,7 @@ import getJobArtifactsQuery from '../graphql/queries/get_job_artifacts.query.gra import { totalArtifactsSizeForJob, mapArchivesToJobNodes, mapBooleansToJobNodes } from '../utils'; import bulkDestroyJobArtifactsMutation from '../graphql/mutations/bulk_destroy_job_artifacts.mutation.graphql'; import { removeArtifactFromStore } from '../graphql/cache_update'; -import jobWithArtifactsUpdatedSubscription from '../graphql/subscriptions/ci_job_created.subscription.graphql'; +import jobWithArtifactsUpdatedSubscription from '../graphql/subscriptions/job_with_artifacts_updated.subscription.graphql'; import { I18N_DOWNLOAD, I18N_BROWSE, @@ -131,8 +131,15 @@ export default { projectId: convertToGraphQLId(TYPENAME_PROJECT, this.projectId), withArtifacts: true, }, - updateQuery(previousData, { subscriptionData: { data: ciJobCreated } }) { - if (ciJobCreated) { + updateQuery( + previousData, + { + subscriptionData: { + data: { ciJobProcessed }, + }, + }, + ) { + if (ciJobProcessed) { const existingJobs = previousData?.project?.jobs?.nodes || []; return { @@ -140,7 +147,7 @@ export default { ...previousData.project, jobs: { ...previousData.project.jobs, - nodes: [ciJobCreated, ...existingJobs], + nodes: [ciJobProcessed, ...existingJobs], }, }, }; diff --git a/app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/job_with_artifacts_updated.subscription.graphql similarity index 87% rename from app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql rename to app/assets/javascripts/ci/artifacts/graphql/subscriptions/job_with_artifacts_updated.subscription.graphql index e90fc51c10660f..de55d4a399837b 100644 --- a/app/assets/javascripts/ci/artifacts/graphql/subscriptions/ci_job_created.subscription.graphql +++ b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/job_with_artifacts_updated.subscription.graphql @@ -1,7 +1,7 @@ #import "~/graphql_shared/fragments/ci_icon.fragment.graphql" subscription jobWithArtifactsUpdated($projectId: ProjectID!, $withArtifacts: Boolean) { - ciJobCreated(projectId: $projectId, withArtifacts: $withArtifacts) { + ciJobProcessed(projectId: $projectId, withArtifacts: $withArtifacts) { id name webPath -- GitLab