diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js index a8c0803f7635682111b9bd9cbfaea95193ac00ec..7e14572b71740a05cba1d896de2658973513826d 100644 --- a/app/assets/javascripts/ci/job_details/index.js +++ b/app/assets/javascripts/ci/job_details/index.js @@ -34,7 +34,6 @@ export const initJobDetails = () => { pipelineTestReportUrl, logViewerPath, duoFeaturesEnabled, - jobGid, } = el.dataset; const fullScreenAPIAvailable = document.fullscreenEnabled; @@ -59,7 +58,6 @@ export const initJobDetails = () => { aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable), duoFeaturesEnabled: parseBoolean(duoFeaturesEnabled), pipelineTestReportUrl, - jobGid, }, render(h) { return h(JobApp, { diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue index e5895ea240c799e49dea697d3d1ca99f7a2393ea..2189e17946ad91f7b3ba0a0f6cbb391098b6b14e 100644 --- a/app/assets/javascripts/ci/job_details/job_app.vue +++ b/app/assets/javascripts/ci/job_details/job_app.vue @@ -137,13 +137,6 @@ export default { jobConfirmationMessage() { return this.job.status?.action?.confirmation_message; }, - jobFailed() { - const { status } = this.job; - - const failedGroups = ['failed', 'failed-with-warnings']; - - return failedGroups.includes(status.group); - }, }, watch: { // Once the job log is loaded, @@ -326,8 +319,14 @@ export default { @enterFullscreen="enterFullscreen" @exitFullscreen="exitFullscreen" /> + <log :search-results="searchResults" /> - <root-cause-analysis-button :job-failed="jobFailed" /> + + <root-cause-analysis-button + :job-id="job.id" + :job-status-group="job.status.group" + :can-troubleshoot-job="glAbilities.troubleshootJobWithAi" + /> </div> <!-- EO job log --> diff --git a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue index 0a4a2a9ef3cfee90fd23fab7250be69bdb9c4248..5a1cf740874677d470eee0182036e9d020ec4484 100644 --- a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue +++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue @@ -7,6 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import SafeHtml from '~/vue_shared/directives/safe_html'; import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants'; import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql'; +import RootCauseAnalysisButton from 'ee_else_ce/ci/job_details/components/root_cause_analysis_button.vue'; export default { components: { @@ -14,6 +15,7 @@ export default { GlButton, GlLink, GlTooltip, + RootCauseAnalysisButton, }, directives: { GlTooltip: GlTooltipDirective, @@ -24,6 +26,10 @@ export default { type: Object, required: true, }, + canTroubleshootJob: { + type: Boolean, + required: true, + }, }, data() { return { @@ -34,8 +40,14 @@ export default { canRetryJob() { return this.job.retryable && this.job.userPermissions.updateBuild && !this.isBridgeJob; }, + detailedStatus() { + return this.job?.detailedStatus; + }, detailsPath() { - return this.job?.detailedStatus?.detailsPath; + return this.detailedStatus?.detailsPath; + }, + statusGroup() { + return this.detailedStatus?.group; }, isBridgeJob() { return this.job.kind === BRIDGE_KIND; @@ -100,16 +112,23 @@ export default { >{{ job.name }}</gl-link > </div> - <div class="col-3 gl-flex gl-items-center" data-testid="job-stage-name"> + <div class="col-2 gl-flex gl-items-center" data-testid="job-stage-name"> {{ job.stage.name }} </div> - <div class="col-3 gl-flex gl-items-center"> + <div class="col-2 gl-flex gl-items-center"> <gl-link :href="detailsPath" data-testid="job-id-link">#{{ parsedJobId }}</gl-link> </div> <gl-tooltip v-if="!canRetryJob" :target="() => $refs.retryBtn" placement="top"> {{ tooltipErrorText }} </gl-tooltip> - <div class="col-2 gl-text-right"> + <div class="col-4 gl-text-right"> + <root-cause-analysis-button + class="gl-mr-2" + :job-gid="job.id" + :job-status-group="statusGroup" + :can-troubleshoot-job="canTroubleshootJob" + /> + <span ref="retryBtn"> <gl-button v-gl-tooltip diff --git a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue index ef993dae53e18c6e03b8ff2cb7ac87420425f06b..9c2df856f18edc7bb5909a063192518fbc209ab4 100644 --- a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue +++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_jobs_list.vue @@ -54,6 +54,7 @@ export default { failedJobs: [], isActive: false, isLoadingMore: false, + canTroubleshootJob: false, }; }, apollo: { @@ -75,6 +76,7 @@ export default { }, result({ data }) { const pipeline = data?.project?.pipeline; + this.canTroubleshootJob = pipeline?.troubleshootJobWithAi || false; if (pipeline?.jobs?.count) { this.$emit('failed-jobs-count', pipeline.jobs.count); @@ -150,8 +152,8 @@ export default { }, columns: [ { text: JOB_NAME_HEADER, class: 'col-4' }, - { text: STAGE_HEADER, class: 'col-3' }, - { text: JOB_ID_HEADER, class: 'col-3' }, + { text: STAGE_HEADER, class: 'col-2' }, + { text: JOB_ID_HEADER, class: 'col-2' }, ], i18n: { maximumJobLimitAlert: { @@ -200,6 +202,7 @@ export default { v-for="job in failedJobs" :key="job.id" :job="job" + :can-troubleshoot-job="canTroubleshootJob" @job-retried="retryJob" /> </div> diff --git a/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql index 26b823ce6ceaaa71951a2108902b7b902435966e..611d28f7669ffb95d7c31a7cfdf279a08517a04c 100644 --- a/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql +++ b/app/assets/javascripts/ci/pipelines_page/graphql/queries/get_pipeline_failed_jobs.query.graphql @@ -4,6 +4,7 @@ query getPipelineFailedJobs($fullPath: ID!, $pipelineIid: ID!) { pipeline(iid: $pipelineIid) { id active + troubleshootJobWithAi jobs(statuses: [FAILED], retried: false) { count nodes { diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb index 61b074312e86520800c27fcf74638ca01f6f9527..2375e0ff8594034f55350fa75bd0a3397804dc60 100644 --- a/app/helpers/ci/jobs_helper.rb +++ b/app/helpers/ci/jobs_helper.rb @@ -14,8 +14,7 @@ def jobs_data(project, build) "runner_settings_url" => project_runners_path(build.project, anchor: 'js-runners-settings'), "retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings.md', anchor: 'prevent-outdated-deployment-jobs'), "pipeline_test_report_url" => test_report_project_pipeline_path(project, build.pipeline), - "log_viewer_path" => viewer_project_job_path(project, build), - "job_gid" => build.to_gid.to_s + "log_viewer_path" => viewer_project_job_path(project, build) } end diff --git a/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue b/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue index c9282231b34f70ef5f3d3f9b6f08fa0075edbd51..63320fb79b9d5e4a136588200e3e23604d503a1d 100644 --- a/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue +++ b/ee/app/assets/javascripts/ci/job_details/components/job_log_top_bar.vue @@ -9,7 +9,6 @@ export default { CeJobLogTopBar, }, mixins: [glFeatureFlagMixin()], - inject: ['jobGid'], props: { size: { type: Number, diff --git a/ee/app/assets/javascripts/ci/job_details/components/root_cause_analysis_button.vue b/ee/app/assets/javascripts/ci/job_details/components/root_cause_analysis_button.vue index fe35610c70f1481e5cf6e6a4a7c8a1dc30ad6a54..b70ea2f7291cc6699de9873a4cc65b7c102ddd47 100644 --- a/ee/app/assets/javascripts/ci/job_details/components/root_cause_analysis_button.vue +++ b/ee/app/assets/javascripts/ci/job_details/components/root_cause_analysis_button.vue @@ -1,27 +1,48 @@ <script> import { GlButton } from '@gitlab/ui'; -import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import glAbilitiesMixin from '~/vue_shared/mixins/gl_abilities_mixin'; import { sendDuoChatCommand } from 'ee/ai/utils'; +import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; export default { components: { GlButton, }, - mixins: [glAbilitiesMixin(), glFeatureFlagMixin()], - inject: ['jobGid'], props: { - jobFailed: { + canTroubleshootJob: { type: Boolean, + required: true, + }, + jobStatusGroup: { + type: String, + required: true, + }, + jobId: { + type: Number, + required: false, + default: null, + }, + jobGid: { + type: String, required: false, - default: false, + default: '', + }, + }, + computed: { + resourceId() { + return this.jobGid || convertToGraphQLId(TYPENAME_CI_BUILD, this.jobId); + }, + jobFailed() { + const failedGroups = ['failed', 'failed-with-warnings']; + + return failedGroups.includes(this.jobStatusGroup); }, }, methods: { callDuo() { sendDuoChatCommand({ question: '/troubleshoot', - resourceId: this.jobGid, + resourceId: this.resourceId, }); }, }, @@ -29,9 +50,8 @@ export default { </script> <template> <gl-button - v-if="glAbilities.troubleshootJobWithAi && jobFailed" + v-if="jobFailed && canTroubleshootJob" icon="duo-chat" - class="gl-mr-3" variant="confirm" data-testid="rca-duo-button" @click="callDuo" diff --git a/ee/spec/frontend/ci/job_details/components/root_cause_analysis_button_spec.js b/ee/spec/frontend/ci/job_details/components/root_cause_analysis_button_spec.js index b389a4930a8edad05e960bd1b55b596616e35976..39690dc97b5d00055a23f87bb2785e511344f461 100644 --- a/ee/spec/frontend/ci/job_details/components/root_cause_analysis_button_spec.js +++ b/ee/spec/frontend/ci/job_details/components/root_cause_analysis_button_spec.js @@ -11,23 +11,16 @@ describe('Root cause analysis button', () => { let wrapper; const defaultProps = { - jobFailed: true, + jobStatusGroup: 'failed', + canTroubleshootJob: true, }; - const jobGid = 'gid://gitlab/Ci::Build/123'; - const createComponent = (props) => { wrapper = shallowMountExtended(RootCauseAnalysisButton, { propsData: { ...defaultProps, ...props, }, - provide: { - glAbilities: { - troubleshootJobWithAi: true, - }, - jobGid, - }, }); }; @@ -40,19 +33,40 @@ describe('Root cause analysis button', () => { }); it('should not display the Troubleshoot button when no failure is detected', () => { - createComponent({ jobFailed: false }); + createComponent({ jobStatusGroup: 'canceled' }); expect(findTroubleshootButton().exists()).toBe(false); }); - it('sends a call to the sendDuoChatCommand utility function', () => { - createComponent(); + it('should not display the Troubleshoot button when user cannot troubleshoot', () => { + createComponent({ canTroubleshootJob: false }); + + expect(findTroubleshootButton().exists()).toBe(false); + }); + + describe('with jobId', () => { + it('sends a call to the sendDuoChatCommand utility function with convereted ID', () => { + createComponent({ jobId: 123 }); + + wrapper.findComponent(GlButton).vm.$emit('click'); + + expect(sendDuoChatCommand).toHaveBeenCalledWith({ + question: '/troubleshoot', + resourceId: 'gid://gitlab/Ci::Build/123', + }); + }); + }); + + describe('with jobGid', () => { + it('sends a call to the sendDuoChatCommand utility function with normal GID', () => { + createComponent({ jobGid: 'gid://gitlab/Ci::Build/11781' }); - wrapper.findComponent(GlButton).vm.$emit('click'); + wrapper.findComponent(GlButton).vm.$emit('click'); - expect(sendDuoChatCommand).toHaveBeenCalledWith({ - question: '/troubleshoot', - resourceId: jobGid, + expect(sendDuoChatCommand).toHaveBeenCalledWith({ + question: '/troubleshoot', + resourceId: 'gid://gitlab/Ci::Build/11781', + }); }); }); }); diff --git a/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js b/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js index 7d22d547c5f68be4fb2a5a0aef5af588303a779b..ad7046696cf791cc6c53a6a371ac5143312d7baa 100644 --- a/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js +++ b/ee/spec/frontend/ci/job_details/components/sidebar/job_log_top_bar_spec.js @@ -24,7 +24,6 @@ describe('EE JobLogTopBar', () => { ...props, }, provide: { - jobGid: 'gid://gitlab/Ci::Build/123', glAbilities: { troubleshootJobWithAi: false, }, diff --git a/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js b/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js index 62475a1b855fdc14e46d777bd96f1549addc6bc1..9db098cfbd62a1c7c6527c656ab332a375f0e7f6 100644 --- a/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js +++ b/spec/frontend/ci/pipelines_page/components/failure_widget/mock.js @@ -56,6 +56,7 @@ const createFailedJobsMock = (nodes, active = false) => { id: 'gid://gitlab/Project/20', pipeline: { active, + troubleshootJobWithAi: true, id: 'gid://gitlab/Pipeline/20', jobs: { count: nodes.length, diff --git a/spec/helpers/ci/jobs_helper_spec.rb b/spec/helpers/ci/jobs_helper_spec.rb index b2637028496642f9f47c12f0666fdd7a9250111e..4eb49def069d9f1b9f225aefaa6c3d53d5773df1 100644 --- a/spec/helpers/ci/jobs_helper_spec.rb +++ b/spec/helpers/ci/jobs_helper_spec.rb @@ -29,8 +29,7 @@ "runner_settings_url" => "/#{project.full_path}/-/runners#js-runners-settings", "retry_outdated_job_docs_url" => "/help/ci/pipelines/settings.md#prevent-outdated-deployment-jobs", "pipeline_test_report_url" => "/#{project.full_path}/-/pipelines/#{job.pipeline.id}/test_report", - "log_viewer_path" => "/#{project.full_path}/-/jobs/#{job.id}/viewer", - "job_gid" => "gid://gitlab/Ci::Build/#{job.id}" + "log_viewer_path" => "/#{project.full_path}/-/jobs/#{job.id}/viewer" }) end