diff options
Diffstat (limited to 'app/assets/javascripts/jobs/components')
27 files changed, 527 insertions, 109 deletions
diff --git a/app/assets/javascripts/jobs/components/filtered_search/constants.js b/app/assets/javascripts/jobs/components/filtered_search/constants.js new file mode 100644 index 00000000000..0daba892375 --- /dev/null +++ b/app/assets/javascripts/jobs/components/filtered_search/constants.js @@ -0,0 +1,13 @@ +export const jobStatusValues = [ + 'CANCELED', + 'CREATED', + 'FAILED', + 'MANUAL', + 'SUCCESS', + 'PENDING', + 'PREPARING', + 'RUNNING', + 'SCHEDULED', + 'SKIPPED', + 'WAITING_FOR_RESOURCE', +]; 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 fe7b7428c6e..e498a735898 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 @@ -11,6 +11,13 @@ export default { components: { GlFilteredSearch, }, + props: { + queryString: { + type: Object, + required: false, + default: null, + }, + }, computed: { tokens() { return [ @@ -24,6 +31,20 @@ export default { }, ]; }, + filteredSearchValue() { + if (this.queryString?.statuses) { + return [ + { + type: 'status', + value: { + data: this.queryString?.statuses, + operator: '=', + }, + }, + ]; + } + return []; + }, }, methods: { onSubmit(filters) { @@ -37,6 +58,7 @@ export default { <gl-filtered-search :placeholder="s__('Jobs|Filter jobs')" :available-tokens="tokens" + :value="filteredSearchValue" @submit="onSubmit" /> </template> diff --git a/app/assets/javascripts/jobs/components/filtered_search/utils.js b/app/assets/javascripts/jobs/components/filtered_search/utils.js new file mode 100644 index 00000000000..696cd8d4706 --- /dev/null +++ b/app/assets/javascripts/jobs/components/filtered_search/utils.js @@ -0,0 +1,27 @@ +import { jobStatusValues } from './constants'; + +// validates query string used for filtered search +// on jobs table to ensure GraphQL query is called correctly +export const validateQueryString = (queryStringObj) => { + // currently only one token is supported `statuses` + // this code will need to be expanded as more tokens + // are introduced + + const filters = Object.keys(queryStringObj); + + if (filters.includes('statuses')) { + const queryStringStatus = { + statuses: queryStringObj.statuses.toUpperCase(), + }; + + const found = jobStatusValues.find((status) => status === queryStringStatus.statuses); + + if (found) { + return queryStringStatus; + } + + return null; + } + + return null; +}; diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue index e31c13f40b0..65b9600e664 100644 --- a/app/assets/javascripts/jobs/components/empty_state.vue +++ b/app/assets/javascripts/jobs/components/job/empty_state.vue @@ -1,12 +1,16 @@ <script> import { GlLink } from '@gitlab/ui'; -import ManualVariablesForm from './manual_variables_form.vue'; +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, @@ -50,6 +54,9 @@ export default { }, }, computed: { + isGraphQL() { + return this.glFeatures?.graphqlJobApp; + }, shouldRenderManualVariables() { return this.playable && !this.scheduled; }, @@ -70,7 +77,12 @@ export default { <p v-if="content" data-testid="job-empty-state-content">{{ content }}</p> </div> - <manual-variables-form v-if="shouldRenderManualVariables" :action="action" /> + <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"> <gl-link diff --git a/app/assets/javascripts/jobs/components/environments_block.vue b/app/assets/javascripts/jobs/components/job/environments_block.vue index 4046e1ade82..4046e1ade82 100644 --- a/app/assets/javascripts/jobs/components/environments_block.vue +++ b/app/assets/javascripts/jobs/components/job/environments_block.vue diff --git a/app/assets/javascripts/jobs/components/erased_block.vue b/app/assets/javascripts/jobs/components/job/erased_block.vue index a815689659e..a815689659e 100644 --- a/app/assets/javascripts/jobs/components/erased_block.vue +++ b/app/assets/javascripts/jobs/components/job/erased_block.vue diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue index d5ee3423d70..81b65d175a7 100644 --- a/app/assets/javascripts/jobs/components/job_app.vue +++ b/app/assets/javascripts/jobs/components/job/job_app.vue @@ -6,15 +6,15 @@ import { mapGetters, mapState, mapActions } from 'vuex'; import { isScrolledToBottom } from '~/lib/utils/scroll_utils'; import { __, sprintf } from '~/locale'; import CiHeader from '~/vue_shared/components/header_ci_component.vue'; -import delayedJobMixin from '../mixins/delayed_job_mixin'; +import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin'; +import Log from '~/jobs/components/log/log.vue'; import EmptyState from './empty_state.vue'; import EnvironmentsBlock from './environments_block.vue'; import ErasedBlock from './erased_block.vue'; import LogTopBar from './job_log_controllers.vue'; -import Log from './log/log.vue'; -import Sidebar from './sidebar.vue'; import StuckBlock from './stuck_block.vue'; import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue'; +import Sidebar from './sidebar/sidebar.vue'; export default { name: 'JobPageApp', @@ -197,7 +197,7 @@ export default { </script> <template> <div> - <gl-loading-icon v-if="isLoading" size="lg" class="qa-loading-animation gl-mt-6" /> + <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-6" /> <template v-else-if="shouldRenderContent"> <div class="build-page" data-testid="job-content"> diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue index e9809ac661b..e9809ac661b 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job/job_log_controllers.vue diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue index 07ef4f054b4..1898e02c94e 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue @@ -77,9 +77,6 @@ export default { }, methods: { ...mapActions(['triggerManualJob']), - canRemove(index) { - return index < this.variables.length - 1; - }, addEmptyVariable() { const lastVar = this.variables[this.variables.length - 1]; @@ -93,12 +90,18 @@ export default { 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; @@ -125,7 +128,7 @@ export default { </gl-input-group-text> </template> <gl-form-input - :ref="`${$options.inputTypes.key}-${variable.id}`" + :ref="inputRef('key', variable.id)" v-model="variable.key" :placeholder="$options.i18n.keyPlaceholder" data-testid="ci-variable-key" @@ -140,20 +143,13 @@ export default { </gl-input-group-text> </template> <gl-form-input - :ref="`${$options.inputTypes.value}-${variable.id}`" + :ref="inputRef('value', variable.id)" v-model="variable.secretValue" :placeholder="$options.i18n.valuePlaceholder" data-testid="ci-variable-value" /> </gl-form-input-group> - <!-- delete variable button placeholder to not break flex layout --> - <div - v-if="!canRemove(index)" - class="gl-w-7 gl-mr-3" - data-testid="delete-variable-btn-placeholder" - ></div> - <gl-button v-if="canRemove(index)" class="gl-flex-grow-0 gl-flex-basis-0" @@ -164,6 +160,9 @@ export default { 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"> diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue new file mode 100644 index 00000000000..2f97301979c --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue @@ -0,0 +1,195 @@ +<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'; + +// 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. + +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/artifacts_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue index 2018942a7e8..2018942a7e8 100644 --- a/app/assets/javascripts/jobs/components/artifacts_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue index 7f25ca8a94d..7f25ca8a94d 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/commit_block.vue diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue index 097ab3b4cf6..097ab3b4cf6 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_container_item.vue diff --git a/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue index e83ed6c6332..913924cc7b1 100644 --- a/app/assets/javascripts/jobs/components/job_retry_forward_deployment_modal.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue @@ -1,6 +1,6 @@ <script> import { GlLink, GlModal } from '@gitlab/ui'; -import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '../constants'; +import { JOB_RETRY_FORWARD_DEPLOYMENT_MODAL } from '~/jobs/constants'; export default { name: 'JobRetryForwardDeploymentModal', diff --git a/app/assets/javascripts/jobs/components/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue index a7bf365d35c..dd620977f0c 100644 --- a/app/assets/javascripts/jobs/components/job_sidebar_retry_button.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue @@ -1,12 +1,12 @@ <script> import { GlButton, GlModalDirective } from '@gitlab/ui'; import { mapGetters } from 'vuex'; -import { JOB_SIDEBAR } from '../constants'; +import { JOB_SIDEBAR_COPY } from '~/jobs/constants'; export default { name: 'JobSidebarRetryButton', i18n: { - retryLabel: JOB_SIDEBAR.retry, + retryLabel: JOB_SIDEBAR_COPY.retry, }, components: { GlButton, diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue index df64b6422c7..df64b6422c7 100644 --- a/app/assets/javascripts/jobs/components/jobs_container.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/jobs_container.vue 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 new file mode 100644 index 00000000000..263b2d141c9 --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue @@ -0,0 +1,99 @@ +<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'; + }, + }, + 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="$options.i18n.retryJobButtonLabel" + :aria-label="$options.i18n.retryJobButtonLabel" + :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/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue index a42e45ee7e4..b0db48df01f 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue @@ -1,48 +1,40 @@ <script> -import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; +import { GlButton, GlIcon } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapActions, mapGetters, mapState } from 'vuex'; -import { s__ } from '~/locale'; -import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue'; -import { JOB_SIDEBAR } from '../constants'; -import ArtifactsBlock from './artifacts_block.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; +import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants'; import CommitBlock from './commit_block.vue'; -import JobRetryForwardDeploymentModal from './job_retry_forward_deployment_modal.vue'; -import JobSidebarRetryButton from './job_sidebar_retry_button.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'; -export const forwardDeploymentFailureModalId = 'forward-deployment-failure'; - export default { name: 'JobSidebar', i18n: { - eraseLogButtonLabel: s__('Job|Erase job log and artifacts'), - eraseLogConfirmText: s__('Job|Are you sure you want to erase this job log and artifacts?'), - cancelJobButtonLabel: s__('Job|Cancel'), - retryJobButtonLabel: s__('Job|Retry'), - ...JOB_SIDEBAR, + ...JOB_SIDEBAR_COPY, }, borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'], forwardDeploymentFailureModalId, - directives: { - GlTooltip: GlTooltipDirective, - }, components: { ArtifactsBlock, CommitBlock, GlButton, GlIcon, JobsContainer, - JobSidebarRetryButton, JobRetryForwardDeploymentModal, JobSidebarDetailsContainer, + LegacySidebarHeader, + SidebarHeader, StagesDropdown, - TooltipOnTruncate, TriggerBlock, }, + mixins: [glFeatureFlagsMixin()], props: { artifactHelpUrl: { type: String, @@ -58,9 +50,6 @@ export default { computed: { ...mapGetters(['hasForwardDeploymentFailure']), ...mapState(['job', 'stages', 'jobs', 'selectedStage']), - retryButtonCategory() { - return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; - }, hasArtifact() { // the artifact object will always have a locked property return Object.keys(this.job.artifact).length > 1; @@ -68,8 +57,8 @@ export default { hasTriggers() { return !isEmpty(this.job.trigger); }, - hasStages() { - return this.job?.pipeline?.stages?.length > 0; + isGraphQL() { + return this.glFeatures?.graphqlJobApp; }, commit() { return this.job?.pipeline?.commit || {}; @@ -79,7 +68,7 @@ export default { }, }, methods: { - ...mapActions(['fetchJobsForStage', 'toggleSidebar']), + ...mapActions(['fetchJobsForStage']), }, }; </script> @@ -87,61 +76,8 @@ export default { <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix"> <div class="sidebar-container"> <div class="blocks-container"> - <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="$options.i18n.retryJobButtonLabel" - :aria-label="$options.i18n.retryJobButtonLabel" - :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" - /> - </div> - - <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> - + <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" /> + <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" /> <div v-if="job.terminal_path || job.new_issue_path" class="gl-py-5" diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue index 05567328660..05567328660 100644 --- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_detail_row.vue diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue new file mode 100644 index 00000000000..523710598bf --- /dev/null +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue @@ -0,0 +1,102 @@ +<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'; + +// 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: { + ...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'; + }, + }, + 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="$options.i18n.retryJobButtonLabel" + :aria-label="$options.i18n.retryJobButtonLabel" + :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/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue index 3b1509e5be5..3b1509e5be5 100644 --- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue index 7c4811b2d6f..e3afe9b7c67 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue @@ -4,14 +4,14 @@ import { isEmpty } from 'lodash'; import Mousetrap from 'mousetrap'; import { s__ } from '~/locale'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; -import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { clickCopyToClipboardButton } from '~/behaviors/copy_to_clipboard'; import { keysFor, MR_COPY_SOURCE_BRANCH_NAME } from '~/behaviors/shortcuts/keybindings'; export default { components: { CiIcon, - clipboardButton, + ClipboardButton, GlDropdown, GlDropdownItem, GlLink, diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue index 1afc1c9a595..1afc1c9a595 100644 --- a/app/assets/javascripts/jobs/components/trigger_block.vue +++ b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue diff --git a/app/assets/javascripts/jobs/components/stuck_block.vue b/app/assets/javascripts/jobs/components/job/stuck_block.vue index d7a26d22406..d7a26d22406 100644 --- a/app/assets/javascripts/jobs/components/stuck_block.vue +++ b/app/assets/javascripts/jobs/components/job/stuck_block.vue diff --git a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue b/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue index c9747ca9f02..c9747ca9f02 100644 --- a/app/assets/javascripts/jobs/components/unmet_prerequisites_block.vue +++ b/app/assets/javascripts/jobs/components/job/unmet_prerequisites_block.vue 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 98b51e8c2c4..851be211b25 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 @@ -11,6 +11,7 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo } nodes { artifacts { + # eslint-disable-next-line @graphql-eslint/require-id-when-available nodes { downloadPath fileType 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 c2f460cb647..0a4757d11a8 100644 --- a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -2,7 +2,9 @@ import { GlAlert, GlSkeletonLoader, GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui'; import { __ } from '~/locale'; import createFlash from '~/flash'; +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 JobsTable from './jobs_table.vue'; import JobsTableEmptyState from './jobs_table_empty_state.vue'; @@ -37,6 +39,7 @@ export default { variables() { return { fullPath: this.fullPath, + ...this.validatedQueryString, }; }, update(data) { @@ -95,6 +98,11 @@ export default { jobsCount() { return this.jobs.count; }, + validatedQueryString() { + const queryStringObject = queryToObject(window.location.search); + + return validateQueryString(queryStringObject); + }, }, watch: { // this watcher ensures that the count on the all tab @@ -133,6 +141,10 @@ export default { } if (filter.type === 'status') { + updateHistory({ + url: setUrlParams({ statuses: filter.value.data }, window.location.href, true), + }); + this.$apollo.queries.jobs.refetch({ statuses: filter.value.data }); } }); @@ -171,12 +183,12 @@ export default { :loading="loading" @fetchJobsByStatus="fetchJobsByStatus" /> - - <jobs-filtered-search - v-if="showFilteredSearch" - :class="$options.filterSearchBoxStyles" - @filterJobsBySearch="filterJobsBySearch" - /> + <div v-if="showFilteredSearch" :class="$options.filterSearchBoxStyles"> + <jobs-filtered-search + :query-string="validatedQueryString" + @filterJobsBySearch="filterJobsBySearch" + /> + </div> <div v-if="showSkeletonLoader" class="gl-mt-5"> <gl-skeleton-loader :width="1248" :height="73"> |