diff options
Diffstat (limited to 'app/assets/javascripts/jobs')
15 files changed, 328 insertions, 412 deletions
diff --git a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue index e498a735898..67cdca6aa0a 100644 --- a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue +++ b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue @@ -1,13 +1,13 @@ <script> import { GlFilteredSearch } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; +import { + OPERATORS_IS, + TOKEN_TITLE_STATUS, + TOKEN_TYPE_STATUS, +} from '~/vue_shared/components/filtered_search_bar/constants'; import JobStatusToken from './tokens/job_status_token.vue'; export default { - tokenTypes: { - status: 'status', - }, components: { GlFilteredSearch, }, @@ -22,12 +22,12 @@ export default { tokens() { return [ { - type: this.$options.tokenTypes.status, + type: TOKEN_TYPE_STATUS, icon: 'status', - title: s__('Jobs|Status'), + title: TOKEN_TITLE_STATUS, unique: true, token: JobStatusToken, - operators: OPERATOR_IS_ONLY, + operators: OPERATORS_IS, }, ]; }, @@ -35,7 +35,7 @@ export default { if (this.queryString?.statuses) { return [ { - type: 'status', + type: TOKEN_TYPE_STATUS, value: { data: this.queryString?.statuses, operator: '=', diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue index 65b9600e664..d0a39025807 100644 --- a/app/assets/javascripts/jobs/components/job/empty_state.vue +++ b/app/assets/javascripts/jobs/components/job/empty_state.vue @@ -1,16 +1,12 @@ <script> import { GlLink } from '@gitlab/ui'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue'; import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue'; export default { components: { GlLink, - LegacyManualVariablesForm, ManualVariablesForm, }, - mixins: [glFeatureFlagsMixin()], props: { illustrationPath: { type: String, @@ -20,6 +16,14 @@ export default { type: String, required: true, }, + isRetryable: { + type: Boolean, + required: true, + }, + jobId: { + type: Number, + required: true, + }, title: { type: String, required: true, @@ -54,9 +58,6 @@ export default { }, }, computed: { - isGraphQL() { - return this.glFeatures?.graphqlJobApp; - }, shouldRenderManualVariables() { return this.playable && !this.scheduled; }, @@ -77,14 +78,14 @@ export default { <p v-if="content" data-testid="job-empty-state-content">{{ content }}</p> </div> - <template v-if="isGraphQL"> - <manual-variables-form v-if="shouldRenderManualVariables" :action="action" /> - </template> - <template v-else> - <legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" /> - </template> - <div class="text-content"> - <div v-if="action && !shouldRenderManualVariables" class="text-center"> + <manual-variables-form + v-if="shouldRenderManualVariables" + :is-retryable="isRetryable" + :job-id="jobId" + @hideManualVariablesForm="$emit('hideManualVariablesForm')" + /> + <div v-if="action && !shouldRenderManualVariables" class="text-content"> + <div class="text-center"> <gl-link :href="action.path" :data-method="action.method" 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 new file mode 100644 index 00000000000..2b79892a072 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql @@ -0,0 +1,16 @@ +mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) { + jobRetry(input: { id: $id, variables: $variables }) { + job { + id + manualVariables { + nodes { + id + key + value + } + } + 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 new file mode 100644 index 00000000000..aaf1dec8e0f --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql @@ -0,0 +1,17 @@ +query getJob($fullPath: ID!, $id: JobID!) { + project(fullPath: $fullPath) { + id + job(id: $id) { + id + manualJob + manualVariables { + nodes { + id + key + value + } + } + name + } + } +} diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue index 81b65d175a7..c6d900ef13e 100644 --- a/app/assets/javascripts/jobs/components/job/job_app.vue +++ b/app/assets/javascripts/jobs/components/job/job_app.vue @@ -1,8 +1,9 @@ <script> -import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from '@gitlab/ui'; +import { GlLoadingIcon, GlIcon, GlAlert } from '@gitlab/ui'; import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils'; import { throttle, isEmpty } from 'lodash'; import { mapGetters, mapState, mapActions } from 'vuex'; +import SafeHtml from '~/vue_shared/directives/safe_html'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { __, sprintf } from '~/locale'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; @@ -71,6 +72,7 @@ export default { data() { return { searchResults: [], + showUpdateVariablesState: false, }; }, computed: { @@ -121,6 +123,10 @@ export default { return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure; }, + isJobRetryable() { + return Boolean(this.job.retry_path); + }, + itemName() { return sprintf(__('Job %{jobName}'), { jobName: this.job.name }); }, @@ -168,10 +174,16 @@ export default { 'toggleScrollButtons', 'toggleScrollAnimation', ]), + onHideManualVariablesForm() { + this.showUpdateVariablesState = false; + }, onResize() { this.updateSidebar(); this.updateScroll(); }, + onUpdateVariables() { + this.showUpdateVariablesState = true; + }, updateSidebar() { const breakpoint = bp.getBreakpointSize(); if (breakpoint === 'xs' || breakpoint === 'sm') { @@ -271,14 +283,12 @@ export default { </div> <!-- job log --> <div - v-if="hasJobLog" + v-if="hasJobLog && !showUpdateVariablesState" class="build-log-container gl-relative" :class="{ 'gl-mt-3': !job.archived }" > <log-top-bar :class="{ - 'sidebar-expanded': isSidebarOpen, - 'sidebar-collapsed': !isSidebarOpen, 'has-archived-block': job.archived, }" :size="jobLogSize" @@ -299,14 +309,17 @@ export default { <!-- empty state --> <empty-state - v-if="!hasJobLog" + v-if="!hasJobLog || showUpdateVariablesState" :illustration-path="emptyStateIllustration.image" :illustration-size-class="emptyStateIllustration.size" + :is-retryable="isJobRetryable" + :job-id="job.id" :title="emptyStateTitle" :content="emptyStateIllustration.content" :action="emptyStateAction" :playable="job.playable" :scheduled="job.scheduled" + @hideManualVariablesForm="onHideManualVariablesForm()" /> <!-- EO empty state --> @@ -320,9 +333,9 @@ export default { 'right-sidebar-expanded': isSidebarOpen, 'right-sidebar-collapsed': !isSidebarOpen, }" - :erase-path="job.erase_path" :artifact-help-url="artifactHelpUrl" data-testid="job-sidebar" + @updateVariables="onUpdateVariables()" /> </div> </template> diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue deleted file mode 100644 index 1898e02c94e..00000000000 --- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue +++ /dev/null @@ -1,192 +0,0 @@ -<script> -import { - GlFormInputGroup, - GlInputGroupText, - GlFormInput, - GlButton, - GlLink, - GlSprintf, -} from '@gitlab/ui'; -import { uniqueId } from 'lodash'; -import { mapActions } from 'vuex'; -import { helpPagePath } from '~/helpers/help_page_helper'; -import { s__ } from '~/locale'; - -export default { - name: 'ManualVariablesForm', - components: { - GlFormInputGroup, - GlInputGroupText, - GlFormInput, - GlButton, - GlLink, - GlSprintf, - }, - props: { - action: { - type: Object, - required: false, - default: null, - validator(value) { - return ( - value === null || - (Object.prototype.hasOwnProperty.call(value, 'path') && - Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'button_title')) - ); - }, - }, - }, - inputTypes: { - key: 'key', - value: 'value', - }, - i18n: { - header: s__('CiVariables|Variables'), - keyLabel: s__('CiVariables|Key'), - valueLabel: s__('CiVariables|Value'), - keyPlaceholder: s__('CiVariables|Input variable key'), - valuePlaceholder: s__('CiVariables|Input variable value'), - formHelpText: s__( - 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', - ), - }, - data() { - return { - variables: [ - { - key: '', - secretValue: '', - id: uniqueId(), - }, - ], - triggerBtnDisabled: false, - }; - }, - computed: { - variableSettings() { - return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' }); - }, - preparedVariables() { - // we need to ensure no empty variables are passed to the API - // and secretValue should be snake_case when passed to the API - return this.variables - .filter((variable) => variable.key !== '') - .map(({ key, secretValue }) => ({ key, secret_value: secretValue })); - }, - }, - methods: { - ...mapActions(['triggerManualJob']), - addEmptyVariable() { - const lastVar = this.variables[this.variables.length - 1]; - - if (lastVar.key === '') { - return; - } - - this.variables.push({ - key: '', - secret_value: '', - id: uniqueId(), - }); - }, - canRemove(index) { - return index < this.variables.length - 1; - }, - deleteVariable(id) { - this.variables.splice( - this.variables.findIndex((el) => el.id === id), - 1, - ); - }, - inputRef(type, id) { - return `${this.$options.inputTypes[type]}-${id}`; - }, - trigger() { - this.triggerBtnDisabled = true; - - this.triggerManualJob(this.preparedVariables); - }, - }, -}; -</script> -<template> - <div class="row gl-justify-content-center"> - <div class="col-10" data-testid="manual-vars-form"> - <label>{{ $options.i18n.header }}</label> - - <div - v-for="(variable, index) in variables" - :key="variable.id" - class="gl-display-flex gl-align-items-center gl-mb-4" - data-testid="ci-variable-row" - > - <gl-form-input-group class="gl-mr-4 gl-flex-grow-1"> - <template #prepend> - <gl-input-group-text> - {{ $options.i18n.keyLabel }} - </gl-input-group-text> - </template> - <gl-form-input - :ref="inputRef('key', variable.id)" - v-model="variable.key" - :placeholder="$options.i18n.keyPlaceholder" - data-testid="ci-variable-key" - @change="addEmptyVariable" - /> - </gl-form-input-group> - - <gl-form-input-group class="gl-flex-grow-2"> - <template #prepend> - <gl-input-group-text> - {{ $options.i18n.valueLabel }} - </gl-input-group-text> - </template> - <gl-form-input - :ref="inputRef('value', variable.id)" - v-model="variable.secretValue" - :placeholder="$options.i18n.valuePlaceholder" - data-testid="ci-variable-value" - /> - </gl-form-input-group> - - <gl-button - v-if="canRemove(index)" - class="gl-flex-grow-0 gl-flex-basis-0" - category="tertiary" - variant="danger" - icon="clear" - :aria-label="__('Delete variable')" - data-testid="delete-variable-btn" - @click="deleteVariable(variable.id)" - /> - - <!-- delete variable button placeholder to not break flex layout --> - <div v-else class="gl-w-7 gl-mr-3" data-testid="delete-variable-btn-placeholder"></div> - </div> - - <div class="gl-text-center gl-mt-5"> - <gl-sprintf :message="$options.i18n.formHelpText"> - <template #link="{ content }"> - <gl-link :href="variableSettings" target="_blank"> - {{ content }} - </gl-link> - </template> - </gl-sprintf> - </div> - <div 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="trigger" - > - {{ action.button_title }} - </gl-button> - </div> - </div> - </div> -</template> 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 2f97301979c..d7bbd6daed2 100644 --- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue @@ -5,15 +5,24 @@ import { GlFormInput, GlButton, GlLink, + GlLoadingIcon, GlSprintf, + GlTooltipDirective, } from '@gitlab/ui'; -import { uniqueId } from 'lodash'; +import { cloneDeep, uniqueId } from 'lodash'; import { mapActions } from 'vuex'; +import { fetchPolicies } from '~/lib/graphql'; +import { createAlert } from '~/flash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; +import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants'; import { helpPagePath } from '~/helpers/help_page_helper'; +import { redirectTo } from '~/lib/utils/url_utility'; import { s__ } from '~/locale'; +import GetJob from './graphql/queries/get_job.query.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 -// It is meant to fetch the job information via GraphQL instead of REST API. +// It is meant to fetch/update the job information via GraphQL instead of REST API. export default { name: 'ManualVariablesForm', @@ -23,59 +32,93 @@ export default { GlFormInput, GlButton, GlLink, + GlLoadingIcon, GlSprintf, }, - props: { - action: { - type: Object, - required: false, - default: null, - validator(value) { - return ( - value === null || - (Object.prototype.hasOwnProperty.call(value, 'path') && - Object.prototype.hasOwnProperty.call(value, 'method') && - Object.prototype.hasOwnProperty.call(value, 'button_title')) - ); + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: ['projectPath'], + apollo: { + variables: { + query: GetJob, + variables() { + return { + fullPath: this.projectPath, + id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId), + }; + }, + fetchPolicy: fetchPolicies.CACHE_AND_NETWORK, + update(data) { + const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes); + return [...jobVariables.reverse(), ...this.variables]; + }, + error() { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText }); }, }, }, + props: { + isRetryable: { + type: Boolean, + required: true, + }, + jobId: { + type: Number, + required: true, + }, + }, inputTypes: { key: 'key', value: 'value', }, i18n: { + clearInputs: s__('CiVariables|Clear inputs'), + formHelpText: s__( + 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', + ), header: s__('CiVariables|Variables'), keyLabel: s__('CiVariables|Key'), - valueLabel: s__('CiVariables|Value'), keyPlaceholder: s__('CiVariables|Input variable key'), + runAgainButtonText: s__('CiVariables|Run job again'), + triggerButtonText: s__('CiVariables|Trigger this manual action'), + valueLabel: s__('CiVariables|Value'), valuePlaceholder: s__('CiVariables|Input variable value'), - formHelpText: s__( - 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default', - ), + }, + variableValueKeys: { + rest: 'secret_value', + gql: 'value', }, data() { return { + job: {}, variables: [ { - key: '', - secretValue: '', id: uniqueId(), + key: '', + value: '', }, ], + runAgainBtnDisabled: false, triggerBtnDisabled: false, }; }, computed: { - variableSettings() { - return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' }); - }, preparedVariables() { - // we need to ensure no empty variables are passed to the API - // and secretValue should be snake_case when passed to the API + // 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, secretValue }) => ({ key, secret_value: secretValue })); + .map(({ key, value }) => ({ key, [this.valueKey]: value })); + }, + valueKey() { + return this.isRetryable + ? this.$options.variableValueKeys.gql + : this.$options.variableValueKeys.rest; + }, + variableSettings() { + return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' }); }, }, methods: { @@ -88,9 +131,9 @@ export default { } this.variables.push({ - key: '', - secret_value: '', id: uniqueId(), + key: '', + value: '', }); }, canRemove(index) { @@ -105,7 +148,34 @@ export default { inputRef(type, id) { return `${this.$options.inputTypes[type]}-${id}`; }, - trigger() { + navigateToRetriedJob(retryPath) { + redirectTo(retryPath); + }, + async retryJob() { + try { + const { data } = await this.$apollo.mutate({ + mutation: retryJobWithVariablesMutation, + variables: { + id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, 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; this.triggerManualJob(this.preparedVariables); @@ -114,7 +184,8 @@ export default { }; </script> <template> - <div class="row gl-justify-content-center"> + <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" /> + <div v-else class="row gl-justify-content-center"> <div class="col-10" data-testid="manual-vars-form"> <label>{{ $options.i18n.header }}</label> @@ -147,7 +218,7 @@ export default { </template> <gl-form-input :ref="inputRef('value', variable.id)" - v-model="variable.secretValue" + v-model="variable.value" :placeholder="$options.i18n.valuePlaceholder" data-testid="ci-variable-value" /> @@ -155,11 +226,13 @@ export default { <gl-button v-if="canRemove(index)" + v-gl-tooltip + :aria-label="$options.i18n.clearInputs" + :title="$options.i18n.clearInputs" class="gl-flex-grow-0 gl-flex-basis-0" category="tertiary" variant="danger" icon="clear" - :aria-label="__('Delete variable')" data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> @@ -177,7 +250,27 @@ export default { </template> </gl-sprintf> </div> - <div class="gl-display-flex gl-justify-content-center gl-mt-5"> + <div v-if="isRetryable" class="gl-display-flex gl-justify-content-center gl-mt-5"> + <gl-button + class="gl-mt-5" + :aria-label="__('Cancel')" + data-testid="cancel-btn" + @click="$emit('hideManualVariablesForm')" + >{{ __('Cancel') }}</gl-button + > + <gl-button + class="gl-mt-5" + variant="confirm" + category="primary" + :aria-label="__('Run manual job again')" + :disabled="runAgainBtnDisabled" + 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" @@ -185,9 +278,9 @@ export default { :aria-label="__('Trigger manual job')" :disabled="triggerBtnDisabled" data-testid="trigger-manual-job-btn" - @click="trigger" + @click="triggerJob" > - {{ action.button_title }} + {{ $options.i18n.triggerButtonText }} </gl-button> </div> </div> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue index dd620977f0c..7183a8b5d03 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue @@ -1,15 +1,17 @@ <script> -import { GlButton, GlModalDirective } from '@gitlab/ui'; +import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui'; import { mapGetters } from 'vuex'; import { JOB_SIDEBAR_COPY } from '~/jobs/constants'; export default { name: 'JobSidebarRetryButton', i18n: { - retryLabel: JOB_SIDEBAR_COPY.retry, + ...JOB_SIDEBAR_COPY, }, components: { GlButton, + GlDropdown, + GlDropdownItem, }, directives: { GlModal: GlModalDirective, @@ -23,6 +25,10 @@ export default { type: String, required: true, }, + isManualJob: { + type: Boolean, + required: true, + }, }, computed: { ...mapGetters(['hasForwardDeploymentFailure']), @@ -33,17 +39,30 @@ export default { <gl-button v-if="hasForwardDeploymentFailure" v-gl-modal="modalId" - :aria-label="$options.i18n.retryLabel" + :aria-label="$options.i18n.retryJobLabel" category="primary" variant="confirm" icon="retry" data-testid="retry-job-button" /> - + <gl-dropdown + v-else-if="isManualJob" + icon="retry" + category="primary" + :right="true" + variant="confirm" + > + <gl-dropdown-item :href="href" data-method="post"> + {{ $options.i18n.runAgainJobButtonLabel }} + </gl-dropdown-item> + <gl-dropdown-item @click="$emit('updateVariablesClicked')"> + {{ $options.i18n.updateVariables }} + </gl-dropdown-item> + </gl-dropdown> <gl-button v-else :href="href" - :aria-label="$options.i18n.retryLabel" + :aria-label="$options.i18n.retryJobLabel" category="primary" variant="confirm" icon="retry" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue deleted file mode 100644 index 64b497c3550..00000000000 --- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue +++ /dev/null @@ -1,104 +0,0 @@ -<script> -import { GlButton, GlTooltipDirective } from '@gitlab/ui'; -import { mapActions } from 'vuex'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; -import JobSidebarRetryButton from './job_sidebar_retry_button.vue'; - -export default { - name: 'LegacySidebarHeader', - i18n: { - ...JOB_SIDEBAR_COPY, - }, - forwardDeploymentFailureModalId, - directives: { - GlTooltip: GlTooltipDirective, - }, - components: { - GlButton, - JobSidebarRetryButton, - TooltipOnTruncate, - }, - props: { - job: { - type: Object, - required: true, - default: () => ({}), - }, - erasePath: { - type: String, - required: false, - default: null, - }, - }, - computed: { - retryButtonCategory() { - return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; - }, - buttonTitle() { - return this.job.status && this.job.status.text === 'passed' - ? this.$options.i18n.runAgainJobButtonLabel - : this.$options.i18n.retryJobButtonLabel; - }, - }, - methods: { - ...mapActions(['toggleSidebar']), - }, -}; -</script> - -<template> - <div class="gl-py-5 gl-display-flex gl-align-items-center"> - <tooltip-on-truncate :title="job.name" truncate-target="child" - ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate"> - {{ job.name }} - </h4> - </tooltip-on-truncate> - <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"> - <gl-button - v-if="erasePath" - v-gl-tooltip.left - :title="$options.i18n.eraseLogButtonLabel" - :aria-label="$options.i18n.eraseLogButtonLabel" - :href="erasePath" - :data-confirm="$options.i18n.eraseLogConfirmText" - class="gl-mr-2" - data-testid="job-log-erase-link" - data-confirm-btn-variant="danger" - data-method="post" - icon="remove" - /> - <job-sidebar-retry-button - v-if="job.retry_path" - v-gl-tooltip.left - :title="buttonTitle" - :aria-label="buttonTitle" - :category="retryButtonCategory" - :href="job.retry_path" - :modal-id="$options.forwardDeploymentFailureModalId" - variant="confirm" - data-qa-selector="retry_button" - data-testid="retry-button" - /> - <gl-button - v-if="job.cancel_path" - v-gl-tooltip.left - :title="$options.i18n.cancelJobButtonLabel" - :aria-label="$options.i18n.cancelJobButtonLabel" - :href="job.cancel_path" - variant="danger" - icon="cancel" - data-method="post" - data-testid="cancel-button" - rel="nofollow" - /> - <gl-button - :aria-label="$options.i18n.toggleSidebar" - category="tertiary" - class="gl-md-display-none gl-ml-2" - icon="chevron-double-lg-right" - @click="toggleSidebar" - /> - </div> - </div> -</template> diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue index aac6a0ad6d3..69271cc9022 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue @@ -2,14 +2,12 @@ import { GlButton, GlIcon } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; +import ArtifactsBlock from './artifacts_block.vue'; import CommitBlock from './commit_block.vue'; import JobsContainer from './jobs_container.vue'; import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue'; import JobSidebarDetailsContainer from './sidebar_job_details_container.vue'; -import ArtifactsBlock from './artifacts_block.vue'; -import LegacySidebarHeader from './legacy_sidebar_header.vue'; import SidebarHeader from './sidebar_header.vue'; import StagesDropdown from './stages_dropdown.vue'; import TriggerBlock from './trigger_block.vue'; @@ -29,23 +27,16 @@ export default { JobsContainer, JobRetryForwardDeploymentModal, JobSidebarDetailsContainer, - LegacySidebarHeader, SidebarHeader, StagesDropdown, TriggerBlock, }, - mixins: [glFeatureFlagsMixin()], props: { artifactHelpUrl: { type: String, required: false, default: '', }, - erasePath: { - type: String, - required: false, - default: null, - }, }, computed: { ...mapGetters(['hasForwardDeploymentFailure']), @@ -57,9 +48,6 @@ export default { hasTriggers() { return !isEmpty(this.job.trigger); }, - isGraphQL() { - return this.glFeatures?.graphqlJobApp; - }, commit() { return this.job?.pipeline?.commit || {}; }, @@ -89,8 +77,11 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" /> - <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" /> + <sidebar-header + :rest-job="job" + :job-id="job.id" + @updateVariables="$emit('updateVariables')" + /> <div v-if="job.terminal_path || job.new_issue_path" class="gl-py-5" 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 523710598bf..40aec0b0536 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue @@ -1,13 +1,19 @@ <script> import { GlButton, GlTooltipDirective } from '@gitlab/ui'; import { mapActions } from 'vuex'; +import { createAlert } from '~/flash'; +import { convertToGraphQLId } from '~/graphql_shared/utils'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; +import { + JOB_GRAPHQL_ERRORS, + GRAPHQL_ID_TYPES, + JOB_SIDEBAR_COPY, + forwardDeploymentFailureModalId, + PASSED_STATUS, +} from '~/jobs/constants'; +import GetJob from '../graphql/queries/get_job.query.graphql'; import JobSidebarRetryButton from './job_sidebar_retry_button.vue'; -// This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue -// It is meant to fetch the job information via GraphQL instead of REST API. - export default { name: 'SidebarHeader', i18n: { @@ -22,21 +28,58 @@ export default { JobSidebarRetryButton, TooltipOnTruncate, }, - props: { + inject: ['projectPath'], + apollo: { job: { + query: GetJob, + variables() { + return { + fullPath: this.projectPath, + id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId), + }; + }, + update(data) { + const { name, manualJob } = data?.project?.job || {}; + return { + name, + manualJob, + }; + }, + error() { + createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText }); + }, + }, + }, + props: { + jobId: { + type: Number, + required: true, + }, + restJob: { type: Object, required: true, default: () => ({}), }, - erasePath: { - type: String, - required: false, - default: null, - }, + }, + data() { + return { + job: {}, + }; }, computed: { + buttonTitle() { + return this.restJob.status?.text === PASSED_STATUS + ? this.$options.i18n.runAgainJobButtonLabel + : this.$options.i18n.retryJobLabel; + }, + canShowJobRetryButton() { + return this.restJob.retry_path && !this.$apollo.queries.job.loading; + }, + isManualJob() { + return this.job?.manualJob; + }, retryButtonCategory() { - return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; + return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary'; }, }, methods: { @@ -48,17 +91,15 @@ export default { <template> <div class="gl-py-5 gl-display-flex gl-align-items-center"> <tooltip-on-truncate :title="job.name" truncate-target="child" - ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate"> - {{ job.name }} - </h4> + ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate" data-testid="job-name">{{ job.name }}</h4> </tooltip-on-truncate> <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"> <gl-button - v-if="erasePath" + v-if="restJob.erase_path" v-gl-tooltip.left :title="$options.i18n.eraseLogButtonLabel" :aria-label="$options.i18n.eraseLogButtonLabel" - :href="erasePath" + :href="restJob.erase_path" :data-confirm="$options.i18n.eraseLogConfirmText" class="gl-mr-2" data-testid="job-log-erase-link" @@ -67,23 +108,25 @@ export default { icon="remove" /> <job-sidebar-retry-button - v-if="job.retry_path" + v-if="canShowJobRetryButton" v-gl-tooltip.left - :title="$options.i18n.retryJobButtonLabel" - :aria-label="$options.i18n.retryJobButtonLabel" + :title="buttonTitle" + :aria-label="buttonTitle" + :is-manual-job="isManualJob" :category="retryButtonCategory" - :href="job.retry_path" + :href="restJob.retry_path" :modal-id="$options.forwardDeploymentFailureModalId" variant="confirm" data-qa-selector="retry_button" data-testid="retry-button" + @updateVariablesClicked="$emit('updateVariables')" /> <gl-button - v-if="job.cancel_path" + v-if="restJob.cancel_path" v-gl-tooltip.left :title="$options.i18n.cancelJobButtonLabel" :aria-label="$options.i18n.cancelJobButtonLabel" - :href="job.cancel_path" + :href="restJob.cancel_path" variant="danger" icon="cancel" data-method="post" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue index 3b1509e5be5..8300a22cb67 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue @@ -1,6 +1,7 @@ <script> import { mapState } from 'vuex'; import { GlBadge } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; import { __, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; @@ -79,7 +80,9 @@ export default { TAGS: __('Tags:'), TIMEOUT: __('Timeout'), }, - RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html', + TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', { + anchor: 'set-a-limit-for-how-long-jobs-can-run', + }), }; </script> @@ -96,7 +99,7 @@ export default { <detail-row v-if="job.queued_duration" :value="queuedDuration" :title="$options.i18n.QUEUED" /> <detail-row v-if="hasTimeout" - :help-url="$options.RUNNER_HELP_URL" + :help-url="$options.TIMEOUT_HELP_URL" :value="timeout" data-testid="job-timeout" :title="$options.i18n.TIMEOUT" diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue index 1afc1c9a595..c9172fe0322 100644 --- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue @@ -2,9 +2,7 @@ import { GlButton, GlTableLite } from '@gitlab/ui'; import { __ } from '~/locale'; -const DEFAULT_TD_CLASSES = 'gl-w-half gl-font-sm! gl-border-gray-200!'; -const DEFAULT_TH_CLASSES = - 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1!'; +const DEFAULT_TD_CLASSES = 'gl-font-sm!'; export default { fields: [ @@ -13,14 +11,12 @@ export default { label: __('Key'), tdAttr: { 'data-testid': 'trigger-build-key' }, tdClass: DEFAULT_TD_CLASSES, - thClass: DEFAULT_TH_CLASSES, }, { key: 'value', label: __('Value'), tdAttr: { 'data-testid': 'trigger-build-value' }, tdClass: DEFAULT_TD_CLASSES, - thClass: DEFAULT_TH_CLASSES, }, ], components: { diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js index e9475994e8b..405aea11181 100644 --- a/app/assets/javascripts/jobs/constants.js +++ b/app/assets/javascripts/jobs/constants.js @@ -5,6 +5,11 @@ const moreInfo = __('More information'); export const forwardDeploymentFailureModalId = 'forward-deployment-failure'; +export const GRAPHQL_ID_TYPES = { + commitStatus: 'CommitStatus', + ciBuild: 'Ci::Build', +}; + export const JOB_SIDEBAR_COPY = { cancel, cancelJobButtonLabel: s__('Job|Cancel'), @@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = { eraseLogButtonLabel: s__('Job|Erase job log and artifacts'), eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'), newIssue: __('New issue'), - retry: __('Retry'), - retryJobButtonLabel: s__('Job|Retry'), + retryJobLabel: s__('Job|Retry'), toggleSidebar: __('Toggle Sidebar'), runAgainJobButtonLabel: s__('Job|Run again'), + updateVariables: s__('Job|Update CI/CD variables'), +}; + +export const JOB_GRAPHQL_ERRORS = { + retryMutationErrorText: __('There was an error running the job. Please try again.'), + jobQueryErrorText: __('There was an error fetching the job.'), }; export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { @@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = { }; export const SUCCESS_STATUS = 'SUCCESS'; +export const PASSED_STATUS = 'passed'; diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js index 9dd47f4046c..44bb1ffb1bc 100644 --- a/app/assets/javascripts/jobs/index.js +++ b/app/assets/javascripts/jobs/index.js @@ -1,10 +1,17 @@ import { GlToast } from '@gitlab/ui'; import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import JobApp from './components/job/job_app.vue'; import createStore from './store'; +Vue.use(VueApollo); Vue.use(GlToast); +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + const initializeJobPage = (element) => { const store = createStore(); @@ -26,11 +33,13 @@ const initializeJobPage = (element) => { return new Vue({ el: element, + apolloProvider, store, components: { JobApp, }, provide: { + projectPath, retryOutdatedJobDocsUrl, }, render(createElement) { |