diff options
Diffstat (limited to 'app/assets/javascripts/jobs/components')
12 files changed, 357 insertions, 20 deletions
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue index eae6b5d5419..7f25ca8a94d 100644 --- a/app/assets/javascripts/jobs/components/commit_block.vue +++ b/app/assets/javascripts/jobs/components/commit_block.vue @@ -1,5 +1,4 @@ <script> -/* eslint-disable @gitlab/vue-require-i18n-strings */ import { GlLink } from '@gitlab/ui'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -23,9 +22,9 @@ export default { </script> <template> <div> - <span class="font-weight-bold">{{ __('Commit') }}</span> + <span class="gl-font-weight-bold">{{ __('Commit') }}</span> - <gl-link :href="commit.commit_path" class="js-commit-sha commit-sha link-commit"> + <gl-link :href="commit.commit_path" class="gl-text-blue-600!" data-testid="commit-sha"> {{ commit.short_id }} </gl-link> @@ -37,8 +36,8 @@ export default { /> <span v-if="mergeRequest"> - in - <gl-link :href="mergeRequest.path" class="js-link-commit link-commit" + {{ __('in') }} + <gl-link :href="mergeRequest.path" class="gl-text-blue-600!" data-testid="link-commit" >!{{ mergeRequest.iid }}</gl-link > </span> diff --git a/app/assets/javascripts/jobs/components/job_container_item.vue b/app/assets/javascripts/jobs/components/job_container_item.vue index 488d838db52..00a570fe2f8 100644 --- a/app/assets/javascripts/jobs/components/job_container_item.vue +++ b/app/assets/javascripts/jobs/components/job_container_item.vue @@ -48,7 +48,7 @@ export default { }" > <gl-link - v-gl-tooltip + v-gl-tooltip:tooltip-container.left :href="job.status.details_path" :title="tooltipText" class="js-job-link gl-display-flex gl-align-items-center" diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue index ce4a85b35b7..ea50a11bed6 100644 --- a/app/assets/javascripts/jobs/components/job_log_controllers.vue +++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue @@ -1,9 +1,15 @@ <script> import { GlTooltipDirective, GlLink, GlButton } from '@gitlab/ui'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { __, sprintf } from '~/locale'; +import { __, s__, sprintf } from '~/locale'; export default { + i18n: { + eraseLogButtonLabel: s__('Job|Erase job log'), + scrollToBottomButtonLabel: s__('Job|Scroll to bottom'), + scrollToTopButtonLabel: s__('Job|Scroll to top'), + showRawButtonLabel: s__('Job|Show complete raw'), + }, components: { GlLink, GlButton, @@ -82,7 +88,8 @@ export default { <gl-button v-if="rawPath" v-gl-tooltip.body - :title="s__('Job|Show complete raw')" + :title="$options.i18n.showRawButtonLabel" + :aria-label="$options.i18n.showRawButtonLabel" :href="rawPath" data-testid="job-raw-link-controller" icon="doc-text" @@ -91,7 +98,8 @@ export default { <gl-button v-if="erasePath" v-gl-tooltip.body - :title="s__('Job|Erase job log')" + :title="$options.i18n.eraseLogButtonLabel" + :aria-label="$options.i18n.eraseLogButtonLabel" :href="erasePath" :data-confirm="__('Are you sure you want to erase this build?')" class="gl-ml-3" @@ -102,23 +110,25 @@ export default { <!-- eo links --> <!-- scroll buttons --> - <div v-gl-tooltip :title="s__('Job|Scroll to top')" class="gl-ml-3"> + <div v-gl-tooltip :title="$options.i18n.scrollToTopButtonLabel" class="gl-ml-3"> <gl-button :disabled="isScrollTopDisabled" class="btn-scroll" data-testid="job-controller-scroll-top" icon="scroll_up" + :aria-label="$options.i18n.scrollToTopButtonLabel" @click="handleScrollToTop" /> </div> - <div v-gl-tooltip :title="s__('Job|Scroll to bottom')" class="gl-ml-3"> + <div v-gl-tooltip :title="$options.i18n.scrollToBottomButtonLabel" class="gl-ml-3"> <gl-button :disabled="isScrollBottomDisabled" class="js-scroll-bottom btn-scroll" data-testid="job-controller-scroll-bottom" icon="scroll_down" :class="{ animate: isScrollingDown }" + :aria-label="$options.i18n.scrollToBottomButtonLabel" @click="handleScrollToBottom" /> </div> diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue index a1f4f7abb77..d45012d2023 100644 --- a/app/assets/javascripts/jobs/components/manual_variables_form.vue +++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue @@ -43,6 +43,7 @@ export default { variables: [], key: '', secretValue: '', + triggerBtnDisabled: false, }; }, computed: { @@ -98,6 +99,11 @@ export default { 1, ); }, + trigger() { + this.triggerBtnDisabled = true; + + this.triggerManualJob(this.variables); + }, }, }; </script> @@ -111,7 +117,12 @@ export default { <div class="table-section section-50" role="rowheader">{{ s__('CiVariables|Value') }}</div> </div> - <div v-for="variable in variables" :key="variable.id" class="gl-responsive-table-row"> + <div + v-for="variable in variables" + :key="variable.id" + class="gl-responsive-table-row" + data-testid="ci-variable-row" + > <div class="table-section section-50"> <div class="table-mobile-header" role="rowheader">{{ s__('Pipeline|Key') }}</div> <div class="table-mobile-content gl-mr-3"> @@ -120,6 +131,7 @@ export default { v-model="variable.key" :placeholder="$options.i18n.keyPlaceholder" class="ci-variable-body-item form-control" + data-testid="ci-variable-key" /> </div> </div> @@ -132,6 +144,7 @@ export default { v-model="variable.secret_value" :placeholder="$options.i18n.valuePlaceholder" class="ci-variable-body-item form-control" + data-testid="ci-variable-value" /> </div> </div> @@ -143,6 +156,7 @@ export default { category="tertiary" icon="clear" :aria-label="__('Delete variable')" + data-testid="delete-variable-btn" @click="deleteVariable(variable.id)" /> </div> @@ -175,14 +189,16 @@ export default { </div> </div> <div class="d-flex gl-mt-3 justify-content-center"> - <p class="text-muted" v-html="helpText"></p> + <p class="text-muted" data-testid="form-help-text" v-html="helpText"></p> </div> <div class="d-flex justify-content-center"> <gl-button variant="info" category="primary" :aria-label="__('Trigger manual job')" - @click="triggerManualJob(variables)" + :disabled="triggerBtnDisabled" + data-testid="trigger-manual-job-btn" + @click="trigger" > {{ action.button_title }} </gl-button> diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue index fcf03dff34e..1b50006239c 100644 --- a/app/assets/javascripts/jobs/components/sidebar.vue +++ b/app/assets/javascripts/jobs/components/sidebar.vue @@ -49,7 +49,8 @@ export default { return this.job.status && this.job.recoverable ? 'primary' : 'secondary'; }, hasArtifact() { - return !isEmpty(this.job.artifact); + // the artifact object will always have a locked property + return Object.keys(this.job.artifact).length > 1; }, hasTriggers() { return !isEmpty(this.job.trigger); diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue index b20d58b6ffe..98badb96ed7 100644 --- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue +++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue @@ -51,7 +51,9 @@ export default { }); }, runnerId() { - return `${this.job.runner.description} (#${this.job.runner.id})`; + const { id, short_sha: token, description } = this.job?.runner; + + return `#${id} (${token}) ${description}`; }, shouldRenderBlock() { return Boolean(this.hasAnyDetail || this.hasTimeout || this.hasTags); diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue index 18de849af88..36b0ad43b14 100644 --- a/app/assets/javascripts/jobs/components/stages_dropdown.vue +++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue @@ -44,13 +44,14 @@ export default { </script> <template> <div class="dropdown"> - <div class="js-pipeline-info"> + <div class="js-pipeline-info" data-testid="pipeline-info"> <ci-icon :status="pipeline.details.status" class="vertical-align-middle" /> <span class="font-weight-bold">{{ s__('Job|Pipeline') }}</span> <gl-link :href="pipeline.path" class="js-pipeline-path link-commit" + data-testid="pipeline-path" data-qa-selector="pipeline_path" >#{{ pipeline.id }}</gl-link > @@ -58,13 +59,17 @@ export default { {{ s__('Job|for') }} <template v-if="isTriggeredByMergeRequest"> - <gl-link :href="pipeline.merge_request.path" class="link-commit ref-name js-mr-link" + <gl-link + :href="pipeline.merge_request.path" + class="link-commit ref-name" + data-testid="mr-link" >!{{ pipeline.merge_request.iid }}</gl-link > {{ s__('Job|with') }} <gl-link :href="pipeline.merge_request.source_branch_path" - class="link-commit ref-name js-source-branch-link" + class="link-commit ref-name" + data-testid="source-branch-link" >{{ pipeline.merge_request.source_branch }}</gl-link > @@ -72,7 +77,8 @@ export default { {{ s__('Job|into') }} <gl-link :href="pipeline.merge_request.target_branch_path" - class="link-commit ref-name js-target-branch-link" + class="link-commit ref-name" + data-testid="target-branch-link" >{{ pipeline.merge_request.target_branch }}</gl-link > </template> 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 new file mode 100644 index 00000000000..d9e51b0345a --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql @@ -0,0 +1,52 @@ +query getJobs($fullPath: ID!, $statuses: [CiJobStatus!]) { + project(fullPath: $fullPath) { + jobs(first: 20, statuses: $statuses) { + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + nodes { + detailedStatus { + icon + label + text + tooltip + action { + buttonTitle + icon + method + path + title + } + } + id + refName + refPath + tags + shortSha + commitPath + pipeline { + id + path + user { + webPath + avatarUrl + } + } + stage { + name + } + name + duration + finishedAt + coverage + retryable + playable + cancelable + active + } + } + } +} diff --git a/app/assets/javascripts/jobs/components/table/index.js b/app/assets/javascripts/jobs/components/table/index.js new file mode 100644 index 00000000000..b6b3bb6d379 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/index.js @@ -0,0 +1,33 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import JobsTableApp from '~/jobs/components/table/jobs_table_app.vue'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export default (containerId = 'js-jobs-table') => { + const containerEl = document.getElementById(containerId); + + if (!containerEl) { + return false; + } + + const { fullPath, jobCounts, jobStatuses } = containerEl.dataset; + + return new Vue({ + el: containerEl, + apolloProvider, + provide: { + fullPath, + jobStatuses: JSON.parse(jobStatuses), + jobCounts: JSON.parse(jobCounts), + }, + render(createElement) { + return createElement(JobsTableApp); + }, + }); +}; diff --git a/app/assets/javascripts/jobs/components/table/jobs_table.vue b/app/assets/javascripts/jobs/components/table/jobs_table.vue new file mode 100644 index 00000000000..32b26d45dfe --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table.vue @@ -0,0 +1,67 @@ +<script> +import { GlTable } from '@gitlab/ui'; +import { __ } from '~/locale'; + +const defaultTableClasses = { + tdClass: 'gl-p-5!', + thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!', +}; + +export default { + fields: [ + { + key: 'status', + label: __('Status'), + ...defaultTableClasses, + }, + { + key: 'job', + label: __('Job'), + ...defaultTableClasses, + }, + { + key: 'pipeline', + label: __('Pipeline'), + ...defaultTableClasses, + }, + { + key: 'stage', + label: __('Stage'), + ...defaultTableClasses, + }, + { + key: 'name', + label: __('Name'), + ...defaultTableClasses, + }, + { + key: 'duration', + label: __('Duration'), + ...defaultTableClasses, + }, + { + key: 'coverage', + label: __('Coverage'), + ...defaultTableClasses, + }, + { + key: 'actions', + label: '', + ...defaultTableClasses, + }, + ], + components: { + GlTable, + }, + props: { + jobs: { + type: Array, + required: true, + }, + }, +}; +</script> + +<template> + <gl-table :items="jobs" :fields="$options.fields" /> +</template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_app.vue b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue new file mode 100644 index 00000000000..55954e31654 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table_app.vue @@ -0,0 +1,85 @@ +<script> +import { GlAlert, GlSkeletonLoader } from '@gitlab/ui'; +import { __ } from '~/locale'; +import GetJobs from './graphql/queries/get_jobs.query.graphql'; +import JobsTable from './jobs_table.vue'; +import JobsTableTabs from './jobs_table_tabs.vue'; + +export default { + i18n: { + errorMsg: __('There was an error fetching the jobs for your project.'), + }, + components: { + GlAlert, + GlSkeletonLoader, + JobsTable, + JobsTableTabs, + }, + inject: { + fullPath: { + default: '', + }, + }, + apollo: { + jobs: { + query: GetJobs, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update({ project }) { + return project?.jobs; + }, + error() { + this.hasError = true; + }, + }, + }, + data() { + return { + jobs: null, + hasError: false, + isAlertDismissed: false, + }; + }, + computed: { + shouldShowAlert() { + return this.hasError && !this.isAlertDismissed; + }, + }, + methods: { + fetchJobsByStatus(scope) { + this.$apollo.queries.jobs.refetch({ statuses: scope }); + }, + }, +}; +</script> + +<template> + <div> + <gl-alert + v-if="shouldShowAlert" + class="gl-mt-2" + variant="danger" + dismissible + @dismiss="isAlertDismissed = true" + > + {{ $options.i18n.errorMsg }} + </gl-alert> + + <jobs-table-tabs @fetchJobsByStatus="fetchJobsByStatus" /> + + <div v-if="$apollo.loading" class="gl-mt-5"> + <gl-skeleton-loader + preserve-aspect-ratio="none" + equal-width-lines + :lines="5" + :width="600" + :height="66" + /> + </div> + + <jobs-table v-else :jobs="jobs.nodes" /> + </div> +</template> diff --git a/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue new file mode 100644 index 00000000000..95d265fce60 --- /dev/null +++ b/app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue @@ -0,0 +1,66 @@ +<script> +import { GlBadge, GlTab, GlTabs } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default { + components: { + GlBadge, + GlTab, + GlTabs, + }, + inject: { + jobCounts: { + default: {}, + }, + jobStatuses: { + default: {}, + }, + }, + computed: { + tabs() { + return [ + { + text: __('All'), + count: this.jobCounts.all, + scope: null, + testId: 'jobs-all-tab', + }, + { + text: __('Pending'), + count: this.jobCounts.pending, + scope: this.jobStatuses.pending, + testId: 'jobs-pending-tab', + }, + { + text: __('Running'), + count: this.jobCounts.running, + scope: this.jobStatuses.running, + testId: 'jobs-running-tab', + }, + { + text: __('Finished'), + count: this.jobCounts.finished, + scope: [this.jobStatuses.success, this.jobStatuses.failed, this.jobStatuses.canceled], + testId: 'jobs-finished-tab', + }, + ]; + }, + }, +}; +</script> + +<template> + <gl-tabs> + <gl-tab + v-for="tab in tabs" + :key="tab.text" + :title-link-attributes="{ 'data-testid': tab.testId }" + @click="$emit('fetchJobsByStatus', tab.scope)" + > + <template #title> + <span>{{ tab.text }}</span> + <gl-badge size="sm" class="gl-tab-counter-badge">{{ tab.count }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> +</template> |