diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-03 21:11:16 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-03-03 21:11:16 +0300 |
commit | 9578c9f9e88421a5dc4d9215f40d932bd30cbabc (patch) | |
tree | 51cc56403430f901de45cb82a6ab5f63c1f37712 /app/assets/javascripts/pipelines | |
parent | 7fcda12793acc54ba8de037f50cc3696dbd0f002 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/pipelines')
10 files changed, 421 insertions, 18 deletions
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue index 9c922f0ec13..fa0f959bce9 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -7,6 +7,7 @@ import PipelineGraph from './graph_component.vue'; import { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, } from './utils'; @@ -60,8 +61,8 @@ export default { update(data) { return unwrapPipelineData(this.pipelineProjectPath, data); }, - error() { - this.reportFailure(LOAD_FAILURE); + error({ gqlError }) { + this.reportFailure(LOAD_FAILURE, serializeGqlErr(gqlError)); }, }, }, @@ -112,10 +113,10 @@ export default { refreshPipelineGraph() { this.$apollo.queries.pipeline.refetch(); }, - reportFailure(type) { + reportFailure(type, err = '') { this.showAlert = true; this.alertType = type; - reportToSentry(this.$options.name, this.alertType); + reportToSentry(this.$options.name, `type: ${this.alertType}, info: ${err}`); }, }, }; diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue index 356207cbdd4..cccabf8619b 100644 --- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue +++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue @@ -6,6 +6,7 @@ import LinkedPipeline from './linked_pipeline.vue'; import { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, validateConfigPaths, @@ -99,12 +100,14 @@ export default { this.loadingPipelineId = null; this.$emit('scrollContainer'); }, - error(err, _vm, _key, type) { + error({ gqlError }, _vm, _key, type) { this.$emit('error', LOAD_FAILURE); reportToSentry( 'linked_pipelines_column', - `error type: ${LOAD_FAILURE}, error: ${err}, apollo error type: ${type}`, + `error type: ${LOAD_FAILURE}, error: ${serializeGqlErr( + gqlError, + )}, apollo error type: ${type}`, ); }, }); diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js index 9e4936c1c24..81d8fe7f489 100644 --- a/app/assets/javascripts/pipelines/components/graph/utils.js +++ b/app/assets/javascripts/pipelines/components/graph/utils.js @@ -23,7 +23,6 @@ const getQueryHeaders = (etagResource) => { }, }; }; -/* eslint-enable @gitlab/require-i18n-strings */ const reportToSentry = (component, failureType) => { Sentry.withScope((scope) => { @@ -32,6 +31,25 @@ const reportToSentry = (component, failureType) => { }); }; +const serializeGqlErr = (gqlError) => { + if (!gqlError) { + return 'gqlError data not available.'; + } + + const { locations, message, path } = gqlError; + + return ` + ${message}. + Locations: ${locations + .flatMap((loc) => Object.entries(loc)) + .flat(2) + .join(' ')}. + Path: ${path.join(', ')}. + `; +}; + +/* eslint-enable @gitlab/require-i18n-strings */ + const toggleQueryPollingByVisibility = (queryRef, interval = 10000) => { const stopStartQuery = (query) => { if (!Visibility.hidden()) { @@ -82,6 +100,7 @@ const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0; export { getQueryHeaders, reportToSentry, + serializeGqlErr, toggleQueryPollingByVisibility, unwrapPipelineData, validateConfigPaths, diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue new file mode 100644 index 00000000000..81eeead2171 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -0,0 +1,119 @@ +<script> +import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui'; +import { __ } from '~/locale'; +import eventHub from '../../event_hub'; +import PipelinesArtifactsComponent from './pipelines_artifacts.vue'; +import PipelinesManualActions from './pipelines_manual_actions.vue'; + +export default { + i18n: { + cancelTitle: __('Cancel'), + redeployTitle: __('Retry'), + }, + directives: { + GlTooltip: GlTooltipDirective, + GlModalDirective, + }, + components: { + GlButton, + PipelinesManualActions, + PipelinesArtifactsComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + cancelingPipeline: { + type: Number, + required: false, + default: null, + }, + }, + data() { + return { + isRetrying: false, + }; + }, + computed: { + displayPipelineActions() { + return ( + this.pipeline.flags.retryable || + this.pipeline.flags.cancelable || + this.pipeline.details.manual_actions.length || + this.pipeline.details.artifacts.length + ); + }, + actions() { + if (!this.pipeline || !this.pipeline.details) { + return []; + } + const { details } = this.pipeline; + return [...(details.manual_actions || []), ...(details.scheduled_actions || [])]; + }, + isCancelling() { + return this.cancelingPipeline === this.pipeline.id; + }, + }, + watch: { + pipeline() { + this.isRetrying = false; + }, + }, + methods: { + handleCancelClick() { + eventHub.$emit('openConfirmationModal', { + pipeline: this.pipeline, + endpoint: this.pipeline.cancel_path, + }); + }, + handleRetryClick() { + this.isRetrying = true; + eventHub.$emit('retryPipeline', this.pipeline.retry_path); + }, + }, +}; +</script> + +<template> + <div v-if="displayPipelineActions" class="gl-text-right"> + <div class="btn-group"> + <pipelines-manual-actions v-if="actions.length > 0" :actions="actions" /> + + <pipelines-artifacts-component + v-if="pipeline.details.artifacts.length" + :artifacts="pipeline.details.artifacts" + /> + + <gl-button + v-if="pipeline.flags.retryable" + v-gl-tooltip.hover + :aria-label="$options.i18n.redeployTitle" + :title="$options.i18n.redeployTitle" + :disabled="isRetrying" + :loading="isRetrying" + class="js-pipelines-retry-button" + data-qa-selector="pipeline_retry_button" + icon="repeat" + variant="default" + category="secondary" + @click="handleRetryClick" + /> + + <gl-button + v-if="pipeline.flags.cancelable" + v-gl-tooltip.hover + v-gl-modal-directive="'confirmation-modal'" + :aria-label="$options.i18n.cancelTitle" + :title="$options.i18n.cancelTitle" + :loading="isCancelling" + :disabled="isCancelling" + icon="close" + variant="danger" + category="primary" + class="js-pipelines-cancel-button" + @click="handleCancelClick" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue index 6955b27cb22..c707b395192 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue @@ -29,7 +29,7 @@ export default { }; </script> <template> - <div :class="classes"> + <div :class="classes" data-testid="pipeline-triggerer"> <user-avatar-link v-if="user" :link-href="user.path" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue index 2a8a0279e62..0de520a2ca7 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue @@ -61,7 +61,7 @@ export default { }; </script> <template> - <div :class="classes"> + <div :class="classes" data-testid="pipeline-url-table-cell"> <gl-link :href="pipeline.path" data-testid="pipeline-url-link" diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue new file mode 100644 index 00000000000..cc676883c1d --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_commit.vue @@ -0,0 +1,85 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CommitComponent from '~/vue_shared/components/commit.vue'; + +export default { + components: { + CommitComponent, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + commitAuthor() { + let commitAuthorInformation; + + if (!this.pipeline || !this.pipeline.commit) { + return null; + } + + // 1. person who is an author of a commit might be a GitLab user + if (this.pipeline.commit.author) { + // 2. if person who is an author of a commit is a GitLab user + // they can have a GitLab avatar + if (this.pipeline.commit.author.avatar_url) { + commitAuthorInformation = this.pipeline.commit.author; + + // 3. If GitLab user does not have avatar, they might have a Gravatar + } else if (this.pipeline.commit.author_gravatar_url) { + commitAuthorInformation = { + ...this.pipeline.commit.author, + avatar_url: this.pipeline.commit.author_gravatar_url, + }; + } + // 4. If committer is not a GitLab User, they can have a Gravatar + } else { + commitAuthorInformation = { + avatar_url: this.pipeline.commit.author_gravatar_url, + path: `mailto:${this.pipeline.commit.author_email}`, + username: this.pipeline.commit.author_name, + }; + } + + return commitAuthorInformation; + }, + commitTag() { + return this.pipeline?.ref?.tag; + }, + commitRef() { + return this.pipeline?.ref; + }, + commitUrl() { + return this.pipeline?.commit?.commit_path; + }, + commitShortSha() { + return this.pipeline?.commit?.short_id; + }, + commitTitle() { + return this.pipeline?.commit?.title; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <commit-component + :tag="commitTag" + :commit-ref="commitRef" + :commit-url="commitUrl" + :merge-request-ref="pipeline.merge_request" + :short-sha="commitShortSha" + :title="commitTitle" + :author="commitAuthor" + :show-ref-info="!isChildView" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue new file mode 100644 index 00000000000..cc3c8d522b3 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue @@ -0,0 +1,37 @@ +<script> +import { CHILD_VIEW } from '~/pipelines/constants'; +import CiBadge from '~/vue_shared/components/ci_badge_link.vue'; + +export default { + components: { + CiBadge, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + viewType: { + type: String, + required: true, + }, + }, + computed: { + pipelineStatus() { + return this.pipeline?.details?.status ?? {}; + }, + isChildView() { + return this.viewType === CHILD_VIEW; + }, + }, +}; +</script> + +<template> + <ci-badge + :status="pipelineStatus" + :show-text="!isChildView" + :icon-classes="'gl-vertical-align-middle!'" + data-qa-selector="pipeline_commit_status" + /> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue index 5894101ebc6..5747287d2ad 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue @@ -1,15 +1,93 @@ <script> import { GlTable, GlTooltipDirective } from '@gitlab/ui'; +import { s__ } from '~/locale'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import eventHub from '../../event_hub'; +import PipelineOperations from './pipeline_operations.vue'; import PipelineStopModal from './pipeline_stop_modal.vue'; +import PipelineTriggerer from './pipeline_triggerer.vue'; +import PipelineUrl from './pipeline_url.vue'; +import PipelinesCommit from './pipelines_commit.vue'; +import PipelinesStatusBadge from './pipelines_status_badge.vue'; import PipelinesTableRowComponent from './pipelines_table_row.vue'; +import PipelineStage from './stage.vue'; +import PipelinesTimeago from './time_ago.vue'; + +const DEFAULT_TD_CLASS = 'gl-p-5!'; +const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!'; +const DEFAULT_TH_CLASSES = + 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1! gl-font-sm!'; export default { + fields: [ + { + key: 'status', + label: s__('Pipeline|Status'), + thClass: DEFAULT_TH_CLASSES, + columnClass: 'gl-w-10p', + tdClass: DEFAULT_TD_CLASS, + thAttr: { 'data-testid': 'status-th' }, + }, + { + key: 'pipeline', + label: s__('Pipeline|Pipeline'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'pipeline-th' }, + }, + { + key: 'triggerer', + label: s__('Pipeline|Triggerer'), + thClass: DEFAULT_TH_CLASSES, + tdClass: `${DEFAULT_TD_CLASS} ${HIDE_TD_ON_MOBILE}`, + columnClass: 'gl-w-10p', + thAttr: { 'data-testid': 'triggerer-th' }, + }, + { + key: 'commit', + label: s__('Pipeline|Commit'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'commit-th' }, + }, + { + key: 'stages', + label: s__('Pipeline|Stages'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'stages-th' }, + }, + { + key: 'timeago', + label: s__('Pipeline|Duration'), + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-15p', + thAttr: { 'data-testid': 'timeago-th' }, + }, + { + key: 'actions', + label: '', + thClass: DEFAULT_TH_CLASSES, + tdClass: DEFAULT_TD_CLASS, + columnClass: 'gl-w-20p', + thAttr: { 'data-testid': 'actions-th' }, + }, + ], components: { GlTable, - PipelinesTableRowComponent, + PipelinesCommit, + PipelineOperations, + PipelineStage, + PipelinesStatusBadge, PipelineStopModal, + PipelinesTableRowComponent, + PipelinesTimeago, + PipelineTriggerer, + PipelineUrl, }, directives: { GlTooltip: GlTooltipDirective, @@ -43,11 +121,6 @@ export default { cancelingPipeline: null, }; }, - computed: { - legacyTableClass() { - return !this.glFeatures.newPipelinesTable ? 'ci-table' : ''; - }, - }, watch: { pipelines() { this.cancelingPipeline = null; @@ -73,8 +146,8 @@ export default { }; </script> <template> - <div :class="legacyTableClass"> - <div v-if="!glFeatures.newPipelinesTable" data-testid="ci-table"> + <div class="ci-table"> + <div v-if="!glFeatures.newPipelinesTable" data-testid="legacy-ci-table"> <div class="gl-responsive-table-row table-row-header" role="row"> <div class="table-section section-10 js-pipeline-status" role="rowheader"> {{ s__('Pipeline|Status') }} @@ -107,7 +180,71 @@ export default { /> </div> - <gl-table v-else /> + <gl-table + v-else + :fields="$options.fields" + :items="pipelines" + tbody-tr-class="commit" + :tbody-tr-attr="{ 'data-testid': 'pipeline-table-row' }" + stacked="lg" + fixed + > + <template #head(actions)> + <slot name="table-header-actions"></slot> + </template> + + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(status)="{ item }"> + <pipelines-status-badge :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(pipeline)="{ item }"> + <pipeline-url + class="gl-text-truncate" + :pipeline="item" + :pipeline-schedule-url="pipelineScheduleUrl" + /> + </template> + + <template #cell(triggerer)="{ item }"> + <pipeline-triggerer :pipeline="item" /> + </template> + + <template #cell(commit)="{ item }"> + <pipelines-commit :pipeline="item" :view-type="viewType" /> + </template> + + <template #cell(stages)="{ item }"> + <div class="stage-cell"> + <div></div> + <template v-if="item.details.stages.length > 0"> + <div + v-for="(stage, index) in item.details.stages" + :key="index" + class="stage-container dropdown" + data-testid="widget-mini-pipeline-graph" + > + <pipeline-stage + :type="$options.pipelinesTable" + :stage="stage" + :update-dropdown="updateGraphDropdown" + /> + </div> + </template> + </div> + </template> + + <template #cell(timeago)="{ item }"> + <pipelines-timeago :pipeline="item" /> + </template> + + <template #cell(actions)="{ item }"> + <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" /> + </template> + </gl-table> <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" /> </div> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 9699f57b4fc..21b114825a6 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -33,3 +33,5 @@ export const LOAD_FAILURE = 'load_failure'; export const PARSE_FAILURE = 'parse_failure'; export const POST_FAILURE = 'post_failure'; export const UNSUPPORTED_DATA = 'unsupported_data'; + +export const CHILD_VIEW = 'child'; |