Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/ci/jobs_page/components')
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue265
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue52
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue171
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue56
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table.vue112
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue36
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue88
7 files changed, 780 insertions, 0 deletions
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
new file mode 100644
index 00000000000..609f2790869
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -0,0 +1,265 @@
+<script>
+import {
+ GlButton,
+ GlButtonGroup,
+ GlModal,
+ GlModalDirective,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { reportMessageToSentry } from '~/ci/utils';
+import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import {
+ ACTIONS_DOWNLOAD_ARTIFACTS,
+ ACTIONS_START_NOW,
+ ACTIONS_UNSCHEDULE,
+ ACTIONS_PLAY,
+ ACTIONS_RETRY,
+ ACTIONS_RUN_AGAIN,
+ CANCEL,
+ GENERIC_ERROR,
+ JOB_SCHEDULED,
+ JOB_SUCCESS,
+ PLAY_JOB_CONFIRMATION_MESSAGE,
+ RUN_JOB_NOW_HEADER_TITLE,
+ FILE_TYPE_ARCHIVE,
+} from '../../constants';
+import eventHub from '../../event_hub';
+import cancelJobMutation from '../../graphql/mutations/job_cancel.mutation.graphql';
+import playJobMutation from '../../graphql/mutations/job_play.mutation.graphql';
+import retryJobMutation from '../../graphql/mutations/job_retry.mutation.graphql';
+import unscheduleJobMutation from '../../graphql/mutations/job_unschedule.mutation.graphql';
+
+export default {
+ ACTIONS_DOWNLOAD_ARTIFACTS,
+ ACTIONS_START_NOW,
+ ACTIONS_UNSCHEDULE,
+ ACTIONS_PLAY,
+ ACTIONS_RETRY,
+ CANCEL,
+ GENERIC_ERROR,
+ PLAY_JOB_CONFIRMATION_MESSAGE,
+ RUN_JOB_NOW_HEADER_TITLE,
+ jobRetry: 'jobRetry',
+ jobCancel: 'jobCancel',
+ jobPlay: 'jobPlay',
+ jobUnschedule: 'jobUnschedule',
+ playJobModalId: 'play-job-modal',
+ name: 'JobActionsCell',
+ components: {
+ GlButton,
+ GlButtonGroup,
+ GlCountdown,
+ GlModal,
+ GlSprintf,
+ },
+ directives: {
+ GlModalDirective,
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ admin: {
+ default: false,
+ },
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ retryBtnDisabled: false,
+ cancelBtnDisabled: false,
+ playManualBtnDisabled: false,
+ unscheduleBtnDisabled: false,
+ };
+ },
+ computed: {
+ hasArtifacts() {
+ return this.job.artifacts.nodes.find((artifact) => artifact.fileType === FILE_TYPE_ARCHIVE);
+ },
+ artifactDownloadPath() {
+ return this.hasArtifacts.downloadPath;
+ },
+ canReadJob() {
+ return this.job.userPermissions?.readBuild;
+ },
+ canUpdateJob() {
+ return this.job.userPermissions?.updateBuild;
+ },
+ canReadArtifacts() {
+ return this.job.userPermissions?.readJobArtifacts;
+ },
+ isActive() {
+ return this.job.active;
+ },
+ manualJobPlayable() {
+ return this.job.playable && !this.admin && this.job.manualJob;
+ },
+ isRetryable() {
+ return this.job.retryable;
+ },
+ isScheduled() {
+ return this.job.status === JOB_SCHEDULED;
+ },
+ scheduledAt() {
+ return this.job.scheduledAt;
+ },
+ currentJobActionPath() {
+ return this.job.detailedStatus?.action?.path;
+ },
+ currentJobMethod() {
+ return this.job.detailedStatus?.action?.method;
+ },
+ shouldDisplayArtifacts() {
+ return this.canReadArtifacts && this.hasArtifacts;
+ },
+ retryButtonTitle() {
+ return this.job.status === JOB_SUCCESS ? ACTIONS_RUN_AGAIN : ACTIONS_RETRY;
+ },
+ },
+ methods: {
+ async postJobAction(name, mutation, redirect = false) {
+ try {
+ const {
+ data: {
+ [name]: { errors, job },
+ },
+ } = await this.$apollo.mutate({
+ mutation,
+ variables: { id: this.job.id },
+ });
+ if (errors.length > 0) {
+ reportMessageToSentry(this.$options.name, errors.join(', '), {});
+ this.showToastMessage();
+ } else if (redirect) {
+ // Retry and Play actions redirect to job detail view
+ // we don't need to refetch with jobActionPerformed event
+ redirectTo(job.detailedStatus.detailsPath); // eslint-disable-line import/no-deprecated
+ } else {
+ eventHub.$emit('jobActionPerformed');
+ }
+ } catch (failure) {
+ reportMessageToSentry(this.$options.name, failure, {});
+ this.showToastMessage();
+ }
+ },
+ showToastMessage() {
+ const toastProps = {
+ text: this.$options.GENERIC_ERROR,
+ variant: 'danger',
+ };
+
+ this.$toast.show(toastProps.text, {
+ variant: toastProps.variant,
+ });
+ },
+ cancelJob() {
+ this.cancelBtnDisabled = true;
+
+ this.postJobAction(this.$options.jobCancel, cancelJobMutation);
+ },
+ retryJob() {
+ this.retryBtnDisabled = true;
+
+ this.postJobAction(this.$options.jobRetry, retryJobMutation, true);
+ },
+ playJob() {
+ this.playManualBtnDisabled = true;
+
+ this.postJobAction(this.$options.jobPlay, playJobMutation, true);
+ },
+ unscheduleJob() {
+ this.unscheduleBtnDisabled = true;
+
+ this.postJobAction(this.$options.jobUnschedule, unscheduleJobMutation);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button-group>
+ <template v-if="canReadJob && canUpdateJob">
+ <gl-button
+ v-if="isActive"
+ v-gl-tooltip
+ icon="cancel"
+ :title="$options.CANCEL"
+ :aria-label="$options.CANCEL"
+ :disabled="cancelBtnDisabled"
+ data-testid="cancel-button"
+ @click="cancelJob()"
+ />
+ <template v-else-if="isScheduled">
+ <gl-button icon="planning" disabled data-testid="countdown">
+ <gl-countdown :end-date-string="scheduledAt" />
+ </gl-button>
+ <gl-button
+ v-gl-modal-directive="$options.playJobModalId"
+ v-gl-tooltip
+ icon="play"
+ :title="$options.ACTIONS_START_NOW"
+ :aria-label="$options.ACTIONS_START_NOW"
+ data-testid="play-scheduled"
+ />
+ <gl-modal
+ :modal-id="$options.playJobModalId"
+ :title="$options.RUN_JOB_NOW_HEADER_TITLE"
+ @primary="playJob()"
+ >
+ <gl-sprintf :message="$options.PLAY_JOB_CONFIRMATION_MESSAGE">
+ <template #job_name>{{ job.name }}</template>
+ </gl-sprintf>
+ </gl-modal>
+ <gl-button
+ v-gl-tooltip
+ icon="time-out"
+ :title="$options.ACTIONS_UNSCHEDULE"
+ :aria-label="$options.ACTIONS_UNSCHEDULE"
+ :disabled="unscheduleBtnDisabled"
+ data-testid="unschedule"
+ @click="unscheduleJob()"
+ />
+ </template>
+ <template v-else>
+ <!--Note: This is the manual job play button -->
+ <gl-button
+ v-if="manualJobPlayable"
+ v-gl-tooltip
+ icon="play"
+ :title="$options.ACTIONS_PLAY"
+ :aria-label="$options.ACTIONS_PLAY"
+ :disabled="playManualBtnDisabled"
+ data-testid="play"
+ @click="playJob()"
+ />
+ <gl-button
+ v-else-if="isRetryable"
+ v-gl-tooltip
+ icon="retry"
+ :title="retryButtonTitle"
+ :aria-label="retryButtonTitle"
+ :method="currentJobMethod"
+ :disabled="retryBtnDisabled"
+ data-testid="retry"
+ @click="retryJob()"
+ />
+ </template>
+ </template>
+ <gl-button
+ v-if="shouldDisplayArtifacts"
+ v-gl-tooltip
+ icon="download"
+ :title="$options.ACTIONS_DOWNLOAD_ARTIFACTS"
+ :aria-label="$options.ACTIONS_DOWNLOAD_ARTIFACTS"
+ :href="artifactDownloadPath"
+ rel="nofollow"
+ download
+ data-testid="download-artifacts"
+ />
+ </gl-button-group>
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
new file mode 100644
index 00000000000..dbf1dfe7a29
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/duration_cell.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlIcon } from '@gitlab/ui';
+import { formatTime } from '~/lib/utils/datetime_utility';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+
+export default {
+ iconSize: 12,
+ components: {
+ GlIcon,
+ TimeAgoTooltip,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ finishedTime() {
+ return this.job?.finishedAt;
+ },
+ duration() {
+ return this.job?.duration;
+ },
+ durationFormatted() {
+ return formatTime(this.duration * 1000);
+ },
+ hasDurationAndFinishedTime() {
+ return this.finishedTime && this.duration;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="duration" data-testid="job-duration">
+ <gl-icon name="timer" :size="$options.iconSize" data-testid="duration-icon" />
+ {{ durationFormatted }}
+ </div>
+ <div
+ v-if="finishedTime"
+ :class="{ 'gl-mt-2': hasDurationAndFinishedTime }"
+ data-testid="job-finished-time"
+ >
+ <gl-icon name="calendar" :size="$options.iconSize" data-testid="finished-time-icon" />
+ <time-ago-tooltip :time="finishedTime" />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
new file mode 100644
index 00000000000..b435eb283fd
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/job_cell.vue
@@ -0,0 +1,171 @@
+<script>
+import { GlBadge, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { s__ } from '~/locale';
+import { SUCCESS_STATUS } from '../../../constants';
+
+export default {
+ iconSize: 12,
+ badgeSize: 'sm',
+ i18n: {
+ stuckText: s__('Jobs|Job is stuck. Check runners.'),
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ components: {
+ GlBadge,
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ jobId() {
+ const id = getIdFromGraphQLId(this.job.id);
+ return `#${id}`;
+ },
+ jobPath() {
+ return this.job.detailedStatus?.detailsPath;
+ },
+ jobRef() {
+ return this.job?.refName;
+ },
+ jobRefPath() {
+ return this.job?.refPath;
+ },
+ jobTags() {
+ return this.job.tags;
+ },
+ createdByTag() {
+ return this.job.createdByTag;
+ },
+ triggered() {
+ return this.job.triggered;
+ },
+ isManualJob() {
+ return this.job.manualJob;
+ },
+ successfulJob() {
+ return this.job.status === SUCCESS_STATUS;
+ },
+ showAllowedToFailBadge() {
+ return this.job.allowFailure && !this.successfulJob;
+ },
+ isScheduledJob() {
+ return Boolean(this.job.scheduledAt);
+ },
+ canReadJob() {
+ return this.job?.userPermissions?.readBuild;
+ },
+ jobStuck() {
+ return this.job?.stuck;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-text-truncate gl-p-3 gl-mt-n3 gl-mx-n3 gl-mb-n2">
+ <gl-link
+ v-if="canReadJob"
+ class="gl-text-blue-600!"
+ :href="jobPath"
+ data-testid="job-id-link"
+ >
+ {{ jobId }}
+ </gl-link>
+
+ <span v-else data-testid="job-id-limited-access">{{ jobId }}</span>
+
+ <gl-icon
+ v-if="jobStuck"
+ v-gl-tooltip="$options.i18n.stuckText"
+ name="warning"
+ :size="$options.iconSize"
+ data-testid="stuck-icon"
+ />
+
+ <div
+ class="gl-display-flex gl-text-gray-700 gl-align-items-center gl-lg-justify-content-start gl-justify-content-end gl-mt-2"
+ >
+ <div
+ v-if="jobRef"
+ class="gl-p-2 gl-rounded-base gl-bg-gray-50 gl-max-w-15 gl-text-truncate"
+ >
+ <gl-icon
+ v-if="createdByTag"
+ name="label"
+ :size="$options.iconSize"
+ data-testid="label-icon"
+ />
+ <gl-icon v-else name="fork" :size="$options.iconSize" data-testid="fork-icon" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.refPath"
+ data-testid="job-ref"
+ >{{ job.refName }}</gl-link
+ >
+ </div>
+
+ <span v-else>{{ __('none') }}</span>
+ <div class="gl-ml-2 gl-p-2 gl-rounded-base gl-bg-gray-50">
+ <gl-icon class="gl-mx-2" name="commit" :size="$options.iconSize" />
+ <gl-link
+ class="gl-font-sm gl-font-monospace gl-text-gray-700 gl-hover-text-gray-900"
+ :href="job.commitPath"
+ data-testid="job-sha"
+ >{{ job.shortSha }}</gl-link
+ >
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <gl-badge
+ v-for="tag in jobTags"
+ :key="tag"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="job-tag-badge"
+ >
+ {{ tag }}
+ </gl-badge>
+
+ <gl-badge
+ v-if="triggered"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="triggered-job-badge"
+ >{{ s__('Job|triggered') }}
+ </gl-badge>
+ <gl-badge
+ v-if="showAllowedToFailBadge"
+ variant="warning"
+ :size="$options.badgeSize"
+ data-testid="fail-job-badge"
+ >{{ s__('Job|allowed to fail') }}
+ </gl-badge>
+ <gl-badge
+ v-if="isScheduledJob"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="delayed-job-badge"
+ >{{ s__('Job|delayed') }}
+ </gl-badge>
+ <gl-badge
+ v-if="isManualJob"
+ variant="info"
+ :size="$options.badgeSize"
+ data-testid="manual-job-badge"
+ >
+ {{ s__('Job|manual') }}
+ </gl-badge>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
new file mode 100644
index 00000000000..18d68ee8a29
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/pipeline_cell.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlAvatar, GlLink } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+export default {
+ components: {
+ GlAvatar,
+ GlLink,
+ },
+ props: {
+ job: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pipelineId() {
+ const id = getIdFromGraphQLId(this.job.pipeline.id);
+ return `#${id}`;
+ },
+ pipelinePath() {
+ return this.job.pipeline?.path;
+ },
+ pipelineUserAvatar() {
+ return this.job.pipeline?.user?.avatarUrl;
+ },
+ userPath() {
+ return this.job.pipeline?.user?.webPath;
+ },
+ showAvatar() {
+ return this.job.pipeline?.user;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-p-3 gl-mt-n3">
+ <gl-link
+ class="gl-text-truncate gl-ml-n3 gl-text-gray-500!"
+ :href="pipelinePath"
+ data-testid="pipeline-id"
+ >
+ {{ pipelineId }}
+ </gl-link>
+ </div>
+ <div class="gl-font-sm gl-text-secondary gl-mt-n2">
+ <span>{{ __('created by') }}</span>
+ <gl-link v-if="showAvatar" :href="userPath" data-testid="pipeline-user-link">
+ <gl-avatar :src="pipelineUserAvatar" :size="16" />
+ </gl-link>
+ <span v-else>{{ __('API') }}</span>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
new file mode 100644
index 00000000000..23100a3f3db
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlTable } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
+import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
+import { DEFAULT_FIELDS } from '../constants';
+import ActionsCell from './job_cells/actions_cell.vue';
+import DurationCell from './job_cells/duration_cell.vue';
+import JobCell from './job_cells/job_cell.vue';
+import PipelineCell from './job_cells/pipeline_cell.vue';
+
+export default {
+ i18n: {
+ emptyText: s__('Jobs|No jobs to show'),
+ },
+ components: {
+ ActionsCell,
+ CiBadgeLink,
+ DurationCell,
+ GlTable,
+ JobCell,
+ PipelineCell,
+ ProjectCell,
+ RunnerCell,
+ },
+ props: {
+ jobs: {
+ type: Array,
+ required: true,
+ },
+ tableFields: {
+ type: Array,
+ required: false,
+ default: () => DEFAULT_FIELDS,
+ },
+ admin: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ methods: {
+ formatCoverage(coverage) {
+ return coverage ? `${coverage}%` : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-table
+ :items="jobs"
+ :fields="tableFields"
+ :tbody-tr-attr="{ 'data-testid': 'jobs-table-row' }"
+ :empty-text="$options.i18n.emptyText"
+ data-testid="jobs-table"
+ show-empty
+ stacked="lg"
+ fixed
+ >
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(status)="{ item }">
+ <ci-badge-link :status="item.detailedStatus" />
+ </template>
+
+ <template #cell(job)="{ item }">
+ <job-cell :job="item" />
+ </template>
+
+ <template #cell(pipeline)="{ item }">
+ <pipeline-cell :job="item" />
+ </template>
+
+ <template v-if="admin" #cell(project)="{ item }">
+ <project-cell :job="item" />
+ </template>
+
+ <template v-if="admin" #cell(runner)="{ item }">
+ <runner-cell :job="item" />
+ </template>
+
+ <template #cell(stage)="{ item }">
+ <div class="gl-text-truncate">
+ <span v-if="item.stage" data-testid="job-stage-name">{{ item.stage.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(name)="{ item }">
+ <div class="gl-text-truncate">
+ <span data-testid="job-name">{{ item.name }}</span>
+ </div>
+ </template>
+
+ <template #cell(duration)="{ item }">
+ <duration-cell :job="item" />
+ </template>
+
+ <template #cell(coverage)="{ item }">
+ <span v-if="item.coverage" data-testid="job-coverage">{{
+ formatCoverage(item.coverage)
+ }}</span>
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <actions-cell class="gl-float-right" :job="item" />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
new file mode 100644
index 00000000000..d2cd27be034
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_empty_state.vue
@@ -0,0 +1,36 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ title: s__('Jobs|Use jobs to automate your tasks'),
+ description: s__(
+ 'Jobs|Jobs are the building blocks of a GitLab CI/CD pipeline. Each job has a specific task, like testing code. To set up jobs in a CI/CD pipeline, add a CI/CD configuration file to your project.',
+ ),
+ buttonText: s__('Jobs|Create CI/CD configuration file'),
+ },
+ components: {
+ GlEmptyState,
+ },
+ inject: {
+ pipelineEditorPath: {
+ default: '',
+ },
+ emptyStateSvgPath: {
+ default: '',
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :description="$options.i18n.description"
+ :svg-path="emptyStateSvgPath"
+ :primary-button-link="pipelineEditorPath"
+ :primary-button-text="$options.i18n.buttonText"
+ data-testid="jobs-empty-state"
+ />
+</template>
diff --git a/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
new file mode 100644
index 00000000000..b753195da9a
--- /dev/null
+++ b/app/assets/javascripts/ci/jobs_page/components/jobs_table_tabs.vue
@@ -0,0 +1,88 @@
+<script>
+import { GlBadge, GlTab, GlTabs, GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import CancelJobs from '~/ci/admin/jobs_table/components/cancel_jobs.vue';
+import { limitedCounterWithDelimiter } from '~/lib/utils/text_utility';
+
+export default {
+ components: {
+ GlBadge,
+ GlTab,
+ GlTabs,
+ GlLoadingIcon,
+ CancelJobs,
+ },
+ inject: {
+ jobStatuses: {
+ default: {},
+ },
+ url: {
+ type: String,
+ default: '',
+ },
+ },
+ props: {
+ allJobsCount: {
+ type: Number,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ showCancelAllJobsButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ tabs() {
+ return [
+ {
+ text: s__('Jobs|All'),
+ count: limitedCounterWithDelimiter(this.allJobsCount),
+ scope: null,
+ testId: 'jobs-all-tab',
+ showBadge: true,
+ },
+ {
+ text: s__('Jobs|Finished'),
+ scope: [this.jobStatuses.success, this.jobStatuses.failed, this.jobStatuses.canceled],
+ testId: 'jobs-finished-tab',
+ showBadge: false,
+ },
+ ];
+ },
+ showLoadingIcon() {
+ return this.loading && !this.allJobsCount;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex align-items-lg-center">
+ <gl-tabs content-class="gl-py-0" class="gl-w-full">
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.text"
+ :title-link-attributes="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
+ 'data-testid': tab.testId,
+ } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ @click="$emit('fetchJobsByStatus', tab.scope)"
+ >
+ <template #title>
+ <span>{{ tab.text }}</span>
+ <gl-loading-icon v-if="showLoadingIcon && tab.showBadge" class="gl-ml-2" />
+
+ <gl-badge v-else-if="tab.showBadge" size="sm" class="gl-tab-counter-badge">
+ {{ tab.count }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ <div class="gl-flex-grow-1"></div>
+ <cancel-jobs v-if="showCancelAllJobsButton" :url="url" />
+ </div>
+</template>