diff options
Diffstat (limited to 'app/assets/javascripts')
12 files changed, 412 insertions, 92 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/pipelines/components/dag/constants.js index b6a98fdc488..cd89055737f 100644 --- a/app/assets/javascripts/pipelines/components/dag/constants.js +++ b/app/assets/javascripts/pipelines/components/dag/constants.js @@ -1,9 +1,3 @@ -/* Error constants */ -export const PARSE_FAILURE = 'parse_failure'; -export const LOAD_FAILURE = 'load_failure'; -export const UNSUPPORTED_DATA = 'unsupported_data'; -export const DEFAULT = 'default'; - /* Interaction handles */ export const IS_HIGHLIGHTED = 'dag-highlighted'; export const LINK_SELECTOR = 'dag-link'; diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue index 8487da3d621..ab736061a2e 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag.vue @@ -6,16 +6,9 @@ import { fetchPolicies } from '~/lib/graphql'; import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql'; import DagGraph from './dag_graph.vue'; import DagAnnotations from './dag_annotations.vue'; -import { - DEFAULT, - PARSE_FAILURE, - LOAD_FAILURE, - UNSUPPORTED_DATA, - ADD_NOTE, - REMOVE_NOTE, - REPLACE_NOTES, -} from './constants'; +import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants'; import { parseData } from './parsing_utils'; +import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants'; export default { // eslint-disable-next-line @gitlab/require-i18n-strings diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue index d12baa9617e..34ff89a5e6f 100644 --- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue +++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue @@ -1,14 +1,7 @@ <script> import * as d3 from 'd3'; import { uniqueId } from 'lodash'; -import { - LINK_SELECTOR, - NODE_SELECTOR, - PARSE_FAILURE, - ADD_NOTE, - REMOVE_NOTE, - REPLACE_NOTES, -} from './constants'; +import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants'; import { currentIsLive, getLiveLinksAsDict, @@ -19,6 +12,7 @@ import { } from './interactions'; import { getMaxNodes, removeOrphanNodes } from './parsing_utils'; import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils'; +import { PARSE_FAILURE } from '../../constants'; export default { viewOptions: { diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index c7b72be36ad..b26f28fa6af 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -1,8 +1,11 @@ <script> -import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui'; -import ciHeader from '~/vue_shared/components/header_ci_component.vue'; -import eventHub from '../event_hub'; +import { GlAlert, GlButton, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui'; import { __ } from '~/locale'; +import axios from '~/lib/utils/axios_utils'; +import ciHeader from '~/vue_shared/components/header_ci_component.vue'; +import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; +import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql'; +import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants'; const DELETE_MODAL_ID = 'pipeline-delete-modal'; @@ -10,57 +13,143 @@ export default { name: 'PipelineHeaderSection', components: { ciHeader, + GlAlert, + GlButton, GlLoadingIcon, GlModal, - GlButton, }, directives: { GlModal: GlModalDirective, }, - props: { - pipeline: { - type: Object, - required: true, + errorTexts: { + [LOAD_FAILURE]: __('We are currently unable to fetch data for the pipeline header.'), + [POST_FAILURE]: __('An error occurred while making the request.'), + [DELETE_FAILURE]: __('An error occurred while deleting the pipeline.'), + [DEFAULT]: __('An unknown error occurred.'), + }, + inject: { + // Receive `cancel`, `delete`, `fullProject` and `retry` + paths: { + default: {}, + }, + pipelineId: { + default: '', }, - isLoading: { - type: Boolean, - required: true, + pipelineIid: { + default: '', + }, + }, + apollo: { + pipeline: { + query: getPipelineQuery, + variables() { + return { + fullPath: this.paths.fullProject, + iid: this.pipelineIid, + }; + }, + update: data => data.project.pipeline, + error() { + this.reportFailure(LOAD_FAILURE); + }, + pollInterval: 10000, + watchLoading(isLoading) { + if (!isLoading) { + // To ensure apollo has updated the cache, + // we only remove the loading state in sync with GraphQL + this.isCanceling = false; + this.isRetrying = false; + } + }, }, }, data() { return { + pipeline: null, + failureType: null, isCanceling: false, isRetrying: false, isDeleting: false, }; }, - computed: { - status() { - return this.pipeline.details && this.pipeline.details.status; - }, - shouldRenderContent() { - return !this.isLoading && Object.keys(this.pipeline).length; - }, deleteModalConfirmationText() { return __( 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.', ); }, + hasError() { + return this.failureType; + }, + hasPipelineData() { + return Boolean(this.pipeline); + }, + isLoadingInitialQuery() { + return this.$apollo.queries.pipeline.loading && !this.hasPipelineData; + }, + status() { + return this.pipeline?.status; + }, + shouldRenderContent() { + return !this.isLoadingInitialQuery && this.hasPipelineData; + }, + failure() { + switch (this.failureType) { + case LOAD_FAILURE: + return { + text: this.$options.errorTexts[LOAD_FAILURE], + variant: 'danger', + }; + case POST_FAILURE: + return { + text: this.$options.errorTexts[POST_FAILURE], + variant: 'danger', + }; + case DELETE_FAILURE: + return { + text: this.$options.errorTexts[DELETE_FAILURE], + variant: 'danger', + }; + default: + return { + text: this.$options.errorTexts[DEFAULT], + variant: 'danger', + }; + } + }, }, - methods: { - cancelPipeline() { + reportFailure(errorType) { + this.failureType = errorType; + }, + async postAction(path) { + try { + await axios.post(path); + this.$apollo.queries.pipeline.refetch(); + } catch { + this.reportFailure(POST_FAILURE); + } + }, + async cancelPipeline() { this.isCanceling = true; - eventHub.$emit('headerPostAction', this.pipeline.cancel_path); + this.postAction(this.paths.cancel); }, - retryPipeline() { + async retryPipeline() { this.isRetrying = true; - eventHub.$emit('headerPostAction', this.pipeline.retry_path); + this.postAction(this.paths.retry); }, - deletePipeline() { + async deletePipeline() { this.isDeleting = true; - eventHub.$emit('headerDeleteAction', this.pipeline.delete_path); + this.$apollo.queries.pipeline.stopPolling(); + + try { + const { request } = await axios.delete(this.paths.delete); + redirectTo(setUrlFragment(request.responseURL, 'delete_success')); + } catch { + this.$apollo.queries.pipeline.startPolling(); + this.reportFailure(DELETE_FAILURE); + this.isDeleting = false; + } }, }, DELETE_MODAL_ID, @@ -68,54 +157,53 @@ export default { </script> <template> <div class="pipeline-header-container"> + <gl-alert v-if="hasError" :variant="failure.variant">{{ failure.text }}</gl-alert> <ci-header v-if="shouldRenderContent" - :status="status" - :item-id="pipeline.id" - :time="pipeline.created_at" + :status="pipeline.detailedStatus" + :time="pipeline.createdAt" :user="pipeline.user" + :item-id="Number(pipelineId)" item-name="Pipeline" > <gl-button - v-if="pipeline.retry_path" + v-if="pipeline.retryable" :loading="isRetrying" :disabled="isRetrying" - data-testid="retryButton" category="secondary" variant="info" + data-testid="retryPipeline" + class="js-retry-button" @click="retryPipeline()" > {{ __('Retry') }} </gl-button> <gl-button - v-if="pipeline.cancel_path" + v-if="pipeline.cancelable" :loading="isCanceling" :disabled="isCanceling" - data-testid="cancelPipeline" - class="gl-ml-3" - category="primary" variant="danger" + data-testid="cancelPipeline" @click="cancelPipeline()" > {{ __('Cancel running') }} </gl-button> <gl-button - v-if="pipeline.delete_path" + v-if="pipeline.userPermissions.destroyPipeline" v-gl-modal="$options.DELETE_MODAL_ID" :loading="isDeleting" :disabled="isDeleting" - data-testid="deletePipeline" class="gl-ml-3" - category="secondary" variant="danger" + category="secondary" + data-testid="deletePipeline" > {{ __('Delete') }} </gl-button> </ci-header> - - <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" /> + <gl-loading-icon v-if="isLoadingInitialQuery" size="lg" class="gl-mt-3 gl-mb-3" /> <gl-modal :modal-id="$options.DELETE_MODAL_ID" diff --git a/app/assets/javascripts/pipelines/components/legacy_header_component.vue b/app/assets/javascripts/pipelines/components/legacy_header_component.vue new file mode 100644 index 00000000000..c7b72be36ad --- /dev/null +++ b/app/assets/javascripts/pipelines/components/legacy_header_component.vue @@ -0,0 +1,132 @@ +<script> +import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui'; +import ciHeader from '~/vue_shared/components/header_ci_component.vue'; +import eventHub from '../event_hub'; +import { __ } from '~/locale'; + +const DELETE_MODAL_ID = 'pipeline-delete-modal'; + +export default { + name: 'PipelineHeaderSection', + components: { + ciHeader, + GlLoadingIcon, + GlModal, + GlButton, + }, + directives: { + GlModal: GlModalDirective, + }, + props: { + pipeline: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + }, + data() { + return { + isCanceling: false, + isRetrying: false, + isDeleting: false, + }; + }, + + computed: { + status() { + return this.pipeline.details && this.pipeline.details.status; + }, + shouldRenderContent() { + return !this.isLoading && Object.keys(this.pipeline).length; + }, + deleteModalConfirmationText() { + return __( + 'Are you sure you want to delete this pipeline? Doing so will expire all pipeline caches and delete all related objects, such as builds, logs, artifacts, and triggers. This action cannot be undone.', + ); + }, + }, + + methods: { + cancelPipeline() { + this.isCanceling = true; + eventHub.$emit('headerPostAction', this.pipeline.cancel_path); + }, + retryPipeline() { + this.isRetrying = true; + eventHub.$emit('headerPostAction', this.pipeline.retry_path); + }, + deletePipeline() { + this.isDeleting = true; + eventHub.$emit('headerDeleteAction', this.pipeline.delete_path); + }, + }, + DELETE_MODAL_ID, +}; +</script> +<template> + <div class="pipeline-header-container"> + <ci-header + v-if="shouldRenderContent" + :status="status" + :item-id="pipeline.id" + :time="pipeline.created_at" + :user="pipeline.user" + item-name="Pipeline" + > + <gl-button + v-if="pipeline.retry_path" + :loading="isRetrying" + :disabled="isRetrying" + data-testid="retryButton" + category="secondary" + variant="info" + @click="retryPipeline()" + > + {{ __('Retry') }} + </gl-button> + + <gl-button + v-if="pipeline.cancel_path" + :loading="isCanceling" + :disabled="isCanceling" + data-testid="cancelPipeline" + class="gl-ml-3" + category="primary" + variant="danger" + @click="cancelPipeline()" + > + {{ __('Cancel running') }} + </gl-button> + + <gl-button + v-if="pipeline.delete_path" + v-gl-modal="$options.DELETE_MODAL_ID" + :loading="isDeleting" + :disabled="isDeleting" + data-testid="deletePipeline" + class="gl-ml-3" + category="secondary" + variant="danger" + > + {{ __('Delete') }} + </gl-button> + </ci-header> + + <gl-loading-icon v-if="isLoading" size="lg" class="gl-mt-3 gl-mb-3" /> + + <gl-modal + :modal-id="$options.DELETE_MODAL_ID" + :title="__('Delete pipeline')" + :ok-title="__('Delete pipeline')" + ok-variant="danger" + @ok="deletePipeline()" + > + <p> + {{ deleteModalConfirmationText }} + </p> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index abe5e1060c8..701d36b71f2 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -21,3 +21,11 @@ export const FETCH_TAG_ERROR_MESSAGE = __('There was a problem fetching project export const RAW_TEXT_WARNING = s__( 'Pipeline|Raw text search is not currently supported. Please use the available search tokens.', ); + +/* Error constants shared across graphs */ +export const DEFAULT = 'default'; +export const DELETE_FAILURE = 'delete_pipeline_failure'; +export const LOAD_FAILURE = 'load_failure'; +export const PARSE_FAILURE = 'parse_failure'; +export const POST_FAILURE = 'post_failure'; +export const UNSUPPORTED_DATA = 'unsupported_data'; diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql new file mode 100644 index 00000000000..06083daeca0 --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/get_pipeline_header_data.query.graphql @@ -0,0 +1,30 @@ +query getPipelineHeaderData($fullPath: ID!, $iid: ID!) { + project(fullPath: $fullPath) { + pipeline(iid: $iid) { + id + status + retryable + cancelable + userPermissions { + destroyPipeline + } + detailedStatus { + detailsPath + icon + group + text + } + createdAt + user { + name + webPath + email + avatarUrl + status { + message + emoji + } + } + } + } +} diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js index 745f5b886a5..67aec12655a 100644 --- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js +++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js @@ -7,10 +7,11 @@ import pipelineGraph from './components/graph/graph_component.vue'; import createDagApp from './pipeline_details_dag'; import GraphBundleMixin from './mixins/graph_pipeline_bundle_mixin'; import PipelinesMediator from './pipeline_details_mediator'; -import pipelineHeader from './components/header_component.vue'; +import legacyPipelineHeader from './components/legacy_header_component.vue'; import eventHub from './event_hub'; import TestReports from './components/test_reports/test_reports.vue'; import createTestReportsStore from './stores/test_reports'; +import { createPipelineHeaderApp } from './pipeline_details_header'; Vue.use(Translate); @@ -56,7 +57,7 @@ const createPipelinesDetailApp = mediator => { }); }; -const createPipelineHeaderApp = mediator => { +const createLegacyPipelineHeaderApp = mediator => { if (!document.querySelector(SELECTORS.PIPELINE_HEADER)) { return; } @@ -64,7 +65,7 @@ const createPipelineHeaderApp = mediator => { new Vue({ el: SELECTORS.PIPELINE_HEADER, components: { - pipelineHeader, + legacyPipelineHeader, }, data() { return { @@ -95,7 +96,7 @@ const createPipelineHeaderApp = mediator => { }, }, render(createElement) { - return createElement('pipeline-header', { + return createElement('legacy-pipeline-header', { props: { isLoading: this.mediator.state.isLoading, pipeline: this.mediator.store.state.pipeline, @@ -132,7 +133,12 @@ export default () => { mediator.fetchPipeline(); createPipelinesDetailApp(mediator); - createPipelineHeaderApp(mediator); + + if (gon.features.graphqlPipelineHeader) { + createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER); + } else { + createLegacyPipelineHeaderApp(mediator); + } createTestDetails(); createDagApp(); }; diff --git a/app/assets/javascripts/pipelines/pipeline_details_header.js b/app/assets/javascripts/pipelines/pipeline_details_header.js new file mode 100644 index 00000000000..27fe9ba3f19 --- /dev/null +++ b/app/assets/javascripts/pipelines/pipeline_details_header.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import pipelineHeader from './components/header_component.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export const createPipelineHeaderApp = elSelector => { + const el = document.querySelector(elSelector); + + if (!el) { + return; + } + + const { cancelPath, deletePath, fullPath, pipelineId, pipelineIid, retryPath } = el?.dataset; + // eslint-disable-next-line no-new + new Vue({ + el, + components: { + pipelineHeader, + }, + apolloProvider, + provide: { + paths: { + cancel: cancelPath, + delete: deletePath, + fullProject: fullPath, + retry: retryPath, + }, + pipelineId, + pipelineIid, + }, + render(createElement) { + return createElement('pipeline-header', {}); + }, + }); +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue index ec0934c5b4b..14c2e9fa828 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue @@ -1,6 +1,6 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlButton } from '@gitlab/ui'; import { escape } from 'lodash'; import simplePoll from '../../../lib/utils/simple_poll'; import eventHub from '../../event_hub'; @@ -12,7 +12,7 @@ export default { name: 'MRWidgetRebase', components: { statusIcon, - GlLoadingIcon, + GlButton, }, props: { mr: { @@ -109,29 +109,29 @@ export default { <div class="rebase-state-find-class-convention media media-body space-children"> <template v-if="mr.rebaseInProgress || isMakingRequest"> - <span class="bold">{{ __('Rebase in progress') }}</span> + <span class="bold" data-testid="rebase-message">{{ __('Rebase in progress') }}</span> </template> <template v-if="!mr.rebaseInProgress && !mr.canPushToSourceBranch"> - <span class="bold" v-html="fastForwardMergeText"></span> + <span class="bold" data-testid="rebase-message" v-html="fastForwardMergeText"></span> </template> <template v-if="!mr.rebaseInProgress && mr.canPushToSourceBranch && !isMakingRequest"> <div class="accept-merge-holder clearfix js-toggle-container accept-action media space-children" > - <button - :disabled="isMakingRequest" - type="button" - class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button" + <gl-button + :loading="isMakingRequest" + variant="success" + class="qa-mr-rebase-button" @click="rebase" > - <gl-loading-icon v-if="isMakingRequest" />{{ __('Rebase') }} - </button> - <span v-if="!rebasingError" class="bold">{{ + {{ __('Rebase') }} + </gl-button> + <span v-if="!rebasingError" class="bold" data-testid="rebase-message">{{ __( 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.', ) }}</span> - <span v-else class="bold danger">{{ rebasingError }}</span> + <span v-else class="bold danger" data-testid="rebase-message">{{ rebasingError }}</span> </div> </template> </div> diff --git a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue index d7af3b3298e..1b7e51b7d02 100644 --- a/app/assets/javascripts/vue_shared/components/ci_badge_link.vue +++ b/app/assets/javascripts/vue_shared/components/ci_badge_link.vue @@ -7,7 +7,7 @@ import CiIcon from './ci_icon.vue'; * * Receives status object containing: * status: { - * details_path: "/gitlab-org/gitlab-foss/pipelines/8150156" // url + * details_path or detailsPath: "/gitlab-org/gitlab-foss/pipelines/8150156" // url * group:"running" // used for CSS class * icon: "icon_status_running" // used to render the icon * label:"running" // used for potential tooltip @@ -46,6 +46,13 @@ export default { }, }, computed: { + title() { + return !this.showText ? this.status?.text : ''; + }, + detailsPath() { + // For now, this can either come from graphQL with camelCase or REST API in snake_case + return this.status.detailsPath || this.status.details_path; + }, cssClass() { const className = this.status.group; return className ? `ci-status ci-${className} qa-status-badge` : 'ci-status qa-status-badge'; @@ -54,12 +61,7 @@ export default { }; </script> <template> - <a - v-gl-tooltip - :href="status.details_path" - :class="cssClass" - :title="!showText ? status.text : ''" - > + <a v-gl-tooltip :href="detailsPath" :class="cssClass" :title="title"> <ci-icon :status="status" :css-classes="iconClasses" /> <template v-if="showText"> diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue index 6ff6f10f786..4679d922861 100644 --- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue +++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue @@ -1,10 +1,11 @@ <script> /* eslint-disable vue/no-v-html */ -import { GlTooltipDirective, GlLink, GlDeprecatedButton } from '@gitlab/ui'; -import { __, sprintf } from '~/locale'; +import { GlTooltipDirective, GlLink, GlDeprecatedButton, GlTooltip } from '@gitlab/ui'; import CiIconBadge from './ci_badge_link.vue'; import TimeagoTooltip from './time_ago_tooltip.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue'; +import { glEmojiTag } from '../../emoji'; +import { __, sprintf } from '../../locale'; /** * Renders header component for job and pipeline page based on UI mockups @@ -20,10 +21,12 @@ export default { UserAvatarImage, GlLink, GlDeprecatedButton, + GlTooltip, }, directives: { GlTooltip: GlTooltipDirective, }, + EMOJI_REF: 'EMOJI_REF', props: { status: { type: Object, @@ -62,6 +65,27 @@ export default { userAvatarAltText() { return sprintf(__(`%{username}'s avatar`), { username: this.user.name }); }, + userPath() { + // GraphQL returns `webPath` and Rest `path` + return this.user?.webPath || this.user?.path; + }, + avatarUrl() { + // GraphQL returns `avatarUrl` and Rest `avatar_url` + return this.user?.avatarUrl || this.user?.avatar_url; + }, + statusTooltipHTML() { + // Rest `status_tooltip_html` which is a ready to work + // html for the emoji and the status text inside a tooltip. + // GraphQL returns `status.emoji` and `status.message` which + // needs to be combined to make the html we want. + const { emoji } = this.user?.status || {}; + const emojiHtml = emoji ? glEmojiTag(emoji) : ''; + + return emojiHtml || this.user?.status_tooltip_html; + }, + message() { + return this.user?.status?.message; + }, }, methods: { @@ -73,7 +97,7 @@ export default { </script> <template> - <header class="page-content-header ci-header-container"> + <header class="page-content-header ci-header-container" data-testid="pipeline-header-content"> <section class="header-main-content"> <ci-icon-badge :status="status" /> @@ -89,12 +113,12 @@ export default { <template v-if="user"> <gl-link v-gl-tooltip - :href="user.path" + :href="userPath" :title="user.email" class="js-user-link commit-committer-link" > <user-avatar-image - :img-src="user.avatar_url" + :img-src="avatarUrl" :img-alt="userAvatarAltText" :tooltip-text="user.name" :img-size="24" @@ -102,7 +126,15 @@ export default { {{ user.name }} </gl-link> - <span v-if="user.status_tooltip_html" v-html="user.status_tooltip_html"></span> + <gl-tooltip v-if="message" :target="() => $refs[$options.EMOJI_REF]"> + {{ message }} + </gl-tooltip> + <span + v-if="statusTooltipHTML" + :ref="$options.EMOJI_REF" + :data-testid="message" + v-html="statusTooltipHTML" + ></span> </template> </section> |