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 8bd5b69ec169f6126415a65119c2f93593f932f3..852cc44ca0632fdd7d8eae4b347e8da0989c46d6 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/job_with_artifacts_updated.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,47 @@ 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: { ciJobProcessed }, + }, + }, + ) { + if (ciJobProcessed) { + const existingJobs = previousData?.project?.jobs?.nodes || []; + + return { + project: { + ...previousData.project, + jobs: { + ...previousData.project.jobs, + nodes: [ciJobProcessed, ...existingJobs], + }, + }, + }; + } + return previousData; + }, + }); + } + }, error() { createAlert({ message: I18N_FETCH_ERROR, @@ -131,6 +175,7 @@ export default { jobArtifactsToDelete: [], isBulkDeleting: false, page: INITIAL_CURRENT_PAGE, + isSubscribed: false, }; }, computed: { @@ -200,6 +245,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/job_with_artifacts_updated.subscription.graphql b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/job_with_artifacts_updated.subscription.graphql new file mode 100644 index 0000000000000000000000000000000000000000..de55d4a399837bb474ae49e7c10757b7c678d345 --- /dev/null +++ b/app/assets/javascripts/ci/artifacts/graphql/subscriptions/job_with_artifacts_updated.subscription.graphql @@ -0,0 +1,33 @@ +#import "~/graphql_shared/fragments/ci_icon.fragment.graphql" + +subscription jobWithArtifactsUpdated($projectId: ProjectID!, $withArtifacts: Boolean) { + ciJobProcessed(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 4d41dc006733e77a92fde8b941f211f4198bd4ed..3abe7496a60333db172cc2e4e30550584102a197 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