summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/jobs/components
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/jobs/components')
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue9
-rw-r--r--app/assets/javascripts/jobs/components/job_container_item.vue2
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue20
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue22
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue3
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue4
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue14
-rw-r--r--app/assets/javascripts/jobs/components/table/graphql/queries/get_jobs.query.graphql52
-rw-r--r--app/assets/javascripts/jobs/components/table/index.js33
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table.vue67
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_app.vue85
-rw-r--r--app/assets/javascripts/jobs/components/table/jobs_table_tabs.vue66
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>