diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /app/assets/javascripts/pipelines | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) | |
download | gitlab-ce-b595cb0c1dec83de5bdee18284abe86614bed33b.tar.gz |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'app/assets/javascripts/pipelines')
17 files changed, 326 insertions, 31 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 f822e2c0874..14872c34afb 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue @@ -281,6 +281,7 @@ export default { :type="graphViewType" :show-links="showLinks" :tip-previously-dismissed="hoverTipPreviouslyDismissed" + :is-pipeline-complete="pipeline.complete" @dismissHoverTip="handleTipDismissal" @updateViewType="updateViewType" @updateShowLinksState="updateShowLinksState" diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue index 1920fed84ec..a8c5d85f4ed 100644 --- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue +++ b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue @@ -1,17 +1,33 @@ <script> -import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui'; +import { + GlAlert, + GlButton, + GlButtonGroup, + GlLoadingIcon, + GlToggle, + GlModalDirective, +} from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import Tracking from '~/tracking'; +import PerformanceInsightsModal from '../performance_insights_modal.vue'; +import { performanceModalId } from '../../constants'; import { STAGE_VIEW, LAYER_VIEW } from './constants'; export default { name: 'GraphViewSelector', + performanceModalId, components: { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle, + PerformanceInsightsModal, }, + directives: { + GlModal: GlModalDirective, + }, + mixins: [Tracking.mixin()], props: { showLinks: { type: Boolean, @@ -25,6 +41,10 @@ export default { type: String, required: true, }, + isPipelineComplete: { + type: Boolean, + required: true, + }, }, data() { return { @@ -39,6 +59,7 @@ export default { hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'), linksLabelText: s__('GraphViewType|Show dependencies'), viewLabelText: __('Group jobs by'), + performanceBtnText: __('Performance insights'), }, views: { [STAGE_VIEW]: { @@ -129,6 +150,9 @@ export default { this.$emit('updateShowLinksState', val); }); }, + trackInsightsClick() { + this.track('click_insights_button', { label: 'performance_insights' }); + }, }, }; </script> @@ -154,6 +178,15 @@ export default { </gl-button> </gl-button-group> + <gl-button + v-if="isPipelineComplete" + v-gl-modal="$options.performanceModalId" + data-testid="pipeline-insights-btn" + @click="trackInsightsClick" + > + {{ $options.i18n.performanceBtnText }} + </gl-button> + <div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center"> <gl-toggle v-model="showLinksActive" @@ -169,5 +202,7 @@ export default { <gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip"> {{ $options.i18n.hoverTipText }} </gl-alert> + + <performance-insights-modal /> </div> </template> diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue index 37878f3fb6d..fabae62fc45 100644 --- a/app/assets/javascripts/pipelines/components/header_component.vue +++ b/app/assets/javascripts/pipelines/components/header_component.vue @@ -63,6 +63,18 @@ export default { default: '', }, }, + modal: { + id: DELETE_MODAL_ID, + actionPrimary: { + text: __('Delete pipeline'), + attributes: { + variant: 'danger', + }, + }, + actionCancel: { + text: __('Cancel'), + }, + }, apollo: { pipeline: { context() { @@ -275,7 +287,7 @@ export default { <gl-button v-if="pipeline.userPermissions.destroyPipeline" - v-gl-modal="$options.DELETE_MODAL_ID" + v-gl-modal="$options.modal.id" :loading="isDeleting" :disabled="isDeleting" class="gl-ml-3" @@ -289,11 +301,11 @@ export default { <gl-loading-icon v-if="isLoadingInitialQuery" size="lg" class="gl-mt-3 gl-mb-3" /> <gl-modal - :modal-id="$options.DELETE_MODAL_ID" + :modal-id="$options.modal.id" :title="__('Delete pipeline')" - :ok-title="__('Delete pipeline')" - ok-variant="danger" - @ok="deletePipeline()" + :action-primary="$options.modal.actionPrimary" + :action-cancel="$options.modal.actionCancel" + @primary="deletePipeline()" > <p> {{ deleteModalConfirmationText }} diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue index 070c5ee59de..0c6b8b9ed2b 100644 --- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue +++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue @@ -96,7 +96,7 @@ export default { <template #cell(actions)="{ item }"> <gl-button v-if="canRetryJob(item)" - icon="repeat" + icon="retry" :title="$options.retry" :aria-label="$options.retry" @click="retryJob(item.id)" diff --git a/app/assets/javascripts/pipelines/components/performance_insights_modal.vue b/app/assets/javascripts/pipelines/components/performance_insights_modal.vue new file mode 100644 index 00000000000..ae6b9186930 --- /dev/null +++ b/app/assets/javascripts/pipelines/components/performance_insights_modal.vue @@ -0,0 +1,168 @@ +<script> +import { GlAlert, GlCard, GlLink, GlLoadingIcon, GlModal } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import { humanizeTimeInterval } from '~/lib/utils/datetime_utility'; +import HelpPopover from '~/vue_shared/components/help_popover.vue'; +import getPerformanceInsightsQuery from '../graphql/queries/get_performance_insights.query.graphql'; +import { performanceModalId } from '../constants'; +import { calculateJobStats, calculateSlowestFiveJobs } from '../utils'; + +export default { + name: 'PerformanceInsightsModal', + i18n: { + queuedCardHeader: s__('Pipeline|Longest queued job'), + queuedCardHelp: s__( + 'Pipeline|The longest queued job is the job that spent the longest time in the pending state, waiting to be picked up by a Runner', + ), + executedCardHeader: s__('Pipeline|Last executed job'), + executedCardHelp: s__( + 'Pipeline|The last executed job is the last job to start in the pipeline.', + ), + viewDependency: s__('Pipeline|View dependency'), + slowJobsTitle: s__('Pipeline|Five slowest jobs'), + feeback: __('Feedback issue'), + insightsLimit: s__('Pipeline|Only able to show first 100 results'), + }, + modal: { + title: s__('Pipeline|Performance insights'), + actionCancel: { + text: __('Close'), + attributes: { + variant: 'confirm', + }, + }, + }, + performanceModalId, + components: { + GlAlert, + GlCard, + GlLink, + GlModal, + GlLoadingIcon, + HelpPopover, + }, + inject: { + pipelineIid: { + default: '', + }, + pipelineProjectPath: { + default: '', + }, + }, + apollo: { + jobs: { + query: getPerformanceInsightsQuery, + variables() { + return { + fullPath: this.pipelineProjectPath, + iid: this.pipelineIid, + }; + }, + update(data) { + return data.project?.pipeline?.jobs; + }, + }, + }, + data() { + return { + jobs: null, + }; + }, + computed: { + longestQueuedJob() { + return calculateJobStats(this.jobs, 'queuedDuration'); + }, + lastExecutedJob() { + return calculateJobStats(this.jobs, 'startedAt'); + }, + slowestFiveJobs() { + return calculateSlowestFiveJobs(this.jobs); + }, + queuedDurationDisplay() { + return humanizeTimeInterval(this.longestQueuedJob.queuedDuration); + }, + showLimitMessage() { + return this.jobs.pageInfo.hasNextPage; + }, + }, +}; +</script> + +<template> + <gl-modal + :modal-id="$options.performanceModalId" + :title="$options.modal.title" + :action-cancel="$options.modal.actionCancel" + > + <gl-loading-icon v-if="$apollo.queries.jobs.loading" size="lg" /> + + <template v-else> + <gl-alert v-if="showLimitMessage" class="gl-mb-4" :dismissible="false"> + <p>{{ $options.i18n.insightsLimit }}</p> + <gl-link href="https://gitlab.com/gitlab-org/gitlab/-/issues/365902" class="gl-mt-5"> + {{ $options.i18n.feeback }} + </gl-link> + </gl-alert> + <div class="gl-display-flex gl-justify-content-space-between gl-mb-7"> + <gl-card class="gl-w-half gl-mr-7 gl-text-center"> + <template #header> + <span class="gl-font-weight-bold">{{ $options.i18n.queuedCardHeader }}</span> + <help-popover> + {{ $options.i18n.queuedCardHelp }} + </help-popover> + </template> + <div class="gl-display-flex gl-flex-direction-column"> + <span + class="gl-font-weight-bold gl-font-size-h2 gl-mb-2" + data-testid="insights-queued-card-data" + > + {{ queuedDurationDisplay }} + </span> + <gl-link + :href="longestQueuedJob.detailedStatus.detailsPath" + data-testid="insights-queued-card-link" + > + {{ longestQueuedJob.name }} + </gl-link> + </div> + </gl-card> + <gl-card class="gl-w-half gl-text-center" data-testid="insights-executed-card"> + <template #header> + <span class="gl-font-weight-bold">{{ $options.i18n.executedCardHeader }}</span> + <help-popover> + {{ $options.i18n.executedCardHelp }} + </help-popover> + </template> + <div class="gl-display-flex gl-flex-direction-column"> + <span + class="gl-font-weight-bold gl-font-size-h2 gl-mb-2" + data-testid="insights-executed-card-data" + > + {{ lastExecutedJob.name }} + </span> + <gl-link + :href="lastExecutedJob.detailedStatus.detailsPath" + data-testid="insights-executed-card-link" + > + {{ $options.i18n.viewDependency }} + </gl-link> + </div> + </gl-card> + </div> + + <div class="gl-mt-7"> + <span class="gl-font-weight-bold">{{ $options.i18n.slowJobsTitle }}</span> + <div + v-for="job in slowestFiveJobs" + :key="job.name" + class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-mt-3 gl-p-4 gl-border-t-1 gl-border-t-solid gl-border-b-0 gl-border-b-solid gl-border-gray-100" + > + <span data-testid="insights-slow-job-stage">{{ job.stage.name }}</span> + <gl-link :href="job.detailedStatus.detailsPath" data-testid="insights-slow-job-link">{{ + job.name + }}</gl-link> + </div> + </div> + </template> + </gl-modal> +</template> diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue index fa0e153b2af..7a08dacb824 100644 --- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue +++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue @@ -80,7 +80,7 @@ export default { class="js-pipelines-retry-button" data-qa-selector="pipeline_retry_button" data-testid="pipelines-retry-button" - icon="repeat" + icon="retry" variant="default" category="secondary" @click="handleRetryClick" diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue index 76ee6ab613b..69509c9088b 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue @@ -1,5 +1,6 @@ <script> -import { GlBadge, GlFriendlyWrap, GlLink, GlModal } from '@gitlab/ui'; +import { GlBadge, GlFriendlyWrap, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui'; +import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue'; import { __, n__, s__, sprintf } from '~/locale'; import CodeBlock from '~/vue_shared/components/code_block.vue'; @@ -11,6 +12,10 @@ export default { GlFriendlyWrap, GlLink, GlModal, + ModalCopyButton, + }, + directives: { + GlTooltip: GlTooltipDirective, }, props: { modalId: { @@ -57,6 +62,7 @@ export default { history: __('History'), trace: __('System output'), attachment: s__('TestReports|Attachment'), + copyTestName: s__('TestReports|Copy test name to rerun locally'), }, modalCloseButton: { text: __('Close'), @@ -85,6 +91,13 @@ export default { {{ testCase.file }} </gl-link> <span v-else>{{ testCase.file }}</span> + <modal-copy-button + :title="$options.text.copyTestName" + :text="testCase.file" + :modal-id="modalId" + category="tertiary" + class="gl-ml-1" + /> </div> </div> diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue index 58d072b0005..3fb46a4f128 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue @@ -1,6 +1,7 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; import { mapActions, mapGetters, mapState } from 'vuex'; +import createTestReportsStore from '../../stores/test_reports'; import EmptyState from './empty_state.vue'; import TestSuiteTable from './test_suite_table.vue'; import TestSummary from './test_summary.vue'; @@ -15,9 +16,10 @@ export default { TestSummary, TestSummaryTable, }, + inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'], computed: { - ...mapState(['isLoading', 'selectedSuiteIndex', 'testReports']), - ...mapGetters(['getSelectedSuite']), + ...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']), + ...mapGetters('testReports', ['getSelectedSuite']), showSuite() { return this.selectedSuiteIndex !== null; }, @@ -27,10 +29,19 @@ export default { }, }, created() { + this.$store.registerModule( + 'testReports', + createTestReportsStore({ + blobPath: this.blobPath, + summaryEndpoint: this.summaryEndpoint, + suiteEndpoint: this.suiteEndpoint, + }), + ); + this.fetchSummary(); }, methods: { - ...mapActions([ + ...mapActions('testReports', [ 'fetchTestSuite', 'fetchSummary', 'setSelectedSuiteIndex', diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue index 1e481d37017..1f438c63fee 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue @@ -51,14 +51,18 @@ export default { }, }, computed: { - ...mapState(['pageInfo']), - ...mapGetters(['getSuiteTests', 'getSuiteTestCount', 'getSuiteArtifactsExpired']), + ...mapState('testReports', ['pageInfo']), + ...mapGetters('testReports', [ + 'getSuiteTests', + 'getSuiteTestCount', + 'getSuiteArtifactsExpired', + ]), hasSuites() { return this.getSuiteTests.length > 0; }, }, methods: { - ...mapActions(['setPage']), + ...mapActions('testReports', ['setPage']), }, wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'], i18n, diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue index 2b44ce57faa..8389c2a5104 100644 --- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue +++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue @@ -19,7 +19,7 @@ export default { }, }, computed: { - ...mapGetters(['getTestSuites']), + ...mapGetters('testReports', ['getTestSuites']), hasSuites() { return this.getTestSuites.length > 0; }, diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js index 0510992e962..2e825016c91 100644 --- a/app/assets/javascripts/pipelines/constants.js +++ b/app/assets/javascripts/pipelines/constants.js @@ -109,3 +109,5 @@ export const DEFAULT_FIELDS = [ columnClass: 'gl-w-20p', }, ]; + +export const performanceModalId = 'performanceInsightsModal'; diff --git a/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql b/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql new file mode 100644 index 00000000000..25e990c8934 --- /dev/null +++ b/app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql @@ -0,0 +1,28 @@ +query getPerformanceInsightsData($fullPath: ID!, $iid: ID!) { + project(fullPath: $fullPath) { + id + pipeline(iid: $iid) { + id + jobs { + pageInfo { + hasNextPage + } + nodes { + id + duration + detailedStatus { + id + detailsPath + } + name + stage { + id + name + } + startedAt + queuedDuration + } + } + } + } +} diff --git a/app/assets/javascripts/pipelines/pipeline_tabs.js b/app/assets/javascripts/pipelines/pipeline_tabs.js index e7c00d89a10..c0e769e2485 100644 --- a/app/assets/javascripts/pipelines/pipeline_tabs.js +++ b/app/assets/javascripts/pipelines/pipeline_tabs.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import VueApollo from 'vue-apollo'; import PipelineTabs from 'ee_else_ce/pipelines/components/pipeline_tabs.vue'; import { removeParams, updateHistory } from '~/lib/utils/url_utility'; @@ -7,6 +8,7 @@ import { parseBoolean } from '~/lib/utils/common_utils'; import { getPipelineDefaultTab, reportToSentry } from './utils'; Vue.use(VueApollo); +Vue.use(Vuex); export const createAppOptions = (selector, apolloProvider) => { const el = document.querySelector(selector); @@ -37,6 +39,7 @@ export const createAppOptions = (selector, apolloProvider) => { PipelineTabs, }, apolloProvider, + store: new Vuex.Store(), provide: { canGenerateCodequalityReports: parseBoolean(canGenerateCodequalityReports), codequalityReportDownloadPath, diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js index 27ab2418440..fe4ca8e9529 100644 --- a/app/assets/javascripts/pipelines/pipeline_test_details.js +++ b/app/assets/javascripts/pipelines/pipeline_test_details.js @@ -1,9 +1,10 @@ import Vue from 'vue'; +import Vuex from 'vuex'; import { parseBoolean } from '~/lib/utils/common_utils'; import Translate from '~/vue_shared/translate'; import TestReports from './components/test_reports/test_reports.vue'; -import createTestReportsStore from './stores/test_reports'; +Vue.use(Vuex); Vue.use(Translate); export const createTestDetails = (selector) => { @@ -16,11 +17,6 @@ export const createTestDetails = (selector) => { suiteEndpoint, artifactsExpiredImagePath, } = el?.dataset || {}; - const testReportsStore = createTestReportsStore({ - blobPath, - summaryEndpoint, - suiteEndpoint, - }); // eslint-disable-next-line no-new new Vue({ @@ -32,8 +28,11 @@ export const createTestDetails = (selector) => { emptyStateImagePath, artifactsExpiredImagePath, hasTestReport: parseBoolean(hasTestReport), + blobPath, + summaryEndpoint, + suiteEndpoint, }, - store: testReportsStore, + store: new Vuex.Store(), render(createElement) { return createElement('test-reports'); }, diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/pipelines/stores/test_reports/constants.js index 8eebfb6b208..83d14e1a109 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/constants.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/constants.js @@ -1 +1 @@ -export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired'; +export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts not found'; diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js index 64d4b8bafb1..f45a53f47b7 100644 --- a/app/assets/javascripts/pipelines/stores/test_reports/index.js +++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js @@ -1,16 +1,14 @@ -import Vue from 'vue'; -import Vuex from 'vuex'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import state from './state'; -Vue.use(Vuex); - -export default (initialState) => - new Vuex.Store({ +export default (initialState) => { + return { + namespaced: true, actions, getters, mutations, state: state(initialState), - }); + }; +}; diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js index 588d15495ab..83e00b80426 100644 --- a/app/assets/javascripts/pipelines/utils.js +++ b/app/assets/javascripts/pipelines/utils.js @@ -153,3 +153,24 @@ export const getPipelineDefaultTab = (url) => { return null; }; + +export const calculateJobStats = (jobs, sortField) => { + const jobNodes = [...jobs.nodes]; + + const sorted = jobNodes.sort((a, b) => { + return b[sortField] - a[sortField]; + }); + + return sorted[0]; +}; + +export const calculateSlowestFiveJobs = (jobs) => { + const jobNodes = [...jobs.nodes]; + const limit = 5; + + return jobNodes + .sort((a, b) => { + return b.duration - a.duration; + }) + .slice(0, limit); +}; |