summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/pipelines
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 15:40:28 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-20 15:40:28 +0000
commitb595cb0c1dec83de5bdee18284abe86614bed33b (patch)
tree8c3d4540f193c5ff98019352f554e921b3a41a72 /app/assets/javascripts/pipelines
parent2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff)
downloadgitlab-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')
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/performance_insights_modal.vue168
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue15
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue17
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue10
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/graphql/queries/get_performance_insights.query.graphql28
-rw-r--r--app/assets/javascripts/pipelines/pipeline_tabs.js3
-rw-r--r--app/assets/javascripts/pipelines/pipeline_test_details.js13
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js12
-rw-r--r--app/assets/javascripts/pipelines/utils.js21
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);
+};