diff options
Diffstat (limited to 'app/assets/javascripts/jobs/components')
14 files changed, 187 insertions, 147 deletions
diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql b/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql new file mode 100644 index 00000000000..f4a0b10672e --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_job.fragment.graphql @@ -0,0 +1,11 @@ +#import "~/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql" + +fragment BaseCiJob on CiJob { + id + manualVariables { + nodes { + ...ManualCiVariable + } + } + __typename +} diff --git a/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql b/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql new file mode 100644 index 00000000000..0479df7bc4c --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/fragments/ci_variable.fragment.graphql @@ -0,0 +1,6 @@ +fragment ManualCiVariable on CiVariable { + __typename + id + key + value +} diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql new file mode 100644 index 00000000000..520deef5136 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_play_with_variables.mutation.graphql @@ -0,0 +1,11 @@ +#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql" + +mutation playJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) { + jobPlay(input: { id: $id, variables: $variables }) { + job { + ...BaseCiJob + webPath + } + errors + } +} diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql index 2b79892a072..e35d603ea71 100644 --- a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql +++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql @@ -1,14 +1,9 @@ +#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql" + mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) { jobRetry(input: { id: $id, variables: $variables }) { job { - id - manualVariables { - nodes { - id - key - value - } - } + ...BaseCiJob webPath } errors diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql index aaf1dec8e0f..95e3521091d 100644 --- a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql +++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql @@ -1,16 +1,11 @@ +#import "~/jobs/components/job/graphql/fragments/ci_job.fragment.graphql" + query getJob($fullPath: ID!, $id: JobID!) { project(fullPath: $fullPath) { id job(id: $id) { - id + ...BaseCiJob manualJob - manualVariables { - nodes { - id - key - value - } - } name } } diff --git a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue index e9809ac661b..ea7e13418f2 100644 --- a/app/assets/javascripts/jobs/components/job/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue @@ -103,6 +103,8 @@ export default { } else { next(); } + }).catch(() => { + this.failureCount = null; }); } }, diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue index 763eb6705aa..19a75ffaa85 100644 --- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue @@ -10,16 +10,17 @@ import { GlTooltipDirective, } from '@gitlab/ui'; import { cloneDeep, uniqueId } from 'lodash'; -import { mapActions } from 'vuex'; import { fetchPolicies } from '~/lib/graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TYPENAME_CI_BUILD, TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { JOB_GRAPHQL_ERRORS } from '~/jobs/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; +import { reportMessageToSentry } from '~/jobs/utils'; import GetJob from './graphql/queries/get_job.query.graphql'; +import playJobWithVariablesMutation from './graphql/mutations/job_play_with_variables.mutation.graphql'; import retryJobWithVariablesMutation from './graphql/mutations/job_retry_with_variables.mutation.graphql'; // This component is a port of ~/jobs/components/job/legacy_manual_variables_form.vue @@ -54,8 +55,9 @@ export default { const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes); return [...jobVariables.reverse(), ...this.variables]; }, - error() { + error(error) { createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText }); + reportMessageToSentry(this.$options.name, error, {}); }, }, }, @@ -69,13 +71,14 @@ export default { required: true, }, }, - clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0'], + clearBtnSharedClasses: ['gl-flex-grow-0 gl-flex-basis-0 gl-m-0! gl-ml-3!'], inputTypes: { key: 'key', value: 'value', }, i18n: { - clearInputs: s__('CiVariables|Clear inputs'), + cancel: s__('CiVariables|Cancel'), + removeInputs: s__('CiVariables|Remove inputs'), formHelpText: s__( 'CiVariables|Specify variable values to be used in this run. The variables specified in the configuration file and %{linkStart}CI/CD settings%{linkEnd} are used by default.', ), @@ -86,14 +89,10 @@ export default { keyLabel: s__('CiVariables|Key'), keyPlaceholder: s__('CiVariables|Input variable key'), runAgainButtonText: s__('CiVariables|Run job again'), - triggerButtonText: s__('CiVariables|Run job'), + runButtonText: s__('CiVariables|Run job'), valueLabel: s__('CiVariables|Value'), valuePlaceholder: s__('CiVariables|Input variable value'), }, - variableValueKeys: { - rest: 'secret_value', - gql: 'value', - }, data() { return { job: {}, @@ -104,30 +103,63 @@ export default { value: '', }, ], - runAgainBtnDisabled: false, - triggerBtnDisabled: false, + runBtnDisabled: false, }; }, computed: { + mutationVariables() { + return { + id: convertToGraphQLId(TYPENAME_CI_BUILD, this.jobId), + variables: this.preparedVariables, + }; + }, preparedVariables() { - // filtering out 'id' along with empty variables to send only key, value in the mutation. - // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268 - return this.variables .filter((variable) => variable.key !== '') - .map(({ key, value }) => ({ key, [this.valueKey]: value })); + .map(({ key, value }) => ({ key, value })); }, - valueKey() { + runBtnText() { return this.isRetryable - ? this.$options.variableValueKeys.gql - : this.$options.variableValueKeys.rest; + ? this.$options.i18n.runAgainButtonText + : this.$options.i18n.runButtonText; }, variableSettings() { return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' }); }, }, methods: { - ...mapActions(['triggerManualJob']), + async playJob() { + try { + const { data } = await this.$apollo.mutate({ + mutation: playJobWithVariablesMutation, + variables: this.mutationVariables, + }); + if (data.jobPlay?.errors?.length) { + createAlert({ message: data.jobPlay.errors[0] }); + } else { + this.navigateToJob(data.jobPlay?.job?.webPath); + } + } catch (error) { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText }); + reportMessageToSentry(this.$options.name, error, {}); + } + }, + async retryJob() { + try { + const { data } = await this.$apollo.mutate({ + mutation: retryJobWithVariablesMutation, + variables: this.mutationVariables, + }); + if (data.jobRetry?.errors?.length) { + createAlert({ message: data.jobRetry.errors[0] }); + } else { + this.navigateToJob(data.jobRetry?.job?.webPath); + } + } catch (error) { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobMutationErrorText }); + reportMessageToSentry(this.$options.name, error, {}); + } + }, addEmptyVariable() { const lastVar = this.variables[this.variables.length - 1]; @@ -153,37 +185,17 @@ export default { inputRef(type, id) { return `${this.$options.inputTypes[type]}-${id}`; }, - navigateToRetriedJob(retryPath) { - redirectTo(retryPath); + navigateToJob(path) { + redirectTo(path); }, - async retryJob() { - try { - const { data } = await this.$apollo.mutate({ - mutation: retryJobWithVariablesMutation, - variables: { - id: convertToGraphQLId(TYPENAME_CI_BUILD, this.jobId), - // we need to ensure no empty variables are passed to the API - variables: this.preparedVariables, - }, - }); - if (data.jobRetry?.errors?.length) { - createAlert({ message: data.jobRetry.errors[0] }); - } else { - this.navigateToRetriedJob(data.jobRetry?.job?.webPath); - } - } catch (error) { - createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText }); - } - }, - runAgain() { - this.runAgainBtnDisabled = true; - - this.retryJob(); - }, - triggerJob() { - this.triggerBtnDisabled = true; + runJob() { + this.runBtnDisabled = true; - this.triggerManualJob(this.preparedVariables); + if (this.isRetryable) { + this.retryJob(); + } else { + this.playJob(); + } }, }, }; @@ -197,7 +209,7 @@ export default { <div v-for="(variable, index) in variables" :key="variable.id" - class="gl-display-flex gl-align-items-center gl-mb-4" + class="gl-display-flex gl-align-items-center gl-mb-5" data-testid="ci-variable-row" > <gl-form-input-group class="gl-mr-4 gl-flex-grow-1"> @@ -232,12 +244,11 @@ export default { <gl-button v-if="canRemove(index)" v-gl-tooltip - :aria-label="$options.i18n.clearInputs" - :title="$options.i18n.clearInputs" + :aria-label="$options.i18n.removeInputs" + :title="$options.i18n.removeInputs" :class="$options.clearBtnSharedClasses" category="tertiary" - variant="danger" - icon="clear" + icon="remove" data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> @@ -248,8 +259,7 @@ export default { :class="$options.clearBtnSharedClasses" data-testid="delete-variable-btn-placeholder" category="tertiary" - variant="danger" - icon="clear" + icon="remove" /> </div> @@ -271,37 +281,23 @@ export default { </template> </gl-sprintf> </div> - <div v-if="isRetryable" class="gl-display-flex gl-justify-content-center gl-mt-5"> + <div class="gl-display-flex gl-justify-content-center gl-mt-5"> <gl-button + v-if="isRetryable" class="gl-mt-5" - :aria-label="__('Cancel')" data-testid="cancel-btn" @click="$emit('hideManualVariablesForm')" - >{{ __('Cancel') }}</gl-button + >{{ $options.i18n.cancel }}</gl-button > <gl-button class="gl-mt-5" variant="confirm" category="primary" - :aria-label="__('Run manual job again')" - :disabled="runAgainBtnDisabled" + :disabled="runBtnDisabled" data-testid="run-manual-job-btn" - @click="runAgain" - > - {{ $options.i18n.runAgainButtonText }} - </gl-button> - </div> - <div v-else class="gl-display-flex gl-justify-content-center gl-mt-5"> - <gl-button - class="gl-mt-5" - variant="confirm" - category="primary" - :aria-label="__('Trigger manual job')" - :disabled="triggerBtnDisabled" - data-testid="trigger-manual-job-btn" - @click="triggerJob" + @click="runJob" > - {{ $options.i18n.triggerButtonText }} + {{ runBtnText }} </gl-button> </div> </div> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue index 913924cc7b1..a3f1a2c4be8 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue @@ -30,18 +30,16 @@ export default { return { primaryProps: { text: this.$options.i18n.primaryText, - attributes: [ - { - 'data-method': 'post', - 'data-testid': 'retry-button-modal', - href: this.href, - variant: 'danger', - }, - ], + attributes: { + 'data-method': 'post', + 'data-testid': 'retry-button-modal', + href: this.href, + variant: 'danger', + }, }, cancelProps: { text: this.$options.i18n.cancel, - attributes: [{ category: 'secondary', variant: 'default' }], + attributes: { category: 'secondary', variant: 'default' }, }, }; }, diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue index 8100bc2d87a..d791705d80d 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue @@ -1,7 +1,7 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { mapActions } from 'vuex'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { TYPENAME_COMMIT_STATUS } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; diff --git a/app/assets/javascripts/jobs/components/table/constants.js b/app/assets/javascripts/jobs/components/table/constants.js index 41ce6e4d64d..1b572e60c58 100644 --- a/app/assets/javascripts/jobs/components/table/constants.js +++ b/app/assets/javascripts/jobs/components/table/constants.js @@ -1,7 +1,6 @@ import { s__, __ } from '~/locale'; /* Error constants */ -export const POST_FAILURE = 'post_failure'; export const DEFAULT = 'default'; export const RAW_TEXT_WARNING = s__( 'Jobs|Raw text search is not currently supported for the jobs filtered search feature. Please use the available search tokens.', diff --git a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js b/app/assets/javascripts/jobs/components/table/graphql/cache_config.js index 8bcd7ffd10f..5390c023da4 100644 --- a/app/assets/javascripts/jobs/components/table/graphql/cache_config.js +++ b/app/assets/javascripts/jobs/components/table/graphql/cache_config.js @@ -11,42 +11,48 @@ export default { }, CiJobConnection: { merge(existing = {}, incoming, { args = {} }) { - let nodes; + if (incoming.nodes) { + let nodes; - const areNodesEqual = isEqual(existing.nodes, incoming.nodes); - const statuses = Array.isArray(args.statuses) ? [...args.statuses] : args.statuses; - const { pageInfo } = incoming; + const areNodesEqual = isEqual(existing.nodes, incoming.nodes); + const statuses = Array.isArray(args.statuses) ? [...args.statuses] : args.statuses; + const { pageInfo } = incoming; - if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) { - if (areNodesEqual) { - if (incoming.pageInfo.hasNextPage) { - nodes = [...existing.nodes, ...incoming.nodes]; + if (Object.keys(existing).length !== 0 && isEqual(existing?.statuses, args?.statuses)) { + if (areNodesEqual) { + if (incoming.pageInfo.hasNextPage) { + nodes = [...existing.nodes, ...incoming.nodes]; + } else { + nodes = [...incoming.nodes]; + } } else { - nodes = [...incoming.nodes]; - } - } else { - if (!existing.pageInfo?.hasNextPage) { - nodes = [...incoming.nodes]; + if (!existing.pageInfo?.hasNextPage) { + nodes = [...incoming.nodes]; - return { - nodes, - statuses, - pageInfo, - count: incoming.count, - }; - } + return { + nodes, + statuses, + pageInfo, + }; + } - nodes = [...existing.nodes, ...incoming.nodes]; + nodes = [...existing.nodes, ...incoming.nodes]; + } + } else { + nodes = [...incoming.nodes]; } - } else { - nodes = [...incoming.nodes]; + + return { + nodes, + statuses, + pageInfo, + }; } return { - nodes, - statuses, - pageInfo, - count: incoming.count, + nodes: existing.nodes, + pageInfo: existing.pageInfo, + statuses: args.statuses, }; }, }, diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql index 851be211b25..69719011079 100644 --- a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -2,7 +2,6 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo project(fullPath: $fullPath) { id jobs(after: $after, first: $first, statuses: $statuses) { - count pageInfo { endCursor hasNextPage diff --git a/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql new file mode 100644 index 00000000000..a4e02ae721a --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs_count.query.graphql @@ -0,0 +1,8 @@ +query getJobsCount($fullPath: ID!, $statuses: [CiJobStatus!]) { + project(fullPath: $fullPath) { + id + jobs(statuses: $statuses) { + count + } + } +} diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue index 3209fc4b90d..3d87cea6445 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -1,11 +1,12 @@ <script> import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { setUrlParams, updateHistory, queryToObject } from '~/lib/utils/url_utility'; import JobsFilteredSearch from '../filtered_search/jobs_filtered_search.vue'; import { validateQueryString } from '../filtered_search/utils'; import GetJobs from './graphql/queries/get_jobs.query.graphql'; +import GetJobsCount from './graphql/queries/get_jobs_count.query.graphql'; import JobsTable from './jobs_table.vue'; import JobsTableEmptyState from './jobs_table_empty_state.vue'; import JobsTableTabs from './jobs_table_tabs.vue'; @@ -13,7 +14,8 @@ import { RAW_TEXT_WARNING } from './constants'; export default { i18n: { - errorMsg: __('There was an error fetching the jobs for your project.'), + jobsFetchErrorMsg: __('There was an error fetching the jobs for your project.'), + jobsCountErrorMsg: __('There was an error fetching the number of jobs for your project.'), loadingAriaLabel: __('Loading'), }, filterSearchBoxStyles: @@ -43,15 +45,32 @@ export default { }; }, update(data) { - const { jobs: { nodes: list = [], pageInfo = {}, count } = {} } = data.project || {}; + const { jobs: { nodes: list = [], pageInfo = {} } = {} } = data.project || {}; return { list, pageInfo, - count, }; }, error() { - this.hasError = true; + this.error = this.$options.i18n.jobsFetchErrorMsg; + }, + }, + jobsCount: { + query: GetJobsCount, + context: { + isSingleRequest: true, + }, + variables() { + return { + fullPath: this.fullPath, + ...this.validatedQueryString, + }; + }, + update({ project }) { + return project?.jobs?.count || 0; + }, + error() { + this.error = this.$options.i18n.jobsCountErrorMsg; }, }, }, @@ -60,11 +79,11 @@ export default { jobs: { list: [], }, - hasError: false, - isAlertDismissed: false, + error: '', scope: null, infiniteScrollingTriggered: false, filterSearchTriggered: false, + jobsCount: null, count: 0, }; }, @@ -72,9 +91,6 @@ export default { loading() { return this.$apollo.queries.jobs.loading; }, - shouldShowAlert() { - return this.hasError && !this.isAlertDismissed; - }, // Show when on All tab with no jobs // Show only when not loading and filtered search has not been triggered // So we don't show empty state when results are empty on a filtered search @@ -95,9 +111,6 @@ export default { showFilteredSearch() { return !this.scope; }, - jobsCount() { - return this.jobs.count; - }, validatedQueryString() { const queryStringObject = queryToObject(window.location.search); @@ -146,6 +159,7 @@ export default { }); this.$apollo.queries.jobs.refetch({ statuses: filter.value.data }); + this.$apollo.queries.jobsCount.refetch({ statuses: filter.value.data }); } }); }, @@ -168,14 +182,14 @@ export default { <template> <div> <gl-alert - v-if="shouldShowAlert" + v-if="error" class="gl-mt-2" variant="danger" data-testid="jobs-table-error-alert" dismissible - @dismiss="isAlertDismissed = true" + @dismiss="error = ''" > - {{ $options.i18n.errorMsg }} + {{ error }} </gl-alert> <jobs-table-tabs |