Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/pipelines/components')
-rw-r--r--app/assets/javascripts/pipelines/components/dag/constants.js9
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue249
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_annotations.vue73
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue329
-rw-r--r--app/assets/javascripts/pipelines/components/dag/drawing_utils.js134
-rw-r--r--app/assets/javascripts/pipelines/components/dag/interactions.js154
-rw-r--r--app/assets/javascripts/pipelines/components/graph/constants.js26
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue261
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue345
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue176
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue110
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue396
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue304
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue247
-rw-r--r--app/assets/javascripts/pipelines/components/graph/perf_utils.js50
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue196
-rw-r--r--app/assets/javascripts/pipelines/components/graph/utils.js116
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/api.js13
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js106
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue161
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue75
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue29
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue65
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue125
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue133
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue136
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue38
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js182
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_details_header.vue631
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue73
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue174
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue22
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js14
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue13
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue168
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue98
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue176
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue132
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue150
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue84
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue63
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_tabs.vue138
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue53
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue106
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue220
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue79
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue165
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue179
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue121
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js19
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue69
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue170
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue172
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue113
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue104
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue37
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue239
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue449
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue72
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue130
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue159
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue50
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue240
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue61
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js52
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue82
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue47
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue104
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue66
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue113
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/empty_state.vue60
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue145
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue91
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue206
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary.vue117
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue144
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js73
78 files changed, 0 insertions, 10488 deletions
diff --git a/app/assets/javascripts/pipelines/components/dag/constants.js b/app/assets/javascripts/pipelines/components/dag/constants.js
deleted file mode 100644
index cd89055737f..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/constants.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* Interaction handles */
-export const IS_HIGHLIGHTED = 'dag-highlighted';
-export const LINK_SELECTOR = 'dag-link';
-export const NODE_SELECTOR = 'dag-node';
-
-/* Annotation types */
-export const ADD_NOTE = 'add';
-export const REMOVE_NOTE = 'remove';
-export const REPLACE_NOTES = 'replace';
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
deleted file mode 100644
index afb5aa05098..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ /dev/null
@@ -1,249 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlAlert, GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
-import { isEmpty } from 'lodash';
-import { fetchPolicies } from '~/lib/graphql';
-import { __ } from '~/locale';
-import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from '../../constants';
-import getDagVisData from '../../graphql/queries/get_dag_vis_data.query.graphql';
-import { parseData } from '../parsing_utils';
-import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import DagAnnotations from './dag_annotations.vue';
-import DagGraph from './dag_graph.vue';
-
-export default {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Dag',
- components: {
- DagAnnotations,
- DagGraph,
- GlAlert,
- GlButton,
- GlEmptyState,
- GlLink,
- GlSprintf,
- },
- inject: {
- aboutDagDocPath: {
- default: null,
- },
- dagDocPath: {
- default: null,
- },
- emptyDagSvgPath: {
- default: '',
- },
- pipelineIid: {
- default: '',
- },
- pipelineProjectPath: {
- default: '',
- },
- },
- apollo: {
- graphData: {
- fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
- query: getDagVisData,
- variables() {
- return {
- projectPath: this.pipelineProjectPath,
- iid: this.pipelineIid,
- };
- },
- update(data) {
- if (!data?.project?.pipeline) {
- return this.graphData;
- }
-
- const {
- stages: { nodes: stages },
- } = data.project.pipeline;
-
- const unwrappedGroups = stages
- .map(({ name, groups: { nodes: groups } }) => {
- return groups.map((group) => {
- return { category: name, ...group };
- });
- })
- .flat(2);
-
- const nodes = unwrappedGroups.map((group) => {
- const jobs = group.jobs.nodes.map(({ name, needs }) => {
- return { name, needs: needs.nodes.map((need) => need.name) };
- });
-
- return { ...group, jobs };
- });
-
- return nodes;
- },
- error() {
- this.reportFailure(LOAD_FAILURE);
- },
- },
- },
- data() {
- return {
- annotationsMap: {},
- failureType: null,
- graphData: null,
- showFailureAlert: false,
- hasNoDependentJobs: false,
- };
- },
- errorTexts: {
- [LOAD_FAILURE]: __('We are currently unable to fetch data for this graph.'),
- [PARSE_FAILURE]: __('There was an error parsing the data for this graph.'),
- [UNSUPPORTED_DATA]: __('DAG visualization requires at least 3 dependent jobs.'),
- [DEFAULT]: __('An unknown error occurred while loading this graph.'),
- },
- emptyStateTexts: {
- title: __('Speed up your pipelines with Needs relationships'),
- firstDescription: __(
- 'Using the %{codeStart}needs%{codeEnd} keyword makes jobs run before their stage is reached. Jobs run as soon as their %{codeStart}needs%{codeEnd} relationships are met, which speeds up your pipelines.',
- ),
- secondDescription: __(
- "If you add %{codeStart}needs%{codeEnd} to jobs in your pipeline you'll be able to view the %{codeStart}needs%{codeEnd} relationships between jobs in this tab as a %{linkStart}Directed Acyclic Graph (DAG)%{linkEnd}.",
- ),
- button: __('Learn more about Needs relationships'),
- },
- computed: {
- failure() {
- switch (this.failureType) {
- case LOAD_FAILURE:
- return {
- text: this.$options.errorTexts[LOAD_FAILURE],
- variant: 'danger',
- };
- case PARSE_FAILURE:
- return {
- text: this.$options.errorTexts[PARSE_FAILURE],
- variant: 'danger',
- };
- case UNSUPPORTED_DATA:
- return {
- text: this.$options.errorTexts[UNSUPPORTED_DATA],
- variant: 'info',
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- };
- }
- },
- processedData() {
- return this.processGraphData(this.graphData);
- },
- shouldDisplayAnnotations() {
- return !isEmpty(this.annotationsMap);
- },
- shouldDisplayGraph() {
- return Boolean(!this.showFailureAlert && !this.hasNoDependentJobs && this.graphData);
- },
- },
- methods: {
- addAnnotationToMap({ uid, source, target }) {
- this.$set(this.annotationsMap, uid, { source, target });
- },
- processGraphData(data) {
- let parsed;
-
- try {
- parsed = parseData(data);
- } catch {
- this.reportFailure(PARSE_FAILURE);
- return {};
- }
-
- if (parsed.links.length === 1) {
- this.reportFailure(UNSUPPORTED_DATA);
- return {};
- }
-
- // If there are no links, we don't report failure
- // as it simply means the user does not use job dependencies
- if (parsed.links.length === 0) {
- this.hasNoDependentJobs = true;
- return {};
- }
-
- return parsed;
- },
- hideAlert() {
- this.showFailureAlert = false;
- },
- removeAnnotationFromMap({ uid }) {
- this.$delete(this.annotationsMap, uid);
- },
- reportFailure(type) {
- this.showFailureAlert = true;
- this.failureType = type;
- },
- updateAnnotation({ type, data }) {
- switch (type) {
- case ADD_NOTE:
- this.addAnnotationToMap(data);
- break;
- case REMOVE_NOTE:
- this.removeAnnotationFromMap(data);
- break;
- case REPLACE_NOTES:
- this.annotationsMap = data;
- break;
- default:
- break;
- }
- },
- },
-};
-</script>
-<template>
- <div>
- <gl-alert v-if="showFailureAlert" :variant="failure.variant" @dismiss="hideAlert">
- {{ failure.text }}
- </gl-alert>
-
- <div class="gl-relative">
- <dag-annotations v-if="shouldDisplayAnnotations" :annotations="annotationsMap" />
- <dag-graph
- v-if="shouldDisplayGraph"
- :graph-data="processedData"
- @onFailure="reportFailure"
- @update-annotation="updateAnnotation"
- />
- <gl-empty-state
- v-else-if="hasNoDependentJobs"
- :svg-path="emptyDagSvgPath"
- :title="$options.emptyStateTexts.title"
- >
- <template #description>
- <div class="gl-text-left">
- <p>
- <gl-sprintf :message="$options.emptyStateTexts.firstDescription">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
- <p>
- <gl-sprintf :message="$options.emptyStateTexts.secondDescription">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #link="{ content }">
- <gl-link :href="aboutDagDocPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
- </template>
- <template v-if="dagDocPath" #actions>
- <gl-button :href="dagDocPath" target="_blank" variant="confirm">
- {{ $options.emptyStateTexts.button }}
- </gl-button>
- </template>
- </gl-empty-state>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue b/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
deleted file mode 100644
index a1500166cdc..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/dag_annotations.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- name: 'DagAnnotations',
- components: {
- GlButton,
- },
- props: {
- annotations: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- showList: true,
- };
- },
- computed: {
- linkText() {
- return this.showList ? __('Hide list') : __('Show list');
- },
- shouldShowLink() {
- return Object.keys(this.annotations).length > 1;
- },
- wrapperClasses() {
- return [
- 'gl-display-flex',
- 'gl-flex-direction-column',
- 'gl-fixed',
- 'gl-right-1',
- 'gl-top-66vh',
- 'gl-w-max-content',
- 'gl-px-5',
- 'gl-py-4',
- 'gl-rounded-base',
- 'gl-bg-white',
- ].join(' ');
- },
- },
- methods: {
- toggleList() {
- this.showList = !this.showList;
- },
- },
-};
-</script>
-<template>
- <div :class="wrapperClasses">
- <div v-if="showList">
- <div
- v-for="note in annotations"
- :key="note.uid"
- class="gl-display-flex gl-align-items-center"
- >
- <div
- data-testid="dag-color-block"
- class="gl-w-6 gl-h-5"
- :style="{
- background: `linear-gradient(0.25turn, ${note.source.color} 40%, ${note.target.color} 60%)`,
- }"
- ></div>
- <div data-testid="dag-note-text" class="gl-px-2 gl-font-base gl-align-items-center">
- {{ note.source.name }} → {{ note.target.name }}
- </div>
- </div>
- </div>
-
- <gl-button v-if="shouldShowLink" variant="link" @click="toggleList">{{ linkText }}</gl-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
deleted file mode 100644
index 7646c11773c..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ /dev/null
@@ -1,329 +0,0 @@
-<script>
-import * as d3 from 'd3';
-import { uniqueId } from 'lodash';
-import { PARSE_FAILURE } from '../../constants';
-import { getMaxNodes, removeOrphanNodes } from '../parsing_utils';
-import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
-import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
-import {
- currentIsLive,
- getLiveLinksAsDict,
- highlightLinks,
- restoreLinks,
- toggleLinkHighlight,
- togglePathHighlights,
-} from './interactions';
-
-export default {
- viewOptions: {
- baseHeight: 300,
- baseWidth: 1000,
- minNodeHeight: 60,
- nodeWidth: 16,
- nodePadding: 25,
- paddingForLabels: 100,
- labelMargin: 8,
-
- baseOpacity: 0.8,
- containerClasses: ['dag-graph-container', 'gl-display-flex', 'gl-flex-direction-column'].join(
- ' ',
- ),
- hoverFadeClasses: [
- 'gl-cursor-pointer',
- 'gl-transition-duration-slow',
- 'gl-transition-timing-function-ease',
- ].join(' '),
- },
- gitLabColorRotation: [
- '#e17223',
- '#83ab4a',
- '#5772ff',
- '#b24800',
- '#25d2d2',
- '#006887',
- '#487900',
- '#d84280',
- '#3547de',
- '#6f3500',
- '#006887',
- '#275600',
- '#b31756',
- ],
- props: {
- graphData: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- color: () => {},
- height: 0,
- width: 0,
- };
- },
- mounted() {
- let countedAndTransformed;
-
- try {
- countedAndTransformed = this.transformData(this.graphData);
- } catch {
- this.$emit('on-failure', PARSE_FAILURE);
- return;
- }
-
- this.drawGraph(countedAndTransformed);
- },
- methods: {
- addSvg() {
- return d3
- .select('.dag-graph-container')
- .append('svg')
- .attr('viewBox', [0, 0, this.width, this.height])
- .attr('width', this.width)
- .attr('height', this.height);
- },
-
- appendLinks(link) {
- return (
- link
- .append('path')
- .attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth))
- .attr('stroke', ({ gradId }) => `url(#${gradId})`)
- .style('stroke-linejoin', 'round')
- // minus two to account for the rounded nodes
- .attr('stroke-width', ({ width }) => Math.max(1, width - 2))
- .attr('clip-path', ({ clipId }) => `url(#${clipId})`)
- );
- },
-
- appendLinkInteractions(link) {
- const { baseOpacity } = this.$options.viewOptions;
- return link
- .on('mouseover', (d, idx, collection) => {
- if (currentIsLive(idx, collection)) {
- return;
- }
- this.$emit('update-annotation', { type: ADD_NOTE, data: d });
- highlightLinks(d, idx, collection);
- })
- .on('mouseout', (d, idx, collection) => {
- if (currentIsLive(idx, collection)) {
- return;
- }
- this.$emit('update-annotation', { type: REMOVE_NOTE, data: d });
- restoreLinks(baseOpacity);
- })
- .on('click', (d, idx, collection) => {
- toggleLinkHighlight(baseOpacity, d, idx, collection);
- this.$emit('update-annotation', { type: REPLACE_NOTES, data: getLiveLinksAsDict() });
- });
- },
-
- appendNodeInteractions(node) {
- return node.on('click', (d, idx, collection) => {
- togglePathHighlights(this.$options.viewOptions.baseOpacity, d, idx, collection);
- this.$emit('update-annotation', { type: REPLACE_NOTES, data: getLiveLinksAsDict() });
- });
- },
-
- appendLabelAsForeignObject(d, i, n) {
- const currentNode = n[i];
- const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, {
- ...this.$options.viewOptions,
- width: this.width,
- });
-
- const labelClasses = [
- 'gl-display-flex',
- 'gl-pointer-events-none',
- 'gl-flex-direction-column',
- 'gl-justify-content-center',
- 'gl-overflow-wrap-break',
- ].join(' ');
-
- return (
- d3
- .select(currentNode)
- .attr('requiredFeatures', 'http://www.w3.org/TR/SVG11/feature#Extensibility')
- .attr('height', height)
- /*
- items with a 'max-content' width will have a wrapperWidth for the foreignObject
- */
- .attr('width', wrapperWidth || width)
- .attr('x', x)
- .attr('y', y)
- .classed('gl-overflow-visible', true)
- .append('xhtml:div')
- .classed(labelClasses, true)
- .style('height', height)
- .style('width', width)
- .style('text-align', textAlign)
- .text(({ name }) => name)
- );
- },
-
- createAndAssignId(datum, field, modifier = '') {
- const id = uniqueId(modifier);
- /* eslint-disable-next-line no-param-reassign */
- datum[field] = id;
- return id;
- },
-
- createClip(link) {
- return link
- .append('clipPath')
- .attr('id', (d) => {
- return this.createAndAssignId(d, 'clipId', 'dag-clip');
- })
- .append('path')
- .attr('d', calculateClip);
- },
-
- createGradient(link) {
- const gradient = link
- .append('linearGradient')
- .attr('id', (d) => {
- return this.createAndAssignId(d, 'gradId', 'dag-grad');
- })
- .attr('gradientUnits', 'userSpaceOnUse')
- .attr('x1', ({ source }) => source.x1)
- .attr('x2', ({ target }) => target.x0);
-
- gradient
- .append('stop')
- .attr('offset', '0%')
- .attr('stop-color', ({ source }) => this.color(source));
-
- gradient
- .append('stop')
- .attr('offset', '100%')
- .attr('stop-color', ({ target }) => this.color(target));
- },
-
- createLinks(svg, linksData) {
- const links = this.generateLinks(svg, linksData);
- this.createGradient(links);
- this.createClip(links);
- this.appendLinks(links);
- this.appendLinkInteractions(links);
- },
-
- createNodes(svg, nodeData) {
- const nodes = this.generateNodes(svg, nodeData);
- this.labelNodes(svg, nodeData);
- this.appendNodeInteractions(nodes);
- },
-
- drawGraph({ maxNodesPerLayer, linksAndNodes }) {
- const {
- baseWidth,
- baseHeight,
- minNodeHeight,
- nodeWidth,
- nodePadding,
- paddingForLabels,
- } = this.$options.viewOptions;
-
- this.width = baseWidth;
- this.height = baseHeight + maxNodesPerLayer * minNodeHeight;
- this.color = this.initColors();
-
- const { links, nodes } = createSankey({
- width: this.width,
- height: this.height,
- nodeWidth,
- nodePadding,
- paddingForLabels,
- })(linksAndNodes);
-
- const svg = this.addSvg();
- this.createLinks(svg, links);
- this.createNodes(svg, nodes);
- },
-
- generateLinks(svg, linksData) {
- return svg
- .append('g')
- .attr('fill', 'none')
- .attr('stroke-opacity', this.$options.viewOptions.baseOpacity)
- .selectAll(`.${LINK_SELECTOR}`)
- .data(linksData)
- .enter()
- .append('g')
- .attr('id', (d) => {
- return this.createAndAssignId(d, 'uid', LINK_SELECTOR);
- })
- .classed(
- `${LINK_SELECTOR} gl-transition-property-stroke-opacity ${this.$options.viewOptions.hoverFadeClasses}`,
- true,
- );
- },
-
- generateNodes(svg, nodeData) {
- const { nodeWidth } = this.$options.viewOptions;
-
- return svg
- .append('g')
- .selectAll(`.${NODE_SELECTOR}`)
- .data(nodeData)
- .enter()
- .append('line')
- .classed(
- `${NODE_SELECTOR} gl-transition-property-stroke ${this.$options.viewOptions.hoverFadeClasses}`,
- true,
- )
- .attr('id', (d) => {
- return this.createAndAssignId(d, 'uid', NODE_SELECTOR);
- })
- .attr('stroke', (d) => {
- const color = this.color(d);
- /* eslint-disable-next-line no-param-reassign */
- d.color = color;
- return color;
- })
- .attr('stroke-width', nodeWidth)
- .attr('stroke-linecap', 'round')
- .attr('x1', (d) => Math.floor((d.x1 + d.x0) / 2))
- .attr('x2', (d) => Math.floor((d.x1 + d.x0) / 2))
- .attr('y1', (d) => d.y0 + 4)
- .attr('y2', (d) => d.y1 - 4);
- },
-
- initColors() {
- const colorFn = d3.scaleOrdinal(this.$options.gitLabColorRotation);
- return ({ name }) => colorFn(name);
- },
-
- labelNodes(svg, nodeData) {
- return svg
- .append('g')
- .classed('gl-font-sm', true)
- .selectAll('text')
- .data(nodeData)
- .enter()
- .append('foreignObject')
- .each(this.appendLabelAsForeignObject);
- },
-
- transformData(parsed) {
- const baseLayout = createSankey()(parsed);
- const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
- const maxNodesPerLayer = getMaxNodes(cleanedNodes);
-
- return {
- maxNodesPerLayer,
- linksAndNodes: {
- links: parsed.links,
- nodes: cleanedNodes,
- },
- };
- },
- },
-};
-</script>
-<template>
- <div :class="$options.viewOptions.containerClasses" data-testid="dag-graph-container">
- <!-- graph goes here -->
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
deleted file mode 100644
index 3cd09d57ffb..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import * as d3 from 'd3';
-import { sankey, sankeyLeft } from 'd3-sankey';
-
-export const calculateClip = ({ y0, y1, source, target, width }) => {
- /*
- Because large link values can overrun their box, we create a clip path
- to trim off the excess in charts that have few nodes per column and are
- therefore tall.
-
- The box is created by
- M: moving to outside midpoint of the source node
- V: drawing a vertical line to maximum of the bottom link edge or
- the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path)
- H: drawing a horizontal line to the outside edge of the destination node
- V: drawing a vertical line back up to the minimum of the top link edge or
- the highest edge of the node (can be d.y0 or d.y1 depending on the link's path)
- H: drawing a horizontal line back to the outside edge of the source node
- Z: closing the path, back to the start point
- */
-
- const bottomLinkEdge = Math.max(y1, y0) + width / 2;
- const topLinkEdge = Math.min(y0, y1) - width / 2;
-
- /* eslint-disable @gitlab/require-i18n-strings */
- return `
- M${source.x0}, ${y1}
- V${Math.max(bottomLinkEdge, y0, y1)}
- H${target.x1}
- V${Math.min(topLinkEdge, y0, y1)}
- H${source.x0}
- Z
- `;
- /* eslint-enable @gitlab/require-i18n-strings */
-};
-
-export const createLinkPath = ({ y0, y1, source, target, width }, idx, nodeWidth) => {
- /*
- Creates a series of staggered midpoints for the link paths, so they
- don't run along one channel and can be distinguished.
-
- First, get a point staggered by index and link width, modulated by the link box
- to find a point roughly between the nodes.
-
- Then offset it by nodeWidth, so it doesn't run under any nodes at the left.
-
- Determine where it would overlap at the right.
-
- Finally, select the leftmost of these options:
- - offset from the source node based on index + fudge;
- - a fuzzy offset from the right node, using Math.random adds a little blur
- - a hard offset from the end node, if random pushes it over
-
- Then draw a line from the start node to the bottom-most point of the midline
- up to the topmost point in that line and then to the middle of the end node
- */
-
- const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0));
- const xValMin = xValRaw + nodeWidth;
- const overlapPoint = source.x1 + (target.x0 - source.x1);
- const xValMax = overlapPoint - nodeWidth * 1.4;
-
- const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax);
-
- return d3.line()([
- [(source.x0 + source.x1) / 2, y0],
- [midPointX, y0],
- [midPointX, y1],
- [(target.x0 + target.x1) / 2, y1],
- ]);
-};
-
-/*
- createSankey calls the d3 layout to generate the relationships and positioning
- values for the nodes and links in the graph.
- */
-
-export const createSankey = ({
- width = 10,
- height = 10,
- nodeWidth = 10,
- nodePadding = 10,
- paddingForLabels = 1,
-} = {}) => {
- const sankeyGenerator = sankey()
- .nodeId(({ name }) => name)
- .nodeAlign(sankeyLeft)
- .nodeWidth(nodeWidth)
- .nodePadding(nodePadding)
- .extent([
- [paddingForLabels, paddingForLabels],
- [width - paddingForLabels, height - paddingForLabels],
- ]);
- return ({ nodes, links }) =>
- sankeyGenerator({
- nodes: nodes.map((d) => ({ ...d })),
- links: links.map((d) => ({ ...d })),
- });
-};
-
-export const labelPosition = ({ x0, x1, y0, y1 }, viewOptions) => {
- const { paddingForLabels, labelMargin, nodePadding, width } = viewOptions;
-
- const firstCol = x0 <= paddingForLabels;
- const lastCol = x1 >= width - paddingForLabels;
-
- if (firstCol) {
- return {
- x: 0 + labelMargin,
- y: y0,
- height: `${y1 - y0}px`,
- width: paddingForLabels - 2 * labelMargin,
- textAlign: 'right',
- };
- }
-
- if (lastCol) {
- return {
- x: width - paddingForLabels + labelMargin,
- y: y0,
- height: `${y1 - y0}px`,
- width: paddingForLabels - 2 * labelMargin,
- textAlign: 'left',
- };
- }
-
- return {
- x: (x1 + x0) / 2,
- y: y0 - nodePadding,
- height: `${nodePadding}px`,
- width: 'max-content',
- wrapperWidth: paddingForLabels - 2 * labelMargin,
- textAlign: x0 < width / 2 ? 'left' : 'right',
- };
-};
diff --git a/app/assets/javascripts/pipelines/components/dag/interactions.js b/app/assets/javascripts/pipelines/components/dag/interactions.js
deleted file mode 100644
index 69f36feeee4..00000000000
--- a/app/assets/javascripts/pipelines/components/dag/interactions.js
+++ /dev/null
@@ -1,154 +0,0 @@
-import * as d3 from 'd3';
-import { LINK_SELECTOR, NODE_SELECTOR, IS_HIGHLIGHTED } from './constants';
-
-export const highlightIn = 1;
-export const highlightOut = 0.2;
-
-const getCurrent = (idx, collection) => d3.select(collection[idx]);
-const getLiveLinks = () => d3.selectAll(`.${LINK_SELECTOR}.${IS_HIGHLIGHTED}`);
-const getOtherLinks = () => d3.selectAll(`.${LINK_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
-const getNodesNotLive = () => d3.selectAll(`.${NODE_SELECTOR}:not(.${IS_HIGHLIGHTED})`);
-
-export const getLiveLinksAsDict = () => {
- return Object.fromEntries(
- getLiveLinks()
- .data()
- .map((d) => [d.uid, d]),
- );
-};
-export const currentIsLive = (idx, collection) =>
- getCurrent(idx, collection).classed(IS_HIGHLIGHTED);
-
-const backgroundLinks = (selection) => selection.style('stroke-opacity', highlightOut);
-const backgroundNodes = (selection) => selection.attr('stroke', '#f2f2f2');
-const foregroundLinks = (selection) => selection.style('stroke-opacity', highlightIn);
-const foregroundNodes = (selection) => selection.attr('stroke', (d) => d.color);
-const renewLinks = (selection, baseOpacity) => selection.style('stroke-opacity', baseOpacity);
-const renewNodes = (selection) => selection.attr('stroke', (d) => d.color);
-
-export const getAllLinkAncestors = (node) => {
- if (node.targetLinks) {
- return node.targetLinks.flatMap((n) => {
- return [n, ...getAllLinkAncestors(n.source)];
- });
- }
-
- return [];
-};
-
-const getAllNodeAncestors = (node) => {
- let allNodes = [];
-
- if (node.targetLinks) {
- allNodes = node.targetLinks.flatMap((n) => {
- return getAllNodeAncestors(n.source);
- });
- }
-
- return [...allNodes, node.uid];
-};
-
-export const highlightLinks = (d, idx, collection) => {
- const currentLink = getCurrent(idx, collection);
- const currentSourceNode = d3.select(`#${d.source.uid}`);
- const currentTargetNode = d3.select(`#${d.target.uid}`);
-
- /* Higlight selected link, de-emphasize others */
- backgroundLinks(getOtherLinks());
- foregroundLinks(currentLink);
-
- /* Do the same to related nodes */
- backgroundNodes(getNodesNotLive());
- foregroundNodes(currentSourceNode);
- foregroundNodes(currentTargetNode);
-};
-
-const highlightPath = (parentLinks, parentNodes) => {
- /* de-emphasize everything else */
- backgroundLinks(getOtherLinks());
- backgroundNodes(getNodesNotLive());
-
- /* highlight correct links */
- parentLinks.forEach(({ uid }) => {
- foregroundLinks(d3.select(`#${uid}`)).classed(IS_HIGHLIGHTED, true);
- });
-
- /* highlight correct nodes */
- parentNodes.forEach((id) => {
- foregroundNodes(d3.select(`#${id}`)).classed(IS_HIGHLIGHTED, true);
- });
-};
-
-const restoreNodes = () => {
- /*
- When paths are unclicked, they can take down nodes that
- are still in use for other paths. This checks the live paths and
- rehighlights their nodes.
- */
-
- getLiveLinks().each((d) => {
- foregroundNodes(d3.select(`#${d.source.uid}`)).classed(IS_HIGHLIGHTED, true);
- foregroundNodes(d3.select(`#${d.target.uid}`)).classed(IS_HIGHLIGHTED, true);
- });
-};
-
-const restorePath = (parentLinks, parentNodes, baseOpacity) => {
- parentLinks.forEach(({ uid }) => {
- renewLinks(d3.select(`#${uid}`), baseOpacity).classed(IS_HIGHLIGHTED, false);
- });
-
- parentNodes.forEach((id) => {
- d3.select(`#${id}`).classed(IS_HIGHLIGHTED, false);
- });
-
- if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
- renewLinks(getOtherLinks(), baseOpacity);
- renewNodes(getNodesNotLive());
- return;
- }
-
- backgroundLinks(getOtherLinks());
- backgroundNodes(getNodesNotLive());
- restoreNodes();
-};
-
-export const restoreLinks = (baseOpacity) => {
- /*
- if there exist live links, reset to highlight out / pale
- otherwise, reset to base
- */
-
- if (d3.selectAll(`.${IS_HIGHLIGHTED}`).empty()) {
- renewLinks(d3.selectAll(`.${LINK_SELECTOR}`), baseOpacity);
- renewNodes(d3.selectAll(`.${NODE_SELECTOR}`));
- return;
- }
-
- backgroundLinks(getOtherLinks());
- backgroundNodes(getNodesNotLive());
-};
-
-export const toggleLinkHighlight = (baseOpacity, d, idx, collection) => {
- if (currentIsLive(idx, collection)) {
- restorePath([d], [d.source.uid, d.target.uid], baseOpacity);
- restoreNodes();
- return;
- }
-
- highlightPath([d], [d.source.uid, d.target.uid]);
-};
-
-export const togglePathHighlights = (baseOpacity, d, idx, collection) => {
- const parentLinks = getAllLinkAncestors(d);
- const parentNodes = getAllNodeAncestors(d);
- const currentNode = getCurrent(idx, collection);
-
- /* if this node is already live, make it unlive and reset its path */
- if (currentIsLive(idx, collection)) {
- currentNode.classed(IS_HIGHLIGHTED, false);
- restorePath(parentLinks, parentNodes, baseOpacity);
- return;
- }
-
- highlightPath(parentLinks, parentNodes);
-};
diff --git a/app/assets/javascripts/pipelines/components/graph/constants.js b/app/assets/javascripts/pipelines/components/graph/constants.js
deleted file mode 100644
index e650a48bc2a..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/constants.js
+++ /dev/null
@@ -1,26 +0,0 @@
-export const DOWNSTREAM = 'downstream';
-export const MAIN = 'main';
-export const UPSTREAM = 'upstream';
-
-/*
- this value is based on the gl-pipeline-job-width class
- plus some extra for the margins
-*/
-export const ONE_COL_WIDTH = 180;
-
-export const STAGE_VIEW = 'stage';
-export const LAYER_VIEW = 'layer';
-
-export const SKIP_RETRY_MODAL_KEY = 'skip_retry_modal';
-export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';
-
-export const SINGLE_JOB = 'single_job';
-export const JOB_DROPDOWN = 'job_dropdown';
-
-export const BUILD_KIND = 'BUILD';
-export const BRIDGE_KIND = 'BRIDGE';
-
-export const ACTION_FAILURE = 'action_failure';
-export const IID_FAILURE = 'missing_iid';
-
-export const RETRY_ACTION_TITLE = 'Retry';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
deleted file mode 100644
index 49df71beeec..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ /dev/null
@@ -1,261 +0,0 @@
-<script>
-import { reportToSentry } from '../../utils';
-import LinkedGraphWrapper from '../graph_shared/linked_graph_wrapper.vue';
-import LinksLayer from '../graph_shared/links_layer.vue';
-import {
- generateColumnsFromLayersListMemoized,
- keepLatestDownstreamPipelines,
-} from '../parsing_utils';
-import { DOWNSTREAM, MAIN, UPSTREAM, ONE_COL_WIDTH, STAGE_VIEW } from './constants';
-import LinkedPipelinesColumn from './linked_pipelines_column.vue';
-import StageColumnComponent from './stage_column_component.vue';
-import { validateConfigPaths } from './utils';
-
-export default {
- name: 'PipelineGraph',
- components: {
- LinksLayer,
- LinkedGraphWrapper,
- LinkedPipelinesColumn,
- StageColumnComponent,
- },
- props: {
- configPaths: {
- type: Object,
- required: true,
- validator: validateConfigPaths,
- },
- pipeline: {
- type: Object,
- required: true,
- },
- showLinks: {
- type: Boolean,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
- isLinkedPipeline: {
- type: Boolean,
- required: false,
- default: false,
- },
- computedPipelineInfo: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- skipRetryModal: {
- type: Boolean,
- required: false,
- default: false,
- },
- type: {
- type: String,
- required: false,
- default: MAIN,
- },
- },
- pipelineTypeConstants: {
- DOWNSTREAM,
- UPSTREAM,
- },
- CONTAINER_REF: 'PIPELINE_LINKS_CONTAINER_REF',
- BASE_CONTAINER_ID: 'pipeline-links-container',
- data() {
- return {
- hoveredJobName: '',
- hoveredSourceJobName: '',
- highlightedJobs: [],
- measurements: {
- width: 0,
- height: 0,
- },
- pipelineExpanded: {
- jobName: '',
- expanded: false,
- },
- };
- },
- computed: {
- containerId() {
- return `${this.$options.BASE_CONTAINER_ID}-${this.pipeline.id}`;
- },
- downstreamPipelines() {
- return this.hasDownstreamPipelines
- ? keepLatestDownstreamPipelines(this.pipeline.downstream)
- : [];
- },
- layout() {
- return this.isStageView
- ? this.pipeline.stages
- : generateColumnsFromLayersListMemoized(
- this.pipeline,
- this.computedPipelineInfo.pipelineLayers,
- );
- },
- hasDownstreamPipelines() {
- return Boolean(this.pipeline?.downstream?.length > 0);
- },
- hasUpstreamPipelines() {
- return Boolean(this.pipeline?.upstream?.length > 0);
- },
- isStageView() {
- return this.viewType === STAGE_VIEW;
- },
- linksData() {
- return this.computedPipelineInfo?.linksData ?? null;
- },
- metricsConfig() {
- return {
- path: this.configPaths.metricsPath,
- collectMetrics: true,
- };
- },
- showJobLinks() {
- return !this.isStageView && this.showLinks;
- },
- // The show downstream check prevents showing redundant linked columns
- showDownstreamPipelines() {
- return (
- this.hasDownstreamPipelines && this.type !== this.$options.pipelineTypeConstants.UPSTREAM
- );
- },
- // The show upstream check prevents showing redundant linked columns
- showUpstreamPipelines() {
- return (
- this.hasUpstreamPipelines && this.type !== this.$options.pipelineTypeConstants.DOWNSTREAM
- );
- },
- upstreamPipelines() {
- return this.hasUpstreamPipelines ? this.pipeline.upstream : [];
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
- },
- mounted() {
- this.getMeasurements();
- },
- methods: {
- getMeasurements() {
- this.measurements = {
- width: this.$refs[this.containerId].scrollWidth,
- height: this.$refs[this.containerId].scrollHeight,
- };
- },
- onError(payload) {
- this.$emit('error', payload);
- },
- setJob(jobName) {
- this.hoveredJobName = jobName;
- },
- setSourceJob(jobName) {
- this.hoveredSourceJobName = jobName;
- },
- slidePipelineContainer() {
- this.$refs.mainPipelineContainer.scrollBy({
- left: ONE_COL_WIDTH,
- top: 0,
- behavior: 'smooth',
- });
- },
- togglePipelineExpanded(jobName, expanded) {
- this.pipelineExpanded = {
- expanded,
- jobName: expanded ? jobName : '',
- };
- },
- updateHighlightedJobs(jobs) {
- this.highlightedJobs = jobs;
- },
- },
-};
-</script>
-<template>
- <div class="js-pipeline-graph">
- <div
- ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
- :class="{
- 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline,
- }"
- >
- <linked-graph-wrapper>
- <template #upstream>
- <linked-pipelines-column
- v-if="showUpstreamPipelines"
- :config-paths="configPaths"
- :linked-pipelines="upstreamPipelines"
- :column-title="__('Upstream')"
- :show-links="showJobLinks"
- :skip-retry-modal="skipRetryModal"
- :type="$options.pipelineTypeConstants.UPSTREAM"
- :view-type="viewType"
- @error="onError"
- @setSkipRetryModal="$emit('setSkipRetryModal')"
- />
- </template>
- <template #main>
- <div :id="containerId" :ref="containerId">
- <links-layer
- :pipeline-data="layout"
- :pipeline-id="pipeline.id"
- :container-id="containerId"
- :container-measurements="measurements"
- :highlighted-job="hoveredJobName"
- :links-data="linksData"
- :metrics-config="metricsConfig"
- :show-links="showJobLinks"
- :view-type="viewType"
- @error="onError"
- @highlightedJobsChange="updateHighlightedJobs"
- >
- <stage-column-component
- v-for="column in layout"
- :key="column.id || column.name"
- :name="column.name"
- :groups="column.groups"
- :action="column.status.action"
- :highlighted-jobs="highlightedJobs"
- :is-stage-view="isStageView"
- :job-hovered="hoveredJobName"
- :skip-retry-modal="skipRetryModal"
- :source-job-hovered="hoveredSourceJobName"
- :pipeline-expanded="pipelineExpanded"
- :pipeline-id="pipeline.id"
- :user-permissions="pipeline.userPermissions"
- @refreshPipelineGraph="$emit('refreshPipelineGraph')"
- @setSkipRetryModal="$emit('setSkipRetryModal')"
- @jobHover="setJob"
- @updateMeasurements="getMeasurements"
- />
- </links-layer>
- </div>
- </template>
- <template #downstream>
- <linked-pipelines-column
- v-if="showDownstreamPipelines"
- class="gl-mr-6"
- :config-paths="configPaths"
- :linked-pipelines="downstreamPipelines"
- :column-title="__('Downstream')"
- :skip-retry-modal="skipRetryModal"
- :show-links="showJobLinks"
- :type="$options.pipelineTypeConstants.DOWNSTREAM"
- :view-type="viewType"
- data-testid="downstream-pipelines"
- @downstreamHovered="setSourceJob"
- @pipelineExpandToggle="togglePipelineExpanded"
- @refreshPipelineGraph="$emit('refreshPipelineGraph')"
- @setSkipRetryModal="$emit('setSkipRetryModal')"
- @scrollContainer="slidePipelineContainer"
- @error="onError"
- />
- </template>
- </linked-graph-wrapper>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
deleted file mode 100644
index b2cef7c37b9..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ /dev/null
@@ -1,345 +0,0 @@
-<script>
-import { GlAlert, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
-import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
-import { __, s__ } from '~/locale';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import { DEFAULT, DRAW_FAILURE, LOAD_FAILURE } from '../../constants';
-import DismissPipelineGraphCallout from '../../graphql/mutations/dismiss_pipeline_notification.graphql';
-import getPipelineQuery from '../../graphql/queries/get_pipeline_header_data.query.graphql';
-import { reportToSentry, reportMessageToSentry } from '../../utils';
-import {
- ACTION_FAILURE,
- IID_FAILURE,
- LAYER_VIEW,
- SKIP_RETRY_MODAL_KEY,
- STAGE_VIEW,
- VIEW_TYPE_KEY,
-} from './constants';
-import PipelineGraph from './graph_component.vue';
-import GraphViewSelector from './graph_view_selector.vue';
-import {
- calculatePipelineLayersInfo,
- getQueryHeaders,
- serializeLoadErrors,
- toggleQueryPollingByVisibility,
- unwrapPipelineData,
-} from './utils';
-
-const featureName = 'pipeline_needs_hover_tip';
-const enumFeatureName = featureName.toUpperCase();
-
-export default {
- name: 'PipelineGraphWrapper',
- components: {
- GlAlert,
- GlLoadingIcon,
- GlSprintf,
- GraphViewSelector,
- LocalStorageSync,
- PipelineGraph,
- },
- inject: {
- graphqlResourceEtag: {
- default: '',
- },
- metricsPath: {
- default: '',
- },
- pipelineIid: {
- default: '',
- },
- pipelineProjectPath: {
- default: '',
- },
- },
- data() {
- return {
- alertType: null,
- callouts: [],
- computedPipelineInfo: null,
- currentViewType: STAGE_VIEW,
- canRefetchHeaderPipeline: false,
- pipeline: null,
- skipRetryModal: false,
- showAlert: false,
- showJobCountWarning: false,
- showLinks: false,
- };
- },
- errors: {
- [ACTION_FAILURE]: {
- text: __('An error occurred while performing this action.'),
- variant: 'danger',
- },
- [DRAW_FAILURE]: {
- text: __('An error occurred while drawing job relationship links.'),
- variant: 'danger',
- },
- [IID_FAILURE]: {
- text: __(
- 'The data in this pipeline is too old to be rendered as a graph. Please check the Jobs tab to access historical data.',
- ),
- variant: 'info',
- },
- [LOAD_FAILURE]: {
- text: __('Currently unable to fetch data for this pipeline.'),
- variant: 'danger',
- },
- [DEFAULT]: {
- text: __('An unknown error occurred while loading this graph.'),
- variant: 'danger',
- },
- },
- apollo: {
- callouts: {
- query: getUserCallouts,
- update(data) {
- return data?.currentUser?.callouts?.nodes.map((callout) => callout.featureName) || [];
- },
- error(err) {
- reportToSentry(
- this.$options.name,
- `type: callout_load_failure, info: ${serializeLoadErrors(err)}`,
- );
- },
- },
- headerPipeline: {
- query: getPipelineQuery,
- // this query is already being called in pipeline_details_header.vue, which shares the same cache as this component
- // the skip here is to prevent sending double network requests on page load
- skip() {
- return !this.canRefetchHeaderPipeline;
- },
- variables() {
- return {
- fullPath: this.pipelineProjectPath,
- iid: this.pipelineIid,
- };
- },
- update(data) {
- return data.project?.pipeline || {};
- },
- error() {
- this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
- },
- },
- pipeline: {
- context() {
- return getQueryHeaders(this.graphqlResourceEtag);
- },
- query: getPipelineDetails,
- pollInterval: 10000,
- variables() {
- return {
- projectPath: this.pipelineProjectPath,
- iid: this.pipelineIid,
- };
- },
- skip() {
- return !(this.pipelineProjectPath && this.pipelineIid);
- },
- update(data) {
- /*
- This check prevents the pipeline from being overwritten
- when a poll times out and the data returned is empty.
- This can be removed once the timeout behavior is updated.
- See: https://gitlab.com/gitlab-org/gitlab/-/issues/323213.
- */
-
- if (!data?.project?.pipeline) {
- return this.pipeline;
- }
-
- return unwrapPipelineData(this.pipelineProjectPath, JSON.parse(JSON.stringify(data)));
- },
- error(err) {
- this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
-
- reportMessageToSentry(
- this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
- {
- graphViewType: this.graphViewType,
- graphqlResourceEtag: this.graphqlResourceEtag,
- metricsPath: this.metricsPath,
- projectPath: this.pipelineProjectPath,
- pipelineIid: this.pipelineIid,
- },
- );
- },
- result({ data, error }) {
- const stages = data?.project?.pipeline?.stages?.nodes || [];
-
- this.showJobCountWarning = stages.some((stage) => {
- return stage.groups.nodes.length >= 100;
- });
- /*
- If there is a successful load after a failure, clear
- the failure notification to avoid confusion.
- */
- if (!error && this.alertType === LOAD_FAILURE) {
- this.hideAlert();
- }
- },
- },
- },
- computed: {
- alert() {
- const { errors } = this.$options;
-
- return {
- text: errors[this.alertType]?.text ?? errors[DEFAULT].text,
- variant: errors[this.alertType]?.variant ?? errors[DEFAULT].variant,
- };
- },
- configPaths() {
- return {
- graphqlResourceEtag: this.graphqlResourceEtag,
- metricsPath: this.metricsPath,
- };
- },
- graphViewType() {
- /* This prevents reading view type off the localStorage value if it does not apply. */
- return this.showGraphViewSelector ? this.currentViewType : STAGE_VIEW;
- },
- hoverTipPreviouslyDismissed() {
- return this.callouts.includes(enumFeatureName);
- },
- showLoadingIcon() {
- /*
- Shows the icon only when the graph is empty, not when it is is
- being refetched, for instance, on action completion
- */
- return this.$apollo.queries.pipeline.loading && !this.pipeline;
- },
- showGraphViewSelector() {
- return this.pipeline?.usesNeeds;
- },
- },
- mounted() {
- if (!this.pipelineIid) {
- this.reportFailure({ type: IID_FAILURE, skipSentry: true });
- }
- toggleQueryPollingByVisibility(this.$apollo.queries.pipeline);
- this.skipRetryModal = Boolean(JSON.parse(localStorage.getItem(SKIP_RETRY_MODAL_KEY)));
- },
- errorCaptured(err, _vm, info) {
- reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
- },
- methods: {
- getPipelineInfo() {
- if (this.currentViewType === LAYER_VIEW && !this.computedPipelineInfo) {
- this.computedPipelineInfo = calculatePipelineLayersInfo(
- this.pipeline,
- this.$options.name,
- this.metricsPath,
- );
- }
-
- return this.computedPipelineInfo;
- },
- handleTipDismissal() {
- try {
- this.$apollo.mutate({
- mutation: DismissPipelineGraphCallout,
- variables: {
- featureName,
- },
- });
- } catch (err) {
- reportToSentry(this.$options.name, `type: callout_dismiss_failure, info: ${err}`);
- }
- },
- hideAlert() {
- this.showAlert = false;
- this.alertType = null;
- },
- refreshPipelineGraph() {
- this.$apollo.queries.pipeline.refetch();
-
- // this will update the status in header_component since they share the same cache
- this.canRefetchHeaderPipeline = true;
- this.$apollo.queries.headerPipeline.refetch();
- },
- // eslint-disable-next-line @gitlab/require-i18n-strings
- reportFailure({ type, err = 'No error string passed.', skipSentry = false }) {
- this.showAlert = true;
- this.alertType = type;
- if (!skipSentry) {
- reportToSentry(this.$options.name, `type: ${type}, info: ${err}`);
- }
- },
- updateShowLinksState(val) {
- this.showLinks = val;
- },
- setSkipRetryModal() {
- this.skipRetryModal = true;
- },
- updateViewType(type) {
- this.currentViewType = type;
- },
- },
- i18n: {
- jobLimitWarning: {
- title: s__('Pipeline|Only the first 100 jobs per stage are displayed'),
- desc: s__('Pipeline|To see the remaining jobs, go to the %{boldStart}Jobs%{boldEnd} tab.'),
- },
- },
- viewTypeKey: VIEW_TYPE_KEY,
-};
-</script>
-<template>
- <div>
- <gl-alert
- v-if="showAlert"
- :variant="alert.variant"
- data-testid="error-alert"
- @dismiss="hideAlert"
- >
- {{ alert.text }}
- </gl-alert>
- <gl-alert
- v-if="showJobCountWarning"
- variant="warning"
- :dismissible="false"
- :title="$options.i18n.jobLimitWarning.title"
- data-testid="job-count-warning"
- >
- <gl-sprintf :message="$options.i18n.jobLimitWarning.desc">
- <template #bold="{ content }">
- <b>{{ content }}</b>
- </template>
- </gl-sprintf>
- </gl-alert>
- <local-storage-sync
- :storage-key="$options.viewTypeKey"
- :value="currentViewType"
- as-string
- @input="updateViewType"
- >
- <graph-view-selector
- v-if="showGraphViewSelector"
- :type="graphViewType"
- :show-links="showLinks"
- :tip-previously-dismissed="hoverTipPreviouslyDismissed"
- @dismissHoverTip="handleTipDismissal"
- @updateViewType="updateViewType"
- @updateShowLinksState="updateShowLinksState"
- />
- </local-storage-sync>
- <gl-loading-icon v-if="showLoadingIcon" class="gl-mx-auto gl-my-4" size="lg" />
- <pipeline-graph
- v-if="pipeline"
- :config-paths="configPaths"
- :pipeline="pipeline"
- :computed-pipeline-info="getPipelineInfo()"
- :skip-retry-modal="skipRetryModal"
- :show-links="showLinks"
- :view-type="graphViewType"
- @error="reportFailure"
- @refreshPipelineGraph="refreshPipelineGraph"
- @setSkipRetryModal="setSkipRetryModal"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue b/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
deleted file mode 100644
index 73143c981ed..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
+++ /dev/null
@@ -1,176 +0,0 @@
-<script>
-import { GlAlert, GlButton, GlButtonGroup, GlLoadingIcon, GlToggle } from '@gitlab/ui';
-import { __, s__ } from '~/locale';
-import { STAGE_VIEW, LAYER_VIEW } from './constants';
-
-export default {
- name: 'GraphViewSelector',
-
- components: {
- GlAlert,
- GlButton,
- GlButtonGroup,
- GlLoadingIcon,
- GlToggle,
- },
-
- props: {
- showLinks: {
- type: Boolean,
- required: true,
- },
- tipPreviouslyDismissed: {
- type: Boolean,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- hoverTipDismissed: false,
- isToggleLoading: false,
- isSwitcherLoading: false,
- segmentSelectedType: this.type,
- showLinksActive: false,
- };
- },
- i18n: {
- hoverTipText: __('Tip: Hover over a job to see the jobs it depends on to run.'),
- linksLabelText: s__('GraphViewType|Show dependencies'),
- viewLabelText: __('Group jobs by'),
- },
- views: {
- [STAGE_VIEW]: {
- type: STAGE_VIEW,
- text: {
- primary: s__('GraphViewType|Stage'),
- },
- },
- [LAYER_VIEW]: {
- type: LAYER_VIEW,
- text: {
- primary: s__('GraphViewType|Job dependencies'),
- },
- },
- },
- computed: {
- showLinksToggle() {
- return this.segmentSelectedType === LAYER_VIEW;
- },
- showTip() {
- return (
- this.showLinksToggle &&
- this.showLinks &&
- this.showLinksActive &&
- !this.tipPreviouslyDismissed &&
- !this.hoverTipDismissed
- );
- },
- viewTypesList() {
- return Object.keys(this.$options.views).map((key) => {
- return {
- value: key,
- text: this.$options.views[key].text.primary,
- };
- });
- },
- },
- watch: {
- /*
- How does this reset the loading? As we note in the methods comment below,
- the loader is set to on before the update work is undertaken (in the parent).
- Once the work is complete, one of these values will change, since that's the
- point of the work. When that happens, the related value will update and we are done.
-
- The bonus for this approach is that it works the same whichever "direction"
- the work goes in.
- */
- showLinks() {
- this.isToggleLoading = false;
- },
- type() {
- this.isSwitcherLoading = false;
- },
- },
- methods: {
- dismissTip() {
- this.hoverTipDismissed = true;
- this.$emit('dismissHoverTip');
- },
- isCurrentType(type) {
- return this.segmentSelectedType === type;
- },
- /*
- In both toggle methods, we use setTimeout so that the loading indicator displays,
- then the work is done to update the DOM. The process is:
- → user clicks
- → call stack: set loading to true
- → render: the loading icon appears on the screen
- → callback queue: now do the work to calculate the new view / links
- (note: this work is done in the parent after the event is emitted)
-
- setTimeout is how we move the work to the callback queue.
- We can't use nextTick because that is called before the render loop.
-
- See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
- */
- setViewType(type) {
- if (!this.isCurrentType(type)) {
- this.isSwitcherLoading = true;
- this.segmentSelectedType = type;
- setTimeout(() => {
- this.$emit('updateViewType', type);
- });
- }
- },
- toggleShowLinksActive(val) {
- this.isToggleLoading = true;
- setTimeout(() => {
- this.$emit('updateShowLinksState', val);
- });
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="gl-relative gl-display-flex gl-align-items-center gl-w-max-content gl-my-4">
- <gl-loading-icon
- v-if="isSwitcherLoading"
- data-testid="switcher-loading-state"
- class="gl-absolute gl-w-full gl-bg-white gl-opacity-5 gl-z-index-2"
- size="lg"
- />
- <span class="gl-font-weight-bold">{{ $options.i18n.viewLabelText }}</span>
- <gl-button-group class="gl-mx-4">
- <gl-button
- v-for="viewType in viewTypesList"
- :key="viewType.value"
- :selected="isCurrentType(viewType.value)"
- @click="setViewType(viewType.value)"
- >
- {{ viewType.text }}
- </gl-button>
- </gl-button-group>
-
- <div v-if="showLinksToggle" class="gl-display-flex gl-align-items-center">
- <gl-toggle
- v-model="showLinksActive"
- data-testid="show-links-toggle"
- class="gl-mx-4"
- :label="$options.i18n.linksLabelText"
- :is-loading="isToggleLoading"
- label-position="left"
- @change="toggleShowLinksActive"
- />
- </div>
- </div>
- <gl-alert v-if="showTip" class="gl-my-5" variant="tip" @dismiss="dismissTip">
- {{ $options.i18n.hoverTipText }}
- </gl-alert>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue b/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
deleted file mode 100644
index d4852224df5..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue
+++ /dev/null
@@ -1,110 +0,0 @@
-<script>
-import { reportToSentry } from '../../utils';
-import { JOB_DROPDOWN, SINGLE_JOB } from './constants';
-import JobItem from './job_item.vue';
-
-/**
- * Renders the dropdown for the pipeline graph.
- *
- * The object provided as `group` corresponds to app/serializers/job_group_entity.rb.
- *
- */
-export default {
- components: {
- JobItem,
- },
- props: {
- group: {
- type: Object,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: false,
- default: -1,
- },
- cssClassJobName: {
- type: [String, Array],
- required: false,
- default: '',
- },
- stageName: {
- type: String,
- required: false,
- default: '',
- },
- },
- jobItemTypes: {
- jobDropdown: JOB_DROPDOWN,
- singleJob: SINGLE_JOB,
- },
- computed: {
- computedJobId() {
- return this.pipelineId > -1 ? `${this.group.name}-${this.pipelineId}` : '';
- },
- tooltipText() {
- const { name, status } = this.group;
- return `${name} - ${status.label}`;
- },
- jobGroupClasses() {
- return [this.cssClassJobName, `job-${this.group.status.group}`];
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('job_group_dropdown', `error: ${err}, info: ${info}`);
- },
- methods: {
- pipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
-};
-</script>
-<template>
- <!-- eslint-disable @gitlab/vue-no-data-toggle -->
- <div
- :id="computedJobId"
- class="ci-job-dropdown-container dropdown dropright"
- data-qa-selector="job_dropdown_container"
- >
- <button
- type="button"
- data-toggle="dropdown"
- data-display="static"
- :class="jobGroupClasses"
- class="dropdown-menu-toggle gl-pipeline-job-width! gl-pr-4!"
- >
- <div class="gl-display-flex gl-align-items-stretch gl-justify-content-space-between">
- <job-item
- :type="$options.jobItemTypes.jobDropdown"
- :group-tooltip="tooltipText"
- :job="group"
- :stage-name="stageName"
- />
-
- <div class="gl-font-weight-100 gl-font-size-lg gl-ml-n4 gl-align-self-center">
- {{ group.size }}
- </div>
- </div>
- </button>
-
- <ul
- class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown"
- data-qa-selector="jobs_dropdown_menu"
- >
- <li class="scrollable-menu">
- <ul>
- <li v-for="job in group.jobs" :key="job.id">
- <job-item
- :dropdown-length="group.size"
- :job="job"
- :type="$options.jobItemTypes.singleJob"
- css-class-job-name="pipeline-job-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- />
- </li>
- </ul>
- </li>
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
deleted file mode 100644
index 22895a31082..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ /dev/null
@@ -1,396 +0,0 @@
-<script>
-import { GlBadge, GlForm, GlFormCheckbox, GlLink, GlModal, GlTooltipDirective } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from './constants';
-
-/**
- * Renders the badge for the pipeline graph and the job's dropdown.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "tooltip": "passed",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
-
-export default {
- confirmationModalDocLink: helpPagePath('/ci/pipelines/downstream_pipelines'),
- i18n: {
- bridgeBadgeText: __('Trigger job'),
- bridgeRetryText: s__(
- 'PipelineGraph|Downstream pipeline might not display in the graph while the new downstream pipeline is being created.',
- ),
- unauthorizedTooltip: __('You are not authorized to run this manual job'),
- confirmationModal: {
- title: s__('PipelineGraph|Are you sure you want to retry %{jobName}?'),
- description: s__(
- 'PipelineGraph|Retrying a trigger job will create a new downstream pipeline.',
- ),
- linkText: s__('PipelineGraph|What is a downstream pipeline?'),
- footer: __("Don't show this again"),
- actionPrimary: { text: __('Retry') },
- actionCancel: { text: __('Cancel') },
- },
- runAgainTooltipText: __('Run again'),
- },
- hoverClass: 'gl-shadow-x0-y0-b3-s1-blue-500',
- components: {
- ActionComponent,
- CiIcon,
- GlBadge,
- GlForm,
- GlFormCheckbox,
- GlLink,
- GlModal,
- JobNameComponent,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [delayedJobMixin],
- props: {
- job: {
- type: Object,
- required: true,
- },
- cssClassJobName: {
- type: [String, Array, Object],
- required: false,
- default: '',
- },
- dropdownLength: {
- type: Number,
- required: false,
- default: Infinity,
- },
- groupTooltip: {
- type: String,
- required: false,
- default: '',
- },
- jobHovered: {
- type: String,
- required: false,
- default: '',
- },
- pipelineExpanded: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- pipelineId: {
- type: Number,
- required: false,
- default: -1,
- },
- skipRetryModal: {
- type: Boolean,
- required: false,
- default: false,
- },
- sourceJobHovered: {
- type: String,
- required: false,
- default: '',
- },
- stageName: {
- type: String,
- required: false,
- default: '',
- },
- type: {
- type: String,
- required: false,
- default: SINGLE_JOB,
- },
- },
- data() {
- return {
- currentSkipModalValue: this.skipRetryModal,
- showConfirmationModal: false,
- shouldTriggerActionClick: false,
- };
- },
- computed: {
- boundary() {
- return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
- },
- computedJobId() {
- return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
- },
- detailsPath() {
- return this.status.detailsPath;
- },
- hasDetails() {
- return this.status.hasDetails;
- },
- hasRetryAction() {
- return Boolean(this.job?.status?.action?.title === RETRY_ACTION_TITLE);
- },
- isRetryableBridge() {
- return this.isBridge && this.hasRetryAction;
- },
- isSingleItem() {
- return this.type === SINGLE_JOB;
- },
- isBridge() {
- return this.kind === BRIDGE_KIND;
- },
- kind() {
- return this.job?.kind || '';
- },
- nameComponent() {
- return this.hasDetails ? 'gl-link' : 'div';
- },
- retryTriggerJobWarningText() {
- return sprintf(this.$options.i18n.confirmationModal.title, {
- jobName: this.job.name,
- });
- },
- showStageName() {
- return Boolean(this.stageName);
- },
- status() {
- return this.job && this.job.status ? this.job.status : {};
- },
- testId() {
- return this.hasDetails ? 'job-with-link' : 'job-without-link';
- },
- tooltipText() {
- if (this.groupTooltip) {
- return this.groupTooltip;
- }
-
- const textBuilder = [];
- const { name: jobName } = this.job;
-
- if (jobName) {
- textBuilder.push(jobName);
- }
-
- const { tooltip: statusTooltip } = this.status;
- if (jobName && statusTooltip) {
- textBuilder.push('-');
- }
-
- if (statusTooltip) {
- if (this.isDelayedJob) {
- textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime }));
- } else {
- textBuilder.push(statusTooltip);
- }
- }
-
- return textBuilder.join(' ');
- },
- /**
- * Verifies if the provided job has an action path
- *
- * @return {Boolean}
- */
- hasAction() {
- return this.job.status && this.job.status.action && this.job.status.action.path;
- },
- hasUnauthorizedManualAction() {
- return (
- !this.hasAction &&
- this.job.status?.group === 'manual' &&
- this.job.status?.label?.includes('(not allowed)')
- );
- },
- unauthorizedManualActionIcon() {
- /*
- The action object is not available when the user cannot run the action.
- So we can show the correct icon, extract the action name from the label instead:
- "manual play action (not allowed)" or "manual stop action (not allowed)"
- */
- return this.job.status?.label?.split(' ')[1];
- },
- relatedDownstreamHovered() {
- return this.job.name === this.sourceJobHovered;
- },
- relatedDownstreamExpanded() {
- return this.job.name === this.pipelineExpanded.jobName && this.pipelineExpanded.expanded;
- },
- jobClasses() {
- return [
- {
- [this.$options.hoverClass]:
- this.relatedDownstreamHovered || this.relatedDownstreamExpanded,
- },
- { 'gl-rounded-lg': this.isBridge },
- this.cssClassJobName,
- {
- [`job-${this.status.group}`]: this.isSingleItem,
- },
- ];
- },
- withConfirmationModal() {
- return this.isRetryableBridge && !this.skipRetryModal;
- },
- jobActionTooltipText() {
- const { group } = this.status;
- const { title, icon } = this.status.action;
-
- return icon === 'retry' && group === 'success'
- ? this.$options.i18n.runAgainTooltipText
- : title;
- },
- },
- watch: {
- skipRetryModal(val) {
- this.currentSkipModalValue = val;
- this.shouldTriggerActionClick = false;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('job_item', `error: ${err}, info: ${info}`);
- },
- methods: {
- handleConfirmationModalPreferences() {
- if (this.currentSkipModalValue) {
- this.$emit('setSkipRetryModal');
- localStorage.setItem(SKIP_RETRY_MODAL_KEY, String(this.currentSkipModalValue));
- }
- },
- hideTooltips() {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- jobItemClick(evt) {
- if (this.isSingleItem) {
- /*
- This is so the jobDropdown still toggles. Issue to refactor:
- https://gitlab.com/gitlab-org/gitlab/-/issues/267117
- */
- evt.stopPropagation();
- }
-
- this.hideTooltips();
- },
- pipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
-
- if (this.isBridge) {
- this.$toast.show(this.$options.i18n.bridgeRetryText);
- }
- },
- executePendingAction() {
- this.shouldTriggerActionClick = true;
- },
- showActionConfirmationModal() {
- this.showConfirmationModal = true;
- },
- toggleSkipRetryModalCheckbox() {
- this.currentSkipModalValue = !this.currentSkipModalValue;
- },
- },
-};
-</script>
-<template>
- <div
- :id="computedJobId"
- class="ci-job-component gl-display-flex gl-justify-content-space-between gl-pipeline-job-width"
- data-qa-selector="job_item_container"
- >
- <component
- :is="nameComponent"
- v-gl-tooltip="{
- boundary: 'viewport',
- placement: 'bottom',
- customClass: 'gl-pointer-events-none',
- }"
- :title="tooltipText"
- :class="jobClasses"
- :href="detailsPath"
- class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none gl-w-full"
- :data-testid="testId"
- data-qa-selector="job_link"
- @click="jobItemClick"
- @mouseout="hideTooltips"
- >
- <div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
- <div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
- <div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
- <div
- v-if="showStageName"
- data-testid="stage-name-in-job"
- class="gl-text-truncate gl-pr-9 gl-font-sm gl-text-gray-500 gl-line-height-normal"
- >
- {{ stageName }}
- </div>
- </div>
- </div>
- <gl-badge v-if="isBridge" class="gl-mt-3" variant="info" size="sm">
- {{ $options.i18n.bridgeBadgeText }}
- </gl-badge>
- </component>
-
- <action-component
- v-if="hasAction"
- :tooltip-text="jobActionTooltipText"
- :link="status.action.path"
- :action-icon="status.action.icon"
- class="gl-mr-1"
- :should-trigger-click="shouldTriggerActionClick"
- :with-confirmation-modal="withConfirmationModal"
- data-qa-selector="job_action_button"
- @actionButtonClicked="handleConfirmationModalPreferences"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
- @showActionConfirmationModal="showActionConfirmationModal"
- />
- <action-component
- v-if="hasUnauthorizedManualAction"
- disabled
- :tooltip-text="$options.i18n.unauthorizedTooltip"
- :action-icon="unauthorizedManualActionIcon"
- :link="`unauthorized-${computedJobId}`"
- class="gl-mr-1"
- />
- <gl-modal
- v-if="showConfirmationModal"
- ref="modal"
- v-model="showConfirmationModal"
- modal-id="action-confirmation-modal"
- :title="retryTriggerJobWarningText"
- :action-cancel="$options.i18n.confirmationModal.actionCancel"
- :action-primary="$options.i18n.confirmationModal.actionPrimary"
- @primary="executePendingAction"
- @close="handleConfirmationModalPreferences"
- @hide="handleConfirmationModalPreferences"
- >
- <p class="gl-mb-1">{{ $options.i18n.confirmationModal.description }}</p>
- <gl-link :href="$options.confirmationModalDocLink" target="_blank">{{
- $options.i18n.confirmationModal.linkText
- }}</gl-link>
- <div class="gl-mt-4 gl-display-flex">
- <gl-form>
- <gl-form-checkbox class="gl-min-h-0" @input="toggleSkipRetryModalCheckbox" />
- </gl-form>
- <p class="gl-m-0">{{ $options.i18n.confirmationModal.footer }}</p>
- </div>
- </gl-modal>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
deleted file mode 100644
index d8b843bdfb0..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ /dev/null
@@ -1,304 +0,0 @@
-<script>
-import {
- GlBadge,
- GlButton,
- GlLink,
- GlLoadingIcon,
- GlTooltip,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { __, sprintf } from '~/locale';
-import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { reportToSentry } from '../../utils';
-import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from './constants';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- CiIcon,
- GlBadge,
- GlButton,
- GlLink,
- GlLoadingIcon,
- GlTooltip,
- },
- styles: {
- actionSizeClasses: ['gl-h-7 gl-w-7'],
- flatLeftBorder: ['gl-rounded-bottom-left-none!', 'gl-rounded-top-left-none!'],
- flatRightBorder: ['gl-rounded-bottom-right-none!', 'gl-rounded-top-right-none!'],
- },
- props: {
- columnTitle: {
- type: String,
- required: true,
- },
- expanded: {
- type: Boolean,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: true,
- },
- pipeline: {
- type: Object,
- required: true,
- },
- type: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- hasActionTooltip: false,
- isActionLoading: false,
- isExpandBtnFocus: false,
- };
- },
- computed: {
- action() {
- if (this.isDownstream) {
- if (this.isCancelable) {
- return {
- icon: 'cancel',
- method: this.cancelPipeline,
- ariaLabel: __('Cancel downstream pipeline'),
- };
- } else if (this.isRetryable) {
- return {
- icon: 'retry',
- method: this.retryPipeline,
- ariaLabel: __('Retry downstream pipeline'),
- };
- }
- }
-
- return {};
- },
- buttonBorderClasses() {
- return this.isUpstream
- ? ['gl-border-r-0!', ...this.$options.styles.flatRightBorder]
- : ['gl-border-l-0!', ...this.$options.styles.flatLeftBorder];
- },
- buttonShadowClass() {
- return this.isExpandBtnFocus ? '' : 'gl-shadow-none!';
- },
- buttonId() {
- return `js-linked-pipeline-${this.pipeline.id}`;
- },
- cardClasses() {
- return this.isDownstream
- ? this.$options.styles.flatRightBorder
- : this.$options.styles.flatLeftBorder;
- },
- expandedIcon() {
- if (this.isUpstream) {
- return this.expanded ? 'chevron-lg-right' : 'chevron-lg-left';
- }
- return this.expanded ? 'chevron-lg-left' : 'chevron-lg-right';
- },
- expandBtnText() {
- return this.expanded ? __('Collapse jobs') : __('Expand jobs');
- },
- childPipeline() {
- return this.isDownstream && this.isSameProject;
- },
- downstreamTitle() {
- return this.childPipeline ? this.sourceJobName : this.pipeline.project.name;
- },
- flexDirection() {
- return this.isUpstream ? 'gl-flex-direction-row-reverse' : 'gl-flex-direction-row';
- },
- graphqlPipelineId() {
- return convertToGraphQLId(TYPENAME_CI_PIPELINE, this.pipeline.id);
- },
- hasUpdatePipelinePermissions() {
- return Boolean(this.pipeline?.userPermissions?.updatePipeline);
- },
- isCancelable() {
- return Boolean(this.pipeline?.cancelable && this.hasUpdatePipelinePermissions);
- },
- isDownstream() {
- return this.type === DOWNSTREAM;
- },
- isRetryable() {
- return Boolean(this.pipeline?.retryable && this.hasUpdatePipelinePermissions);
- },
- isSameProject() {
- return !this.pipeline.multiproject;
- },
- isUpstream() {
- return this.type === UPSTREAM;
- },
- label() {
- if (this.parentPipeline) {
- return __('Parent');
- } else if (this.childPipeline) {
- return __('Child');
- }
- return __('Multi-project');
- },
- parentPipeline() {
- return this.isUpstream && this.isSameProject;
- },
- pipelineIsLoading() {
- return Boolean(this.isLoading || this.pipeline.isLoading);
- },
- pipelineStatus() {
- return this.pipeline.status;
- },
- projectName() {
- return this.pipeline.project.name;
- },
- showAction() {
- return Boolean(this.action?.method && this.action?.icon && this.action?.ariaLabel);
- },
- showCardTooltip() {
- return !this.hasActionTooltip && !this.isExpandBtnFocus;
- },
- sourceJobName() {
- return this.pipeline.sourceJob?.name ?? '';
- },
- sourceJobInfo() {
- return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';
- },
- cardTooltipText() {
- return `${this.downstreamTitle} #${this.pipeline.id} - ${this.pipelineStatus.label} -
- ${this.sourceJobInfo}`;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('linked_pipeline', `error: ${err}, info: ${info}`);
- },
- methods: {
- cancelPipeline() {
- this.executePipelineAction(CancelPipelineMutation);
- },
- async executePipelineAction(mutation) {
- try {
- this.isActionLoading = true;
-
- await this.$apollo.mutate({
- mutation,
- variables: {
- id: this.graphqlPipelineId,
- },
- });
- this.$emit('refreshPipelineGraph');
- } catch {
- this.$emit('error', { type: ACTION_FAILURE });
- } finally {
- this.isActionLoading = false;
- }
- },
- hideTooltips() {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- },
- onClickLinkedPipeline() {
- this.hideTooltips();
- this.$emit('pipelineClicked', this.$refs.linkedPipeline);
- this.$emit('pipelineExpandToggle', this.sourceJobName, !this.expanded);
- },
- onDownstreamHovered() {
- this.$emit('downstreamHovered', this.sourceJobName);
- },
- onDownstreamHoverLeave() {
- this.$emit('downstreamHovered', '');
- },
- retryPipeline() {
- this.executePipelineAction(RetryPipelineMutation);
- },
- setActionTooltip(flag) {
- this.hasActionTooltip = flag;
- },
- setExpandBtnActiveState(flag) {
- this.isExpandBtnFocus = flag;
- },
- },
-};
-</script>
-
-<template>
- <div
- ref="linkedPipeline"
- class="gl-h-full gl-display-flex! gl-px-2"
- :class="flexDirection"
- data-qa-selector="linked_pipeline_container"
- @mouseover="onDownstreamHovered"
- @mouseleave="onDownstreamHoverLeave"
- >
- <gl-tooltip v-if="showCardTooltip" :target="() => $refs.linkedPipeline">
- {{ cardTooltipText }}
- </gl-tooltip>
- <div class="gl-bg-white gl-border gl-p-3 gl-rounded-lg gl-w-full" :class="cardClasses">
- <div class="gl-display-flex gl-gap-x-3">
- <ci-icon v-if="!pipelineIsLoading" :status="pipelineStatus" :size="24" />
- <div v-else class="gl-pr-3"><gl-loading-icon size="sm" inline /></div>
- <div
- class="gl-display-flex gl-downstream-pipeline-job-width gl-flex-direction-column gl-line-height-normal"
- >
- <span
- class="gl-text-truncate"
- data-testid="downstream-title"
- data-qa-selector="downstream_title_content"
- >
- {{ downstreamTitle }}
- </span>
- <div class="gl-text-truncate">
- <gl-link
- class="gl-text-blue-500! gl-font-sm"
- :href="pipeline.path"
- data-testid="pipelineLink"
- >#{{ pipeline.id }}</gl-link
- >
- </div>
- </div>
- <gl-button
- v-if="showAction"
- v-gl-tooltip
- :title="action.ariaLabel"
- :loading="isActionLoading"
- :icon="action.icon"
- class="gl-rounded-full!"
- :class="$options.styles.actionSizeClasses"
- :aria-label="action.ariaLabel"
- @click="action.method"
- @mouseover="setActionTooltip(true)"
- @mouseout="setActionTooltip(false)"
- />
- <div v-else :class="$options.styles.actionSizeClasses"></div>
- </div>
- <div class="gl-pt-2">
- <gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
- {{ label }}
- </gl-badge>
- </div>
- </div>
- <div class="gl-display-flex">
- <gl-button
- :id="buttonId"
- v-gl-tooltip
- :title="expandBtnText"
- class="gl-border! gl-rounded-lg!"
- :class="[`js-pipeline-expand-${pipeline.id}`, buttonBorderClasses, buttonShadowClass]"
- :icon="expandedIcon"
- :aria-label="expandBtnText"
- data-testid="expand-pipeline-button"
- data-qa-selector="expand_linked_pipeline_button"
- @mouseover="setExpandBtnActiveState(true)"
- @mouseout="setExpandBtnActiveState(false)"
- @focus="setExpandBtnActiveState(true)"
- @blur="setExpandBtnActiveState(false)"
- @click="onClickLinkedPipeline"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
deleted file mode 100644
index 02e426064c9..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ /dev/null
@@ -1,247 +0,0 @@
-<script>
-import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
-import { LOAD_FAILURE } from '../../constants';
-import { reportToSentry } from '../../utils';
-import { ONE_COL_WIDTH, UPSTREAM, LAYER_VIEW, STAGE_VIEW } from './constants';
-import LinkedPipeline from './linked_pipeline.vue';
-import {
- calculatePipelineLayersInfo,
- getQueryHeaders,
- serializeLoadErrors,
- toggleQueryPollingByVisibility,
- unwrapPipelineData,
- validateConfigPaths,
-} from './utils';
-
-export default {
- components: {
- LinkedPipeline,
- PipelineGraph: () => import('./graph_component.vue'),
- },
- props: {
- columnTitle: {
- type: String,
- required: true,
- },
- configPaths: {
- type: Object,
- required: true,
- validator: validateConfigPaths,
- },
- linkedPipelines: {
- type: Array,
- required: true,
- },
- showLinks: {
- type: Boolean,
- required: true,
- },
- skipRetryModal: {
- type: Boolean,
- required: false,
- default: false,
- },
- type: {
- type: String,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- currentPipeline: null,
- loadingPipelineId: null,
- pipelineLayers: {},
- pipelineExpanded: false,
- };
- },
- titleClasses: [
- 'gl-font-weight-bold',
- 'gl-pipeline-job-width',
- 'gl-text-truncate',
- 'gl-line-height-36',
- 'gl-pl-3',
- 'gl-mb-5',
- ],
- minWidth: `${ONE_COL_WIDTH}px`,
- computed: {
- columnClass() {
- const positionValues = {
- right: 'gl-ml-6',
- left: 'gl-mx-6',
- };
-
- return `graph-position-${this.graphPosition} ${positionValues[this.graphPosition]}`;
- },
- computedTitleClasses() {
- const positionalClasses = this.isUpstream ? ['gl-w-full', 'gl-linked-pipeline-padding'] : [];
-
- return [...this.$options.titleClasses, ...positionalClasses];
- },
- graphPosition() {
- return this.isUpstream ? 'left' : 'right';
- },
- graphViewType() {
- return this.currentPipeline?.usesNeeds ? this.viewType : STAGE_VIEW;
- },
- isUpstream() {
- return this.type === UPSTREAM;
- },
- minWidth() {
- return this.isUpstream ? 0 : this.$options.minWidth;
- },
- },
- methods: {
- getPipelineData(pipeline) {
- const projectPath = pipeline.project.fullPath;
-
- this.$apollo.addSmartQuery('currentPipeline', {
- query: getPipelineDetails,
- pollInterval: 10000,
- context() {
- return getQueryHeaders(this.configPaths.graphqlResourceEtag);
- },
- variables() {
- return {
- projectPath,
- iid: pipeline.iid,
- };
- },
- update(data) {
- /*
- This check prevents the pipeline from being overwritten
- when a poll times out and the data returned is empty.
- This can be removed once the timeout behavior is updated.
- See: https://gitlab.com/gitlab-org/gitlab/-/issues/323213.
- */
-
- if (!data?.project?.pipeline) {
- return this.currentPipeline;
- }
-
- return unwrapPipelineData(projectPath, JSON.parse(JSON.stringify(data)));
- },
- result() {
- this.loadingPipelineId = null;
- this.$emit('scrollContainer');
- },
- error(err) {
- this.$emit('error', { type: LOAD_FAILURE, skipSentry: true });
-
- reportToSentry(
- 'linked_pipelines_column',
- `error type: ${LOAD_FAILURE}, error: ${serializeLoadErrors(err)}`,
- );
- },
- });
-
- toggleQueryPollingByVisibility(this.$apollo.queries.currentPipeline);
- },
- getPipelineLayers(id) {
- if (this.viewType === LAYER_VIEW && !this.pipelineLayers[id]) {
- this.pipelineLayers[id] = calculatePipelineLayersInfo(
- this.currentPipeline,
- this.$options.name,
- this.configPaths.metricsPath,
- );
- }
-
- return this.pipelineLayers[id];
- },
- isExpanded(id) {
- return Boolean(this.currentPipeline?.id && id === this.currentPipeline.id);
- },
- isLoadingPipeline(id) {
- return this.loadingPipelineId === id;
- },
- onPipelineClick(pipeline) {
- /* If the clicked pipeline has been expanded already, close it, clear, exit */
- if (this.currentPipeline?.id === pipeline.id) {
- this.pipelineExpanded = false;
- this.currentPipeline = null;
- return;
- }
-
- /* Set the loading id */
- this.loadingPipelineId = pipeline.id;
-
- /*
- Expand the pipeline.
- If this was not a toggle close action, and
- it was already showing a different pipeline, then
- this will be a no-op, but that doesn't matter.
- */
- this.pipelineExpanded = true;
-
- this.getPipelineData(pipeline);
- },
- onDownstreamHovered(jobName) {
- this.$emit('downstreamHovered', jobName);
- },
- onPipelineExpandToggle(jobName, expanded) {
- // Highlighting only applies to downstream pipelines
- if (this.isUpstream) {
- return;
- }
-
- this.$emit('pipelineExpandToggle', jobName, expanded);
- },
- showContainer(id) {
- return this.isExpanded(id) || this.isLoadingPipeline(id);
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex">
- <div :class="columnClass" class="linked-pipelines-column">
- <div data-testid="linked-column-title" :class="computedTitleClasses">
- {{ columnTitle }}
- </div>
- <ul class="gl-pl-0">
- <li
- v-for="pipeline in linkedPipelines"
- :key="pipeline.id"
- class="gl-display-flex gl-mb-3"
- :class="{ 'gl-flex-direction-row-reverse': isUpstream }"
- >
- <linked-pipeline
- class="gl-display-inline-block"
- :is-loading="isLoadingPipeline(pipeline.id)"
- :pipeline="pipeline"
- :column-title="columnTitle"
- :type="type"
- :expanded="isExpanded(pipeline.id)"
- @downstreamHovered="onDownstreamHovered"
- @pipelineClicked="onPipelineClick(pipeline)"
- @pipelineExpandToggle="onPipelineExpandToggle"
- @refreshPipelineGraph="$emit('refreshPipelineGraph')"
- />
- <div
- v-if="showContainer(pipeline.id)"
- :style="{ minWidth }"
- class="gl-display-inline-block"
- >
- <pipeline-graph
- v-if="isExpanded(pipeline.id)"
- :type="type"
- class="gl-inline-block gl-mt-n2"
- :config-paths="configPaths"
- :pipeline="currentPipeline"
- :computed-pipeline-info="getPipelineLayers(pipeline.id)"
- :show-links="showLinks"
- :skip-retry-modal="skipRetryModal"
- :is-linked-pipeline="true"
- :view-type="graphViewType"
- @setSkipRetryModal="$emit('setSkipRetryModal')"
- />
- </div>
- </li>
- </ul>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/perf_utils.js b/app/assets/javascripts/pipelines/components/graph/perf_utils.js
deleted file mode 100644
index 3737a209f5c..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/perf_utils.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import {
- PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
- PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
- PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- PIPELINES_DETAIL_LINK_DURATION,
- PIPELINES_DETAIL_LINKS_TOTAL,
- PIPELINES_DETAIL_LINKS_JOB_RATIO,
-} from '~/performance/constants';
-
-import { performanceMarkAndMeasure } from '~/performance/utils';
-import { reportPerformance } from '../graph_shared/api';
-
-export const beginPerfMeasure = () => {
- performanceMarkAndMeasure({ mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START });
-};
-
-export const finishPerfMeasureAndSend = (numLinks, numGroups, metricsPath) => {
- performanceMarkAndMeasure({
- mark: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_END,
- measures: [
- {
- name: PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- start: PIPELINES_DETAIL_LINKS_MARK_CALCULATE_START,
- },
- ],
- });
-
- window.requestAnimationFrame(() => {
- const duration = window.performance.getEntriesByName(
- PIPELINES_DETAIL_LINKS_MEASURE_CALCULATION,
- )[0]?.duration;
-
- if (!duration) {
- return;
- }
-
- const data = {
- histograms: [
- { name: PIPELINES_DETAIL_LINK_DURATION, value: duration / 1000 },
- { name: PIPELINES_DETAIL_LINKS_TOTAL, value: numLinks },
- {
- name: PIPELINES_DETAIL_LINKS_JOB_RATIO,
- value: numLinks / numGroups,
- },
- ],
- };
-
- reportPerformance(metricsPath, data);
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
deleted file mode 100644
index ffd0fec2ca8..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ /dev/null
@@ -1,196 +0,0 @@
-<script>
-import { escape, isEmpty } from 'lodash';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { reportToSentry } from '../../utils';
-import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobGroupDropdown from './job_group_dropdown.vue';
-import JobItem from './job_item.vue';
-
-export default {
- components: {
- ActionComponent,
- JobGroupDropdown,
- JobItem,
- MainGraphWrapper,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- groups: {
- type: Array,
- required: true,
- },
- name: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- action: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- highlightedJobs: {
- type: Array,
- required: false,
- default: () => [],
- },
- isStageView: {
- type: Boolean,
- required: false,
- default: false,
- },
- jobHovered: {
- type: String,
- required: false,
- default: '',
- },
- pipelineExpanded: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- skipRetryModal: {
- type: Boolean,
- required: false,
- default: false,
- },
- sourceJobHovered: {
- type: String,
- required: false,
- default: '',
- },
- userPermissions: {
- type: Object,
- required: true,
- },
- },
- jobClasses: [
- 'gl-p-3',
- 'gl-border-gray-100',
- 'gl-border-solid',
- 'gl-border-1',
- 'gl-bg-white',
- 'gl-rounded-7',
- 'gl-hover-bg-gray-50',
- 'gl-focus-bg-gray-50',
- 'gl-hover-text-gray-900',
- 'gl-focus-text-gray-900',
- 'gl-hover-border-gray-200',
- 'gl-focus-border-gray-200',
- ],
- titleClasses: [
- 'gl-font-weight-bold',
- 'gl-pipeline-job-width',
- 'gl-text-truncate',
- 'gl-line-height-36',
- 'gl-pl-3',
- ],
- computed: {
- canUpdatePipeline() {
- return this.userPermissions.updatePipeline;
- },
- columnSpacingClass() {
- return this.isStageView ? 'gl-px-6' : 'gl-px-9';
- },
- hasAction() {
- return !isEmpty(this.action);
- },
- showStageName() {
- return !this.isStageView;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
- },
- mounted() {
- this.$emit('updateMeasurements');
- },
- methods: {
- getGroupId(group) {
- return group.name;
- },
- groupId(group) {
- return `ci-badge-${escape(group.name)}`;
- },
- isFadedOut(jobName) {
- return this.highlightedJobs.length > 1 && !this.highlightedJobs.includes(jobName);
- },
- isParallel(group) {
- return group.size > 1 && group.jobs.length > 1;
- },
- singleJobExists(group) {
- const firstJobDefined = Boolean(group.jobs?.[0]);
-
- if (!firstJobDefined) {
- reportToSentry('stage_column_component', 'undefined_job_hunt');
- }
-
- return group.size === 1 && firstJobDefined;
- },
- },
-};
-</script>
-<template>
- <main-graph-wrapper :class="columnSpacingClass" data-testid="stage-column">
- <template #stages>
- <div
- data-testid="stage-column-title"
- class="gl-display-flex gl-justify-content-space-between gl-relative"
- :class="$options.titleClasses"
- >
- <span :title="name" class="gl-text-truncate gl-pr-3 gl-w-85p">
- {{ name }}
- </span>
- <action-component
- v-if="hasAction && canUpdatePipeline"
- :action-icon="action.icon"
- :tooltip-text="action.title"
- :link="action.path"
- class="js-stage-action"
- @pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
- />
- </div>
- </template>
- <template #jobs>
- <div
- v-for="group in groups"
- :id="groupId(group)"
- :key="getGroupId(group)"
- data-testid="stage-column-group"
- class="gl-relative gl-mb-3 gl-white-space-normal gl-pipeline-job-width"
- @mouseenter="$emit('jobHover', group.name)"
- @mouseleave="$emit('jobHover', '')"
- >
- <job-item
- v-if="singleJobExists(group)"
- :job="group.jobs[0]"
- :job-hovered="jobHovered"
- :skip-retry-modal="skipRetryModal"
- :source-job-hovered="sourceJobHovered"
- :pipeline-expanded="pipelineExpanded"
- :pipeline-id="pipelineId"
- :stage-name="showStageName ? group.stageName : ''"
- :css-class-job-name="$options.jobClasses"
- :class="[
- { 'gl-opacity-3': isFadedOut(group.name) },
- 'gl-transition-duration-slow gl-transition-timing-function-ease',
- ]"
- @pipelineActionRequestComplete="$emit('refreshPipelineGraph')"
- @setSkipRetryModal="$emit('setSkipRetryModal')"
- />
- <div v-else-if="isParallel(group)" :class="{ 'gl-opacity-3': isFadedOut(group.name) }">
- <job-group-dropdown
- :group="group"
- :stage-name="showStageName ? group.stageName : ''"
- :pipeline-id="pipelineId"
- :css-class-job-name="$options.jobClasses"
- />
- </div>
- </div>
- </template>
- </main-graph-wrapper>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph/utils.js b/app/assets/javascripts/pipelines/components/graph/utils.js
deleted file mode 100644
index c888c8a5537..00000000000
--- a/app/assets/javascripts/pipelines/components/graph/utils.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { isEmpty } from 'lodash';
-import { getIdFromGraphQLId, etagQueryHeaders } from '~/graphql_shared/utils';
-import { reportToSentry } from '../../utils';
-import { listByLayers } from '../parsing_utils';
-import { unwrapStagesWithNeedsAndLookup } from '../unwrapping_utils';
-import { beginPerfMeasure, finishPerfMeasureAndSend } from './perf_utils';
-
-export { toggleQueryPollingByVisibility } from '~/graphql_shared/utils';
-
-const addMulti = (mainPipelineProjectPath, linkedPipeline) => {
- return {
- ...linkedPipeline,
- multiproject: mainPipelineProjectPath !== linkedPipeline.project.fullPath,
- };
-};
-
-const calculatePipelineLayersInfo = (pipeline, componentName, metricsPath) => {
- const shouldCollectMetrics = Boolean(metricsPath);
-
- if (shouldCollectMetrics) {
- beginPerfMeasure();
- }
-
- let layers = null;
-
- try {
- layers = listByLayers(pipeline);
-
- if (shouldCollectMetrics) {
- finishPerfMeasureAndSend(layers.linksData.length, layers.numGroups, metricsPath);
- }
- } catch (err) {
- reportToSentry(componentName, err);
- }
-
- return layers;
-};
-
-const getQueryHeaders = (etagResource) =>
- etagQueryHeaders('verify/ci/pipeline-graph', etagResource);
-
-const serializeGqlErr = (gqlError) => {
- const { locations = [], message = '', path = [] } = gqlError;
-
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return `
- ${message}.
- Locations: ${locations
- .flatMap((loc) => Object.entries(loc))
- .flat(2)
- .join(' ')}.
- Path: ${path.join(', ')}.
- `;
-};
-
-const serializeLoadErrors = (errors) => {
- const { gqlError, graphQLErrors, networkError, message } = errors;
-
- if (!isEmpty(graphQLErrors)) {
- return graphQLErrors.map((err) => serializeGqlErr(err)).join('; ');
- }
-
- if (!isEmpty(gqlError)) {
- return serializeGqlErr(gqlError);
- }
-
- if (!isEmpty(networkError)) {
- return `Network error: ${networkError.message}`; // eslint-disable-line @gitlab/require-i18n-strings
- }
-
- return message;
-};
-
-const transformId = (linkedPipeline) => {
- return { ...linkedPipeline, id: getIdFromGraphQLId(linkedPipeline.id) };
-};
-
-const unwrapPipelineData = (mainPipelineProjectPath, data) => {
- if (!data?.project?.pipeline) {
- return null;
- }
-
- const { pipeline } = data.project;
-
- const {
- upstream,
- downstream,
- stages: { nodes: stages },
- } = pipeline;
-
- const { stages: updatedStages, lookup } = unwrapStagesWithNeedsAndLookup(stages);
-
- return {
- ...pipeline,
- id: getIdFromGraphQLId(pipeline.id),
- stages: updatedStages,
- stagesLookup: lookup,
- upstream: upstream
- ? [upstream].map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
- : [],
- downstream: downstream
- ? downstream.nodes.map(addMulti.bind(null, mainPipelineProjectPath)).map(transformId)
- : [],
- };
-};
-
-const validateConfigPaths = (value) => value.graphqlResourceEtag?.length > 0;
-
-export {
- calculatePipelineLayersInfo,
- getQueryHeaders,
- serializeGqlErr,
- serializeLoadErrors,
- unwrapPipelineData,
- validateConfigPaths,
-};
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/api.js b/app/assets/javascripts/pipelines/components/graph_shared/api.js
deleted file mode 100644
index 0fe7d9ffda3..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/api.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-import { reportToSentry } from '../../utils';
-
-export const reportPerformance = (path, stats) => {
- // FIXME: https://gitlab.com/gitlab-org/gitlab/-/issues/330245
- if (!path) {
- return;
- }
-
- axios.post(path, stats).catch((err) => {
- reportToSentry('links_inner_perf', `error: ${err}`);
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
deleted file mode 100644
index d6d9ea94c13..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import * as d3 from 'd3';
-
-export const createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
-
-/**
- * This function expects its first argument data structure
- * to be the same shaped as the one generated by `parseData`,
- * which contains nodes and links. For each link,
- * we find the nodes in the graph, calculate their coordinates and
- * trace the lines that represent the needs of each job.
- * @param {Object} nodeDict - Resulting object of `parseData` with nodes and links
- * @param {String} containerID - Id for the svg the links will be draw in
- * @returns {Array} Links that contain all the information about them
- */
-
-export const generateLinksData = (links, containerID, modifier = '') => {
- const containerEl = document.getElementById(containerID);
-
- return links.map((link) => {
- const path = d3.path();
-
- const sourceId = link.source;
- const targetId = link.target;
-
- const modifiedSourceId = `${sourceId}${modifier}`;
- const modifiedTargetId = `${targetId}${modifier}`;
-
- const sourceNodeEl = document.getElementById(modifiedSourceId);
- const targetNodeEl = document.getElementById(modifiedTargetId);
-
- const sourceNodeCoordinates = sourceNodeEl.getBoundingClientRect();
- const targetNodeCoordinates = targetNodeEl.getBoundingClientRect();
- const containerCoordinates = containerEl.getBoundingClientRect();
-
- // Because we add the svg dynamically and calculate the coordinates
- // with plain JS and not D3, we need to account for the fact that
- // the coordinates we are getting are absolutes, but we want to draw
- // relative to the svg container, which starts at `containerCoordinates(x,y)`
- // so we substract these from the total. We also need to remove the padding
- // from the total to make sure it's aligned properly. We then make the line
- // positioned in the center of the job node by adding half the height
- // of the job pill.
- const paddingLeft = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-left') || 0,
- );
- const paddingTop = parseFloat(
- window.getComputedStyle(containerEl, null).getPropertyValue('padding-top') || 0,
- );
-
- const sourceNodeX = sourceNodeCoordinates.right - containerCoordinates.x - paddingLeft;
- const sourceNodeY =
- sourceNodeCoordinates.top -
- containerCoordinates.y -
- paddingTop +
- sourceNodeCoordinates.height / 2;
- const targetNodeX = targetNodeCoordinates.x - containerCoordinates.x - paddingLeft;
- const targetNodeY =
- targetNodeCoordinates.y -
- containerCoordinates.y -
- paddingTop +
- sourceNodeCoordinates.height / 2;
-
- const sourceNodeLeftX = sourceNodeCoordinates.left - containerCoordinates.x - paddingLeft;
-
- // If the source and target X values are the same,
- // it means the nodes are in the same column so we
- // want to start the line on the left of the pill
- // instead of the right to have a nice curve.
- const firstPointCoordinateX = sourceNodeLeftX === targetNodeX ? sourceNodeLeftX : sourceNodeX;
-
- // First point
- path.moveTo(firstPointCoordinateX, sourceNodeY);
-
- // Make cross-stages lines a straight line all the way
- // until we can safely draw the bezier to look nice.
- // The adjustment number here is a magic number to make things
- // look nice and should change if the padding changes. This goes well
- // with gl-px-9 which we translate with 100px here.
- const straightLineDestinationX = targetNodeX - 100;
- const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
-
- if (straightLineDestinationX > firstPointCoordinateX) {
- path.lineTo(straightLineDestinationX, sourceNodeY);
- }
-
- // Add bezier curve. The first 4 coordinates are the 2 control
- // points to create the curve, and the last one is the end point (x, y).
- // We want our control points to be in the middle of the line
- path.bezierCurveTo(
- controlPointX,
- sourceNodeY,
- controlPointX,
- targetNodeY,
- targetNodeX,
- targetNodeY,
- );
-
- return {
- ...link,
- source: sourceId,
- target: targetId,
- ref: createUniqueLinkId(sourceId, targetId),
- path: path.toString(),
- };
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
deleted file mode 100644
index fb2280d971a..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/linked_graph_wrapper.vue
+++ /dev/null
@@ -1,7 +0,0 @@
-<template>
- <div class="gl-display-flex">
- <slot name="upstream"></slot>
- <slot name="main"></slot>
- <slot name="downstream"></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
deleted file mode 100644
index 1189c2ebad8..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_inner.vue
+++ /dev/null
@@ -1,161 +0,0 @@
-<script>
-import { isEmpty } from 'lodash';
-import { DRAW_FAILURE } from '../../constants';
-import { createJobsHash, generateJobNeedsDict, reportToSentry } from '../../utils';
-import { STAGE_VIEW } from '../graph/constants';
-import { generateLinksData } from './drawing_utils';
-
-export default {
- name: 'LinksInner',
- STROKE_WIDTH: 2,
- props: {
- containerId: {
- type: String,
- required: true,
- },
- containerMeasurements: {
- type: Object,
- required: true,
- },
- linksData: {
- type: Array,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- pipelineData: {
- type: Array,
- required: true,
- },
- defaultLinkColor: {
- type: String,
- required: false,
- default: 'gl-stroke-gray-200',
- },
- highlightedJob: {
- type: String,
- required: false,
- default: '',
- },
- viewType: {
- type: String,
- required: false,
- default: STAGE_VIEW,
- },
- },
- data() {
- return {
- links: [],
- needsObject: null,
- };
- },
- computed: {
- hasHighlightedJob() {
- return Boolean(this.highlightedJob);
- },
- isPipelineDataEmpty() {
- return isEmpty(this.pipelineData);
- },
- highlightedJobs() {
- // If you are hovering on a job, then the jobs we want to highlight are:
- // The job you are currently hovering + all of its needs.
- return this.hasHighlightedJob
- ? [this.highlightedJob, ...this.needsObject[this.highlightedJob]]
- : [];
- },
- highlightedLinks() {
- // If you are hovering on a job, then the links we want to highlight are:
- // All the links whose `source` and `target` are highlighted jobs.
- if (this.hasHighlightedJob) {
- const filteredLinks = this.links.filter((link) => {
- return (
- this.highlightedJobs.includes(link.source) && this.highlightedJobs.includes(link.target)
- );
- });
-
- return filteredLinks.map((link) => link.ref);
- }
-
- return [];
- },
- viewBox() {
- return [0, 0, this.containerMeasurements.width, this.containerMeasurements.height];
- },
- },
- watch: {
- highlightedJob() {
- // On first hover, generate the needs reference
- if (!this.needsObject) {
- const jobs = createJobsHash(this.pipelineData);
- this.needsObject = generateJobNeedsDict(jobs) ?? {};
- }
- },
- highlightedJobs(jobs) {
- this.$emit('highlightedJobsChange', jobs);
- },
- linksData() {
- this.calculateLinkData();
- },
- viewType() {
- /*
- We need to wait a tick so that the layout reflows
- before the links refresh.
- */
- this.$nextTick(() => {
- this.calculateLinkData();
- });
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
- },
- mounted() {
- if (!isEmpty(this.linksData)) {
- this.calculateLinkData();
- }
- },
- methods: {
- isLinkHighlighted(linkRef) {
- return this.highlightedLinks.includes(linkRef);
- },
- calculateLinkData() {
- try {
- this.links = generateLinksData(this.linksData, this.containerId, `-${this.pipelineId}`);
- } catch (err) {
- this.$emit('error', { type: DRAW_FAILURE, reportToSentry: false });
- reportToSentry(this.$options.name, err);
- }
- },
- getLinkClasses(link) {
- return [
- this.isLinkHighlighted(link.ref) ? 'gl-stroke-blue-400' : this.defaultLinkColor,
- { 'gl-opacity-3': this.hasHighlightedJob && !this.isLinkHighlighted(link.ref) },
- ];
- },
- },
-};
-</script>
-<template>
- <div class="gl-display-flex gl-relative">
- <svg
- id="link-svg"
- class="gl-absolute gl-pointer-events-none"
- :viewBox="viewBox"
- :width="`${containerMeasurements.width}px`"
- :height="`${containerMeasurements.height}px`"
- >
- <path
- v-for="link in links"
- :key="link.path"
- :ref="link.ref"
- :d="link.path"
- class="gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="getLinkClasses(link)"
- :stroke-width="$options.STROKE_WIDTH"
- />
- </svg>
- <slot></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue b/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
deleted file mode 100644
index ef24694e494..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/links_layer.vue
+++ /dev/null
@@ -1,75 +0,0 @@
-<script>
-import { memoize } from 'lodash';
-import { reportToSentry } from '../../utils';
-import { parseData } from '../parsing_utils';
-import LinksInner from './links_inner.vue';
-
-const parseForLinksBare = (pipeline) => {
- const arrayOfJobs = pipeline.flatMap(({ groups }) => groups);
- return parseData(arrayOfJobs).links;
-};
-
-const parseForLinks = memoize(parseForLinksBare);
-
-export default {
- name: 'LinksLayer',
- components: {
- LinksInner,
- },
- props: {
- containerMeasurements: {
- type: Object,
- required: true,
- },
- pipelineData: {
- type: Array,
- required: true,
- },
- linksData: {
- type: Array,
- required: false,
- default: () => [],
- },
- showLinks: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- computed: {
- containerZero() {
- return !this.containerMeasurements.width || !this.containerMeasurements.height;
- },
- getLinksData() {
- if (this.linksData.length > 0) {
- return this.linksData;
- }
-
- return parseForLinks(this.pipelineData);
- },
- showLinkedLayers() {
- return this.showLinks && !this.containerZero;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry(this.$options.name, `error: ${err}, info: ${info}`);
- },
-};
-</script>
-<template>
- <links-inner
- v-if="showLinkedLayers"
- :container-measurements="containerMeasurements"
- :links-data="getLinksData"
- :pipeline-data="pipelineData"
- v-bind="$attrs"
- v-on="$listeners"
- >
- <slot></slot>
- </links-inner>
- <div v-else>
- <div class="gl-display-flex gl-relative">
- <slot></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue b/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
deleted file mode 100644
index bcd7705669e..00000000000
--- a/app/assets/javascripts/pipelines/components/graph_shared/main_graph_wrapper.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<script>
-export default {
- props: {
- stageClasses: {
- type: String,
- required: false,
- default: '',
- },
- jobClasses: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-<template>
- <div>
- <div class="gl-display-flex gl-align-items-center gl-w-full gl-mb-5" :class="stageClasses">
- <slot name="stages"> </slot>
- </div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full"
- :class="jobClasses"
- >
- <slot name="jobs"> </slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
deleted file mode 100644
index c24862f828b..00000000000
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_app.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import GetFailedJobsQuery from '../../graphql/queries/get_failed_jobs.query.graphql';
-import FailedJobsTable from './failed_jobs_table.vue';
-
-export default {
- components: {
- GlLoadingIcon,
- FailedJobsTable,
- },
- inject: {
- projectPath: {
- default: '',
- },
- pipelineIid: {
- default: '',
- },
- },
- apollo: {
- failedJobs: {
- query: GetFailedJobsQuery,
- variables() {
- return {
- fullPath: this.projectPath,
- pipelineIid: this.pipelineIid,
- };
- },
- update({ project }) {
- const jobNodes = project?.pipeline?.jobs?.nodes || [];
-
- return jobNodes.map((job) => {
- return {
- ...job,
- // this field is needed for the slot row-details
- // on the failed_jobs_table.vue component
- _showDetails: true,
- };
- });
- },
- error() {
- createAlert({ message: s__('Jobs|There was a problem fetching the failed jobs.') });
- },
- },
- },
- data() {
- return {
- failedJobs: [],
- };
- },
- computed: {
- loading() {
- return this.$apollo.queries.failedJobs.loading;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-loading-icon v-if="loading" size="lg" class="gl-mt-4" />
- <failed-jobs-table v-else :failed-jobs="failedJobs" />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
deleted file mode 100644
index f84ae13180d..00000000000
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ /dev/null
@@ -1,125 +0,0 @@
-<script>
-import { GlButton, GlLink, GlTableLite } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { __, s__ } from '~/locale';
-import { createAlert } from '~/alert';
-import Tracking from '~/tracking';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import RetryFailedJobMutation from '../../graphql/mutations/retry_failed_job.mutation.graphql';
-import { DEFAULT_FIELDS, TRACKING_CATEGORIES } from '../../constants';
-
-export default {
- fields: DEFAULT_FIELDS,
- retry: __('Retry'),
- components: {
- CiBadgeLink,
- GlButton,
- GlLink,
- GlTableLite,
- },
- directives: {
- SafeHtml,
- },
- mixins: [Tracking.mixin()],
- props: {
- failedJobs: {
- type: Array,
- required: true,
- },
- },
- methods: {
- async retryJob(id) {
- this.track('click_retry', { label: TRACKING_CATEGORIES.failed });
-
- try {
- const {
- data: {
- jobRetry: { errors, job },
- },
- } = await this.$apollo.mutate({
- mutation: RetryFailedJobMutation,
- variables: { id },
- });
- if (errors.length > 0) {
- this.showErrorMessage();
- } else {
- redirectTo(job.detailedStatus.detailsPath); // eslint-disable-line import/no-deprecated
- }
- } catch {
- this.showErrorMessage();
- }
- },
- canRetryJob(job) {
- return job.retryable && job.userPermissions.updateBuild;
- },
- showErrorMessage() {
- createAlert({ message: s__('Job|There was a problem retrying the failed job.') });
- },
- failureSummary(trace) {
- return trace ? trace.htmlSummary : s__('Job|No job log');
- },
- },
-};
-</script>
-
-<template>
- <gl-table-lite
- :items="failedJobs"
- :fields="$options.fields"
- stacked="lg"
- fixed
- data-testId="tab-failures"
- >
- <template #table-colgroup="{ fields }">
- <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
- </template>
-
- <template #cell(name)="{ item }">
- <div
- class="gl-display-flex gl-align-items-center gl-lg-justify-content-start gl-justify-content-end"
- >
- <ci-badge-link :status="item.detailedStatus" :show-text="false" class="gl-mr-3" />
- <div class="gl-text-truncate">
- <gl-link
- :href="item.detailedStatus.detailsPath"
- class="gl-font-weight-bold gl-text-gray-900!"
- >
- {{ item.name }}
- </gl-link>
- </div>
- </div>
- </template>
-
- <template #cell(stage)="{ item }">
- <div class="gl-text-truncate">
- <span>{{ item.stage.name }}</span>
- </div>
- </template>
-
- <template #cell(failureMessage)="{ item }">
- <span data-testid="job-failure-message">{{ item.failureMessage }}</span>
- </template>
-
- <template #cell(actions)="{ item }">
- <gl-button
- v-if="canRetryJob(item)"
- icon="retry"
- :title="$options.retry"
- :aria-label="$options.retry"
- @click="retryJob(item.id)"
- />
- </template>
-
- <template #row-details="{ item }">
- <pre
- v-if="item.userPermissions.readBuild"
- class="gl-w-full gl-text-left gl-border-none"
- data-testid="job-log"
- >
- <code v-safe-html="failureSummary(item.trace)" class="gl-reset-bg gl-p-0" data-testid="job-trace-summary">
- </code>
- </pre>
- </template>
- </gl-table-lite>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
deleted file mode 100644
index 61748860983..00000000000
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ /dev/null
@@ -1,133 +0,0 @@
-<script>
-import { GlIntersectionObserver, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
-import produce from 'immer';
-import { createAlert } from '~/alert';
-import { __ } from '~/locale';
-import eventHub from '~/jobs/components/table/event_hub';
-import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import { JOBS_TAB_FIELDS } from '~/jobs/components/table/constants';
-import getPipelineJobs from '../../graphql/queries/get_pipeline_jobs.query.graphql';
-
-export default {
- fields: JOBS_TAB_FIELDS,
- components: {
- GlIntersectionObserver,
- GlLoadingIcon,
- GlSkeletonLoader,
- JobsTable,
- },
- inject: {
- projectPath: {
- default: '',
- },
- pipelineIid: {
- default: '',
- },
- },
- apollo: {
- jobs: {
- query: getPipelineJobs,
- variables() {
- return {
- ...this.queryVariables,
- };
- },
- update(data) {
- return data.project?.pipeline?.jobs?.nodes || [];
- },
- result({ data }) {
- if (!data) {
- return;
- }
- this.jobsPageInfo = data.project?.pipeline?.jobs?.pageInfo || {};
- },
- error() {
- createAlert({ message: __('An error occurred while fetching the pipelines jobs.') });
- },
- },
- },
- data() {
- return {
- jobs: [],
- jobsPageInfo: {},
- firstLoad: true,
- };
- },
- computed: {
- queryVariables() {
- return {
- fullPath: this.projectPath,
- iid: this.pipelineIid,
- };
- },
- loading() {
- return this.$apollo.queries.jobs.loading;
- },
- showSkeletonLoader() {
- return this.firstLoad && this.loading;
- },
- showLoadingSpinner() {
- return !this.firstLoad && this.loading;
- },
- },
- mounted() {
- eventHub.$on('jobActionPerformed', this.handleJobAction);
- },
- beforeDestroy() {
- eventHub.$off('jobActionPerformed', this.handleJobAction);
- },
- methods: {
- handleJobAction() {
- this.firstLoad = false;
-
- this.$apollo.queries.jobs.refetch();
- },
- fetchMoreJobs() {
- this.firstLoad = false;
-
- this.$apollo.queries.jobs.fetchMore({
- variables: {
- ...this.queryVariables,
- after: this.jobsPageInfo.endCursor,
- },
- updateQuery: (previousResult, { fetchMoreResult }) => {
- const results = produce(fetchMoreResult, (draftData) => {
- draftData.project.pipeline.jobs.nodes = [
- ...previousResult.project.pipeline.jobs.nodes,
- ...draftData.project.pipeline.jobs.nodes,
- ];
- });
- return results;
- },
- });
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div v-if="showSkeletonLoader" class="gl-mt-5">
- <gl-skeleton-loader :width="1248" :height="73">
- <circle cx="748.031" cy="37.7193" r="15.0307" />
- <circle cx="787.241" cy="37.7193" r="15.0307" />
- <circle cx="827.759" cy="37.7193" r="15.0307" />
- <circle cx="866.969" cy="37.7193" r="15.0307" />
- <circle cx="380" cy="37" r="18" />
- <rect x="432" y="19" width="126.587" height="15" />
- <rect x="432" y="41" width="247" height="15" />
- <rect x="158" y="19" width="86.1" height="15" />
- <rect x="158" y="41" width="168" height="15" />
- <rect x="22" y="19" width="96" height="36" />
- <rect x="924" y="30" width="96" height="15" />
- <rect x="1057" y="20" width="166" height="35" />
- </gl-skeleton-loader>
- </div>
-
- <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" />
-
- <gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
- <gl-loading-icon v-if="showLoadingSpinner" size="lg" />
- </gl-intersection-observer>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
deleted file mode 100644
index ffb6ab71b22..00000000000
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-import { GlTooltipDirective, GlButton, GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import { dasherize } from '~/lib/utils/text_utility';
-import { __ } from '~/locale';
-import { reportToSentry } from '../../utils';
-
-/**
- * Renders either a cancel, retry or play icon button and handles the post request
- *
- * Used in:
- * - mr widget mini pipeline graph: `mr_widget_pipeline.vue`
- * - pipelines table
- * - pipelines table in merge request page
- * - pipelines table in commit page
- * - pipelines detail page in big graph
- */
-export default {
- components: {
- GlIcon,
- GlButton,
- GlLoadingIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- tooltipText: {
- type: String,
- required: true,
- },
- link: {
- type: String,
- required: true,
- },
- actionIcon: {
- type: String,
- required: true,
- },
- withConfirmationModal: {
- type: Boolean,
- required: false,
- default: false,
- },
- shouldTriggerClick: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isDisabled: false,
- isLoading: false,
- };
- },
- computed: {
- cssClass() {
- const actionIconDash = dasherize(this.actionIcon);
- return `${actionIconDash} js-icon-${actionIconDash}`;
- },
- },
- watch: {
- shouldTriggerClick(flag) {
- if (flag && this.withConfirmationModal) {
- this.executeAction();
- this.$emit('actionButtonClicked');
- }
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('action_component', `error: ${err}, info: ${info}`);
- },
- methods: {
- /**
- * The request should not be handled here.
- * However due to this component being used in several
- * different apps it avoids repetition & complexity.
- *
- */
- onClickAction() {
- if (this.withConfirmationModal) {
- this.$emit('showActionConfirmationModal');
- } else {
- this.executeAction();
- }
- },
- executeAction() {
- this.$root.$emit(BV_HIDE_TOOLTIP, `js-ci-action-${this.link}`);
- this.isDisabled = true;
- this.isLoading = true;
-
- axios
- .post(`${this.link}.json`)
- .then(() => {
- this.isLoading = false;
-
- this.$emit('pipelineActionRequestComplete');
- })
- .catch((err) => {
- this.isDisabled = false;
- this.isLoading = false;
-
- reportToSentry('action_component', err);
-
- createAlert({
- message: __('An error occurred while making the request.'),
- });
- });
- },
- },
-};
-</script>
-<template>
- <gl-button
- :id="`js-ci-action-${link}`"
- ref="button"
- :class="cssClass"
- :disabled="isDisabled"
- class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
- data-testid="ci-action-component"
- @click.stop="onClickAction"
- >
- <div
- v-gl-tooltip.viewport
- :title="tooltipText"
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-h-full"
- data-testid="ci-action-icon-tooltip-wrapper"
- >
- <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
- <gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
- </div>
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
deleted file mode 100644
index 1c7f5a7476d..00000000000
--- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
+++ /dev/null
@@ -1,38 +0,0 @@
-<script>
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-
-/**
- * Component that renders both the CI icon status and the job name.
- * Used in
- * - Badge component
- * - Dropdown badge components
- */
-export default {
- components: {
- CiIcon,
- },
- props: {
- name: {
- type: String,
- required: true,
- },
- status: {
- type: Object,
- required: true,
- },
- iconSize: {
- type: Number,
- required: false,
- default: 16,
- },
- },
-};
-</script>
-<template>
- <span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
- <span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
- {{ name }}
- </span>
- </span>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
deleted file mode 100644
index e158f8809b5..00000000000
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import { memoize } from 'lodash';
-import { createNodeDict } from '../utils';
-import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
-import { createSankey } from './dag/drawing_utils';
-
-/*
- A peformant alternative to lodash's isEqual. Because findIndex always finds
- the first instance of a match, if the found index is not the first, we know
- it is in fact a duplicate.
-*/
-const deduplicate = (item, itemIndex, arr) => {
- const foundIdx = arr.findIndex((test) => {
- return test.source === item.source && test.target === item.target;
- });
-
- return foundIdx === itemIndex;
-};
-
-export const makeLinksFromNodes = (nodes, nodeDict, { needsKey = NEEDS_PROPERTY } = {}) => {
- const constantLinkValue = 10; // all links are the same weight
- return nodes
- .map(({ jobs, name: groupName }) =>
- jobs.map((job) => {
- const needs = job[needsKey] || [];
-
- return needs.reduce((acc, needed) => {
- // It's possible that we have an optional job, which
- // is being needed by another job. In that scenario,
- // the needed job doesn't exist, so we don't want to
- // create link for it.
- if (nodeDict[needed]?.name) {
- acc.push({
- source: nodeDict[needed].name,
- target: groupName,
- value: constantLinkValue,
- });
- }
-
- return acc;
- }, []);
- }),
- )
- .flat(2);
-};
-
-export const getAllAncestors = (nodes, nodeDict) => {
- const needs = nodes
- .map((node) => {
- return nodeDict[node]?.needs || '';
- })
- .flat()
- .filter(Boolean)
- .filter(deduplicate);
-
- if (needs.length) {
- return [...needs, ...getAllAncestors(needs, nodeDict)];
- }
-
- return [];
-};
-
-export const filterByAncestors = (links, nodeDict) =>
- links.filter(({ target, source }) => {
- /*
-
- for every link, check out it's target
- for every target, get the target node's needs
- then drop the current link source from that list
-
- call a function to get all ancestors, recursively
- is the current link's source in the list of all parents?
- then we drop this link
-
- */
- const targetNode = target;
- const targetNodeNeeds = nodeDict[targetNode].needs;
- const targetNodeNeedsMinusSource = targetNodeNeeds.filter((need) => need !== source);
- const allAncestors = getAllAncestors(targetNodeNeedsMinusSource, nodeDict);
- return !allAncestors.includes(source);
- });
-
-export const parseData = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
- const nodeDict = createNodeDict(nodes, { needsKey });
- const allLinks = makeLinksFromNodes(nodes, nodeDict, { needsKey });
- const filteredLinks = allLinks.filter(deduplicate);
- const links = filterByAncestors(filteredLinks, nodeDict);
-
- return { nodes, links };
-};
-
-/*
- The number of nodes in the most populous generation drives the height of the graph.
-*/
-
-export const getMaxNodes = (nodes) => {
- const counts = nodes.reduce((acc, { layer }) => {
- if (!acc[layer]) {
- acc[layer] = 0;
- }
-
- acc[layer] += 1;
-
- return acc;
- }, []);
-
- return Math.max(...counts);
-};
-
-/*
- Because we cannot know if a node is part of a relationship until after we
- generate the links with createSankey, this function is used after the first call
- to find nodes that have no relations.
-*/
-
-export const removeOrphanNodes = (sankeyfiedNodes) => {
- return sankeyfiedNodes.filter((node) => node.sourceLinks.length || node.targetLinks.length);
-};
-
-/*
- This utility accepts unwrapped pipeline data in the format returned from
- our standard pipeline GraphQL query and returns a list of names by layer
- for the layer view. It can be combined with the stageLookup on the pipeline
- to generate columns by layer.
-*/
-
-export const listByLayers = ({ stages }) => {
- const arrayOfJobs = stages.flatMap(({ groups }) => groups);
- const parsedData = parseData(arrayOfJobs);
- const explicitParsedData = parseData(arrayOfJobs, { needsKey: EXPLICIT_NEEDS_PROPERTY });
- const dataWithLayers = createSankey()(explicitParsedData);
-
- const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
- /* sort groups by layer */
-
- if (!acc[layer]) {
- acc[layer] = [];
- }
-
- acc[layer].push(name);
-
- return acc;
- }, []);
-
- return {
- linksData: parsedData.links,
- numGroups: arrayOfJobs.length,
- pipelineLayers,
- };
-};
-
-export const generateColumnsFromLayersListBare = ({ stages, stagesLookup }, pipelineLayers) => {
- return pipelineLayers.map((layers, idx) => {
- /*
- Look up the groups in each layer,
- then add each set of layer groups to a stage-like object.
- */
-
- const groups = layers.map((id) => {
- const { stageIdx, groupIdx } = stagesLookup[id];
- return stages[stageIdx]?.groups?.[groupIdx];
- });
-
- return {
- name: '',
- id: `layer-${idx}`,
- status: { action: null },
- groups: groups.filter(Boolean),
- };
- });
-};
-
-export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFromLayersListBare);
-
-export const keepLatestDownstreamPipelines = (downstreamPipelines = []) => {
- return downstreamPipelines.filter((pipeline) => {
- if (pipeline.source_job) {
- return !pipeline?.source_job?.retried || false;
- }
-
- return !pipeline?.sourceJob?.retried || false;
- });
-};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue b/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
deleted file mode 100644
index c53321f82bd..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_details_header.vue
+++ /dev/null
@@ -1,631 +0,0 @@
-<script>
-import {
- GlAlert,
- GlBadge,
- GlButton,
- GlIcon,
- GlLink,
- GlLoadingIcon,
- GlModal,
- GlModalDirective,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
-import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import { __, s__, sprintf, formatNumber } from '~/locale';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import {
- LOAD_FAILURE,
- POST_FAILURE,
- DELETE_FAILURE,
- DEFAULT,
- BUTTON_TOOLTIP_RETRY,
- BUTTON_TOOLTIP_CANCEL,
-} from '../constants';
-import cancelPipelineMutation from '../graphql/mutations/cancel_pipeline.mutation.graphql';
-import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
-import getPipelineQuery from '../graphql/queries/get_pipeline_header_data.query.graphql';
-import { getQueryHeaders } from './graph/utils';
-
-const DELETE_MODAL_ID = 'pipeline-delete-modal';
-const POLL_INTERVAL = 10000;
-
-export default {
- name: 'PipelineDetailsHeader',
- BUTTON_TOOLTIP_RETRY,
- BUTTON_TOOLTIP_CANCEL,
- pipelineCancel: 'pipelineCancel',
- pipelineRetry: 'pipelineRetry',
- finishedStatuses: ['FAILED', 'SUCCESS', 'CANCELED'],
- components: {
- CiBadgeLink,
- ClipboardButton,
- GlAlert,
- GlBadge,
- GlButton,
- GlIcon,
- GlLink,
- GlLoadingIcon,
- GlModal,
- GlSprintf,
- TimeAgoTooltip,
- },
- directives: {
- GlModal: GlModalDirective,
- GlTooltip: GlTooltipDirective,
- SafeHtml,
- },
- i18n: {
- scheduleBadgeText: s__('Pipelines|Scheduled'),
- scheduleBadgeTooltip: __('This pipeline was triggered by a schedule'),
- childBadgeText: s__('Pipelines|Child pipeline (%{linkStart}parent%{linkEnd})'),
- childBadgeTooltip: __('This is a child pipeline within the parent pipeline'),
- latestBadgeText: s__('Pipelines|latest'),
- latestBadgeTooltip: __('Latest pipeline for the most recent commit on this branch'),
- mergeTrainBadgeText: s__('Pipelines|merge train'),
- mergeTrainBadgeTooltip: s__(
- 'Pipelines|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
- ),
- invalidBadgeText: s__('Pipelines|yaml invalid'),
- failedBadgeText: s__('Pipelines|error'),
- autoDevopsBadgeText: s__('Pipelines|Auto DevOps'),
- autoDevopsBadgeTooltip: __(
- 'This pipeline makes use of a predefined CI/CD configuration enabled by Auto DevOps.',
- ),
- detachedBadgeText: s__('Pipelines|merge request'),
- detachedBadgeTooltip: s__(
- "Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.",
- ),
- stuckBadgeText: s__('Pipelines|stuck'),
- stuckBadgeTooltip: s__('Pipelines|This pipeline is stuck'),
- computeMinutesTooltip: s__('Pipelines|Total amount of compute minutes used for the pipeline'),
- totalJobsTooltip: s__('Pipelines|Total number of jobs for the pipeline'),
- retryPipelineText: __('Retry'),
- cancelPipelineText: __('Cancel pipeline'),
- deletePipelineText: __('Delete'),
- clipboardTooltip: __('Copy commit SHA'),
- createdText: s__('Pipelines|created'),
- finishedText: s__('Pipelines|finished'),
- },
- 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.'),
- },
- modal: {
- id: DELETE_MODAL_ID,
- title: __('Delete pipeline'),
- deleteConfirmationText: __(
- '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.',
- ),
- actionPrimary: {
- text: __('Delete pipeline'),
- attributes: {
- variant: 'danger',
- },
- },
- actionCancel: {
- text: __('Cancel'),
- },
- },
- inject: {
- graphqlResourceEtag: {
- default: '',
- },
- paths: {
- default: {},
- },
- pipelineIid: {
- default: '',
- },
- },
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- totalJobs: {
- type: String,
- required: false,
- default: '',
- },
- computeMinutes: {
- type: String,
- required: false,
- default: '',
- },
- yamlErrors: {
- type: String,
- required: false,
- default: '',
- },
- failureReason: {
- type: String,
- required: false,
- default: '',
- },
- refText: {
- type: String,
- required: false,
- default: '',
- },
- badges: {
- type: Object,
- required: false,
- default: () => {},
- },
- },
- apollo: {
- pipeline: {
- context() {
- return getQueryHeaders(this.graphqlResourceEtag);
- },
- query: getPipelineQuery,
- variables() {
- return {
- fullPath: this.paths.fullProject,
- iid: this.pipelineIid,
- };
- },
- update(data) {
- return data.project.pipeline;
- },
- error() {
- this.reportFailure(LOAD_FAILURE);
- },
- pollInterval: POLL_INTERVAL,
- 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,
- failureMessages: [],
- failureType: null,
- isCanceling: false,
- isRetrying: false,
- isDeleting: false,
- };
- },
- computed: {
- loading() {
- return this.$apollo.queries.pipeline.loading;
- },
- hasError() {
- return this.failureType;
- },
- hasPipelineData() {
- return Boolean(this.pipeline);
- },
- isLoadingInitialQuery() {
- return this.$apollo.queries.pipeline.loading && !this.hasPipelineData;
- },
- detailedStatus() {
- return this.pipeline?.detailedStatus || {};
- },
- status() {
- return this.pipeline?.status;
- },
- isFinished() {
- return this.$options.finishedStatuses.includes(this.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',
- };
- }
- },
- user() {
- return this.pipeline?.user;
- },
- userId() {
- return getIdFromGraphQLId(this.user?.id);
- },
- shortId() {
- return this.pipeline?.commit?.shortId || '';
- },
- commitPath() {
- return this.pipeline?.commit?.webPath || '';
- },
- commitTitle() {
- return this.pipeline?.commit?.title || '';
- },
- totalJobsText() {
- return sprintf(__('%{jobs} Jobs'), {
- jobs: this.totalJobs,
- });
- },
- triggeredText() {
- return sprintf(__('triggered pipeline for commit %{linkStart}%{shortId}%{linkEnd}'), {
- shortId: this.shortId,
- });
- },
- inProgress() {
- return this.status === 'RUNNING';
- },
- duration() {
- return this.pipeline?.duration || 0;
- },
- showDuration() {
- return this.duration && this.isFinished;
- },
- durationFormatted() {
- return timeIntervalInWords(this.duration);
- },
- queuedDuration() {
- return this.pipeline?.queuedDuration || 0;
- },
- inProgressText() {
- return sprintf(__('In progress, queued for %{queuedDuration} seconds'), {
- queuedDuration: formatNumber(this.queuedDuration),
- });
- },
- durationText() {
- return sprintf(__('%{duration}, queued for %{queuedDuration} seconds'), {
- duration: this.durationFormatted,
- queuedDuration: formatNumber(this.queuedDuration),
- });
- },
- canRetryPipeline() {
- const { retryable, userPermissions } = this.pipeline;
-
- return retryable && userPermissions.updatePipeline;
- },
- canCancelPipeline() {
- const { cancelable, userPermissions } = this.pipeline;
-
- return cancelable && userPermissions.updatePipeline;
- },
- showComputeMinutes() {
- return this.isFinished && this.computeMinutes !== '0.0';
- },
- },
- methods: {
- reportFailure(errorType, errorMessages = []) {
- this.failureType = errorType;
- this.failureMessages = errorMessages;
- },
- async postPipelineAction(name, mutation) {
- try {
- const {
- data: {
- [name]: { errors },
- },
- } = await this.$apollo.mutate({
- mutation,
- variables: { id: this.pipeline.id },
- });
-
- if (errors.length > 0) {
- this.isRetrying = false;
-
- this.reportFailure(POST_FAILURE, errors);
- } else {
- await this.$apollo.queries.pipeline.refetch();
- if (!this.isFinished) {
- this.$apollo.queries.pipeline.startPolling(POLL_INTERVAL);
- }
- }
- } catch {
- this.isRetrying = false;
-
- this.reportFailure(POST_FAILURE);
- }
- },
- cancelPipeline() {
- this.isCanceling = true;
- this.postPipelineAction(this.$options.pipelineCancel, cancelPipelineMutation);
- },
- retryPipeline() {
- this.isRetrying = true;
- this.postPipelineAction(this.$options.pipelineRetry, retryPipelineMutation);
- },
- async deletePipeline() {
- this.isDeleting = true;
- this.$apollo.queries.pipeline.stopPolling();
-
- try {
- const {
- data: {
- pipelineDestroy: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: deletePipelineMutation,
- variables: {
- id: this.pipeline.id,
- },
- });
-
- if (errors.length > 0) {
- this.reportFailure(DELETE_FAILURE, errors);
- this.isDeleting = false;
- } else {
- redirectTo(setUrlFragment(this.paths.pipelinesPath, 'delete_success')); // eslint-disable-line import/no-deprecated
- }
- } catch {
- this.$apollo.queries.pipeline.startPolling(POLL_INTERVAL);
- this.reportFailure(DELETE_FAILURE);
- this.isDeleting = false;
- }
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-my-4" data-testid="pipeline-details-header">
- <gl-alert
- v-if="hasError"
- class="gl-mb-4"
- :title="failure.text"
- :variant="failure.variant"
- :dismissible="false"
- >
- <div v-for="(failureMessage, index) in failureMessages" :key="`failure-message-${index}`">
- {{ failureMessage }}
- </div>
- </gl-alert>
- <gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
- <div
- v-else
- class="gl-display-flex gl-justify-content-space-between gl-flex-wrap"
- data-qa-selector="pipeline_details_header"
- >
- <div>
- <h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
- <h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
- {{ commitTitle }}
- </h3>
- <div>
- <ci-badge-link :status="detailedStatus" />
- <div class="gl-ml-2 gl-mb-3 gl-display-inline-block gl-h-6">
- <gl-link
- v-if="user"
- :href="user.webUrl"
- class="gl-display-inline-block gl-text-gray-900 gl-font-weight-bold js-user-link"
- :data-user-id="userId"
- :data-username="user.username"
- data-testid="pipeline-user-link"
- >
- {{ user.name }}
- </gl-link>
- <gl-sprintf :message="triggeredText">
- <template #link="{ content }">
- <gl-link
- :href="commitPath"
- class="gl-bg-blue-50 gl-rounded-base gl-px-2 gl-mx-2"
- data-testid="commit-link"
- target="_blank"
- >
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- <clipboard-button
- :text="shortId"
- category="tertiary"
- :title="$options.i18n.clipboardTooltip"
- size="small"
- />
- <span v-if="inProgress" data-testid="pipeline-created-time-ago">
- {{ $options.i18n.createdText }}
- <time-ago-tooltip :time="pipeline.createdAt" />
- </span>
- <span v-if="isFinished" data-testid="pipeline-finished-time-ago">
- {{ $options.i18n.finishedText }}
- <time-ago-tooltip :time="pipeline.finishedAt" />
- </span>
- </div>
- </div>
- <div v-safe-html="refText" class="gl-mb-3" data-testid="pipeline-ref-text"></div>
- <div>
- <gl-badge
- v-if="badges.schedule"
- v-gl-tooltip
- :title="$options.i18n.scheduleBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.scheduleBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.child"
- v-gl-tooltip
- :title="$options.i18n.childBadgeTooltip"
- variant="info"
- size="sm"
- >
- <gl-sprintf :message="$options.i18n.childBadgeText">
- <template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </gl-badge>
- <gl-badge
- v-if="badges.latest"
- v-gl-tooltip
- :title="$options.i18n.latestBadgeTooltip"
- variant="success"
- size="sm"
- >
- {{ $options.i18n.latestBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.mergeTrainPipeline"
- v-gl-tooltip
- :title="$options.i18n.mergeTrainBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.mergeTrainBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.invalid"
- v-gl-tooltip
- :title="yamlErrors"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.invalidBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.failed"
- v-gl-tooltip
- :title="failureReason"
- variant="danger"
- size="sm"
- >
- {{ $options.i18n.failedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.autoDevops"
- v-gl-tooltip
- :title="$options.i18n.autoDevopsBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.autoDevopsBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.detached"
- v-gl-tooltip
- :title="$options.i18n.detachedBadgeTooltip"
- variant="info"
- size="sm"
- >
- {{ $options.i18n.detachedBadgeText }}
- </gl-badge>
- <gl-badge
- v-if="badges.stuck"
- v-gl-tooltip
- :title="$options.i18n.stuckBadgeTooltip"
- variant="warning"
- size="sm"
- >
- {{ $options.i18n.stuckBadgeText }}
- </gl-badge>
- <span
- v-gl-tooltip
- :title="$options.i18n.totalJobsTooltip"
- class="gl-ml-2"
- data-testid="total-jobs"
- >
- <gl-icon name="pipeline" />
- {{ totalJobsText }}
- </span>
- <span
- v-if="showComputeMinutes"
- v-gl-tooltip
- :title="$options.i18n.computeMinutesTooltip"
- class="gl-ml-2"
- data-testid="compute-minutes"
- >
- <gl-icon name="quota" />
- {{ computeMinutes }}
- </span>
- <span v-if="inProgress" class="gl-ml-2" data-testid="pipeline-running-text">
- <gl-icon name="timer" />
- {{ inProgressText }}
- </span>
- <span v-if="showDuration" class="gl-ml-2" data-testid="pipeline-duration-text">
- <gl-icon name="timer" />
- {{ durationText }}
- </span>
- </div>
- </div>
- <div class="gl-mt-5 gl-lg-mt-0">
- <gl-button
- v-if="canRetryPipeline"
- v-gl-tooltip
- :aria-label="$options.BUTTON_TOOLTIP_RETRY"
- :title="$options.BUTTON_TOOLTIP_RETRY"
- :loading="isRetrying"
- :disabled="isRetrying"
- variant="confirm"
- data-testid="retry-pipeline"
- class="js-retry-button"
- @click="retryPipeline()"
- >
- {{ $options.i18n.retryPipelineText }}
- </gl-button>
-
- <gl-button
- v-if="canCancelPipeline"
- v-gl-tooltip
- :aria-label="$options.BUTTON_TOOLTIP_CANCEL"
- :title="$options.BUTTON_TOOLTIP_CANCEL"
- :loading="isCanceling"
- :disabled="isCanceling"
- class="gl-ml-3"
- variant="danger"
- data-testid="cancel-pipeline"
- @click="cancelPipeline()"
- >
- {{ $options.i18n.cancelPipelineText }}
- </gl-button>
-
- <gl-button
- v-if="pipeline.userPermissions.destroyPipeline"
- v-gl-modal="$options.modal.id"
- :loading="isDeleting"
- :disabled="isDeleting"
- class="gl-ml-3"
- variant="danger"
- category="secondary"
- data-testid="delete-pipeline"
- >
- {{ $options.i18n.deletePipelineText }}
- </gl-button>
- </div>
- </div>
- <gl-modal
- :modal-id="$options.modal.id"
- :title="$options.modal.title"
- :action-primary="$options.modal.actionPrimary"
- :action-cancel="$options.modal.actionCancel"
- @primary="deletePipeline()"
- >
- <p>
- {{ $options.modal.deleteConfirmationText }}
- </p>
- </gl-modal>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
deleted file mode 100644
index 3f1d7255a2b..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script>
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-
-export default {
- components: {
- TooltipOnTruncate,
- },
- props: {
- jobName: {
- type: String,
- required: true,
- },
- pipelineId: {
- type: Number,
- required: true,
- },
- isHovered: {
- type: Boolean,
- required: false,
- default: false,
- },
- isFadedOut: {
- type: Boolean,
- required: false,
- default: false,
- },
- handleMouseOver: {
- type: Function,
- required: false,
- default: () => {},
- },
- handleMouseLeave: {
- type: Function,
- required: false,
- default: () => {},
- },
- },
- computed: {
- id() {
- return `${this.jobName}-${this.pipelineId}`;
- },
- jobPillClasses() {
- return [
- { 'gl-opacity-3': this.isFadedOut },
- { 'gl-bg-gray-50 gl-inset-border-1-gray-200': this.isHovered },
- ];
- },
- },
- methods: {
- onMouseEnter() {
- this.$emit('on-mouse-enter', this.jobName);
- },
- onMouseLeave() {
- this.$emit('on-mouse-leave');
- },
- },
-};
-</script>
-<template>
- <div class="gl-w-full">
- <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
- <div
- :id="id"
- class="gl-bg-white gl-inset-border-1-gray-100 gl-text-center gl-text-truncate gl-rounded-6 gl-mb-3 gl-px-5 gl-py-3 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="jobPillClasses"
- @mouseover="onMouseEnter"
- @mouseleave="onMouseLeave"
- >
- {{ jobName }}
- </div>
- </tooltip-on-truncate>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
deleted file mode 100644
index 8daf85e2b2e..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ /dev/null
@@ -1,174 +0,0 @@
-<script>
-import { GlAlert } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { DRAW_FAILURE, DEFAULT } from '../../constants';
-import LinksLayer from '../graph_shared/links_layer.vue';
-import JobPill from './job_pill.vue';
-import StageName from './stage_name.vue';
-
-export default {
- components: {
- GlAlert,
- JobPill,
- LinksLayer,
- StageName,
- },
- CONTAINER_REF: 'PIPELINE_GRAPH_CONTAINER_REF',
- BASE_CONTAINER_ID: 'pipeline-graph-container',
- PIPELINE_ID: 0,
- STROKE_WIDTH: 2,
- errorTexts: {
- [DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
- [DEFAULT]: __('An unknown error occurred.'),
- },
- // The combination of gl-w-full gl-min-w-full and gl-max-w-15 is necessary.
- // The max width and the width make sure the ellipsis to work and the min width
- // is for when there is less text than the stage column width (which the width 100% does not fix)
- jobWrapperClasses:
- 'gl-display-flex gl-flex-direction-column gl-align-items-stretch gl-w-full gl-px-8 gl-min-w-full gl-max-w-15',
- props: {
- pipelineData: {
- required: true,
- type: Object,
- },
- },
- data() {
- return {
- failureType: null,
- highlightedJob: null,
- highlightedJobs: [],
- measurements: {
- height: 0,
- width: 0,
- },
- };
- },
- computed: {
- containerId() {
- return `${this.$options.BASE_CONTAINER_ID}-${this.$options.PIPELINE_ID}`;
- },
- failure() {
- switch (this.failureType) {
- case DRAW_FAILURE:
- return {
- text: this.$options.errorTexts[DRAW_FAILURE],
- variant: 'danger',
- dismissible: true,
- };
- default:
- return {
- text: this.$options.errorTexts[DEFAULT],
- variant: 'danger',
- dismissible: true,
- };
- }
- },
- hasError() {
- return this.failureType;
- },
- hasHighlightedJob() {
- return Boolean(this.highlightedJob);
- },
- pipelineStages() {
- return this.pipelineData?.stages || [];
- },
- },
- watch: {
- pipelineData: {
- immediate: true,
- handler() {
- this.$nextTick(() => {
- this.computeGraphDimensions();
- });
- },
- },
- },
- methods: {
- computeGraphDimensions() {
- this.measurements = {
- width: this.$refs[this.$options.CONTAINER_REF].scrollWidth,
- height: this.$refs[this.$options.CONTAINER_REF].scrollHeight,
- };
- },
- isFadedOut(jobName) {
- return this.highlightedJobs.length > 1 && !this.isJobHighlighted(jobName);
- },
- isJobHighlighted(jobName) {
- return this.highlightedJobs.includes(jobName);
- },
- onError(error) {
- this.reportFailure(error.type);
- },
- removeHoveredJob() {
- this.highlightedJob = null;
- },
- reportFailure(errorType) {
- this.failureType = errorType;
- },
- resetFailure() {
- this.failureType = null;
- },
- setHoveredJob(jobName) {
- this.highlightedJob = jobName;
- },
- updateHighlightedJobs(jobs) {
- this.highlightedJobs = jobs;
- },
- },
-};
-</script>
-<template>
- <div>
- <gl-alert
- v-if="hasError"
- :variant="failure.variant"
- :dismissible="failure.dismissible"
- @dismiss="resetFailure"
- >
- {{ failure.text }}
- </gl-alert>
- <div
- :id="containerId"
- :ref="$options.CONTAINER_REF"
- class="gl-bg-gray-10 gl-overflow-auto"
- data-testid="graph-container"
- data-qa-selector="pipeline_graph_container"
- >
- <links-layer
- :pipeline-data="pipelineStages"
- :pipeline-id="$options.PIPELINE_ID"
- :container-id="containerId"
- :container-measurements="measurements"
- :highlighted-job="highlightedJob"
- @highlightedJobsChange="updateHighlightedJobs"
- @error="onError"
- >
- <div
- v-for="(stage, index) in pipelineStages"
- :key="`${stage.name}-${index}`"
- class="gl-flex-direction-column"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5"
- data-qa-selector="stage_container"
- >
- <stage-name :stage-name="stage.name" />
- </div>
- <div :class="$options.jobWrapperClasses">
- <job-pill
- v-for="group in stage.groups"
- :key="group.name"
- :job-name="group.name"
- :pipeline-id="$options.PIPELINE_ID"
- :is-hovered="highlightedJob === group.name"
- :is-faded-out="isFadedOut(group.name)"
- data-qa-selector="job_container"
- @on-mouse-enter="setHoveredJob"
- @on-mouse-leave="removeHoveredJob"
- />
- </div>
- </div>
- </links-layer>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
deleted file mode 100644
index 600832b7633..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
+++ /dev/null
@@ -1,22 +0,0 @@
-<script>
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-
-export default {
- components: {
- TooltipOnTruncate,
- },
- props: {
- stageName: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-<template>
- <tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
- <div class="gl-py-2 gl-text-truncate gl-font-weight-bold gl-w-20">
- {{ stageName }}
- </div>
- </tooltip-on-truncate>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
deleted file mode 100644
index 1ca9e35c008..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/accessors/linked_pipelines_accessors.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { get } from 'lodash';
-
-export const accessors = {
- rest: {
- detailedStatus: ['details', 'status'],
- },
- graphql: {
- detailedStatus: 'detailedStatus',
- },
-};
-
-export const accessValue = (pipeline, dataMethod, path) => {
- return get(pipeline, accessors[dataMethod][path]);
-};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
deleted file mode 100644
index 7f97097def6..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ /dev/null
@@ -1,13 +0,0 @@
-<script>
-export default {
- props: {
- job: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-<template>
- <div>{{ job.id }}</div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue
deleted file mode 100644
index d6e585d093b..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_job_item.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script>
-import { GlTooltipDirective, GlLink } from '@gitlab/ui';
-import delayedJobMixin from '~/jobs/mixins/delayed_job_mixin';
-import { s__, sprintf } from '~/locale';
-import { reportToSentry } from '../../utils';
-import ActionComponent from '../jobs_shared/action_component.vue';
-import JobNameComponent from '../jobs_shared/job_name_component.vue';
-import { ICONS } from '../../constants';
-
-/**
- * Renders the badge for the pipeline graph and the job's dropdown.
- *
- * The following object should be provided as `job`:
- *
- * {
- * "id": 4256,
- * "name": "test",
- * "status": {
- * "icon": "status_success",
- * "text": "passed",
- * "label": "passed",
- * "group": "success",
- * "tooltip": "passed",
- * "details_path": "/root/ci-mock/builds/4256",
- * "action": {
- * "icon": "retry",
- * "title": "Retry",
- * "path": "/root/ci-mock/builds/4256/retry",
- * "method": "post"
- * }
- * }
- * }
- */
-
-export default {
- i18n: {
- runAgainTooltipText: s__('Pipeline|Run again'),
- },
- tooltipConfig: {
- boundary: 'viewport',
- placement: 'bottom',
- customClass: 'gl-pointer-events-none',
- },
- components: {
- ActionComponent,
- JobNameComponent,
- GlLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [delayedJobMixin],
- props: {
- job: {
- type: Object,
- required: true,
- },
- cssClassJobName: {
- type: String,
- required: false,
- default: '',
- },
- dropdownLength: {
- type: Number,
- required: false,
- default: Infinity,
- },
- pipelineId: {
- type: Number,
- required: false,
- default: -1,
- },
- },
- computed: {
- boundary() {
- return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
- },
- detailsPath() {
- return this.status?.details_path;
- },
- hasDetails() {
- return this.status?.has_details;
- },
- status() {
- return this.job?.status ? this.job.status : {};
- },
- tooltipText() {
- const textBuilder = [];
- const { name: jobName } = this.job;
-
- if (jobName) {
- textBuilder.push(jobName);
- }
-
- const { tooltip: statusTooltip } = this.status;
- if (jobName && statusTooltip) {
- textBuilder.push('-');
- }
-
- if (statusTooltip) {
- if (this.isDelayedJob) {
- textBuilder.push(sprintf(statusTooltip, { remainingTime: this.remainingTime }));
- } else {
- textBuilder.push(statusTooltip);
- }
- }
-
- return textBuilder.join(' ');
- },
- /**
- * Verifies if the provided job has an action path
- *
- * @return {Boolean}
- */
- hasJobAction() {
- return Boolean(this.job?.status?.action?.path);
- },
- jobActionTooltipText() {
- const { group } = this.status;
- const { title, icon } = this.status.action;
-
- return icon === ICONS.RETRY && group === ICONS.SUCCESS
- ? this.$options.i18n.runAgainTooltipText
- : title;
- },
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipelines_job_item', `pipelines_job_item error: ${err}, info: ${info}`);
- },
-};
-</script>
-<template>
- <div
- class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
- data-qa-selector="job_item_container"
- >
- <gl-link
- v-if="hasDetails"
- v-gl-tooltip="$options.tooltipConfig"
- :href="detailsPath"
- :title="tooltipText"
- :class="cssClassJobName"
- class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none"
- data-testid="job-with-link"
- >
- <job-name-component :name="job.name" :status="job.status" />
- </gl-link>
-
- <div
- v-else
- v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
- :title="tooltipText"
- :class="cssClassJobName"
- class="js-job-component-tooltip non-details-job-component menu-item"
- data-testid="job-without-link"
- >
- <job-name-component :name="job.name" :status="job.status" />
- </div>
-
- <action-component
- v-if="hasJobAction"
- :tooltip-text="jobActionTooltipText"
- :link="status.action.path"
- :action-icon="status.action.icon"
- data-qa-selector="action_button"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
deleted file mode 100644
index 8c0e65d1d39..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue
+++ /dev/null
@@ -1,98 +0,0 @@
-<script>
-import { GlIcon } from '@gitlab/ui';
-import PipelineStages from './pipeline_stages.vue';
-import LinkedPipelinesMiniList from './linked_pipelines_mini_list.vue';
-/**
- * Renders the pipeline mini graph.
- * TODO: After all apps have updated to GraphQL data and use the `pipeline_mini_graph.vue` file as an entry,
- * we should rename this file to `pipeline_mini_graph_wrapper.vue`
- */
-export default {
- components: {
- GlIcon,
- LinkedPipelinesMiniList,
- PipelineStages,
- },
- arrowStyles: [
- 'arrow-icon gl-display-inline-block gl-mx-1 gl-text-gray-500 gl-vertical-align-middle!',
- ],
- props: {
- downstreamPipelines: {
- type: Array,
- required: false,
- default: () => [],
- },
- isGraphql: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMergeTrain: {
- type: Boolean,
- required: false,
- default: false,
- },
- pipelinePath: {
- type: String,
- required: false,
- default: '',
- },
- stages: {
- type: Array,
- required: true,
- default: () => [],
- },
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- upstreamPipeline: {
- type: Object,
- required: false,
- default: () => {},
- },
- },
- computed: {
- hasDownstreamPipelines() {
- return Boolean(this.downstreamPipelines.length);
- },
- },
-};
-</script>
-<template>
- <div data-testid="pipeline-mini-graph">
- <linked-pipelines-mini-list
- v-if="upstreamPipeline"
- :triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- upstreamPipeline,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- data-testid="pipeline-mini-graph-upstream"
- />
- <gl-icon
- v-if="upstreamPipeline"
- :class="$options.arrowStyles"
- name="long-arrow"
- data-testid="upstream-arrow-icon"
- />
- <pipeline-stages
- :is-graphql="isGraphql"
- :is-merge-train="isMergeTrain"
- :stages="stages"
- :update-dropdown="updateDropdown"
- @miniGraphStageClick="$emit('miniGraphStageClick')"
- />
- <gl-icon
- v-if="hasDownstreamPipelines"
- :class="$options.arrowStyles"
- name="long-arrow"
- data-testid="downstream-arrow-icon"
- />
- <linked-pipelines-mini-list
- v-if="hasDownstreamPipelines"
- :triggered="downstreamPipelines"
- :pipeline-path="pipelinePath"
- data-testid="pipeline-mini-graph-downstream"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue
deleted file mode 100644
index 048e42731c7..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ /dev/null
@@ -1,176 +0,0 @@
-<script>
-/**
- * Renders each stage of the pipeline mini graph.
- *
- * Given the provided endpoint will make a request to
- * fetch the dropdown data when the stage is clicked.
- *
- * Request is made inside this component to make it reusable between:
- * 1. Pipelines main table
- * 2. Pipelines table in commit and Merge request views
- * 3. Merge request widget
- * 4. Commit widget
- */
-
-import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { __, s__, sprintf } from '~/locale';
-import eventHub from '../../event_hub';
-import LegacyJobItem from './legacy_job_item.vue';
-
-export default {
- i18n: {
- errorMessage: __('Something went wrong on our end.'),
- loadingText: __('Loading...'),
- mergeTrainMessage: s__('Pipeline|Merge train pipeline jobs can not be retried'),
- stage: __('Stage:'),
- viewStageLabel: __('View Stage: %{title}'),
- },
- dropdownPopperOpts: {
- placement: 'bottom',
- positionFixed: true,
- },
- components: {
- CiIcon,
- GlLoadingIcon,
- GlDropdown,
- LegacyJobItem,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- stage: {
- type: Object,
- required: true,
- },
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMergeTrain: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isDropdownOpen: false,
- isLoading: false,
- dropdownContent: [],
- stageName: '',
- };
- },
- watch: {
- updateDropdown() {
- if (this.updateDropdown && this.isDropdownOpen && !this.isLoading) {
- this.fetchJobs();
- }
- },
- },
- methods: {
- onHideDropdown() {
- this.isDropdownOpen = false;
- },
- onShowDropdown() {
- eventHub.$emit('clickedDropdown');
- this.isDropdownOpen = true;
- this.isLoading = true;
- this.fetchJobs();
-
- // used for tracking and is separate from event hub
- // to avoid complexity with mixin
- this.$emit('miniGraphStageClick');
- },
- fetchJobs() {
- axios
- .get(this.stage.dropdown_path)
- .then(({ data }) => {
- this.dropdownContent = data.latest_statuses;
- this.stageName = data.name;
- this.isLoading = false;
- })
- .catch(() => {
- this.$refs.dropdown.hide();
- this.isLoading = false;
-
- createAlert({
- message: this.$options.i18n.errorMessage,
- });
- });
- },
- stageAriaLabel(title) {
- return sprintf(this.$options.i18n.viewStageLabel, { title });
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- ref="dropdown"
- v-gl-tooltip.hover.ds0
- v-gl-tooltip="stage.title"
- data-testid="mini-pipeline-graph-dropdown"
- variant="link"
- :aria-label="stageAriaLabel(stage.title)"
- :lazy="true"
- :popper-opts="$options.dropdownPopperOpts"
- :toggle-class="['gl-rounded-full!']"
- menu-class="mini-pipeline-graph-dropdown-menu"
- @hide="onHideDropdown"
- @show="onShowDropdown"
- >
- <template #button-content>
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :is-active="isDropdownOpen"
- :size="24"
- :status="stage.status"
- class="gl-display-inline-flex gl-align-items-center gl-border gl-z-index-1"
- />
- </template>
- <div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
- <gl-loading-icon size="sm" class="gl-mr-3" />
- <p class="gl-line-height-normal gl-mb-0">{{ $options.i18n.loadingText }}</p>
- </div>
- <ul
- v-else
- class="js-builds-dropdown-list scrollable-menu"
- data-testid="mini-pipeline-graph-dropdown-menu-list"
- >
- <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-mb-3 gl-pb-3">
- <span class="gl-mr-1">{{ $options.i18n.stage }}</span>
- <span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
- </div>
- <li v-for="job in dropdownContent" :key="job.id">
- <legacy-job-item
- :dropdown-length="dropdownContent.length"
- :job="job"
- css-class-job-name="pipeline-job-item"
- />
- </li>
- <template v-if="isMergeTrain">
- <li class="gl-dropdown-divider" role="presentation">
- <hr role="separator" aria-orientation="horizontal" class="dropdown-divider" />
- </li>
- <li>
- <div
- class="gl-display-flex gl-align-items-center"
- data-testid="warning-message-merge-trains"
- >
- <div class="menu-item gl-font-sm gl-text-gray-300!">
- {{ $options.i18n.mergeTrainMessage }}
- </div>
- </div>
- </li>
- </template>
- </ul>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue
deleted file mode 100644
index a5c6dc98694..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { accessValue } from './accessors/linked_pipelines_accessors';
-/**
- * Renders the upstream/downstream portions of the pipeline mini graph.
- */
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- CiIcon,
- },
- inject: {
- dataMethod: {
- default: 'rest',
- },
- },
- props: {
- triggeredBy: {
- type: Array,
- required: false,
- default: () => [],
- },
- triggered: {
- type: Array,
- required: false,
- default: () => [],
- },
- pipelinePath: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- maxRenderedPipelines: 3,
- };
- },
- computed: {
- // Exactly one of these (triggeredBy and triggered) must be truthy. Never both. Never neither.
- isUpstream() {
- return Boolean(this.triggeredBy.length) && !this.triggered.length;
- },
- isDownstream() {
- return !this.triggeredBy.length && Boolean(this.triggered.length);
- },
- linkedPipelines() {
- return this.isUpstream ? this.triggeredBy : this.triggered;
- },
- totalPipelineCount() {
- return this.linkedPipelines.length;
- },
- linkedPipelinesTrimmed() {
- return this.totalPipelineCount > this.maxRenderedPipelines
- ? this.linkedPipelines.slice(0, this.maxRenderedPipelines)
- : this.linkedPipelines;
- },
- shouldRenderCounter() {
- return this.isDownstream && this.linkedPipelines.length > this.maxRenderedPipelines;
- },
- counterLabel() {
- return `+${this.linkedPipelines.length - this.maxRenderedPipelines}`;
- },
- counterTooltipText() {
- return sprintf(s__('LinkedPipelines|%{counterLabel} more downstream pipelines'), {
- counterLabel: this.counterLabel,
- });
- },
- },
- methods: {
- pipelineTooltipText(pipeline) {
- const { label } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
-
- return `${pipeline.project.name} - ${label}`;
- },
- pipelineStatus(pipeline) {
- // detailedStatus is graphQL, details.status is REST
- return pipeline?.detailedStatus || pipeline?.details?.status;
- },
- triggerButtonClass(pipeline) {
- const { group } = accessValue(pipeline, this.dataMethod, 'detailedStatus');
-
- return `ci-status-icon-${group}`;
- },
- },
-};
-</script>
-
-<template>
- <span
- v-if="linkedPipelines"
- :class="{
- 'is-upstream': isUpstream,
- 'is-downstream': isDownstream,
- }"
- class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
- >
- <a
- v-for="pipeline in linkedPipelinesTrimmed"
- :key="pipeline.id"
- v-gl-tooltip="{ title: pipelineTooltipText(pipeline) }"
- :href="pipeline.path"
- :class="triggerButtonClass(pipeline)"
- class="linked-pipeline-mini-item gl-display-inline-block gl-h-6 gl-mr-2 gl-my-2 gl-rounded-full gl-vertical-align-middle"
- data-testid="linked-pipeline-mini-item"
- >
- <ci-icon
- is-borderless
- is-interactive
- css-classes="gl-rounded-full"
- :size="24"
- :status="pipelineStatus(pipeline)"
- class="gl-align-items-center gl-border gl-display-inline-flex"
- />
- </a>
-
- <a
- v-if="shouldRenderCounter"
- v-gl-tooltip="{ title: counterTooltipText }"
- :title="counterTooltipText"
- :href="pipelinePath"
- class="gl-align-items-center gl-bg-gray-50 gl-display-inline-flex gl-font-sm gl-h-6 gl-justify-content-center gl-rounded-pill gl-text-decoration-none gl-text-gray-500 gl-w-7 linked-pipelines-counter linked-pipeline-mini-item"
- data-testid="linked-pipeline-counter"
- >
- {{ counterLabel }}
- </a>
- </span>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
deleted file mode 100644
index 7cdaec81466..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ /dev/null
@@ -1,150 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { __ } from '~/locale';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import getLinkedPipelinesQuery from '~/pipelines/graphql/queries/get_linked_pipelines.query.graphql';
-import getPipelineStagesQuery from '~/pipelines/graphql/queries/get_pipeline_stages.query.graphql';
-import LegacyPipelineMiniGraph from './legacy_pipeline_mini_graph.vue';
-
-export default {
- i18n: {
- linkedPipelinesFetchError: __('There was a problem fetching linked pipelines.'),
- stagesFetchError: __('There was a problem fetching the pipeline stages.'),
- },
- components: {
- GlLoadingIcon,
- LegacyPipelineMiniGraph,
- },
- props: {
- pipelineEtag: {
- type: String,
- required: true,
- },
- fullPath: {
- type: String,
- required: true,
- },
- iid: {
- type: String,
- required: true,
- },
- isMergeTrain: {
- type: Boolean,
- required: false,
- default: false,
- },
- pollInterval: {
- type: Number,
- required: false,
- default: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
- },
- },
- data() {
- return {
- linkedPipelines: null,
- pipelineStages: [],
- };
- },
- apollo: {
- linkedPipelines: {
- context() {
- return getQueryHeaders(this.pipelineEtag);
- },
- query: getLinkedPipelinesQuery,
- pollInterval() {
- return this.pollInterval;
- },
- variables() {
- return {
- fullPath: this.fullPath,
- iid: this.iid,
- };
- },
- update({ project }) {
- return project?.pipeline || this.linkedpipelines;
- },
- error() {
- createAlert({ message: this.$options.i18n.linkedPipelinesFetchError });
- },
- },
- pipelineStages: {
- context() {
- return getQueryHeaders(this.pipelineEtag);
- },
- query: getPipelineStagesQuery,
- pollInterval() {
- return this.pollInterval;
- },
- variables() {
- return {
- fullPath: this.fullPath,
- iid: this.iid,
- };
- },
- update({ project }) {
- return project?.pipeline?.stages?.nodes || this.pipelineStages;
- },
- error() {
- createAlert({ message: this.$options.i18n.stagesFetchError });
- },
- },
- },
- computed: {
- downstreamPipelines() {
- return keepLatestDownstreamPipelines(this.linkedPipelines?.downstream?.nodes);
- },
- formattedStages() {
- return this.pipelineStages.map((stage) => {
- const { name, detailedStatus } = stage;
- return {
- // TODO: Once we fetch stage by ID with GraphQL,
- // this method will change.
- // see https://gitlab.com/gitlab-org/gitlab/-/issues/384853
- id: stage.id,
- dropdown_path: `${this.pipelinePath}/stage.json?stage=${name}`,
- name,
- path: `${this.pipelinePath}#${name}`,
- status: {
- details_path: `${this.pipelinePath}#${name}`,
- has_details: detailedStatus?.hasDetails || false,
- ...detailedStatus,
- },
- title: `${name}: ${detailedStatus?.text || ''}`,
- };
- });
- },
- pipelinePath() {
- return this.linkedPipelines?.path || '';
- },
- upstreamPipeline() {
- return this.linkedPipelines?.upstream;
- },
- },
- mounted() {
- toggleQueryPollingByVisibility(this.$apollo.queries.linkedPipelines);
- toggleQueryPollingByVisibility(this.$apollo.queries.pipelineStages);
- },
-};
-</script>
-
-<template>
- <div>
- <gl-loading-icon v-if="$apollo.queries.pipelineStages.loading" />
- <legacy-pipeline-mini-graph
- v-else
- data-testid="pipeline-mini-graph"
- is-graphql
- :downstream-pipelines="downstreamPipelines"
- :is-merge-train="isMergeTrain"
- :pipeline-path="pipelinePath"
- :stages="formattedStages"
- :upstream-pipeline="upstreamPipeline"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
deleted file mode 100644
index 8e22f440089..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ /dev/null
@@ -1,84 +0,0 @@
-<script>
-import { createAlert } from '~/alert';
-import { __ } from '~/locale';
-import { PIPELINE_MINI_GRAPH_POLL_INTERVAL } from '~/pipelines/constants';
-import {
- getQueryHeaders,
- toggleQueryPollingByVisibility,
-} from '~/pipelines/components/graph/utils';
-import getPipelineStageQuery from '~/pipelines/graphql/queries/get_pipeline_stage.query.graphql';
-import JobItem from './job_item.vue';
-
-export default {
- i18n: {
- stageFetchError: __('There was a problem fetching the pipeline stage.'),
- },
-
- components: {
- JobItem,
- },
- props: {
- isMergeTrain: {
- type: Boolean,
- required: false,
- default: false,
- },
- pipelineEtag: {
- type: String,
- required: true,
- },
- pollInterval: {
- type: Number,
- required: false,
- default: PIPELINE_MINI_GRAPH_POLL_INTERVAL,
- },
- stageId: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- jobs: [],
- stage: null,
- };
- },
- apollo: {
- stage: {
- context() {
- return getQueryHeaders(this.pipelineEtag);
- },
- query: getPipelineStageQuery,
- pollInterval() {
- return this.pollInterval;
- },
- variables() {
- return {
- id: this.stageId,
- };
- },
- skip() {
- return !this.stageId;
- },
- update(data) {
- this.jobs = data?.ciPipelineStage?.jobs.nodes;
- return data?.ciPipelineStage;
- },
- error() {
- createAlert({ message: this.$options.i18n.stageFetchError });
- },
- },
- },
- mounted() {
- toggleQueryPollingByVisibility(this.$apollo.queries.stage);
- },
-};
-</script>
-
-<template>
- <div data-testid="pipeline-stage">
- <ul v-for="job in jobs" :key="job.id">
- <job-item :job="job" />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
deleted file mode 100644
index 02dba9ba30f..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import PipelineStage from './pipeline_stage.vue';
-import LegacyPipelineStage from './legacy_pipeline_stage.vue';
-/**
- * Renders the pipeline stages portion of the pipeline mini graph.
- */
-export default {
- components: {
- LegacyPipelineStage,
- PipelineStage,
- },
- props: {
- stages: {
- type: Array,
- required: true,
- },
- updateDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- isGraphql: {
- type: Boolean,
- required: false,
- default: false,
- },
- isMergeTrain: {
- type: Boolean,
- required: false,
- default: false,
- },
- pipelineEtag: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-<template>
- <div class="gl-display-inline gl-vertical-align-middle">
- <div
- v-for="stage in stages"
- :key="stage.name"
- class="pipeline-mini-graph-stage-container dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle"
- >
- <pipeline-stage
- v-if="isGraphql"
- :stage-id="stage.id"
- :is-merge-train="isMergeTrain"
- :pipeline-etag="pipelineEtag"
- @miniGraphStageClick="$emit('miniGraphStageClick')"
- />
- <legacy-pipeline-stage
- v-else
- :stage="stage"
- :update-dropdown="updateDropdown"
- :is-merge-train="isMergeTrain"
- @miniGraphStageClick="$emit('miniGraphStageClick')"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue b/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
deleted file mode 100644
index 35dde6379dd..00000000000
--- a/app/assets/javascripts/pipelines/components/pipeline_tabs.vue
+++ /dev/null
@@ -1,138 +0,0 @@
-<script>
-import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import {
- failedJobsTabName,
- jobsTabName,
- needsTabName,
- pipelineTabName,
- testReportTabName,
- TRACKING_CATEGORIES,
-} from '../constants';
-
-export default {
- i18n: {
- tabs: {
- failedJobsTitle: __('Failed Jobs'),
- jobsTitle: __('Jobs'),
- needsTitle: __('Needs'),
- pipelineTitle: __('Pipeline'),
- testsTitle: __('Tests'),
- },
- },
- tabNames: {
- pipeline: pipelineTabName,
- needs: needsTabName,
- jobs: jobsTabName,
- failures: failedJobsTabName,
- tests: testReportTabName,
- },
- components: {
- GlBadge,
- GlTab,
- GlTabs,
- },
- mixins: [Tracking.mixin()],
- inject: ['defaultTabValue', 'failedJobsCount', 'totalJobCount', 'testsCount'],
- data() {
- return {
- activeTab: this.defaultTabValue,
- };
- },
- computed: {
- showFailedJobsTab() {
- return this.failedJobsCount > 0;
- },
- },
- watch: {
- $route(to) {
- this.activeTab = to.name;
- },
- },
- methods: {
- isActive(tabName) {
- return tabName === this.activeTab;
- },
- navigateTo(tabName) {
- if (this.isActive(tabName)) return;
-
- this.$router.push({ name: tabName });
- },
- failedJobsTabClick() {
- this.track('click_tab', { label: TRACKING_CATEGORIES.failed });
-
- this.navigateTo(this.$options.tabNames.failures);
- },
- testsTabClick() {
- this.track('click_tab', { label: TRACKING_CATEGORIES.tests });
-
- this.navigateTo(this.$options.tabNames.tests);
- },
- },
-};
-</script>
-
-<template>
- <gl-tabs>
- <gl-tab
- ref="pipelineTab"
- :title="$options.i18n.tabs.pipelineTitle"
- :active="isActive($options.tabNames.pipeline)"
- data-testid="pipeline-tab"
- lazy
- @click="navigateTo($options.tabNames.pipeline)"
- >
- <router-view />
- </gl-tab>
- <gl-tab
- ref="dagTab"
- :title="$options.i18n.tabs.needsTitle"
- :active="isActive($options.tabNames.needs)"
- data-testid="dag-tab"
- lazy
- @click="navigateTo($options.tabNames.needs)"
- >
- <router-view />
- </gl-tab>
- <gl-tab
- :active="isActive($options.tabNames.jobs)"
- data-testid="jobs-tab"
- lazy
- @click="navigateTo($options.tabNames.jobs)"
- >
- <template #title>
- <span class="gl-mr-2">{{ $options.i18n.tabs.jobsTitle }}</span>
- <gl-badge size="sm" data-testid="builds-counter">{{ totalJobCount }}</gl-badge>
- </template>
- <router-view />
- </gl-tab>
- <gl-tab
- v-if="showFailedJobsTab"
- :title="$options.i18n.tabs.failedJobsTitle"
- :active="isActive($options.tabNames.failures)"
- data-testid="failed-jobs-tab"
- lazy
- @click="failedJobsTabClick"
- >
- <template #title>
- <span class="gl-mr-2">{{ $options.i18n.tabs.failedJobsTitle }}</span>
- <gl-badge size="sm" data-testid="failed-builds-counter">{{ failedJobsCount }}</gl-badge>
- </template>
- <router-view />
- </gl-tab>
- <gl-tab
- :active="isActive($options.tabNames.tests)"
- data-testid="tests-tab"
- lazy
- @click="testsTabClick"
- >
- <template #title>
- <span class="gl-mr-2">{{ $options.i18n.tabs.testsTitle }}</span>
- <gl-badge size="sm" data-testid="tests-counter">{{ testsCount }}</gl-badge>
- </template>
- <router-view />
- </gl-tab>
- <slot></slot>
- </gl-tabs>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
deleted file mode 100644
index 3bbdfc73e1b..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
-import PipelinesCiTemplates from './empty_state/pipelines_ci_templates.vue';
-import IosTemplates from './empty_state/ios_templates.vue';
-
-export default {
- i18n: {
- noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'),
- },
- name: 'PipelinesEmptyState',
- components: {
- GlEmptyState,
- GitlabExperiment,
- PipelinesCiTemplates,
- IosTemplates,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- canSetCi: {
- type: Boolean,
- required: true,
- },
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- },
-};
-</script>
-<template>
- <div>
- <gitlab-experiment v-if="canSetCi" name="ios_specific_templates">
- <template #control>
- <pipelines-ci-templates />
- </template>
- <template #candidate>
- <ios-templates :registration-token="registrationToken" />
- </template>
- </gitlab-experiment>
- <gl-empty-state
- v-else
- title=""
- :svg-path="emptyStateSvgPath"
- :description="$options.i18n.noCiDescription"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
deleted file mode 100644
index 439dc0eb253..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ci_templates.vue
+++ /dev/null
@@ -1,106 +0,0 @@
-<script>
-import { GlAvatar, GlButton } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-import Tracking from '~/tracking';
-
-export default {
- components: {
- GlAvatar,
- GlButton,
- },
- mixins: [Tracking.mixin()],
- inject: ['pipelineEditorPath', 'suggestedCiTemplates'],
- props: {
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- filterTemplates: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- const templates = this.suggestedCiTemplates
- .filter(
- (template) => !this.filterTemplates.length || this.filterTemplates.includes(template.name),
- )
- .map(({ name, logo, title }) => {
- return {
- name: title || name,
- description: sprintf(this.$options.i18n.description, { name: title || name }),
- isPng: logo.endsWith('png'),
- logo,
- link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
- };
- });
-
- return {
- templates,
- };
- },
- methods: {
- trackEvent(template) {
- this.track('template_clicked', {
- label: template,
- });
- },
- logoStyle(template) {
- return template.isPng ? { objectFit: 'contain' } : '';
- },
- },
- i18n: {
- description: s__(
- 'Pipelines|Continuous integration and deployment template to test and deploy your %{name} project.',
- ),
- cta: s__('Pipelines|Use template'),
- },
- AVATAR_SHAPE_OPTION_RECT,
-};
-</script>
-<template>
- <ul class="gl-list-style-none gl-pl-0">
- <li v-for="template in templates" :key="template.name">
- <div
- class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-pt-3"
- >
- <div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
- <gl-avatar
- :alt="template.name"
- class="gl-mr-5 gl-bg-white dark-mode-override"
- :class="{ 'gl-p-2': template.isPng }"
- :style="logoStyle(template)"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- :size="48"
- :src="template.logo"
- data-testid="template-logo"
- />
- <div class="gl-flex-direction-row">
- <div class="gl-mb-3">
- <strong class="gl-text-gray-800" data-testid="template-name">
- {{ template.name }}
- </strong>
- </div>
- <p class="gl-mb-0 gl-font-sm" data-testid="template-description">
- {{ template.description }}
- </p>
- </div>
- </div>
- <gl-button
- :disabled="disabled"
- category="primary"
- variant="confirm"
- :href="template.link"
- data-testid="template-link"
- @click="trackEvent(template.name)"
- >
- {{ $options.i18n.cta }}
- </gl-button>
- </div>
- </li>
- </ul>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
deleted file mode 100644
index 5208f9a3ce7..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/ios_templates.vue
+++ /dev/null
@@ -1,220 +0,0 @@
-<script>
-import { GlButton, GlCard, GlSprintf, GlLink, GlPopover, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { mergeUrlParams, DOCS_URL } from '~/lib/utils/url_utility';
-import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
-import apolloProvider from '~/pipelines/graphql/provider';
-import CiTemplates from './ci_templates.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlSprintf,
- GlLink,
- GlPopover,
- RunnerInstructionsModal,
- CiTemplates,
- },
- directives: {
- GlModalDirective,
- },
- inject: ['pipelineEditorPath', 'iosRunnersAvailable'],
- props: {
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- },
- apolloProvider,
- iOSTemplateName: 'iOS-Fastlane',
- modalId: 'runner-instructions-modal',
- runnerDocsLink: `${DOCS_URL}/runner/install/osx`,
- whatElseLink: helpPagePath('ci/index.md'),
- i18n: {
- title: s__('Pipelines|Get started with GitLab CI/CD'),
- subtitle: s__('Pipelines|Building for iOS?'),
- explanation: s__("Pipelines|We'll walk you through how to deploy to iOS in two easy steps."),
- runnerSetupTitle: s__('Pipelines|1. Set up a runner'),
- runnerSetupButton: s__('Pipelines|Set up a runner'),
- runnerSetupBodyUnfinished: s__(
- 'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline.',
- ),
- runnerSetupBodyFinished: s__(
- 'Pipelines|You have runners available to run your job now. No need to do anything else.',
- ),
- runnerSetupPopoverTitle: s__(
- "Pipelines|Let's get that runner set up! %{emojiStart}tada%{emojiEnd}",
- ),
- runnerSetupPopoverBodyLine1: s__(
- 'Pipelines|Follow these instructions to install GitLab Runner on macOS.',
- ),
- runnerSetupPopoverBodyLine2: s__(
- 'Pipelines|Need more information to set up your runner? %{linkStart}Check out our documentation%{linkEnd}.',
- ),
- configurePipelineTitle: s__('Pipelines|2. Configure deployment pipeline'),
- configurePipelineBody: s__("Pipelines|We'll guide you through a simple pipeline set-up."),
- configurePipelineButton: s__('Pipelines|Configure pipeline'),
- noWalkthroughTitle: s__("Pipelines|Don't need a guide? Jump in right away with a template."),
- noWalkthroughExplanation: s__('Pipelines|Based on your project, we recommend this template:'),
- notBuildingForIos: s__(
- "Pipelines|Not building for iOS or not what you're looking for? %{linkStart}See what else%{linkEnd} GitLab CI/CD has to offer.",
- ),
- },
- data() {
- return {
- isModalShown: false,
- isPopoverShown: false,
- isRunnerSetupFinished: this.iosRunnersAvailable,
- popoverTarget: `${this.$options.modalId}___BV_modal_content_`,
- configurePipelineLink: mergeUrlParams(
- { template: this.$options.iOSTemplateName },
- this.pipelineEditorPath,
- ),
- };
- },
- computed: {
- runnerSetupBodyText() {
- return this.iosRunnersAvailable
- ? this.$options.i18n.runnerSetupBodyFinished
- : this.$options.i18n.runnerSetupBodyUnfinished;
- },
- },
- methods: {
- showModal() {
- this.isModalShown = true;
- },
- hideModal() {
- this.togglePopover();
- this.isRunnerSetupFinished = true;
- },
- togglePopover() {
- this.isPopoverShown = !this.isPopoverShown;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.i18n.title }}</h2>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-1">{{ $options.i18n.subtitle }}</h3>
- <p>{{ $options.i18n.explanation }}</p>
-
- <div class="gl-lg-display-flex">
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5">
- <gl-emoji
- v-show="isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="white_check_mark"
- data-testid="runner-setup-marked-completed"
- />
- <gl-emoji
- v-show="!isRunnerSetupFinished"
- class="gl-font-size-h2-xl"
- data-name="tools"
- data-testid="runner-setup-marked-todo"
- />
- </div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.runnerSetupTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ runnerSetupBodyText }}</p>
- </div>
-
- <gl-button
- v-if="!iosRunnersAvailable"
- v-gl-modal-directive="$options.modalId"
- category="primary"
- variant="confirm"
- @click="showModal"
- >
- {{ $options.i18n.runnerSetupButton }}
- </gl-button>
- <runner-instructions-modal
- v-if="isModalShown"
- :modal-id="$options.modalId"
- :registration-token="registrationToken"
- default-platform-name="osx"
- @shown="togglePopover"
- @hide="hideModal"
- />
- <gl-popover
- v-if="isPopoverShown"
- :show="true"
- :show-close-button="true"
- :target="popoverTarget"
- triggers="manual"
- placement="left"
- fallback-placement="clockwise"
- >
- <template #title>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverTitle">
- <template #emoji="{ content }">
- <gl-emoji class="gl-ml-2" :data-name="content" />
- </template>
- </gl-sprintf>
- </template>
- <div class="gl-mb-5">
- {{ $options.i18n.runnerSetupPopoverBodyLine1 }}
- </div>
- <gl-sprintf :message="$options.i18n.runnerSetupPopoverBodyLine2">
- <template #link="{ content }">
- <gl-link :href="$options.runnerDocsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-popover>
- </div>
- </gl-card>
- </div>
- <div class="gl-lg-display-flex gl-lg-w-25p gl-lg-pr-4 gl-mb-4">
- <gl-card body-class="gl-display-flex gl-flex-grow-1">
- <div
- class="gl-display-flex gl-flex-grow-1 gl-flex-direction-column gl-justify-content-space-between gl-align-items-flex-start"
- >
- <div>
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="tools" /></div>
- <span class="gl-text-gray-800 gl-font-weight-bold">
- {{ $options.i18n.configurePipelineTitle }}
- </span>
- <p class="gl-font-sm gl-mt-3">{{ $options.i18n.configurePipelineBody }}</p>
- </div>
-
- <gl-button
- :disabled="!isRunnerSetupFinished"
- category="primary"
- variant="confirm"
- data-testid="configure-pipeline-link"
- :href="configurePipelineLink"
- >
- {{ $options.i18n.configurePipelineButton }}
- </gl-button>
- </div>
- </gl-card>
- </div>
- </div>
- <h3 class="gl-font-lg gl-text-gray-900 gl-mt-5">{{ $options.i18n.noWalkthroughTitle }}</h3>
- <p>{{ $options.i18n.noWalkthroughExplanation }}</p>
- <ci-templates
- :filter-templates="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- $options.iOSTemplateName,
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- :disabled="!isRunnerSetupFinished"
- />
- <p>
- <gl-sprintf :message="$options.i18n.notBuildingForIos">
- <template #link="{ content }">
- <gl-link :href="$options.whatElseLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
deleted file mode 100644
index a6297213402..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ /dev/null
@@ -1,79 +0,0 @@
-<script>
-import { GlButton, GlCard, GlSprintf } from '@gitlab/ui';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { STARTER_TEMPLATE_NAME, I18N } from '~/ci/pipeline_editor/constants';
-import Tracking from '~/tracking';
-import CiTemplates from './ci_templates.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlSprintf,
- CiTemplates,
- },
- mixins: [Tracking.mixin()],
- STARTER_TEMPLATE_NAME,
- I18N,
- inject: ['pipelineEditorPath'],
- data() {
- return {
- gettingStartedTemplateUrl: mergeUrlParams(
- { template: STARTER_TEMPLATE_NAME },
- this.pipelineEditorPath,
- ),
- tracker: null,
- };
- },
- methods: {
- trackEvent(template) {
- this.track('template_clicked', {
- label: template,
- });
- },
- },
-};
-</script>
-<template>
- <div>
- <h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.I18N.title }}</h2>
-
- <h2 class="gl-font-lg gl-text-gray-900">{{ $options.I18N.learnBasics.title }}</h2>
- <p class="gl-text-gray-800 gl-mb-6">
- <gl-sprintf :message="$options.I18N.learnBasics.subtitle">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- </gl-sprintf>
- </p>
-
- <div class="gl-lg-w-25p gl-lg-pr-5 gl-mb-8">
- <gl-card>
- <div class="gl-flex-direction-row">
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
- <div class="gl-mb-3">
- <strong class="gl-text-gray-800 gl-mb-2">
- {{ $options.I18N.learnBasics.gettingStarted.title }}
- </strong>
- </div>
- <p class="gl-font-sm">{{ $options.I18N.learnBasics.gettingStarted.description }}</p>
- </div>
-
- <gl-button
- category="primary"
- variant="confirm"
- :href="gettingStartedTemplateUrl"
- data-testid="test-template-link"
- @click="trackEvent($options.STARTER_TEMPLATE_NAME)"
- >
- {{ $options.I18N.learnBasics.gettingStarted.cta }}
- </gl-button>
- </gl-card>
- </div>
-
- <h2 class="gl-font-lg gl-text-gray-900">{{ $options.I18N.templates.title }}</h2>
- <p class="gl-text-gray-800 gl-mb-6">{{ $options.I18N.templates.subtitle }}</p>
-
- <ci-templates />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
deleted file mode 100644
index edf4cc87a87..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<script>
-import { GlButton, GlIcon, GlLink, GlTooltip } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { __, s__, sprintf } from '~/locale';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { BRIDGE_KIND } from '~/pipelines/components/graph/constants';
-import RetryMrFailedJobMutation from '../../../graphql/mutations/retry_mr_failed_job.mutation.graphql';
-
-export default {
- components: {
- CiIcon,
- GlButton,
- GlIcon,
- GlLink,
- GlTooltip,
- },
- directives: {
- SafeHtml,
- },
- props: {
- job: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isHovered: false,
- isJobLogVisible: false,
- isLoadingAction: false,
- };
- },
- computed: {
- canReadBuild() {
- return this.job.userPermissions.readBuild;
- },
- canRetryJob() {
- return this.job.retryable && this.job.userPermissions.updateBuild && !this.isBridgeJob;
- },
- isBridgeJob() {
- return this.job.kind === BRIDGE_KIND;
- },
- jobChevronName() {
- return this.isJobLogVisible ? 'chevron-down' : 'chevron-right';
- },
- jobTrace() {
- if (this.canReadBuild) {
- return this.job?.trace?.htmlSummary || this.$options.i18n.noTraceText;
- }
-
- return this.$options.i18n.cannotReadBuild;
- },
- parsedJobId() {
- return getIdFromGraphQLId(this.job.id);
- },
- tooltipErrorText() {
- return this.isBridgeJob
- ? this.$options.i18n.cannotRetryTrigger
- : this.$options.i18n.cannotRetry;
- },
- tooltipText() {
- return sprintf(this.$options.i18n.jobActionTooltipText, { jobName: this.job.name });
- },
- },
- methods: {
- setActiveRow() {
- this.isHovered = true;
- },
- resetActiveRow() {
- this.isHovered = false;
- },
- async retryJob() {
- try {
- this.isLoadingAction = true;
-
- const {
- data: {
- jobRetry: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: RetryMrFailedJobMutation,
- variables: { id: this.job.id },
- });
-
- if (errors.length > 0) {
- throw new Error(errors[0]);
- }
-
- this.$emit('job-retried', this.job.name);
- } catch (error) {
- createAlert({ message: error?.message || this.$options.i18n.retryError });
- } finally {
- this.isLoadingAction = false;
- }
- },
- toggleJobLog(event) {
- // Do not toggle the log visibility when clicking on a link
- if (event.target.tagName === 'A') {
- return;
- }
- this.isJobLogVisible = !this.isJobLogVisible;
- },
- },
- i18n: {
- cannotReadBuild: s__("Job|You do not have permission to read this job's log."),
- cannotRetry: s__('Job|You do not have permission to run this job again.'),
- cannotRetryTrigger: s__('Job|You cannot rerun trigger jobs from this list.'),
- jobActionTooltipText: s__('Pipelines|Retry %{jobName} Job'),
- noTraceText: s__('Job|No job log'),
- retry: __('Retry'),
- retryError: __('There was an error while retrying this job'),
- },
-};
-</script>
-<template>
- <div class="container-fluid gl-grid-tpl-rows-auto">
- <div
- class="row gl-my-3 gl-cursor-pointer gl-display-flex gl-align-items-center"
- :aria-pressed="isJobLogVisible"
- role="button"
- tabindex="0"
- data-testid="widget-row"
- @click="toggleJobLog"
- @keyup.enter="toggleJobLog"
- @keyup.space="toggleJobLog"
- @mouseover="setActiveRow"
- @mouseout="resetActiveRow"
- >
- <div class="col-6 gl-text-gray-900 gl-font-weight-bold gl-text-left">
- <gl-icon :name="jobChevronName" />
- <ci-icon :status="job.detailedStatus" />
- {{ job.name }}
- </div>
- <div class="col-2 gl-text-left">{{ job.stage.name }}</div>
- <div class="col-2 gl-text-left">
- <gl-link :href="job.detailedStatus.detailsPath">#{{ parsedJobId }}</gl-link>
- </div>
- <gl-tooltip v-if="!canRetryJob" :target="() => $refs.retryBtn" placement="top">
- {{ tooltipErrorText }}
- </gl-tooltip>
- <div class="col-2 gl-text-right">
- <span ref="retryBtn">
- <gl-button
- :disabled="!canRetryJob"
- icon="retry"
- category="tertiary"
- :loading="isLoadingAction"
- :title="$options.i18n.retry"
- :aria-label="$options.i18n.retry"
- @click.stop="retryJob"
- />
- </span>
- </div>
- </div>
- <div v-if="isJobLogVisible" class="row">
- <pre
- v-safe-html="jobTrace"
- class="gl-bg-gray-900 gl-text-white gl-w-full"
- data-testid="job-log"
- ></pre>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue
deleted file mode 100644
index 2c5aa84bc4f..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue
+++ /dev/null
@@ -1,179 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import { __, s__, sprintf } from '~/locale';
-import { getQueryHeaders } from '~/pipelines/components/graph/utils';
-import getPipelineFailedJobs from '../../../graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import { graphqlEtagPipelinePath, sortJobsByStatus } from './utils';
-import FailedJobDetails from './failed_job_details.vue';
-
-const POLL_INTERVAL = 10000;
-
-const JOB_ID_HEADER = __('ID');
-const JOB_NAME_HEADER = __('Name');
-const STAGE_HEADER = __('Stage');
-
-export default {
- components: {
- GlLoadingIcon,
- FailedJobDetails,
- },
- inject: ['graphqlPath'],
- props: {
- failedJobsCount: {
- required: true,
- type: Number,
- },
- isPipelineActive: {
- required: true,
- type: Boolean,
- },
- pipelineIid: {
- type: Number,
- required: true,
- },
- projectPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- failedJobs: [],
- isActive: false,
- isLoadingMore: false,
- };
- },
- apollo: {
- failedJobs: {
- context() {
- return getQueryHeaders(this.graphqlResourceEtag);
- },
- query: getPipelineFailedJobs,
- pollInterval: POLL_INTERVAL,
- variables() {
- return {
- fullPath: this.projectPath,
- pipelineIid: this.pipelineIid,
- };
- },
- update(data) {
- const jobs = data?.project?.pipeline?.jobs?.nodes || [];
- return sortJobsByStatus(jobs);
- },
- result({ data }) {
- const pipeline = data?.project?.pipeline;
-
- if (pipeline?.jobs?.count) {
- this.$emit('failed-jobs-count', pipeline.jobs.count);
- this.isActive = pipeline.active;
- }
- },
- error(e) {
- createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
- },
- },
- },
- computed: {
- graphqlResourceEtag() {
- return graphqlEtagPipelinePath(this.graphqlPath, this.pipelineIid);
- },
- hasFailedJobs() {
- return this.failedJobs.length > 0;
- },
- isInitialLoading() {
- return this.isLoading && !this.isLoadingMore;
- },
- isLoading() {
- return this.$apollo.queries.failedJobs.loading;
- },
- },
- watch: {
- isPipelineActive(flag) {
- // Turn polling on and off based on REST actions
- // By refetching jobs, we will get the graphql `active`
- // field to update properly and cascade the polling changes
- this.refetchJobs();
- this.handlePolling(flag);
- },
- isActive(flag) {
- this.handlePolling(flag);
- },
- failedJobsCount(count) {
- // If the REST data is updated first, we force a refetch
- // to keep them in sync
- if (this.failedJobs.length !== count) {
- this.$apollo.queries.failedJobs.refetch();
- }
- },
- },
- mounted() {
- if (!this.isActive && !this.isPipelineActive) {
- this.handlePolling(false);
- }
- },
- methods: {
- handlePolling(isActive) {
- // If the pipeline status has changed and the widget is not expanded,
- // We start polling.
- if (isActive) {
- this.$apollo.queries.failedJobs.startPolling(POLL_INTERVAL);
- } else {
- this.$apollo.queries.failedJobs.stopPolling();
- }
- },
- async retryJob(jobName) {
- await this.refetchJobs();
-
- this.$toast.show(sprintf(this.$options.i18n.retriedJobsSuccess, { jobName }));
- },
- async refetchJobs() {
- this.isLoadingMore = true;
-
- try {
- await this.$apollo.queries.failedJobs.refetch();
- } catch {
- createAlert(this.$options.i18n.fetchError);
- } finally {
- this.isLoadingMore = false;
- }
- },
- },
- columns: [
- { text: JOB_NAME_HEADER, class: 'col-6' },
- { text: STAGE_HEADER, class: 'col-2' },
- { text: JOB_ID_HEADER, class: 'col-2' },
- ],
- i18n: {
- fetchError: __('There was a problem fetching failed jobs'),
- noFailedJobs: s__('Pipeline|No failed jobs in this pipeline 🎉'),
- retriedJobsSuccess: __('%{jobName} job is being retried'),
- },
-};
-</script>
-
-<template>
- <div>
- <gl-loading-icon v-if="isInitialLoading" class="gl-p-4" />
- <div v-else-if="!hasFailedJobs" class="gl-p-4">{{ $options.i18n.noFailedJobs }}</div>
- <div v-else class="container-fluid gl-grid-tpl-rows-auto">
- <div class="row gl-my-4 gl-text-gray-900">
- <div
- v-for="col in $options.columns"
- :key="col.text"
- class="gl-font-weight-bold gl-text-left"
- :class="col.class"
- data-testid="header"
- >
- {{ col.text }}
- </div>
- </div>
- </div>
- <failed-job-details
- v-for="job in failedJobs"
- :key="job.id"
- :job="job"
- @job-retried="retryJob"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
deleted file mode 100644
index 60c429459bf..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue
+++ /dev/null
@@ -1,121 +0,0 @@
-<script>
-import { GlButton, GlCard, GlIcon, GlLink, GlPopover, GlSprintf } from '@gitlab/ui';
-import { __, s__, sprintf } from '~/locale';
-import FailedJobsList from './failed_jobs_list.vue';
-
-export default {
- components: {
- GlButton,
- GlCard,
- GlIcon,
- GlLink,
- GlPopover,
- GlSprintf,
- FailedJobsList,
- },
- inject: ['fullPath'],
- props: {
- failedJobsCount: {
- required: true,
- type: Number,
- },
- isPipelineActive: {
- required: true,
- type: Boolean,
- },
- pipelineIid: {
- required: true,
- type: Number,
- },
- pipelinePath: {
- required: true,
- type: String,
- },
- projectPath: {
- required: true,
- type: String,
- },
- },
- data() {
- return {
- currentFailedJobsCount: this.failedJobsCount,
- isActive: false,
- isExpanded: false,
- };
- },
- computed: {
- bodyClasses() {
- return this.isExpanded ? '' : 'gl-display-none';
- },
- failedJobsCountText() {
- return sprintf(this.$options.i18n.failedJobsLabel, { count: this.currentFailedJobsCount });
- },
- iconName() {
- return this.isExpanded ? 'chevron-down' : 'chevron-right';
- },
- popoverId() {
- return `popover-${this.pipelineIid}`;
- },
- },
- watch: {
- failedJobsCount(val) {
- this.currentFailedJobsCount = val;
- },
- },
- methods: {
- setFailedJobsCount(count) {
- this.currentFailedJobsCount = count;
- },
- toggleWidget() {
- this.isExpanded = !this.isExpanded;
- },
- },
- i18n: {
- additionalInfoPopover: s__(
- 'Pipelines|You will see a maximum of 100 jobs in this list. To view all failed jobs, %{linkStart}go to the details page%{linkEnd} of this pipeline.',
- ),
- additionalInfoTitle: __('Limitation on this view'),
- failedJobsLabel: __('Failed jobs (%{count})'),
- },
-};
-</script>
-<template>
- <gl-card
- class="gl-new-card"
- :class="{ 'gl-border-white gl-hover-border-gray-100': !isExpanded }"
- header-class="gl-new-card-header gl-px-3 gl-py-3"
- body-class="gl-new-card-body"
- data-testid="failed-jobs-card"
- :aria-expanded="isExpanded.toString()"
- >
- <template #header>
- <gl-button
- variant="link"
- class="gl-text-gray-700! gl-font-weight-semibold"
- @click="toggleWidget"
- >
- <gl-icon :name="iconName" />
- {{ failedJobsCountText }}
- <gl-icon :id="popoverId" name="information-o" class="gl-ml-2" />
- <gl-popover :target="popoverId" placement="top">
- <template #title> {{ $options.i18n.additionalInfoTitle }} </template>
- <slot>
- <gl-sprintf :message="$options.i18n.additionalInfoPopover">
- <template #link="{ content }">
- <gl-link class="gl-font-sm" :href="pipelinePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </slot>
- </gl-popover>
- </gl-button>
- </template>
- <failed-jobs-list
- v-if="isExpanded"
- :failed-jobs-count="failedJobsCount"
- :is-pipeline-active="isPipelineActive"
- :pipeline-iid="pipelineIid"
- :project-path="projectPath"
- @failed-jobs-count="setFailedJobsCount"
- />
- </gl-card>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js b/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
deleted file mode 100644
index 2d0c467c54f..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/failure_widget/utils.js
+++ /dev/null
@@ -1,19 +0,0 @@
-export const isFailedJob = (job = {}) => {
- return job?.detailedStatus?.group === 'failed' || false;
-};
-
-export const sortJobsByStatus = (jobs = []) => {
- const newJobs = [...jobs];
-
- return newJobs.sort((a) => {
- if (isFailedJob(a)) {
- return -1;
- }
-
- return 1;
- });
-};
-
-export const graphqlEtagPipelinePath = (graphqlPath, pipelineId) => {
- return `${graphqlPath}pipelines/id/${pipelineId}`;
-};
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue b/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
deleted file mode 100644
index 235126fea0c..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue
+++ /dev/null
@@ -1,69 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-
-export default {
- name: 'PipelineNavControls',
- components: {
- GlButton,
- },
- props: {
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
-
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
-
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
-
- isResetCacheButtonLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- onClickResetCache() {
- this.$emit('resetRunnersCache', this.resetCachePath);
- },
- },
-};
-</script>
-<template>
- <div class="nav-controls">
- <gl-button
- v-if="resetCachePath"
- :loading="isResetCacheButtonLoading"
- class="js-clear-cache"
- data-testid="clear-cache-button"
- @click="onClickResetCache"
- >
- {{ s__('Pipelines|Clear runner caches') }}
- </gl-button>
-
- <gl-button v-if="ciLintPath" :href="ciLintPath" class="js-ci-lint" data-testid="ci-lint-button">
- {{ s__('Pipelines|CI lint') }}
- </gl-button>
-
- <gl-button
- v-if="newPipelinePath"
- :href="newPipelinePath"
- variant="confirm"
- category="primary"
- class="js-run-pipeline"
- data-testid="run-pipeline-button"
- data-qa-selector="run_pipeline_button"
- >
- {{ s__('Pipeline|Run pipeline') }}
- </gl-button>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue
deleted file mode 100644
index 40b2454b8c1..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_labels.vue
+++ /dev/null
@@ -1,170 +0,0 @@
-<script>
-import { GlLink, GlPopover, GlSprintf, GlTooltipDirective, GlBadge } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { SCHEDULE_ORIGIN } from '../../constants';
-
-export default {
- components: {
- GlBadge,
- GlLink,
- GlPopover,
- GlSprintf,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- inject: {
- targetProjectFullPath: {
- default: '',
- },
- },
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- pipelineScheduleUrl: {
- type: String,
- required: true,
- },
- },
- computed: {
- isScheduled() {
- return this.pipeline.source === SCHEDULE_ORIGIN;
- },
- isInFork() {
- return Boolean(
- this.targetProjectFullPath &&
- this.pipeline?.project?.full_path !== `/${this.targetProjectFullPath}`,
- );
- },
- autoDevopsTagId() {
- return `pipeline-url-autodevops-${this.pipeline.id}`;
- },
- autoDevopsHelpPath() {
- return helpPagePath('topics/autodevops/index.md');
- },
- },
-};
-</script>
-<template>
- <div class="label-container gl-mt-1">
- <gl-badge
- v-if="isScheduled"
- v-gl-tooltip
- :href="pipelineScheduleUrl"
- target="__blank"
- :title="__('This pipeline was triggered by a schedule.')"
- variant="info"
- size="sm"
- data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</gl-badge
- >
- <gl-badge
- v-if="pipeline.flags.latest"
- v-gl-tooltip
- :title="__('Latest pipeline for the most recent commit on this branch')"
- variant="success"
- size="sm"
- data-testid="pipeline-url-latest"
- >{{ __('latest') }}</gl-badge
- >
- <gl-badge
- v-if="pipeline.flags.merge_train_pipeline"
- v-gl-tooltip
- :title="
- s__(
- 'Pipeline|This pipeline ran on the contents of this merge request combined with the contents of all other merge requests queued for merging into the target branch.',
- )
- "
- variant="info"
- size="sm"
- data-testid="pipeline-url-train"
- >{{ s__('Pipeline|merge train') }}</gl-badge
- >
- <gl-badge
- v-if="pipeline.flags.yaml_errors"
- v-gl-tooltip
- :title="pipeline.yaml_errors"
- variant="danger"
- size="sm"
- data-testid="pipeline-url-yaml"
- >{{ __('yaml invalid') }}</gl-badge
- >
- <gl-badge
- v-if="pipeline.flags.failure_reason"
- v-gl-tooltip
- :title="pipeline.failure_reason"
- variant="danger"
- size="sm"
- data-testid="pipeline-url-failure"
- >{{ __('error') }}</gl-badge
- >
- <template v-if="pipeline.flags.auto_devops">
- <gl-link
- :id="autoDevopsTagId"
- tabindex="0"
- data-testid="pipeline-url-autodevops"
- role="button"
- >
- <gl-badge variant="info" size="sm">
- {{ __('Auto DevOps') }}
- </gl-badge>
- </gl-link>
- <gl-popover :target="autoDevopsTagId" triggers="focus" placement="top">
- <template #title>
- <div class="gl-font-weight-normal gl-line-height-normal">
- <gl-sprintf
- :message="
- __(
- 'This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}',
- )
- "
- >
- <template #strong="{ content }">
- <b>{{ content }}</b>
- </template>
- </gl-sprintf>
- </div>
- </template>
- <gl-link
- :href="autoDevopsHelpPath"
- data-testid="pipeline-url-autodevops-link"
- target="_blank"
- >
- {{ __('Learn more about Auto DevOps') }}
- </gl-link>
- </gl-popover>
- </template>
-
- <gl-badge
- v-if="pipeline.flags.stuck"
- variant="warning"
- size="sm"
- data-testid="pipeline-url-stuck"
- >{{ __('stuck') }}</gl-badge
- >
- <gl-badge
- v-if="pipeline.flags.detached_merge_request_pipeline"
- v-gl-tooltip
- :title="
- s__(
- `Pipeline|This pipeline ran on the contents of this merge request's source branch, not the target branch.`,
- )
- "
- variant="info"
- size="sm"
- data-testid="pipeline-url-detached"
- >{{ s__('Pipeline|merge request') }}</gl-badge
- >
- <gl-badge
- v-if="isInFork"
- v-gl-tooltip
- :title="__('Pipeline ran in fork of project')"
- variant="info"
- size="sm"
- data-testid="pipeline-url-fork"
- >{{ __('fork') }}</gl-badge
- >
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
deleted file mode 100644
index 747d94d92f2..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ /dev/null
@@ -1,172 +0,0 @@
-<script>
-import {
- GlAlert,
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import fuzzaldrinPlus from 'fuzzaldrin-plus';
-import axios from '~/lib/utils/axios_utils';
-import { __, s__ } from '~/locale';
-import Tracking from '~/tracking';
-import { TRACKING_CATEGORIES } from '../../constants';
-
-export const i18n = {
- downloadArtifacts: __('Download artifacts'),
- artifactsFetchErrorMessage: s__('Pipelines|Could not load artifacts.'),
- artifactsFetchWarningMessage: s__(
- 'Pipelines|Failed to update. Please reload page to update the list of artifacts.',
- ),
- emptyArtifactsMessage: __('No artifacts found'),
-};
-
-export default {
- i18n,
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlAlert,
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
- },
- mixins: [Tracking.mixin()],
- inject: {
- artifactsEndpoint: {
- default: '',
- },
- artifactsEndpointPlaceholder: {
- default: '',
- },
- },
- props: {
- pipelineId: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- artifacts: [],
- hasError: false,
- isLoading: false,
- searchQuery: '',
- isNewPipeline: false,
- };
- },
- computed: {
- hasArtifacts() {
- return this.artifacts.length > 0;
- },
- filteredArtifacts() {
- return this.searchQuery.length > 0
- ? fuzzaldrinPlus.filter(this.artifacts, this.searchQuery, { key: 'name' })
- : this.artifacts;
- },
- },
- watch: {
- pipelineId() {
- this.isNewPipeline = true;
- },
- },
- methods: {
- fetchArtifacts() {
- // refactor tracking based on action once this dropdown supports
- // actions other than artifacts
- this.track('click_artifacts_dropdown', { label: TRACKING_CATEGORIES.table });
-
- // Preserve the last good list and present it if a request fails
- const oldArtifacts = [...this.artifacts];
- this.artifacts = [];
-
- this.hasError = false;
- this.isLoading = true;
-
- // Replace the placeholder with the ID of the pipeline we are viewing
- const endpoint = this.artifactsEndpoint.replace(
- this.artifactsEndpointPlaceholder,
- this.pipelineId,
- );
- return axios
- .get(endpoint)
- .then(({ data }) => {
- this.artifacts = data.artifacts;
- this.isNewPipeline = false;
- })
- .catch(() => {
- this.hasError = true;
- if (!this.isNewPipeline) {
- this.artifacts = oldArtifacts;
- }
- })
- .finally(() => {
- this.isLoading = false;
- });
- },
- handleDropdownShown() {
- if (this.hasArtifacts) {
- this.$refs.searchInput.focusInput();
- }
- },
- },
-};
-</script>
-<template>
- <gl-dropdown
- v-gl-tooltip
- :title="$options.i18n.downloadArtifacts"
- :text="$options.i18n.downloadArtifacts"
- :aria-label="$options.i18n.downloadArtifacts"
- :header-text="$options.i18n.downloadArtifacts"
- icon="download"
- data-testid="pipeline-multi-actions-dropdown"
- right
- lazy
- text-sr-only
- @show="fetchArtifacts"
- @shown="handleDropdownShown"
- >
- <gl-alert v-if="hasError && !hasArtifacts" variant="danger" :dismissible="false">
- {{ $options.i18n.artifactsFetchErrorMessage }}
- </gl-alert>
-
- <gl-loading-icon v-else-if="isLoading" size="sm" />
-
- <gl-dropdown-item v-else-if="!hasArtifacts" data-testid="artifacts-empty-message">
- {{ $options.i18n.emptyArtifactsMessage }}
- </gl-dropdown-item>
-
- <template #header>
- <gl-search-box-by-type v-if="hasArtifacts" ref="searchInput" v-model.trim="searchQuery" />
- </template>
-
- <gl-dropdown-item
- v-for="(artifact, i) in filteredArtifacts"
- :key="i"
- :href="artifact.path"
- rel="nofollow"
- download
- class="gl-word-break-word"
- data-testid="artifact-item"
- >
- {{ artifact.name }}
- </gl-dropdown-item>
-
- <template #footer>
- <gl-dropdown-item
- v-if="hasError && hasArtifacts"
- class="gl-list-style-none"
- disabled
- data-testid="artifacts-fetch-warning"
- >
- <span class="gl-font-sm">
- {{ $options.i18n.artifactsFetchWarningMessage }}
- </span>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
-</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
deleted file mode 100644
index caeee7edefe..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_operations.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
-import Tracking from '~/tracking';
-import eventHub from '../../event_hub';
-import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '../../constants';
-import PipelineMultiActions from './pipeline_multi_actions.vue';
-import PipelinesManualActions from './pipelines_manual_actions.vue';
-
-export default {
- BUTTON_TOOLTIP_RETRY,
- BUTTON_TOOLTIP_CANCEL,
- directives: {
- GlTooltip: GlTooltipDirective,
- GlModalDirective,
- },
- components: {
- GlButton,
- PipelineMultiActions,
- PipelinesManualActions,
- },
- mixins: [Tracking.mixin()],
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- cancelingPipeline: {
- type: Number,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- isRetrying: false,
- };
- },
- computed: {
- hasActions() {
- return (
- this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
- );
- },
- isCancelling() {
- return this.cancelingPipeline === this.pipeline.id;
- },
- },
- watch: {
- pipeline() {
- this.isRetrying = false;
- },
- },
- methods: {
- handleCancelClick() {
- this.trackClick('click_cancel_button');
- eventHub.$emit('openConfirmationModal', {
- pipeline: this.pipeline,
- endpoint: this.pipeline.cancel_path,
- });
- },
- handleRetryClick() {
- this.isRetrying = true;
- this.trackClick('click_retry_button');
- eventHub.$emit('retryPipeline', this.pipeline.retry_path);
- },
- trackClick(action) {
- this.track(action, { label: TRACKING_CATEGORIES.table });
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-text-right">
- <div class="btn-group">
- <pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
-
- <gl-button
- v-if="pipeline.flags.retryable"
- v-gl-tooltip.hover
- :aria-label="$options.BUTTON_TOOLTIP_RETRY"
- :title="$options.BUTTON_TOOLTIP_RETRY"
- :disabled="isRetrying"
- :loading="isRetrying"
- class="js-pipelines-retry-button"
- data-qa-selector="pipeline_retry_button"
- data-testid="pipelines-retry-button"
- icon="retry"
- 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.BUTTON_TOOLTIP_CANCEL"
- :title="$options.BUTTON_TOOLTIP_CANCEL"
- :loading="isCancelling"
- :disabled="isCancelling"
- icon="cancel"
- variant="danger"
- category="primary"
- class="js-pipelines-cancel-button gl-ml-1"
- data-testid="pipelines-cancel-button"
- @click="handleCancelClick"
- />
-
- <pipeline-multi-actions :pipeline-id="pipeline.id" />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
deleted file mode 100644
index 9f38be668f2..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stop_modal.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
-import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
-import { isEmpty } from 'lodash';
-import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-
-/**
- * Pipeline Stop Modal.
- *
- * Renders the modal used to confirm stopping a pipeline.
- */
-export default {
- components: {
- GlModal,
- GlLink,
- GlSprintf,
- CiIcon,
- },
- props: {
- pipeline: {
- type: Object,
- required: true,
- deep: true,
- },
- },
- computed: {
- modalTitle() {
- return sprintf(
- s__('Pipeline|Stop pipeline #%{pipelineId}?'),
- {
- pipelineId: `${this.pipeline.id}`,
- },
- false,
- );
- },
- modalText() {
- return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
- },
- hasRef() {
- return !isEmpty(this.pipeline.ref);
- },
- primaryProps() {
- return {
- text: s__('Pipeline|Stop pipeline'),
- attributes: { variant: 'danger' },
- };
- },
- cancelProps() {
- return {
- text: __('Cancel'),
- };
- },
- },
- methods: {
- emitSubmit(event) {
- this.$emit('submit', event);
- },
- },
-};
-</script>
-<template>
- <gl-modal
- modal-id="confirmation-modal"
- :title="modalTitle"
- :action-primary="primaryProps"
- :action-cancel="cancelProps"
- @primary="emitSubmit($event)"
- >
- <p>
- <gl-sprintf :message="modalText">
- <template #pipelineId>
- <strong>{{ pipeline.id }}</strong>
- </template>
- </gl-sprintf>
- </p>
-
- <p v-if="pipeline">
- <ci-icon
- v-if="pipeline.details"
- :status="pipeline.details.status"
- class="vertical-align-middle"
- />
-
- <span class="font-weight-bold">{{ __('Pipeline') }}</span>
-
- <a :href="pipeline.path" class="js-pipeline-path link-commit">#{{ pipeline.id }}</a>
- <template v-if="hasRef">
- {{ __('from') }}
- <a :href="pipeline.ref.path" class="link-commit ref-name">{{ pipeline.ref.name }}</a>
- </template>
- </p>
-
- <template v-if="pipeline.commit">
- <p>
- <span class="font-weight-bold">{{ __('Commit') }}</span>
-
- <gl-link :href="pipeline.commit.commit_path" class="js-commit-sha commit-sha link-commit">
- {{ pipeline.commit.short_id }}
- </gl-link>
- </p>
- <p>{{ pipeline.commit.title }}</p>
- </template>
- </gl-modal>
-</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
deleted file mode 100644
index 2a73795db0a..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_triggerer.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import { GlAvatarLink, GlAvatar, GlTooltipDirective } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
-export default {
- components: {
- GlAvatarLink,
- GlAvatar,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [glFeatureFlagMixin()],
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- },
- computed: {
- user() {
- return this.pipeline.user;
- },
- },
-};
-</script>
-<template>
- <div class="pipeline-triggerer" data-testid="pipeline-triggerer">
- <gl-avatar-link v-if="user" v-gl-tooltip :href="user.path" :title="user.name" class="gl-ml-3">
- <gl-avatar :size="32" :src="user.avatar_url" />
- </gl-avatar-link>
-
- <span v-else class="gl-ml-3">
- {{ s__('Pipelines|API') }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
deleted file mode 100644
index ff1a01d5037..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ /dev/null
@@ -1,239 +0,0 @@
-<script>
-import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
-import { ICONS, TRACKING_CATEGORIES } from '../../constants';
-import PipelineLabels from './pipeline_labels.vue';
-
-export default {
- components: {
- GlIcon,
- GlLink,
- PipelineLabels,
- TooltipOnTruncate,
- UserAvatarLink,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [Tracking.mixin()],
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- pipelineScheduleUrl: {
- type: String,
- required: true,
- },
- pipelineKey: {
- type: String,
- required: true,
- },
- refClass: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- mergeRequestRef() {
- return this.pipeline?.merge_request;
- },
- commitRef() {
- return this.pipeline?.ref;
- },
- commitTag() {
- return this.commitRef?.tag;
- },
- commitUrl() {
- return this.pipeline?.commit?.commit_path;
- },
- commitShortSha() {
- return this.pipeline?.commit?.short_id;
- },
- refUrl() {
- return this.commitRef?.ref_url || this.commitRef?.path;
- },
- tooltipTitle() {
- return this.mergeRequestRef?.title || this.commitRef?.name;
- },
- commitAuthor() {
- let commitAuthorInformation;
- const pipelineCommit = this.pipeline?.commit;
- const pipelineCommitAuthor = pipelineCommit?.author;
-
- if (!pipelineCommit) {
- return null;
- }
-
- // 1. person who is an author of a commit might be a GitLab user
- if (pipelineCommitAuthor) {
- // 2. if person who is an author of a commit is a GitLab user
- // they can have a GitLab avatar
- if (pipelineCommitAuthor?.avatar_url) {
- commitAuthorInformation = pipelineCommitAuthor;
-
- // 3. If GitLab user does not have avatar, they might have a Gravatar
- } else if (pipelineCommit.author_gravatar_url) {
- commitAuthorInformation = {
- ...pipelineCommitAuthor,
- avatar_url: pipelineCommit.author_gravatar_url,
- };
- }
- // 4. If committer is not a GitLab User, they can have a Gravatar
- } else {
- commitAuthorInformation = {
- avatar_url: pipelineCommit.author_gravatar_url,
- path: `mailto:${pipelineCommit.author_email}`,
- username: pipelineCommit.author_name,
- };
- }
-
- return commitAuthorInformation;
- },
- commitIcon() {
- let name = '';
-
- if (this.commitTag) {
- name = ICONS.TAG;
- } else if (this.mergeRequestRef) {
- name = ICONS.MR;
- } else {
- name = ICONS.BRANCH;
- }
-
- return name;
- },
- commitIconTooltipTitle() {
- switch (this.commitIcon) {
- case ICONS.TAG:
- return __('Tag');
- case ICONS.MR:
- return __('Merge Request');
- default:
- return __('Branch');
- }
- },
- commitTitle() {
- return this.pipeline?.commit?.title;
- },
- pipelineName() {
- return this.pipeline?.name;
- },
- },
- methods: {
- trackClick(action) {
- this.track(action, { label: TRACKING_CATEGORIES.table });
- },
- },
-};
-</script>
-<template>
- <div class="pipeline-tags" data-testid="pipeline-url-table-cell">
- <div v-if="pipelineName" class="gl-mb-2" data-testid="pipeline-name-container">
- <span class="gl-display-flex">
- <tooltip-on-truncate
- :title="pipelineName"
- class="gl-flex-grow-1 gl-text-truncate gl-text-gray-900"
- >
- <gl-link
- :href="pipeline.path"
- class="gl-text-blue-600!"
- data-testid="pipeline-url-link"
- >{{ pipelineName }}</gl-link
- >
- </tooltip-on-truncate>
- </span>
- </div>
-
- <div v-if="!pipelineName" class="commit-title gl-mb-2" data-testid="commit-title-container">
- <span v-if="commitTitle" class="gl-display-flex">
- <tooltip-on-truncate :title="commitTitle" class="gl-flex-grow-1 gl-text-truncate">
- <gl-link
- :href="commitUrl"
- class="commit-row-message gl-text-blue-600!"
- data-testid="commit-title"
- @click="trackClick('click_commit_title')"
- >{{ commitTitle }}</gl-link
- >
- </tooltip-on-truncate>
- </span>
- <span v-else class="gl-text-gray-500">{{
- __("Can't find HEAD commit for this branch")
- }}</span>
- </div>
- <div class="gl-mb-2">
- <gl-link
- :href="pipeline.path"
- class="gl-mr-1 gl-text-blue-500!"
- data-testid="pipeline-url-link"
- data-qa-selector="pipeline_url_link"
- @click="trackClick('click_pipeline_id')"
- >#{{ pipeline[pipelineKey] }}</gl-link
- >
- <!--Commit row-->
- <div class="gl-display-inline-flex gl-rounded-base gl-px-2 gl-bg-gray-50 gl-text-gray-700">
- <tooltip-on-truncate :title="tooltipTitle" truncate-target="child" placement="top">
- <gl-icon
- v-gl-tooltip
- :name="commitIcon"
- :title="commitIconTooltipTitle"
- :size="12"
- data-testid="commit-icon-type"
- />
- <gl-link
- v-if="mergeRequestRef"
- :href="mergeRequestRef.path"
- class="gl-font-sm gl-font-monospace gl-text-gray-700! gl-hover-text-gray-900!"
- :class="refClass"
- data-testid="merge-request-ref"
- @click="trackClick('click_mr_ref')"
- >{{ mergeRequestRef.iid }}</gl-link
- >
- <gl-link
- v-else
- :href="refUrl"
- class="gl-font-sm gl-font-monospace gl-text-gray-700! gl-hover-text-gray-900!"
- :class="refClass"
- data-testid="commit-ref-name"
- @click="trackClick('click_commit_name')"
- >{{ commitRef.name }}</gl-link
- >
- </tooltip-on-truncate>
- </div>
- <div
- class="gl-display-inline-block gl-rounded-base gl-font-sm gl-px-2 gl-bg-gray-50 gl-text-black-normal"
- >
- <gl-icon
- v-gl-tooltip
- name="commit"
- class="commit-icon gl-mr-1"
- :title="__('Commit')"
- :size="12"
- data-testid="commit-icon"
- />
- <gl-link
- :href="commitUrl"
- class="gl-font-sm gl-font-monospace gl-mr-0 gl-text-gray-700!"
- data-testid="commit-short-sha"
- @click="trackClick('click_commit_sha')"
- >{{ commitShortSha }}</gl-link
- >
- </div>
- <user-avatar-link
- v-if="commitAuthor"
- :link-href="commitAuthor.path"
- :img-src="commitAuthor.avatar_url"
- :img-size="16"
- :img-alt="commitAuthor.name"
- :tooltip-text="commitAuthor.name"
- class="gl-ml-1"
- />
- <!--End of commit row-->
- </div>
- <pipeline-labels :pipeline-schedule-url="pipelineScheduleUrl" :pipeline="pipeline" />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
deleted file mode 100644
index 574d291a767..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ /dev/null
@@ -1,449 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlEmptyState, GlIcon, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
-import { isEqual } from 'lodash';
-import * as Sentry from '@sentry/browser';
-import { createAlert, VARIANT_INFO, VARIANT_WARNING } from '~/alert';
-import { getParameterByName } from '~/lib/utils/url_utility';
-import { __, s__ } from '~/locale';
-import Tracking from '~/tracking';
-import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
-import { isLoggedIn } from '~/lib/utils/common_utils';
-import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
-import {
- ANY_TRIGGER_AUTHOR,
- RAW_TEXT_WARNING,
- FILTER_TAG_IDENTIFIER,
- PipelineKeyOptions,
- TRACKING_CATEGORIES,
-} from '../../constants';
-import PipelinesMixin from '../../mixins/pipelines_mixin';
-import PipelinesService from '../../services/pipelines_service';
-import { validateParams } from '../../utils';
-import EmptyState from './empty_state.vue';
-import NavigationControls from './nav_controls.vue';
-import PipelinesFilteredSearch from './pipelines_filtered_search.vue';
-import PipelinesTableComponent from './pipelines_table.vue';
-
-export default {
- PipelineKeyOptions,
- components: {
- EmptyState,
- GlCollapsibleListbox,
- GlEmptyState,
- GlIcon,
- GlLoadingIcon,
- NavigationTabs,
- NavigationControls,
- PipelinesFilteredSearch,
- PipelinesTableComponent,
- TablePagination,
- },
- mixins: [PipelinesMixin, Tracking.mixin()],
- props: {
- store: {
- type: Object,
- required: true,
- },
- // Can be rendered in 3 different places, with some visual differences
- // Accepts root | child
- // `root` -> main view
- // `child` -> rendered inside MR or Commit View
- viewType: {
- type: String,
- required: false,
- default: 'root',
- },
- endpoint: {
- type: String,
- required: true,
- },
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- noPipelinesSvgPath: {
- type: String,
- required: true,
- },
- hasGitlabCi: {
- type: Boolean,
- required: true,
- },
- canCreatePipeline: {
- type: Boolean,
- required: true,
- },
- ciLintPath: {
- type: String,
- required: false,
- default: null,
- },
- resetCachePath: {
- type: String,
- required: false,
- default: null,
- },
- newPipelinePath: {
- type: String,
- required: false,
- default: null,
- },
- projectId: {
- type: String,
- required: true,
- },
- defaultBranchName: {
- type: String,
- required: false,
- default: null,
- },
- params: {
- type: Object,
- required: true,
- },
- registrationToken: {
- type: String,
- required: false,
- default: null,
- },
- defaultVisibilityPipelineIdType: {
- type: String,
- required: false,
- default: null,
- },
- },
- data() {
- return {
- // Start with loading state to avoid a glitch when the empty state will be rendered
- isLoading: true,
- state: this.store.state,
- scope: getParameterByName('scope') || 'all',
- page: getParameterByName('page') || '1',
- requestData: {},
- isResetCacheButtonLoading: false,
- visibilityPipelineIdType: this.defaultVisibilityPipelineIdType,
- };
- },
- stateMap: {
- // with tabs
- loading: 'loading',
- tableList: 'tableList',
- error: 'error',
- emptyTab: 'emptyTab',
-
- // without tabs
- emptyState: 'emptyState',
- },
- scopes: {
- all: 'all',
- finished: 'finished',
- branches: 'branches',
- tags: 'tags',
- },
- computed: {
- /**
- * `hasGitlabCi` handles both internal and external CI.
- * The order on which the checks are made in this method is
- * important to guarantee we handle all the corner cases.
- */
- stateToRender() {
- const { stateMap } = this.$options;
-
- if (this.isLoading) {
- return stateMap.loading;
- }
-
- if (this.hasError) {
- return stateMap.error;
- }
-
- if (this.state.pipelines.length) {
- return stateMap.tableList;
- }
-
- if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
- return stateMap.emptyTab;
- }
-
- return stateMap.emptyState;
- },
- /**
- * Tabs are rendered in all states except empty state.
- * They are not rendered before the first request to avoid a flicker on first load.
- */
- shouldRenderTabs() {
- const { stateMap } = this.$options;
- return (
- this.hasMadeRequest &&
- [stateMap.loading, stateMap.tableList, stateMap.error, stateMap.emptyTab].includes(
- this.stateToRender,
- )
- );
- },
-
- shouldRenderButtons() {
- return (
- (this.newPipelinePath || this.resetCachePath || this.ciLintPath) && this.shouldRenderTabs
- );
- },
-
- shouldRenderPagination() {
- return !this.isLoading && !this.hasError;
- },
-
- emptyTabMessage() {
- if (this.scope === this.$options.scopes.finished) {
- return s__('Pipelines|There are currently no finished pipelines.');
- }
-
- return s__('Pipelines|There are currently no pipelines.');
- },
-
- tabs() {
- const { count } = this.state;
- const { scopes } = this.$options;
-
- return [
- {
- name: __('All'),
- scope: scopes.all,
- count: count.all,
- isActive: this.scope === 'all',
- },
- {
- name: __('Finished'),
- scope: scopes.finished,
- isActive: this.scope === 'finished',
- },
- {
- name: __('Branches'),
- scope: scopes.branches,
- isActive: this.scope === 'branches',
- },
- {
- name: __('Tags'),
- scope: scopes.tags,
- isActive: this.scope === 'tags',
- },
- ];
- },
- validatedParams() {
- return validateParams(this.params);
- },
- selectedPipelineKeyOption() {
- return (
- this.$options.PipelineKeyOptions.find((e) => this.visibilityPipelineIdType === e.value) ||
- this.$options.PipelineKeyOptions[0]
- );
- },
- },
- created() {
- this.service = new PipelinesService(this.endpoint);
- this.requestData = { page: this.page, scope: this.scope, ...this.validatedParams };
- },
- methods: {
- onChangeTab(scope) {
- if (this.scope === scope) {
- return;
- }
-
- let params = {
- scope,
- page: '1',
- };
-
- params = this.onChangeWithFilter(params);
-
- this.updateContent(params);
-
- this.track('click_filter_tabs', { label: TRACKING_CATEGORIES.tabs, property: scope });
- },
- successCallback(resp) {
- // Because we are polling & the user is interacting verify if the response received
- // matches the last request made
- if (isEqual(resp.config.params, this.requestData)) {
- this.store.storeCount(resp.data.count);
- this.store.storePagination(resp.headers);
- this.setCommonData(resp.data.pipelines);
- }
- },
- handleResetRunnersCache(endpoint) {
- this.isResetCacheButtonLoading = true;
-
- this.service
- .postAction(endpoint)
- .then(() => {
- this.isResetCacheButtonLoading = false;
- createAlert({
- message: s__('Pipelines|Project cache successfully reset.'),
- variant: VARIANT_INFO,
- });
- })
- .catch(() => {
- this.isResetCacheButtonLoading = false;
- createAlert({
- message: s__('Pipelines|Something went wrong while cleaning runners cache.'),
- });
- });
- },
- resetRequestData() {
- this.requestData = { page: this.page, scope: this.scope };
- },
- filterPipelines(filters) {
- this.resetRequestData();
-
- filters.forEach((filter) => {
- // do not add Any for username query param, so we
- // can fetch all trigger authors
- if (
- filter.type &&
- filter.value.data !== ANY_TRIGGER_AUTHOR &&
- filter.type !== FILTER_TAG_IDENTIFIER
- ) {
- this.requestData[filter.type] = filter.value.data;
- }
-
- if (filter.type === FILTER_TAG_IDENTIFIER) {
- this.requestData.ref = filter.value.data;
- }
-
- if (!filter.type) {
- createAlert({
- message: RAW_TEXT_WARNING,
- variant: VARIANT_WARNING,
- });
- }
- });
-
- if (filters.length === 0) {
- this.resetRequestData();
- }
-
- this.updateContent({ ...this.requestData, page: '1' });
- },
- changeVisibilityPipelineIDType(idType) {
- this.visibilityPipelineIdType = idType;
- this.saveVisibilityPipelineIDType(idType);
- },
- saveVisibilityPipelineIDType(idType) {
- if (!isLoggedIn()) return;
-
- this.$apollo
- .mutate({
- mutation: setSortPreferenceMutation,
- variables: { input: { visibilityPipelineIdType: idType.toUpperCase() } },
- })
- .then(({ data }) => {
- if (data.userPreferencesUpdate.errors.length) {
- throw new Error(data.userPreferencesUpdate.errors);
- }
- })
- .catch((error) => {
- Sentry.captureException(error);
- });
- },
- },
-};
-</script>
-<template>
- <div class="pipelines-container">
- <div
- v-if="shouldRenderTabs || shouldRenderButtons"
- class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-none"
- >
- <div class="fade-left"><gl-icon name="chevron-lg-left" :size="12" /></div>
- <div class="fade-right"><gl-icon name="chevron-lg-right" :size="12" /></div>
-
- <navigation-tabs
- v-if="shouldRenderTabs"
- :tabs="tabs"
- scope="pipelines"
- @onChangeTab="onChangeTab"
- />
-
- <navigation-controls
- v-if="shouldRenderButtons"
- :new-pipeline-path="newPipelinePath"
- :reset-cache-path="resetCachePath"
- :ci-lint-path="ciLintPath"
- :is-reset-cache-button-loading="isResetCacheButtonLoading"
- @resetRunnersCache="handleResetRunnersCache"
- />
- </div>
-
- <div v-if="stateToRender !== $options.stateMap.emptyState" class="gl-display-flex">
- <div class="row-content-block gl-display-flex gl-flex-grow-1 gl-border-b-0">
- <pipelines-filtered-search
- class="gl-display-flex gl-flex-grow-1 gl-mr-4"
- :project-id="projectId"
- :default-branch-name="defaultBranchName"
- :params="validatedParams"
- @filterPipelines="filterPipelines"
- />
- <gl-collapsible-listbox
- v-model="visibilityPipelineIdType"
- data-testid="pipeline-key-collapsible-box"
- :toggle-text="selectedPipelineKeyOption.text"
- :items="$options.PipelineKeyOptions"
- @select="changeVisibilityPipelineIDType"
- />
- </div>
- </div>
-
- <div class="content-list pipelines">
- <gl-loading-icon
- v-if="stateToRender === $options.stateMap.loading"
- :label="s__('Pipelines|Loading Pipelines')"
- size="lg"
- class="prepend-top-20"
- />
-
- <empty-state
- v-else-if="stateToRender === $options.stateMap.emptyState"
- :empty-state-svg-path="emptyStateSvgPath"
- :can-set-ci="canCreatePipeline"
- :registration-token="registrationToken"
- />
-
- <gl-empty-state
- v-else-if="stateToRender === $options.stateMap.error"
- :svg-path="errorStateSvgPath"
- :title="s__('Pipelines|There was an error fetching the pipelines.')"
- :description="s__('Pipelines|Try again in a few moments or contact your support team.')"
- />
-
- <gl-empty-state
- v-else-if="stateToRender === $options.stateMap.emptyTab"
- :svg-path="noPipelinesSvgPath"
- :title="emptyTabMessage"
- />
-
- <div v-else-if="stateToRender === $options.stateMap.tableList">
- <pipelines-table-component
- :pipelines="state.pipelines"
- :pipeline-schedule-url="pipelineScheduleUrl"
- :update-graph-dropdown="updateGraphDropdown"
- :view-type="viewType"
- :pipeline-key-option="selectedPipelineKeyOption"
- />
- </div>
-
- <table-pagination
- v-if="shouldRenderPagination"
- :change="onChangePage"
- :page-info="state.pageInfo"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
deleted file mode 100644
index 4452db64b0a..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import { GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export const i18n = {
- artifacts: __('Artifacts'),
- artifactSectionHeader: __('Download artifacts'),
-};
-
-export default {
- i18n,
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlDisclosureDropdown,
- },
- inject: {
- artifactsEndpoint: {
- default: '',
- },
- artifactsEndpointPlaceholder: {
- default: '',
- },
- },
- props: {
- pipelineId: {
- type: Number,
- required: true,
- },
- artifacts: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- items() {
- return [
- {
- name: this.$options.i18n.artifactSectionHeader,
- items: this.artifacts.map(({ name, path }) => ({
- text: name,
- href: path,
- extraAttrs: {
- download: '',
- rel: 'nofollow',
- },
- })),
- },
- ];
- },
- shouldShowDropdown() {
- return this.artifacts?.length;
- },
- },
-};
-</script>
-<template>
- <gl-disclosure-dropdown
- v-if="shouldShowDropdown"
- v-gl-tooltip
- class="build-artifacts js-pipeline-dropdown-download"
- :title="$options.i18n.artifacts"
- :toggle-text="$options.i18n.artifacts"
- :aria-label="$options.i18n.artifacts"
- icon="download"
- placement="right"
- text-sr-only
- :items="items"
- />
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
deleted file mode 100644
index 7dc1e60610e..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ /dev/null
@@ -1,130 +0,0 @@
-<script>
-import { GlFilteredSearch } from '@gitlab/ui';
-import { map } from 'lodash';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
-import { TRACKING_CATEGORIES } from '../../constants';
-import PipelineBranchNameToken from './tokens/pipeline_branch_name_token.vue';
-import PipelineSourceToken from './tokens/pipeline_source_token.vue';
-import PipelineStatusToken from './tokens/pipeline_status_token.vue';
-import PipelineTagNameToken from './tokens/pipeline_tag_name_token.vue';
-import PipelineTriggerAuthorToken from './tokens/pipeline_trigger_author_token.vue';
-
-export default {
- userType: 'username',
- branchType: 'ref',
- tagType: 'tag',
- statusType: 'status',
- sourceType: 'source',
- defaultTokensLength: 1,
- components: {
- GlFilteredSearch,
- },
- mixins: [Tracking.mixin()],
- props: {
- projectId: {
- type: String,
- required: true,
- },
- defaultBranchName: {
- type: String,
- required: false,
- default: null,
- },
- params: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- internalValue: [],
- };
- },
- computed: {
- selectedTypes() {
- return this.value.map((i) => i.type);
- },
- tokens() {
- return [
- {
- type: this.$options.userType,
- icon: 'user',
- title: s__('Pipeline|Trigger author'),
- unique: true,
- token: PipelineTriggerAuthorToken,
- operators: OPERATORS_IS,
- projectId: this.projectId,
- },
- {
- type: this.$options.branchType,
- icon: 'branch',
- title: s__('Pipeline|Branch name'),
- unique: true,
- token: PipelineBranchNameToken,
- operators: OPERATORS_IS,
- projectId: this.projectId,
- defaultBranchName: this.defaultBranchName,
- disabled: this.selectedTypes.includes(this.$options.tagType),
- },
- {
- type: this.$options.tagType,
- icon: 'tag',
- title: s__('Pipeline|Tag name'),
- unique: true,
- token: PipelineTagNameToken,
- operators: OPERATORS_IS,
- projectId: this.projectId,
- disabled: this.selectedTypes.includes(this.$options.branchType),
- },
- {
- type: this.$options.statusType,
- icon: 'status',
- title: s__('Pipeline|Status'),
- unique: true,
- token: PipelineStatusToken,
- operators: OPERATORS_IS,
- },
- {
- type: this.$options.sourceType,
- icon: 'trigger-source',
- title: s__('Pipeline|Source'),
- unique: true,
- token: PipelineSourceToken,
- operators: OPERATORS_IS,
- },
- ];
- },
- parsedParams() {
- return map(this.params, (val, key) => ({
- type: key,
- value: { data: val, operator: '=' },
- }));
- },
- value: {
- get() {
- return this.internalValue.length > 0 ? this.internalValue : this.parsedParams;
- },
- set(value) {
- this.internalValue = value;
- },
- },
- },
- methods: {
- onSubmit(filters) {
- this.track('click_filtered_search', { label: TRACKING_CATEGORIES.search });
- this.$emit('filterPipelines', filters);
- },
- },
-};
-</script>
-
-<template>
- <gl-filtered-search
- v-model="value"
- :placeholder="__('Filter pipelines')"
- :available-tokens="tokens"
- @submit="onSubmit"
- />
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
deleted file mode 100644
index 262e82677a7..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_manual_actions.vue
+++ /dev/null
@@ -1,159 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { s__, __, sprintf } from '~/locale';
-import Tracking from '~/tracking';
-import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import eventHub from '../../event_hub';
-import { TRACKING_CATEGORIES } from '../../constants';
-import getPipelineActionsQuery from '../../graphql/queries/get_pipeline_actions.query.graphql';
-
-export default {
- name: 'PipelinesManualActions',
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlCountdown,
- GlDropdown,
- GlDropdownItem,
- GlIcon,
- GlLoadingIcon,
- },
- mixins: [Tracking.mixin()],
- inject: ['fullPath', 'manualActionsLimit'],
- props: {
- iid: {
- type: Number,
- required: true,
- },
- },
- apollo: {
- actions: {
- query: getPipelineActionsQuery,
- variables() {
- return {
- fullPath: this.fullPath,
- iid: this.iid,
- limit: this.manualActionsLimit,
- };
- },
- skip() {
- return !this.hasDropdownBeenShown;
- },
- update({ project }) {
- return project?.pipeline?.jobs?.nodes || [];
- },
- },
- },
- data() {
- return {
- isLoading: false,
- actions: [],
- hasDropdownBeenShown: false,
- };
- },
- computed: {
- isActionsLoading() {
- return this.$apollo.queries.actions.loading;
- },
- isDropdownLimitReached() {
- return this.actions.length === this.manualActionsLimit;
- },
- },
- methods: {
- async onClickAction(action) {
- if (action.scheduledAt) {
- const confirmationMessage = sprintf(
- s__(
- 'DelayedJobs|Are you sure you want to run %{jobName} immediately? Otherwise this job will run automatically after its timer finishes.',
- ),
- { jobName: action.name },
- );
-
- const confirmed = await confirmAction(confirmationMessage);
-
- if (!confirmed) {
- return;
- }
- }
-
- this.isLoading = true;
-
- /**
- * Ideally, the component would not make an api call directly.
- * However, in order to use the eventhub and know when to
- * toggle back the `isLoading` property we'd need an ID
- * to track the request with a watcher - since this component
- * is rendered at least 20 times in the same page, moving the
- * api call directly here is the most performant solution
- */
- axios
- .post(`${action.playPath}.json`)
- .then(() => {
- this.isLoading = false;
- eventHub.$emit('updateTable');
- })
- .catch(() => {
- this.isLoading = false;
- createAlert({ message: __('An error occurred while making the request.') });
- });
- },
- fetchActions() {
- this.hasDropdownBeenShown = true;
-
- this.$apollo.queries.actions.refetch();
-
- this.trackClick();
- },
- trackClick() {
- this.track('click_manual_actions', { label: TRACKING_CATEGORIES.table });
- },
- },
-};
-</script>
-<template>
- <gl-dropdown
- v-gl-tooltip
- :title="__('Run manual or delayed jobs')"
- :loading="isLoading"
- data-testid="pipelines-manual-actions-dropdown"
- right
- lazy
- icon="play"
- @shown="fetchActions"
- >
- <gl-dropdown-item v-if="isActionsLoading">
- <div class="gl-display-flex">
- <gl-loading-icon class="mr-2" />
- <span>{{ __('Loading...') }}</span>
- </div>
- </gl-dropdown-item>
-
- <gl-dropdown-item
- v-for="action in actions"
- v-else
- :key="action.id"
- :disabled="!action.canPlayJob"
- @click="onClickAction(action)"
- >
- <div class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
- {{ action.name }}
- <span v-if="action.scheduledAt">
- <gl-icon name="clock" />
- <gl-countdown :end-date-string="action.scheduledAt" />
- </span>
- </div>
- </gl-dropdown-item>
-
- <template #footer>
- <gl-dropdown-item v-if="isDropdownLimitReached">
- <span class="gl-font-sm gl-text-gray-300!" data-testid="limit-reached-msg">
- {{ __('Showing first 50 actions.') }}
- </span>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
-</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
deleted file mode 100644
index 00ab8a25ca1..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_status_badge.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<script>
-import { CHILD_VIEW, TRACKING_CATEGORIES } from '~/pipelines/constants';
-import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-import Tracking from '~/tracking';
-import PipelinesTimeago from './time_ago.vue';
-
-export default {
- components: {
- CiBadgeLink,
- PipelinesTimeago,
- },
- mixins: [Tracking.mixin()],
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- viewType: {
- type: String,
- required: true,
- },
- },
- computed: {
- pipelineStatus() {
- return this.pipeline?.details?.status ?? {};
- },
- isChildView() {
- return this.viewType === CHILD_VIEW;
- },
- },
- methods: {
- trackClick() {
- this.track('click_ci_status_badge', { label: TRACKING_CATEGORIES.table });
- },
- },
-};
-</script>
-
-<template>
- <div>
- <ci-badge-link
- class="gl-mb-3"
- :status="pipelineStatus"
- :show-text="!isChildView"
- data-qa-selector="pipeline_commit_status"
- @ciStatusBadgeClick="trackClick"
- />
- <pipelines-timeago :pipeline="pipeline" />
- </div>
-</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
deleted file mode 100644
index c03085e6419..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ /dev/null
@@ -1,240 +0,0 @@
-<script>
-import { GlTableLite, GlTooltipDirective } from '@gitlab/ui';
-import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
-import { s__, __ } from '~/locale';
-import Tracking from '~/tracking';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
-import LegacyPipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '../../event_hub';
-import { TRACKING_CATEGORIES } from '../../constants';
-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 PipelinesStatusBadge from './pipelines_status_badge.vue';
-
-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!';
-
-export default {
- components: {
- GlTableLite,
- LegacyPipelineMiniGraph,
- PipelineFailedJobsWidget,
- PipelineOperations,
- PipelinesStatusBadge,
- PipelineStopModal,
- PipelineTriggerer,
- PipelineUrl,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [Tracking.mixin(), glFeatureFlagMixin()],
- inject: {
- withFailedJobsDetails: {
- default: false,
- },
- },
- props: {
- pipelines: {
- type: Array,
- required: true,
- },
- pipelineScheduleUrl: {
- type: String,
- required: false,
- default: '',
- },
- updateGraphDropdown: {
- type: Boolean,
- required: false,
- default: false,
- },
- viewType: {
- type: String,
- required: true,
- },
- pipelineKeyOption: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- pipelineId: 0,
- pipeline: {},
- endpoint: '',
- cancelingPipeline: null,
- };
- },
- computed: {
- showFailedJobsWidget() {
- return this.glFeatures.ciJobFailuresInMr;
- },
- tableFields() {
- return [
- {
- key: 'status',
- label: s__('Pipeline|Status'),
- thClass: DEFAULT_TH_CLASSES,
- columnClass: 'gl-w-15p',
- tdClass: this.tdClasses,
- thAttr: { 'data-testid': 'status-th' },
- },
- {
- key: 'pipeline',
- label: __('Pipeline'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: `${this.tdClasses}`,
- columnClass: 'gl-w-30p',
- thAttr: { 'data-testid': 'pipeline-th' },
- },
- {
- key: 'triggerer',
- label: s__('Pipeline|Triggerer'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: `${this.tdClasses} ${HIDE_TD_ON_MOBILE}`,
- columnClass: 'gl-w-10p',
- thAttr: { 'data-testid': 'triggerer-th' },
- },
- {
- key: 'stages',
- label: s__('Pipeline|Stages'),
- thClass: DEFAULT_TH_CLASSES,
- tdClass: this.tdClasses,
- columnClass: 'gl-w-quarter',
- thAttr: { 'data-testid': 'stages-th' },
- },
- {
- key: 'actions',
- thClass: DEFAULT_TH_CLASSES,
- tdClass: this.tdClasses,
- columnClass: 'gl-w-15p',
- thAttr: { 'data-testid': 'actions-th' },
- },
- ];
- },
- tdClasses() {
- return this.withFailedJobsDetails ? 'gl-pb-0! gl-border-none!' : 'pl-p-5!';
- },
- pipelinesWithDetails() {
- if (this.withFailedJobsDetails) {
- return this.pipelines.map((p) => {
- return { ...p, _showDetails: true };
- });
- }
-
- return this.pipelines;
- },
- },
- watch: {
- pipelines() {
- this.cancelingPipeline = null;
- },
- },
- created() {
- eventHub.$on('openConfirmationModal', this.setModalData);
- },
- beforeDestroy() {
- eventHub.$off('openConfirmationModal', this.setModalData);
- },
- methods: {
- getDownstreamPipelines(pipeline) {
- const downstream = pipeline.triggered;
- return keepLatestDownstreamPipelines(downstream);
- },
- getProjectPath(item) {
- return cleanLeadingSeparator(item.project.full_path);
- },
- failedJobsCount(pipeline) {
- return pipeline?.failed_builds?.length || 0;
- },
- setModalData(data) {
- this.pipelineId = data.pipeline.id;
- this.pipeline = data.pipeline;
- this.endpoint = data.endpoint;
- },
- onSubmit() {
- eventHub.$emit('postAction', this.endpoint);
- this.cancelingPipeline = this.pipelineId;
- },
- trackPipelineMiniGraph() {
- this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
- },
- },
- TBODY_TR_ATTR: {
- 'data-testid': 'pipeline-table-row',
- 'data-qa-selector': 'pipeline_row_container',
- },
-};
-</script>
-<template>
- <div class="ci-table">
- <gl-table-lite
- :fields="tableFields"
- :items="pipelinesWithDetails"
- :tbody-tr-attr="$options.TBODY_TR_ATTR"
- stacked="lg"
- fixed
- >
- <template #head(actions)>
- <span class="gl-display-block gl-lg-display-none!">{{ s__('Pipeline|Actions') }}</span>
- <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
- :pipeline="item"
- :pipeline-schedule-url="pipelineScheduleUrl"
- :pipeline-key="pipelineKeyOption.value"
- ref-color="gl-text-black-normal"
- />
- </template>
-
- <template #cell(triggerer)="{ item }">
- <pipeline-triggerer :pipeline="item" />
- </template>
-
- <template #cell(stages)="{ item }">
- <legacy-pipeline-mini-graph
- :downstream-pipelines="getDownstreamPipelines(item)"
- :pipeline-path="item.path"
- :stages="item.details.stages"
- :update-dropdown="updateGraphDropdown"
- :upstream-pipeline="item.triggered_by"
- @miniGraphStageClick="trackPipelineMiniGraph"
- />
- </template>
-
- <template #cell(actions)="{ item }">
- <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
- </template>
-
- <template #row-details="{ item }">
- <pipeline-failed-jobs-widget
- v-if="showFailedJobsWidget"
- :failed-jobs-count="failedJobsCount(item)"
- :is-pipeline-active="item.active"
- :pipeline-iid="item.iid"
- :pipeline-path="item.path"
- :project-path="getProjectPath(item)"
- class="gl-ml-n4 gl-mt-n3 gl-mb-n1"
- />
- </template>
- </gl-table-lite>
-
- <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue b/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
deleted file mode 100644
index 70343544638..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/time_ago.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { formatTime } from '~/lib/utils/datetime_utility';
-import timeagoMixin from '~/vue_shared/mixins/timeago';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: { GlIcon },
- mixins: [timeagoMixin],
- props: {
- pipeline: {
- type: Object,
- required: true,
- },
- fontSize: {
- type: String,
- required: false,
- default: 'gl-font-sm',
- validator: (fontSize) => ['gl-font-sm', 'gl-font-md'].includes(fontSize),
- },
- },
- computed: {
- duration() {
- return this.pipeline?.details?.duration;
- },
- durationFormatted() {
- return formatTime(this.duration * 1000);
- },
- finishedTime() {
- return this.pipeline?.details?.finished_at || this.pipeline?.finishedAt;
- },
- },
-};
-</script>
-<template>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-flex-end gl-lg-align-items-flex-start"
- :class="fontSize"
- >
- <p v-if="duration" class="duration gl-display-inline-flex gl-align-items-center">
- <gl-icon name="timer" class="gl-mr-2" :size="12" />
- {{ durationFormatted }}
- </p>
-
- <p v-if="finishedTime" class="finished-at gl-display-inline-flex gl-align-items-center">
- <gl-icon name="calendar" class="gl-mr-2" :size="12" data-testid="calendar-icon" />
-
- <time
- v-gl-tooltip
- :title="tooltipTitle(finishedTime)"
- :datetime="finishedTime"
- data-placement="top"
- data-container="body"
- >
- {{ timeFormatted(finishedTime) }}
- </time>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
deleted file mode 100644
index d8f15cfde91..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/constants.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import { s__ } from '~/locale';
-
-export const PIPELINE_SOURCES = [
- {
- text: s__('PipelineSource|Push'),
- value: 'push',
- },
- {
- text: s__('PipelineSource|Web'),
- value: 'web',
- },
- {
- text: s__('PipelineSource|Trigger'),
- value: 'trigger',
- },
- {
- text: s__('PipelineSource|Schedule'),
- value: 'schedule',
- },
- {
- text: s__('PipelineSource|API'),
- value: 'api',
- },
- {
- text: s__('PipelineSource|External'),
- value: 'external',
- },
- {
- text: s__('PipelineSource|Pipeline'),
- value: 'pipeline',
- },
- {
- text: s__('PipelineSource|Chat'),
- value: 'chat',
- },
- {
- text: s__('PipelineSource|Web IDE'),
- value: 'webide',
- },
- {
- text: s__('PipelineSource|Merge Request'),
- value: 'merge_request_event',
- },
- {
- text: s__('PipelineSource|External Pull Request'),
- value: 'external_pull_request_event',
- },
- {
- text: s__('PipelineSource|Parent Pipeline'),
- value: 'parent_pipeline',
- },
-];
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
deleted file mode 100644
index 81f46d5f2f9..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
-import { debounce } from 'lodash';
-import Api from '~/api';
-import { createAlert } from '~/alert';
-import { FETCH_BRANCH_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
-
-export default {
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlLoadingIcon,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- branches: null,
- loading: true,
- };
- },
- created() {
- this.fetchBranches();
- },
- methods: {
- fetchBranches(searchterm) {
- Api.branches(this.config.projectId, searchterm)
- .then(({ data }) => {
- this.branches = data.map((branch) => branch.name);
- if (!searchterm && this.config.defaultBranchName) {
- // Shift the default branch to the top of the list
- this.branches = this.branches.filter(
- (branch) => branch !== this.config.defaultBranchName,
- );
- this.branches.unshift(this.config.defaultBranchName);
- }
- this.loading = false;
- })
- .catch((err) => {
- createAlert({
- message: FETCH_BRANCH_ERROR_MESSAGE,
- });
- this.loading = false;
- throw err;
- });
- },
- searchBranches: debounce(function debounceSearch({ data }) {
- this.fetchBranches(data);
- }, FILTER_PIPELINES_SEARCH_DELAY),
- },
-};
-</script>
-
-<template>
- <gl-filtered-search-token
- :config="config"
- v-bind="{ ...$props, ...$attrs }"
- v-on="$listeners"
- @input="searchBranches"
- >
- <template #suggestions>
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="(branch, index) in branches"
- :key="index"
- :value="branch"
- >
- {{ branch }}
- </gl-filtered-search-suggestion>
- </template>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
deleted file mode 100644
index 9643ddfbd21..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_source_token.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
-import { PIPELINE_SOURCES } from 'ee_else_ce/pipelines/components/pipelines_list/tokens/constants';
-
-export default {
- PIPELINE_SOURCES,
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- computed: {
- activeSource() {
- return PIPELINE_SOURCES.find((source) => source.value === this.value.data);
- },
- },
-};
-</script>
-
-<template>
- <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
- <template #view>
- <div class="gl-display-flex gl-align-items-center">
- <span>{{ activeSource.text }}</span>
- </div>
- </template>
-
- <template #suggestions>
- <gl-filtered-search-suggestion
- v-for="source in $options.PIPELINE_SOURCES"
- :key="source.value"
- :value="source.value"
- >
- {{ source.text }}
- </gl-filtered-search-suggestion>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
deleted file mode 100644
index 020a08b8cee..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_status_token.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlIcon } from '@gitlab/ui';
-import { s__ } from '~/locale';
-
-export default {
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlIcon,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- computed: {
- statuses() {
- return [
- {
- class: 'ci-status-icon-canceled',
- icon: 'status_canceled',
- text: s__('Pipeline|Canceled'),
- value: 'canceled',
- },
- {
- class: 'ci-status-icon-created',
- icon: 'status_created',
- text: s__('Pipeline|Created'),
- value: 'created',
- },
- {
- class: 'ci-status-icon-failed',
- icon: 'status_failed',
- text: s__('Pipeline|Failed'),
- value: 'failed',
- },
- {
- class: 'ci-status-icon-manual',
- icon: 'status_manual',
- text: s__('Pipeline|Manual'),
- value: 'manual',
- },
- {
- class: 'ci-status-icon-success',
- icon: 'status_success',
- text: s__('Pipeline|Passed'),
- value: 'success',
- },
- {
- class: 'ci-status-icon-pending',
- icon: 'status_pending',
- text: s__('Pipeline|Pending'),
- value: 'pending',
- },
- {
- class: 'ci-status-icon-running',
- icon: 'status_running',
- text: s__('Pipeline|Running'),
- value: 'running',
- },
- {
- class: 'ci-status-icon-skipped',
- icon: 'status_skipped',
- text: s__('Pipeline|Skipped'),
- value: 'skipped',
- },
- ];
- },
- findActiveStatus() {
- return this.statuses.find((status) => status.value === this.value.data);
- },
- },
-};
-</script>
-
-<template>
- <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners">
- <template #view>
- <div class="gl-display-flex gl-align-items-center">
- <div :class="findActiveStatus.class">
- <gl-icon :name="findActiveStatus.icon" class="gl-mr-2 gl-display-block" />
- </div>
- <span>{{ findActiveStatus.text }}</span>
- </div>
- </template>
- <template #suggestions>
- <gl-filtered-search-suggestion
- v-for="(status, index) in statuses"
- :key="index"
- :value="status.value"
- >
- <div class="gl-display-flex" :class="status.class">
- <gl-icon :name="status.icon" class="gl-mr-3" />
- <span>{{ status.text }}</span>
- </div>
- </gl-filtered-search-suggestion>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
deleted file mode 100644
index b32f5de2d7e..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
-import { debounce } from 'lodash';
-import Api from '~/api';
-import { createAlert } from '~/alert';
-import { FETCH_TAG_ERROR_MESSAGE, FILTER_PIPELINES_SEARCH_DELAY } from '../../../constants';
-
-export default {
- components: {
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlLoadingIcon,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- tags: null,
- loading: true,
- };
- },
- created() {
- this.fetchTags();
- },
- methods: {
- fetchTags(searchTerm) {
- Api.tags(this.config.projectId, searchTerm)
- .then(({ data }) => {
- this.tags = data.map((tag) => tag.name);
- this.loading = false;
- })
- .catch((err) => {
- createAlert({
- message: FETCH_TAG_ERROR_MESSAGE,
- });
- this.loading = false;
- throw err;
- });
- },
- searchTags: debounce(function debounceSearch({ data }) {
- this.fetchTags(data);
- }, FILTER_PIPELINES_SEARCH_DELAY),
- },
-};
-</script>
-
-<template>
- <gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners" @input="searchTags">
- <template #suggestions>
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion v-for="(tag, index) in tags" :key="index" :value="tag">
- {{ tag }}
- </gl-filtered-search-suggestion>
- </template>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
deleted file mode 100644
index a89354c671a..00000000000
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ /dev/null
@@ -1,113 +0,0 @@
-<script>
-import {
- GlFilteredSearchToken,
- GlAvatar,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
-import Api from '~/api';
-import { createAlert } from '~/alert';
-import {
- ANY_TRIGGER_AUTHOR,
- FETCH_AUTHOR_ERROR_MESSAGE,
- FILTER_PIPELINES_SEARCH_DELAY,
-} from '../../../constants';
-
-export default {
- anyTriggerAuthor: ANY_TRIGGER_AUTHOR,
- components: {
- GlFilteredSearchToken,
- GlAvatar,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
- },
- props: {
- config: {
- type: Object,
- required: true,
- },
- value: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- users: [],
- loading: true,
- };
- },
- computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeUser() {
- return this.users.find((user) => {
- return user.username.toLowerCase() === this.currentValue;
- });
- },
- },
- created() {
- this.fetchProjectUsers();
- },
- methods: {
- fetchProjectUsers(searchTerm) {
- Api.projectUsers(this.config.projectId, searchTerm)
- .then((users) => {
- this.users = users;
- this.loading = false;
- })
- .catch((err) => {
- createAlert({
- message: FETCH_AUTHOR_ERROR_MESSAGE,
- });
- this.loading = false;
- throw err;
- });
- },
- searchAuthors: debounce(function debounceSearch({ data }) {
- this.fetchProjectUsers(data);
- }, FILTER_PIPELINES_SEARCH_DELAY),
- },
-};
-</script>
-
-<template>
- <gl-filtered-search-token
- :config="config"
- v-bind="{ ...$props, ...$attrs }"
- v-on="$listeners"
- @input="searchAuthors"
- >
- <template #view="{ inputValue }">
- <gl-avatar v-if="activeUser" :size="16" :src="activeUser.avatar_url" class="gl-mr-2" />
- <span>{{ activeUser ? activeUser.name : inputValue }}</span>
- </template>
- <template #suggestions>
- <gl-filtered-search-suggestion :value="$options.anyTriggerAuthor">{{
- $options.anyTriggerAuthor
- }}</gl-filtered-search-suggestion>
- <gl-dropdown-divider />
-
- <gl-loading-icon v-if="loading" size="sm" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="user in users"
- :key="user.username"
- :value="user.username"
- >
- <div class="d-flex">
- <gl-avatar :size="32" :src="user.avatar_url" />
- <div>
- <div>{{ user.name }}</div>
- <div>@{{ user.username }}</div>
- </div>
- </div>
- </gl-filtered-search-suggestion>
- </template>
- </template>
- </gl-filtered-search-token>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue b/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
deleted file mode 100644
index 3e7827dc416..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/empty_state.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-
-export const i18n = {
- noTestsButton: s__('TestReports|Learn more about pipeline test reports'),
- noTestsDescription: s__('TestReports|No test cases were found in the test report.'),
- noTestsTitle: s__('TestReports|There are no tests to display'),
- noReportsButton: s__('TestReports|Learn how to upload pipeline test reports'),
- noReportsDescription: s__(
- 'TestReports|You can configure your job to use unit test reports, and GitLab displays a report here and in the related merge request.',
- ),
- noReportsTitle: s__('TestReports|There are no test reports for this pipeline'),
-};
-
-export default {
- i18n,
- components: {
- GlEmptyState,
- },
- inject: {
- emptyStateImagePath: {
- default: '',
- },
- hasTestReport: {
- default: false,
- },
- },
- computed: {
- emptyStateText() {
- if (this.hasTestReport) {
- return {
- button: this.$options.i18n.noTestsButton,
- description: this.$options.i18n.noTestsDescription,
- title: this.$options.i18n.noTestsTitle,
- };
- }
- return {
- button: this.$options.i18n.noReportsButton,
- description: this.$options.i18n.noReportsDescription,
- title: this.$options.i18n.noReportsTitle,
- };
- },
- testReportDocPath() {
- return helpPagePath('ci/testing/unit_test_reports');
- },
- },
-};
-</script>
-
-<template>
- <gl-empty-state
- :title="emptyStateText.title"
- :description="emptyStateText.description"
- :svg-path="emptyStateImagePath"
- :primary-button-link="testReportDocPath"
- :primary-button-text="emptyStateText.button"
- />
-</template>
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
deleted file mode 100644
index 10db3e1c56b..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/test_case_details.vue
+++ /dev/null
@@ -1,145 +0,0 @@
-<script>
-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';
-
-export default {
- name: 'TestCaseDetails',
- components: {
- CodeBlock,
- GlBadge,
- GlFriendlyWrap,
- GlLink,
- GlModal,
- ModalCopyButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- modalId: {
- type: String,
- required: true,
- },
- testCase: {
- type: Object,
- required: false,
- default: () => {
- return {};
- },
- },
- },
- computed: {
- failureHistoryMessage() {
- if (!this.hasRecentFailures) {
- return null;
- }
-
- return sprintf(
- n__(
- 'Reports|Failed %{count} time in %{baseBranch} in the last 14 days',
- 'Reports|Failed %{count} times in %{baseBranch} in the last 14 days',
- this.recentFailures.count,
- ),
- {
- count: this.recentFailures.count,
- baseBranch: this.recentFailures.base_branch,
- },
- );
- },
- hasRecentFailures() {
- return Boolean(this.recentFailures);
- },
- recentFailures() {
- return this.testCase.recent_failures;
- },
- },
- text: {
- name: __('Name'),
- file: __('File'),
- duration: __('Execution time'),
- history: __('History'),
- trace: __('System output'),
- attachment: s__('TestReports|Attachment'),
- copyTestName: s__('TestReports|Copy test name to rerun locally'),
- },
- modalCloseButton: {
- text: __('Close'),
- attributes: { variant: 'confirm' },
- },
-};
-</script>
-
-<template>
- <gl-modal
- data-testid="test-case-details-modal"
- :modal-id="modalId"
- :title="testCase.classname"
- :action-primary="$options.modalCloseButton"
- >
- <div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
- <strong class="gl-text-right col-sm-3">{{ $options.text.name }}</strong>
- <div class="col-sm-9" data-testid="test-case-name">
- {{ testCase.name }}
- </div>
- </div>
-
- <div v-if="testCase.file" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
- <strong class="gl-text-right col-sm-3">{{ $options.text.file }}</strong>
- <div class="col-sm-9" data-testid="test-case-file">
- <gl-link v-if="testCase.filePath" :href="testCase.filePath">
- {{ 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>
-
- <div class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
- <strong class="gl-text-right col-sm-3">{{ $options.text.duration }}</strong>
- <div v-if="testCase.formattedTime" class="col-sm-9" data-testid="test-case-duration">
- {{ testCase.formattedTime }}
- </div>
- <div v-else-if="testCase.execution_time" class="col-sm-9" data-testid="test-case-duration">
- {{ sprintf('%{value} s', { value: testCase.execution_time }) }}
- </div>
- </div>
-
- <div v-if="testCase.recent_failures" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
- <strong class="gl-text-right col-sm-3">{{ $options.text.history }}</strong>
- <div class="col-sm-9" data-testid="test-case-recent-failures">
- <gl-badge variant="warning">{{ failureHistoryMessage }}</gl-badge>
- </div>
- </div>
-
- <div v-if="testCase.attachment_url" class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3">
- <strong class="gl-text-right col-sm-3">{{ $options.text.attachment }}</strong>
- <gl-link
- class="col-sm-9"
- :href="testCase.attachment_url"
- target="_blank"
- data-testid="test-case-attachment-url"
- >
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.attachment_url" />
- </gl-link>
- </div>
-
- <div
- v-if="testCase.system_output"
- class="gl-display-flex gl-flex-wrap gl-mx-n4 gl-my-3"
- data-testid="test-case-trace"
- >
- <strong class="gl-text-right col-sm-3">{{ $options.text.trace }}</strong>
- <div class="col-sm-9">
- <code-block :code="testCase.system_output" />
- </div>
- </div>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
deleted file mode 100644
index a7737d33285..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ /dev/null
@@ -1,91 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapGetters, mapState } from 'vuex';
-import EmptyState from './empty_state.vue';
-import TestSuiteTable from './test_suite_table.vue';
-import TestSummary from './test_summary.vue';
-import TestSummaryTable from './test_summary_table.vue';
-
-export default {
- name: 'TestReports',
- components: {
- EmptyState,
- GlLoadingIcon,
- TestSuiteTable,
- TestSummary,
- TestSummaryTable,
- },
- inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'],
- computed: {
- ...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']),
- ...mapGetters('testReports', ['getSelectedSuite']),
- showSuite() {
- return this.selectedSuiteIndex !== null;
- },
- showTests() {
- const { test_suites: testSuites = [] } = this.testReports;
- return testSuites.length > 0;
- },
- },
- created() {
- this.fetchSummary();
- },
- methods: {
- ...mapActions('testReports', [
- 'fetchTestSuite',
- 'fetchSummary',
- 'setSelectedSuiteIndex',
- 'removeSelectedSuiteIndex',
- ]),
- summaryBackClick() {
- this.removeSelectedSuiteIndex();
- },
- summaryTableRowClick(index) {
- this.setSelectedSuiteIndex(index);
-
- // Fetch the test suite when the user clicks to see more details
- this.fetchTestSuite(index);
- },
- beforeEnterTransition() {
- document.documentElement.style.overflowX = 'hidden';
- },
- afterLeaveTransition() {
- document.documentElement.style.overflowX = '';
- },
- },
-};
-</script>
-
-<template>
- <div v-if="isLoading">
- <gl-loading-icon size="lg" class="gl-mt-3" />
- </div>
-
- <div
- v-else-if="!isLoading && showTests"
- ref="container"
- class="gl-relative"
- data-testid="tests-detail"
- >
- <transition
- name="slide"
- @before-enter="beforeEnterTransition"
- @after-leave="afterLeaveTransition"
- >
- <div v-if="showSuite" key="detail" class="gl-w-full slide-enter-to-element">
- <test-summary :report="getSelectedSuite" show-back @on-back-click="summaryBackClick" />
-
- <test-suite-table />
- </div>
-
- <div v-else key="summary" class="gl-w-full slide-enter-from-element">
- <test-summary :report="testReports" />
-
- <test-summary-table @row-click="summaryTableRowClick" />
- </div>
- </transition>
- </div>
-
- <empty-state v-else />
-</template>
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
deleted file mode 100644
index d8af926a796..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ /dev/null
@@ -1,206 +0,0 @@
-<script>
-import {
- GlModalDirective,
- GlTooltipDirective,
- GlFriendlyWrap,
- GlIcon,
- GlLink,
- GlButton,
- GlPagination,
- GlEmptyState,
- GlSprintf,
-} from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapGetters, mapActions } from 'vuex';
-import { __, s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import TestCaseDetails from './test_case_details.vue';
-
-export const i18n = {
- expiredArtifactsTitle: s__('TestReports|Job artifacts are expired'),
- expiredArtifactsDescription: s__(
- 'TestReports|Test reports require job artifacts but all artifacts are expired. %{linkStart}Learn more%{linkEnd}',
- ),
-};
-
-export default {
- name: 'TestsSuiteTable',
- components: {
- GlIcon,
- GlFriendlyWrap,
- GlLink,
- GlButton,
- GlPagination,
- GlEmptyState,
- GlSprintf,
- TestCaseDetails,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- GlModalDirective,
- },
- inject: {
- artifactsExpiredImagePath: {
- default: '',
- },
- },
- props: {
- heading: {
- type: String,
- required: false,
- default: __('Tests'),
- },
- },
- computed: {
- ...mapState('testReports', ['pageInfo']),
- ...mapGetters('testReports', [
- 'getSuiteTests',
- 'getSuiteTestCount',
- 'getSuiteArtifactsExpired',
- ]),
- hasSuites() {
- return this.getSuiteTests.length > 0;
- },
- },
- methods: {
- ...mapActions('testReports', ['setPage']),
- },
- wrapSymbols: ['::', '#', '.', '_', '-', '/', '\\'],
- i18n,
- learnMorePath: helpPagePath('ci/testing/unit_test_reports', {
- anchor: 'viewing-unit-test-reports-on-gitlab',
- }),
-};
-</script>
-
-<template>
- <div>
- <div v-if="hasSuites" class="test-reports-table gl-mb-3 js-test-cases-table">
- <div class="row gl-mt-3">
- <div class="col-12">
- <h4>{{ heading }}</h4>
- </div>
- </div>
- <div
- role="row"
- class="gl-responsive-table-row table-row-header gl-font-weight-bold gl-fill-gray-700"
- >
- <div role="rowheader" class="table-section section-20">
- {{ __('Suite') }}
- </div>
- <div role="rowheader" class="table-section section-40">
- {{ __('Name') }}
- </div>
- <div role="rowheader" class="table-section section-10">
- {{ __('Filename') }}
- </div>
- <div role="rowheader" class="table-section section-10 text-center">
- {{ __('Status') }}
- </div>
- <div role="rowheader" class="table-section section-10">
- {{ __('Duration') }}
- </div>
- <div role="rowheader" class="table-section section-10">
- {{ __('Details') }}
- </div>
- </div>
-
- <div
- v-for="(testCase, index) in getSuiteTests"
- :key="index"
- class="gl-responsive-table-row gl-rounded-base gl-align-items-flex-start"
- data-testid="test-case-row"
- >
- <div class="table-section section-20 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Suite') }}</div>
- <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.classname" />
- </div>
- </div>
-
- <div class="table-section section-40 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Name') }}</div>
- <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.name" />
- </div>
- </div>
-
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Filename') }}</div>
- <div class="table-mobile-content gl-pr-0 gl-sm-pr-2 gl-overflow-wrap-break">
- <gl-link v-if="testCase.file" :href="testCase.filePath" target="_blank">
- <gl-friendly-wrap :symbols="$options.wrapSymbols" :text="testCase.file" />
- </gl-link>
- <gl-button
- v-if="testCase.file"
- v-gl-tooltip
- size="small"
- category="tertiary"
- icon="copy-to-clipboard"
- :title="__('Copy to clipboard')"
- :data-clipboard-text="testCase.file"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </div>
-
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Status') }}</div>
- <div class="table-mobile-content gl-md-display-flex gl-justify-content-center">
- <div class="ci-status-icon" :class="`ci-status-icon-${testCase.status}`">
- <gl-icon :size="24" :name="testCase.icon" />
- </div>
- </div>
- </div>
-
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content gl-pr-0 gl-sm-pr-2">
- {{ testCase.formattedTime }}
- </div>
- </div>
-
- <div class="table-section section-10 section-wrap">
- <div role="rowheader" class="table-mobile-header">{{ __('Details') }}</div>
- <div class="table-mobile-content">
- <gl-button v-gl-modal-directive="`test-case-details-${index}`">{{
- __('View details')
- }}</gl-button>
- <test-case-details :modal-id="`test-case-details-${index}`" :test-case="testCase" />
- </div>
- </div>
- </div>
-
- <gl-pagination
- v-model="pageInfo.page"
- class="gl-display-flex gl-justify-content-center"
- :per-page="pageInfo.perPage"
- :total-items="getSuiteTestCount"
- @input="setPage"
- />
- </div>
-
- <div v-else>
- <gl-empty-state
- v-if="getSuiteArtifactsExpired"
- :title="$options.i18n.expiredArtifactsTitle"
- :svg-path="artifactsExpiredImagePath"
- :svg-height="100"
- data-testid="artifacts-expired"
- >
- <template #description>
- <gl-sprintf :message="$options.i18n.expiredArtifactsDescription">
- <template #link="{ content }">
- <gl-link :href="$options.learnMorePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </template>
- </gl-empty-state>
- <p v-else data-testid="no-test-cases">
- {{ s__('TestReports|There are no test cases to display.') }}
- </p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
deleted file mode 100644
index 6b723ad5481..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-import { GlButton, GlProgressBar } from '@gitlab/ui';
-import { __ } from '~/locale';
-import { formattedTime } from '../../stores/test_reports/utils';
-
-export default {
- name: 'TestSummary',
- components: {
- GlButton,
- GlProgressBar,
- },
- props: {
- report: {
- type: Object,
- required: true,
- },
- showBack: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- heading() {
- return this.report.name || __('Summary');
- },
- successPercentage() {
- // Returns a full number when the decimals equal .00.
- // Otherwise returns a float to two decimal points
- // Do not include skipped tests as part of the total when doing success calculations.
-
- const totalCompletedCount = this.report.total_count - this.report.skipped_count;
-
- if (totalCompletedCount > 0) {
- return Number(((this.report.success_count / totalCompletedCount) * 100 || 0).toFixed(2));
- }
- return 0;
- },
- formattedDuration() {
- return formattedTime(this.report.total_time);
- },
- progressBarVariant() {
- if (this.successPercentage < 33) {
- return 'danger';
- }
-
- if (this.successPercentage >= 33 && this.successPercentage < 66) {
- return 'warning';
- }
-
- if (this.successPercentage >= 66 && this.successPercentage < 90) {
- return 'primary';
- }
-
- return 'success';
- },
- },
- methods: {
- onBackClick() {
- this.$emit('on-back-click');
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="gl-w-full gl-display-flex gl-mt-3 gl-align-items-center">
- <gl-button
- v-if="showBack"
- size="small"
- class="gl-mr-3 js-back-button"
- icon="chevron-lg-left"
- :aria-label="__('Go back')"
- @click="onBackClick"
- />
-
- <h4>{{ heading }}</h4>
- </div>
-
- <div
- class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-w-full gl-mt-3"
- >
- <div class="gl-display-flex gl-justify-content-space-between gl-flex-basis-half">
- <span class="js-total-tests gl-flex-grow-1">{{
- sprintf(s__('TestReports|%{count} tests'), { count: report.total_count })
- }}</span>
-
- <span class="js-failed-tests gl-flex-grow-1">{{
- sprintf(s__('TestReports|%{count} failures'), { count: report.failed_count })
- }}</span>
-
- <span class="js-errored-tests">{{
- sprintf(s__('TestReports|%{count} errors'), { count: report.error_count })
- }}</span>
- </div>
- <div class="gl-display-flex gl-justify-content-space-between gl-flex-grow-1">
- <div class="gl-display-none gl-md-display-block gl-flex-grow-1"></div>
- <span class="js-success-rate gl-flex-grow-1">{{
- sprintf(s__('TestReports|%{rate}%{sign} success rate'), {
- rate: successPercentage,
- sign: '%',
- })
- }}</span>
-
- <span class="js-duration">{{ formattedDuration }}</span>
- </div>
- </div>
-
- <gl-progress-bar
- class="gl-mt-5"
- :value="successPercentage"
- :variant="progressBarVariant"
- height="10px"
- />
- </div>
-</template>
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
deleted file mode 100644
index 9141947ea04..00000000000
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ /dev/null
@@ -1,144 +0,0 @@
-<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters } from 'vuex';
-import { s__ } from '~/locale';
-
-export default {
- name: 'TestsSummaryTable',
- components: {
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- heading: {
- type: String,
- required: false,
- default: s__('TestReports|Jobs'),
- },
- },
- computed: {
- ...mapGetters('testReports', ['getTestSuites']),
- hasSuites() {
- return this.getTestSuites.length > 0;
- },
- },
- methods: {
- tableRowClick(index) {
- this.$emit('row-click', index);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="gl-mt-5">
- <h4>{{ heading }}</h4>
- </div>
-
- <div v-if="hasSuites" class="js-test-suites-table">
- <div role="row" class="gl-responsive-table-row table-row-header gl-font-weight-bold">
- <div role="rowheader" class="table-section section-25 gl-pl-5">
- {{ __('Job') }}
- </div>
- <div role="rowheader" class="table-section section-25">
- {{ __('Duration') }}
- </div>
- <div role="rowheader" class="table-section section-10 gl-text-center">
- {{ __('Failed') }}
- </div>
- <div role="rowheader" class="table-section section-10 gl-text-center">
- {{ __('Errors') }}
- </div>
- <div role="rowheader" class="table-section section-10 gl-text-center">
- {{ __('Skipped') }}
- </div>
- <div role="rowheader" class="table-section section-10 gl-text-center">
- {{ __('Passed') }}
- </div>
- <div role="rowheader" class="table-section section-10 gl-pr-5 gl-text-right">
- {{ __('Total') }}
- </div>
- </div>
-
- <div
- v-for="(testSuite, index) in getTestSuites"
- :key="index"
- role="row"
- class="gl-responsive-table-row gl-rounded-base js-suite-row"
- :class="{
- 'gl-responsive-table-row-clickable gl-cursor-pointer': !testSuite.suite_error,
- }"
- @click="tableRowClick(index)"
- >
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Suite') }}
- </div>
- <div class="table-mobile-content underline gl-text-gray-900 gl-pl-5">
- {{ testSuite.name }}
- <gl-icon
- v-if="testSuite.suite_error"
- ref="suiteErrorIcon"
- v-gl-tooltip
- name="error"
- :title="testSuite.suite_error"
- class="vertical-align-middle"
- />
- </div>
- </div>
-
- <div class="table-section section-25">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Duration') }}
- </div>
- <div class="table-mobile-content gl-text-left">
- {{ testSuite.formattedTime }}
- </div>
- </div>
-
- <div class="table-section section-10 gl-text-center">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Failed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.failed_count }}</div>
- </div>
-
- <div class="table-section section-10 gl-text-center">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Errors') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.error_count }}</div>
- </div>
-
- <div class="table-section section-10 gl-text-center">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Skipped') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.skipped_count }}</div>
- </div>
-
- <div class="table-section section-10 gl-text-center">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Passed') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.success_count }}</div>
- </div>
-
- <div class="table-section section-10 gl-text-right pr-md-3">
- <div role="rowheader" class="table-mobile-header gl-font-weight-bold">
- {{ __('Total') }}
- </div>
- <div class="table-mobile-content">{{ testSuite.total_count }}</div>
- </div>
- </div>
- </div>
-
- <div v-else>
- <p class="js-no-tests-suites">{{ s__('TestReports|There are no test suites to show.') }}</p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
deleted file mode 100644
index d42a11c3aba..00000000000
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ /dev/null
@@ -1,73 +0,0 @@
-import { reportToSentry } from '../utils';
-import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
-
-const unwrapGroups = (stages) => {
- return stages.map((stage, idx) => {
- const {
- groups: { nodes: groups },
- } = stage;
-
- /*
- Being peformance conscious here means we don't want to spread and copy the
- group value just to add one parameter.
- */
- /* eslint-disable no-param-reassign */
- const groupsWithStageName = groups.map((group) => {
- group.stageName = stage.name;
- return group;
- });
- /* eslint-enable no-param-reassign */
-
- return { node: { ...stage, groups: groupsWithStageName }, lookup: { stageIdx: idx } };
- });
-};
-
-const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
- if (jobArray.length < 1) {
- reportToSentry('unwrapping_utils', 'undefined_job_hunt, array empty from backend');
- }
-
- return jobArray.map((job) => {
- if (job[prop]) {
- return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
- }
- return job;
- });
-};
-
-const unwrapJobWithNeeds = (denodedJobArray) => {
- const explicitNeedsUnwrapped = unwrapNodesWithName(denodedJobArray, EXPLICIT_NEEDS_PROPERTY);
- return unwrapNodesWithName(explicitNeedsUnwrapped, NEEDS_PROPERTY);
-};
-
-const unwrapStagesWithNeedsAndLookup = (denodedStages) => {
- const unwrappedNestedGroups = unwrapGroups(denodedStages);
-
- const lookupMap = {};
-
- const nodes = unwrappedNestedGroups.map(({ node, lookup }) => {
- const { groups } = node;
- const groupsWithJobs = groups.map((group, idx) => {
- const jobs = unwrapJobWithNeeds(group.jobs.nodes);
-
- lookupMap[group.name] = { ...lookup, groupIdx: idx };
- return { ...group, jobs };
- });
-
- return { ...node, groups: groupsWithJobs };
- });
-
- return { stages: nodes, lookup: lookupMap };
-};
-
-const unwrapStagesWithNeeds = (denodedStages) => {
- return unwrapStagesWithNeedsAndLookup(denodedStages).stages;
-};
-
-export {
- unwrapGroups,
- unwrapJobWithNeeds,
- unwrapNodesWithName,
- unwrapStagesWithNeeds,
- unwrapStagesWithNeedsAndLookup,
-};