Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
cfb1a5b025
commit
af2edb33e5
|
|
@ -3433,12 +3433,15 @@ Gitlab/BoundedContexts:
|
|||
- 'ee/app/services/package_metadata/compressed_package_data_object.rb'
|
||||
- 'ee/app/services/package_metadata/data_object.rb'
|
||||
- 'ee/app/services/package_metadata/data_object_fabricator.rb'
|
||||
- 'ee/app/services/package_metadata/data_objects/cve_enrichment.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/advisory/advisory_ingestion_task.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/advisory/affected_package_ingestion_task.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/advisory/ingestion_service.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/compressed_package/ingestion_service.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/compressed_package/license_ingestion_task.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/compressed_package/package_ingestion_task.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/cve_enrichment/ingestion_service.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/cve_enrichment/cve_enrichment_ingestion_task.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/data_map.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/ingestion_service.rb'
|
||||
- 'ee/app/services/package_metadata/ingestion/tasks/base.rb'
|
||||
|
|
|
|||
|
|
@ -18,14 +18,11 @@ export const DEFAULT = 'default';
|
|||
export const DELETE_FAILURE = 'delete_pipeline_failure';
|
||||
export const DRAW_FAILURE = 'draw_failure';
|
||||
export const LOAD_FAILURE = 'load_failure';
|
||||
export const PARSE_FAILURE = 'parse_failure';
|
||||
export const POST_FAILURE = 'post_failure';
|
||||
export const UNSUPPORTED_DATA = 'unsupported_data';
|
||||
|
||||
// Pipeline tabs
|
||||
|
||||
export const pipelineTabName = 'graph';
|
||||
export const needsTabName = 'dag';
|
||||
export const jobsTabName = 'builds';
|
||||
export const failedJobsTabName = 'failures';
|
||||
export const testReportTabName = 'test_report';
|
||||
|
|
@ -35,7 +32,6 @@ export const licensesTabName = 'licenses';
|
|||
export const codeQualityTabName = 'codequality_report';
|
||||
|
||||
export const validPipelineTabNames = [
|
||||
needsTabName,
|
||||
jobsTabName,
|
||||
failedJobsTabName,
|
||||
testReportTabName,
|
||||
|
|
|
|||
|
|
@ -1,69 +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-flex',
|
||||
'gl-flex-col',
|
||||
'gl-fixed',
|
||||
'gl-right-1',
|
||||
'gl-top-2/3',
|
||||
'gl-w-max',
|
||||
'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-flex gl-items-center">
|
||||
<div
|
||||
data-testid="dag-color-block"
|
||||
class="gl-h-5 gl-w-6"
|
||||
:style="{
|
||||
background: `linear-gradient(0.25turn, ${note.source.color} 40%, ${note.target.color} 60%)`,
|
||||
}"
|
||||
></div>
|
||||
<div data-testid="dag-note-text" class="gl-items-center gl-px-2 gl-text-base">
|
||||
{{ note.source.name }} → {{ note.target.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-button v-if="shouldShowLink" variant="link" @click="toggleList">{{ linkText }}</gl-button>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,317 +0,0 @@
|
|||
<script>
|
||||
import * as d3 from 'd3';
|
||||
import { uniqueId } from 'lodash';
|
||||
import { getMaxNodes, removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import { PARSE_FAILURE } from '../../constants';
|
||||
import { LINK_SELECTOR, NODE_SELECTOR, ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '../constants';
|
||||
import { calculateClip, createLinkPath, createSankey, labelPosition } from '../utils/drawing_utils';
|
||||
import {
|
||||
currentIsLive,
|
||||
getLiveLinksAsDict,
|
||||
highlightLinks,
|
||||
restoreLinks,
|
||||
toggleLinkHighlight,
|
||||
togglePathHighlights,
|
||||
} from '../utils/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-flex', 'gl-flex-col'].join(' '),
|
||||
hoverFadeClasses: ['gl-cursor-pointer', 'gl-duration-slow', 'gl-ease-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-flex',
|
||||
'gl-pointer-events-none',
|
||||
'gl-flex-col',
|
||||
'gl-justify-center',
|
||||
'gl-break-words',
|
||||
].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-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-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-text-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>
|
||||
|
|
@ -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';
|
||||
|
|
@ -1,306 +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 { s__, __ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import {
|
||||
DEFAULT,
|
||||
PARSE_FAILURE,
|
||||
LOAD_FAILURE,
|
||||
UNSUPPORTED_DATA,
|
||||
} from '~/ci/pipeline_details/constants';
|
||||
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import getDagVisData from './graphql/queries/get_dag_vis_data.query.graphql';
|
||||
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from './constants';
|
||||
import DagAnnotations from './components/dag_annotations.vue';
|
||||
import DagGraph from './components/dag_graph.vue';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
name: 'Dag',
|
||||
i18n: {
|
||||
deprecationAlert: {
|
||||
title: s__('Pipelines|Upcoming visualization change'),
|
||||
message: s__(
|
||||
'Pipelines|The visualization in this tab will be %{linkStart}removed%{linkEnd}. Instead, view %{codeStart}needs%{codeEnd} relationships with the %{strongStart}Job dependency%{strongEnd} option in the %{strongStart}Pipeline%{strongEnd} tab.',
|
||||
),
|
||||
},
|
||||
},
|
||||
deprecationInfoLink: helpPagePath('update/deprecations', {
|
||||
anchor: 'removed-needs-tab-from-the-pipeline-view',
|
||||
}),
|
||||
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,
|
||||
showDeprecationAlert: true,
|
||||
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]: __('Needs 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} dependencies between jobs in this tab.",
|
||||
),
|
||||
button: __('Learn more about needs dependencies'),
|
||||
},
|
||||
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.annotationsMap = {
|
||||
...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;
|
||||
},
|
||||
hideDeprecationAlert() {
|
||||
this.showDeprecationAlert = false;
|
||||
},
|
||||
hideFailureAlert() {
|
||||
this.showFailureAlert = false;
|
||||
},
|
||||
removeAnnotationFromMap({ uid }) {
|
||||
const copy = { ...this.annotationsMap };
|
||||
delete copy[uid];
|
||||
this.annotationsMap = copy;
|
||||
},
|
||||
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="showDeprecationAlert"
|
||||
:title="$options.i18n.deprecationAlert.title"
|
||||
variant="warning"
|
||||
data-testid="deprecation-alert"
|
||||
@dismiss="hideDeprecationAlert"
|
||||
>
|
||||
<gl-sprintf :message="$options.i18n.deprecationAlert.message">
|
||||
<template #code="{ content }">
|
||||
<code>{{ content }}</code>
|
||||
</template>
|
||||
<template #strong="{ content }">
|
||||
<strong>{{ content }}</strong>
|
||||
</template>
|
||||
<template #link="{ content }">
|
||||
<gl-link
|
||||
:href="$options.deprecationInfoLink"
|
||||
class="gl-inline-flex !gl-no-underline"
|
||||
target="_blank"
|
||||
>
|
||||
{{ content }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</gl-alert>
|
||||
<gl-alert
|
||||
v-if="showFailureAlert"
|
||||
:variant="failure.variant"
|
||||
data-testid="failure-alert"
|
||||
@dismiss="hideFailureAlert"
|
||||
>
|
||||
{{ 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"
|
||||
:svg-height="null"
|
||||
: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>
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
query getDagVisData($projectPath: ID!, $iid: ID!) {
|
||||
project(fullPath: $projectPath) {
|
||||
id
|
||||
pipeline(iid: $iid) {
|
||||
id
|
||||
stages {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
groups {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
size
|
||||
jobs {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
needs {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
};
|
||||
};
|
||||
|
|
@ -1,156 +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());
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
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]);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line max-params
|
||||
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);
|
||||
};
|
||||
|
|
@ -43,7 +43,6 @@ export const createAppOptions = (selector, apolloProvider, router) => {
|
|||
suiteEndpoint,
|
||||
blobPath,
|
||||
hasTestReport,
|
||||
emptyDagSvgPath,
|
||||
emptyStateImagePath,
|
||||
artifactsExpiredImagePath,
|
||||
isFullCodequalityReportAvailable,
|
||||
|
|
@ -95,7 +94,6 @@ export const createAppOptions = (selector, apolloProvider, router) => {
|
|||
suiteEndpoint,
|
||||
blobPath,
|
||||
hasTestReport,
|
||||
emptyDagSvgPath,
|
||||
emptyStateImagePath,
|
||||
artifactsExpiredImagePath,
|
||||
securityPoliciesPath,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,10 @@
|
|||
import PipelineGraphWrapper from './graph/graph_component_wrapper.vue';
|
||||
import Dag from './dag/dag.vue';
|
||||
import FailedJobsApp from './jobs/failed_jobs_app.vue';
|
||||
import JobsApp from './jobs/jobs_app.vue';
|
||||
import TestReports from './test_reports/test_reports.vue';
|
||||
import ManualVariables from './manual_variables/manual_variables.vue';
|
||||
import {
|
||||
pipelineTabName,
|
||||
needsTabName,
|
||||
jobsTabName,
|
||||
failedJobsTabName,
|
||||
testReportTabName,
|
||||
|
|
@ -15,7 +13,6 @@ import {
|
|||
|
||||
export const routes = [
|
||||
{ name: pipelineTabName, path: '/', component: PipelineGraphWrapper },
|
||||
{ name: needsTabName, path: '/dag', component: Dag },
|
||||
{ name: jobsTabName, path: '/builds', component: JobsApp },
|
||||
{ name: failedJobsTabName, path: '/failures', component: FailedJobsApp },
|
||||
{ name: testReportTabName, path: '/test_report', component: TestReports },
|
||||
|
|
|
|||
|
|
@ -1,4 +1,33 @@
|
|||
import * as d3 from 'd3';
|
||||
import { sankey, sankeyLeft } from 'd3-sankey';
|
||||
|
||||
/*
|
||||
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 createUniqueLinkId = (stageName, jobName) => `${stageName}-${jobName}`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { memoize } from 'lodash';
|
||||
import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
|
||||
import { createSankey } from '../dag/utils/drawing_utils';
|
||||
import { createSankey } from './drawing_utils';
|
||||
import { createNodeDict } from './index';
|
||||
|
||||
/*
|
||||
|
|
@ -106,16 +106,6 @@ export const getMaxNodes = (nodes) => {
|
|||
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
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ html {
|
|||
}
|
||||
|
||||
.alert-wrapper {
|
||||
.gl-alert:first-child {
|
||||
&:not(:empty) {
|
||||
@apply gl-mt-3;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ class JwtController < ApplicationController
|
|||
::Auth::DependencyProxyAuthenticationService::AUDIENCE => ::Auth::DependencyProxyAuthenticationService
|
||||
}.freeze
|
||||
|
||||
# Currently POST requests for this route return a 404 by default and are allowed through in our readonly middleware -
|
||||
# ee/lib/ee/gitlab/middleware/read_only/controller.rb
|
||||
# If the action here changes to allow POST requests then a check for maintenance mode should be added
|
||||
def auth
|
||||
service = SERVICES[params[:service]]
|
||||
return head :not_found unless service
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
= render Pajamas::AlertComponent.new(title: _('OpenSSL version 3'),
|
||||
variant: :warning,
|
||||
alert_options: { class: 'gl-my-5 js-openssl-callout',
|
||||
alert_options: { class: 'js-openssl-callout',
|
||||
data: { feature_id: Users::CalloutsHelper::OPENSSL_CALLOUT,
|
||||
dismiss_endpoint: callouts_path }},
|
||||
close_button_options: { data: { testid: 'close-openssl-callout' }}) do |c|
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ module Packages
|
|||
|
||||
def after_destroy
|
||||
pkg = artifact.package
|
||||
# Ml::ModelVersion need the package to be able to upload files later
|
||||
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/461322
|
||||
return if pkg.ml_model?
|
||||
|
||||
pkg.transaction do
|
||||
pkg.destroy if model.for_package_ids(pkg.id).empty?
|
||||
|
|
|
|||
|
|
@ -5,4 +5,4 @@ feature_category: source_code_management
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160838
|
||||
milestone: '17.3'
|
||||
queued_migration_version: 20240726081618
|
||||
finalized_by: # version of the migration that finalized this BBM
|
||||
finalized_by: '20240906133341'
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTemporaryIndexForSnippets < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
add_concurrent_index :snippets, :id,
|
||||
where: "type = 'ProjectSnippet' and organization_id IS NOT NULL",
|
||||
name: 'tmp_index_snippets_for_organization_id'
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
remove_concurrent_index_by_name :snippets, 'tmp_index_snippets_for_organization_id'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FixNonNullableSnippets < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
retry_count = 0
|
||||
begin
|
||||
model = define_batchable_model(:snippets)
|
||||
model.transaction do
|
||||
relation = model.where(type: 'ProjectSnippet').where.not(organization_id: nil)
|
||||
|
||||
relation.select(:id).find_each do |snippet|
|
||||
snippet.update_column(:organization_id, nil)
|
||||
end
|
||||
end
|
||||
rescue ActiveRecord::QueryCanceled # rubocop:disable Database/RescueQueryCanceled -- to reuse a buffer cache to process stuck records
|
||||
retry_count += 1
|
||||
retry if retry_count < 5
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RemoveTemporaryIndexForSnippets < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
return unless Gitlab.com?
|
||||
|
||||
remove_concurrent_index_by_name :snippets, 'tmp_index_snippets_for_organization_id'
|
||||
end
|
||||
|
||||
def down
|
||||
return unless Gitlab.com?
|
||||
|
||||
add_concurrent_index :snippets, :id,
|
||||
where: "type = 'ProjectSnippet' and organization_id IS NOT NULL",
|
||||
name: 'tmp_index_snippets_for_organization_id'
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class FinalizeNullifyOrganizationIdForSnippets < Gitlab::Database::Migration[2.2]
|
||||
milestone '17.4'
|
||||
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
ensure_batched_background_migration_is_finished(
|
||||
job_class_name: 'NullifyOrganizationIdForSnippets',
|
||||
table_name: 'snippets',
|
||||
column_name: 'id',
|
||||
job_arguments: [],
|
||||
finalize: true
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1 @@
|
|||
ed8265250fec53324c839f5ea5e9405ec8a74ab1777010b3a93b5f63d37c646f
|
||||
|
|
@ -0,0 +1 @@
|
|||
151fef83e897583bb108b74a181063f8e73e18d2bafda6db6c8eff158263a89e
|
||||
|
|
@ -0,0 +1 @@
|
|||
e40f25686e1352ddff17405937fb1b973d879bdab9d10c8cb080b5d5d9868139
|
||||
|
|
@ -0,0 +1 @@
|
|||
b33ed25f04eb775aa69aa1e27b6da5ba2a46d5c8754de16092091c65e8a93c89
|
||||
|
|
@ -28,11 +28,10 @@ Please do not rely on this information for purchasing or planning purposes.
|
|||
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
|
||||
sole discretion of GitLab Inc.
|
||||
|
||||
To configure your GitLab instance to access the available self-hosted large language
|
||||
models (LLMs) in your infrastructure:
|
||||
To configure your GitLab instance to access the available self-hosted models in your infrastructure:
|
||||
|
||||
1. Configure the self-hosted model.
|
||||
1. Configure the GitLab Duo AI-powered features to use your self-hosted models.
|
||||
1. Configure the GitLab Duo features to use your self-hosted model.
|
||||
|
||||
## Configure the self-hosted model
|
||||
|
||||
|
|
@ -50,22 +49,23 @@ To configure a self-hosted model:
|
|||
1. In **Subscription details**, to the right of **Last sync**, select
|
||||
synchronize subscription (**{retry}**).
|
||||
1. Select **Models**.
|
||||
1. Set your model details by selecting **New self-hosted model**.
|
||||
1. Select **New self-hosted model**.
|
||||
1. Complete the fields:
|
||||
- Enter the model name, for example, Mistral.
|
||||
- From the **Model** dropdown list, select the model. Only GitLab-approved models are listed here.
|
||||
- For **Endpoint**, select the self-hosted model endpoint, for example, the server hosting the model.
|
||||
- Enter the model name, for example, `Mistral`.
|
||||
- From the **Model** dropdown list, select the model. Only GitLab-approved models
|
||||
are in this list.
|
||||
- For **Endpoint**, select the self-hosted model endpoint. For example, the
|
||||
server hosting the model.
|
||||
- Optional. For **API token**, add an API key if you need one to access the model.
|
||||
|
||||
1. Select **Create model**.
|
||||
|
||||
## Configure the features to your models
|
||||
## Configure GitLab Duo features to use self-hosted models
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- You must be an administrator.
|
||||
|
||||
### View the configured features
|
||||
### View configured features
|
||||
|
||||
1. On the left sidebar, at the bottom, select **Admin**.
|
||||
1. Select **AI-powered features**.
|
||||
|
|
@ -76,21 +76,22 @@ Prerequisites:
|
|||
synchronize subscription (**{retry}**).
|
||||
1. Select **Features**.
|
||||
|
||||
### Configure the features to use self-hosted models
|
||||
### Configure the feature to use a self-hosted model
|
||||
|
||||
Use a self-hosted AI Gateway to execute queries to the configured self-hosted model:
|
||||
Configure the GitLab Duo feature to send queries to the configured self-hosted model:
|
||||
|
||||
1. For the feature you want to set, select **Edit**.
|
||||
1. In **Features**, for the feature you want to set, select **Edit**.
|
||||
For example, **Code Generation**.
|
||||
1. Select the model provider for the feature:
|
||||
- From the list, select **Self-Hosted Model**.
|
||||
- Choose the self-hosted model you would like to use, for example, Mistral.
|
||||
- Choose the self-hosted model you want to use, for example, `Mistral`.
|
||||
1. Select **Save Changes**.
|
||||
|
||||
### Configure the features to use GitLab AI Vendor models
|
||||
|
||||
You can set a GitLab Duo feature's model provider to the GitLab AI Vendor. That feature then uses GitLab hosted models through the Cloud Connector:
|
||||
You can choose a GitLab AI vendor to be the GitLab Duo feature's model provider. The
|
||||
feature then uses the GitLab-hosted model through the GitLab Cloud Connector:
|
||||
|
||||
1. For the feature you want to set, select **Edit**.
|
||||
1. In **Features**, for the feature you want to set, select **Edit**.
|
||||
1. In the list of model providers for the feature, select **AI Vendor**.
|
||||
1. Select **Save Changes**.
|
||||
|
|
|
|||
|
|
@ -25,32 +25,35 @@ DISCLAIMER:
|
|||
This page contains information related to upcoming products, features, and functionality.
|
||||
It is important to note that the information presented is for informational purposes only.
|
||||
Please do not rely on this information for purchasing or planning purposes.
|
||||
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
|
||||
sole discretion of GitLab Inc.
|
||||
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
|
||||
|
||||
When you deploy a self-hosted large language model (LLM), you can:
|
||||
When you deploy a self-hosted model, you can:
|
||||
|
||||
- Manage the end-to-end transmission of requests to enterprise-hosted LLM backends for GitLab Duo features.
|
||||
- Keep all of these requests within their enterprise network, ensuring no calls to external architecture.
|
||||
- Isolate the GitLab instance, AI Gateway, and self-hosted AI model within their own
|
||||
- Manage the end-to-end transmission of requests to enterprise-hosted large
|
||||
language model (LLM) backends for GitLab Duo features.
|
||||
- Keep all of these requests within that enterprise network, ensuring no calls
|
||||
to external architecture.
|
||||
- Isolate the GitLab instance, AI Gateway, and self-hosted model within their own
|
||||
environment, ensuring complete privacy and high security for using AI features, with
|
||||
no reliance on public services.
|
||||
|
||||
When you use self-hosted models, you:
|
||||
|
||||
- Can choose any GitLab-approved LLM model.
|
||||
- Can choose any GitLab-approved LLM.
|
||||
- Can keep all data and request/response logs in your own domain.
|
||||
- Can select specific GitLab Duo features for your users.
|
||||
- Do not have to rely on the GitLab shared AI Gateway.
|
||||
|
||||
You can connect supported models to LLM features. Model-specific prompts
|
||||
and GitLab Duo feature support is provided by the self-hosted models feature. For
|
||||
more information about this offering, see [subscriptions](../../subscriptions/self_managed/index.md) and the [Blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/custom_models/).
|
||||
and GitLab Duo feature support is provided by the GitLab Duo Self-Hosted Models
|
||||
feature. For more information about this offering, see
|
||||
[subscriptions](../../subscriptions/self_managed/index.md) and the
|
||||
[Blueprint](https://handbook.gitlab.com/handbook/engineering/architecture/design-documents/custom_models/).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- You must be able to manage your own LLM infrastructure.
|
||||
- You must have the [GitLab Enterprise Edition](../../administration/license.md).
|
||||
- You must have [GitLab Enterprise Edition](../../administration/license.md).
|
||||
|
||||
## Deploy a self-hosted model
|
||||
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ DISCLAIMER:
|
|||
This page contains information related to upcoming products, features, and functionality.
|
||||
It is important to note that the information presented is for informational purposes only.
|
||||
Please do not rely on this information for purchasing or planning purposes.
|
||||
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the
|
||||
sole discretion of GitLab Inc.
|
||||
The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
|
||||
|
||||
By self-hosting the model, AI Gateway, and GitLab instance, there are no calls to external architecture, ensuring maximum levels of security.
|
||||
By self-hosting the model, AI Gateway, and GitLab instance, there are no calls to
|
||||
external architecture, ensuring maximum levels of security.
|
||||
|
||||
To set up your self-hosted model deployment infrastructure:
|
||||
|
||||
|
|
@ -36,10 +36,15 @@ To set up your self-hosted model deployment infrastructure:
|
|||
1. Configure your GitLab instance.
|
||||
1. Install the GitLab AI Gateway.
|
||||
|
||||
- [Installation video guide](https://youtu.be/UNmD9-sgUvw)
|
||||
- [Installation video guide (French version)](https://www.youtube.com/watch?v=aU5vnzO-MSM)
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For an installation video guide, see [Self-Hosted Models Deployment](https://youtu.be/UNmD9-sgUvw).
|
||||
<!-- Video published on 2024-05-30 -->
|
||||
|
||||
## Step 1: Install LLM serving infrastructure
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
For an installation video guide in French, see [Self-Hosted Models Deployment (French Language version)](https://youtu.be/UNmD9-sgUvw).
|
||||
<!-- Video published on 2024-05-30 -->
|
||||
|
||||
## Install large language model serving infrastructure
|
||||
|
||||
Install one of the following GitLab-approved LLM models:
|
||||
|
||||
|
|
@ -55,9 +60,10 @@ Install one of the following GitLab-approved LLM models:
|
|||
| [Mixtral 8x22B](https://huggingface.co/mistral-community/Mixtral-8x22B-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
| [Mixtral 8x7B](https://huggingface.co/mistralai/Mixtral-8x7B-Instruct-v0.1) | **{dotted-circle}** No | **{check-circle}** Yes | **{check-circle}** Yes |
|
||||
|
||||
### Recommended serving architectures
|
||||
### Use a serving architecture
|
||||
|
||||
You should use one of the following architectures:
|
||||
You should use one of the following serving architectures with your
|
||||
installed LLM:
|
||||
|
||||
- [vLLM](https://docs.vllm.ai/en/stable/)
|
||||
- [TensorRT-LLM](https://docs.mistral.ai/deployment/self-deployment/overview/)
|
||||
|
|
@ -109,9 +115,9 @@ model_list:
|
|||
api_base: YOUR_HOSTING_SERVER
|
||||
```
|
||||
|
||||
## Step 2: Configure your GitLab instance
|
||||
## Configure your GitLab instance
|
||||
|
||||
1. For the GitLab instance to know where AI Gateway is located so it can access
|
||||
1. For the GitLab instance to know where the AI Gateway is located so it can access
|
||||
the gateway, set the environment variable `AI_GATEWAY_URL` inside your GitLab
|
||||
instance environment variables:
|
||||
|
||||
|
|
@ -119,7 +125,8 @@ model_list:
|
|||
AI_GATEWAY_URL=https://<your_ai_gitlab_domain>
|
||||
```
|
||||
|
||||
1. Where your GitLab instance is installed, [run the following Rake task](../../raketasks/index.md) to activate GitLab Duo features:
|
||||
1. Where your GitLab instance is installed, [run the following Rake task](../../raketasks/index.md)
|
||||
to activate GitLab Duo features:
|
||||
|
||||
```shell
|
||||
sudo gitlab-rake gitlab:duo:enable_feature_flags
|
||||
|
|
@ -139,7 +146,7 @@ model_list:
|
|||
|
||||
Exit the Rails console.
|
||||
|
||||
## Step 3: Install the GitLab AI Gateway
|
||||
## Install the GitLab AI Gateway
|
||||
|
||||
### Install by using Docker
|
||||
|
||||
|
|
@ -148,7 +155,8 @@ Prerequisites:
|
|||
- Install a Docker container engine, such as [Docker](https://docs.docker.com/engine/install/#server).
|
||||
- Use a valid hostname accessible within your network. Do not use `localhost`.
|
||||
|
||||
The GitLab AI Gateway Docker image contains all necessary code and dependencies in a single container.
|
||||
The GitLab AI Gateway Docker image contains all necessary code and dependencies
|
||||
in a single container.
|
||||
|
||||
#### Find the AI Gateway release
|
||||
|
||||
|
|
@ -158,7 +166,9 @@ Find the GitLab official Docker image at:
|
|||
- [AI Gateway Docker image on DockerHub](https://hub.docker.com/repository/docker/gitlab/model-gateway/tags).
|
||||
- [Release process for self-hosted AI Gateway](https://gitlab.com/gitlab-org/modelops/applied-ml/code-suggestions/ai-assist/-/blob/main/docs/release.md).
|
||||
|
||||
Use the image tag that corresponds to your GitLab version. For example, if the GitLab version is `v17.4.0`, use `self-hosted-v17.4.0-ee` tag.
|
||||
Use the image tag that corresponds to your GitLab version. For example, if the
|
||||
GitLab version is `v17.4.0`, use `self-hosted-v17.4.0-ee` tag.
|
||||
|
||||
For version `v17.3.0-ee`, use image tag `gitlab-v17.3.0`.
|
||||
|
||||
WARNING:
|
||||
|
|
@ -169,8 +179,9 @@ to community resources (such as IRC or forums) to seek help from other users.
|
|||
|
||||
#### Set up the volumes location
|
||||
|
||||
Create a directory where the logs will reside on the Docker host. It can be under your user's home directory (for example
|
||||
`~/gitlab-agw`), or in a directory like `/srv/gitlab-agw`. To create that directory, run:
|
||||
Create a directory where the logs will reside on the Docker host. It can be under
|
||||
your user's home directory (for example `~/gitlab-agw`), or in a directory like
|
||||
`/srv/gitlab-agw`. To create that directory, run:
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /srv/gitlab-agw
|
||||
|
|
@ -187,7 +198,7 @@ This only applies to AI Gateway image tag `gitlab-17.3.0-ee` and earlier. For im
|
|||
To improve results when asking GitLab Duo Chat questions about GitLab, you can
|
||||
index GitLab documentation and provide it as a file to the AI Gateway.
|
||||
|
||||
To index the documentation in your local installation,run:
|
||||
To index the documentation in your local installation, run:
|
||||
|
||||
```shell
|
||||
pip install requests langchain langchain_text_splitters
|
||||
|
|
@ -210,7 +221,7 @@ For Docker images with version `self-hosted-17.4.0-ee` and later, run the follow
|
|||
docker run -e AIGW_GITLAB_URL=<your_gitlab_instance> <image>
|
||||
```
|
||||
|
||||
For Docker images with version `gitlab-17.3.0-ee` and `gitlab-17.2.0`:
|
||||
For Docker images with version `gitlab-17.3.0-ee` and `gitlab-17.2.0`, run:
|
||||
|
||||
```shell
|
||||
docker run -e AIGW_CUSTOM_MODELS__ENABLED=true \
|
||||
|
|
@ -237,7 +248,7 @@ should open the AI Gateway API documentation.
|
|||
AIGW_GITLAB_API_URL=https://<your_gitlab_domain>/api/v4/
|
||||
```
|
||||
|
||||
1. [Configure the GitLab instance](#step-2-configure-your-gitlab-instance).
|
||||
1. [Configure the GitLab instance](#configure-your-gitlab-instance).
|
||||
|
||||
1. After you've set up the environment variables, [run the image](#start-a-container-from-the-image).
|
||||
|
||||
|
|
@ -250,36 +261,15 @@ should open the AI Gateway API documentation.
|
|||
After starting the container, visit `gitlab-aigw.example.com`. It might take
|
||||
a while before the Docker container starts to respond to queries.
|
||||
|
||||
### Upgrade the AI Gateway Docker image
|
||||
|
||||
To upgrade the AI Gateway, download the newest Docker image tag.
|
||||
|
||||
1. Stop the running container:
|
||||
|
||||
```shell
|
||||
sudo docker stop gitlab-aigw
|
||||
```
|
||||
|
||||
1. Remove the existing container:
|
||||
|
||||
```shell
|
||||
sudo docker rm gitlab-aigw
|
||||
```
|
||||
|
||||
1. Pull and [run the new image](#start-a-container-from-the-image).
|
||||
|
||||
1. Ensure that the environment variables are all set correctly
|
||||
|
||||
### Install by using the AI Gateway Helm chart
|
||||
|
||||
#### Prerequisites
|
||||
Prerequisites:
|
||||
|
||||
To complete this guide, you must have the following:
|
||||
|
||||
- A domain you own, that you can add a DNS record to.
|
||||
- A Kubernetes cluster.
|
||||
- A working installation of `kubectl`.
|
||||
- A working installation of Helm, version v3.11.0 or later.
|
||||
- You must have a:
|
||||
- Domain you own, that you can add a DNS record to.
|
||||
- Kubernetes cluster.
|
||||
- Working installation of `kubectl`.
|
||||
- Working installation of Helm, version v3.11.0 or later.
|
||||
|
||||
For more information, see [Test the GitLab chart on GKE or EKS](https://docs.gitlab.com/charts/quickstart/index.html).
|
||||
|
||||
|
|
@ -308,8 +298,8 @@ https://gitlab.com/api/v4/projects/gitlab-org%2fcharts%2fai-gateway-helm-chart/p
|
|||
```
|
||||
|
||||
1. For the AI Gateway to access the API, it must know where the GitLab instance
|
||||
is located. To do this, set the `gitlab.url` and
|
||||
`gitlab.apiUrl` together with the `ingress.hosts` and `ingress.tls` values as follows:
|
||||
is located. To do this, set the `gitlab.url` and `gitlab.apiUrl` together with
|
||||
the `ingress.hosts` and `ingress.tls` values as follows:
|
||||
|
||||
```shell
|
||||
helm repo add ai-gateway \
|
||||
|
|
@ -332,7 +322,8 @@ is located. To do this, set the `gitlab.url` and
|
|||
--timeout=300s --wait --wait-for-jobs
|
||||
```
|
||||
|
||||
This step can take will take a few seconds in order for all resources to be allocated and the AI Gateway to start.
|
||||
This step can take will take a few seconds in order for all resources to be allocated
|
||||
and the AI Gateway to start.
|
||||
|
||||
Wait for your pods to get up and running:
|
||||
|
||||
|
|
@ -344,30 +335,43 @@ kubectl wait pod \
|
|||
--timeout=300s
|
||||
```
|
||||
|
||||
When it's done, you can proceed with setting up your IP ingresses and DNS records.
|
||||
When your pods are up and running, you can set up your IP ingresses and DNS records.
|
||||
|
||||
#### Installation steps in the GitLab instance
|
||||
#### Configure the GitLab instance
|
||||
|
||||
[Configure the GitLab instance](#step-2-configure-your-gitlab-instance).
|
||||
[Configure the GitLab instance](#configure-your-gitlab-instance).
|
||||
|
||||
With those steps completed, your Helm chart installation is complete.
|
||||
|
||||
## Upgrade the AI Gateway Docker image
|
||||
|
||||
To upgrade the AI Gateway, download the newest Docker image tag.
|
||||
|
||||
1. Stop the running container:
|
||||
|
||||
```shell
|
||||
sudo docker stop gitlab-aigw
|
||||
```
|
||||
|
||||
1. Remove the existing container:
|
||||
|
||||
```shell
|
||||
sudo docker rm gitlab-aigw
|
||||
```
|
||||
|
||||
1. Pull and [run the new image](#start-a-container-from-the-image).
|
||||
|
||||
1. Ensure that the environment variables are all set correctly.
|
||||
|
||||
## Alternative installation methods
|
||||
|
||||
For information on alternative ways to install the AI Gateway, see [issue 463773](https://gitlab.com/gitlab-org/gitlab/-/issues/463773).
|
||||
For information on alternative ways to install the AI Gateway, see
|
||||
[issue 463773](https://gitlab.com/gitlab-org/gitlab/-/issues/463773).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
First, run the [debugging scripts](troubleshooting.md#use-debugging-scripts) to verify your self-hosted model setup.
|
||||
First, run the [debugging scripts](troubleshooting.md#use-debugging-scripts) to
|
||||
verify your self-hosted model setup.
|
||||
|
||||
For more information on other actions to take, see the [troubleshooting documentation](troubleshooting.md).
|
||||
|
||||
### The image's platform does not match the host
|
||||
|
||||
When [finding the AI Gateway release](#find-the-ai-gateway-release), you might get an error that states `The requested image’s platform (linux/amd64) does not match the detected host`.
|
||||
|
||||
To work around this error, add `--platform linux/amd64` to the `docker run` command:
|
||||
|
||||
```shell
|
||||
docker run --platform linux/amd64 -e AIGW_GITLAB_URL=<your-gitlab-endpoint> <image>
|
||||
```
|
||||
For more information on other actions to take, see the
|
||||
[troubleshooting documentation](troubleshooting.md).
|
||||
|
|
|
|||
|
|
@ -5,33 +5,32 @@ description: Troubleshooting tips for deploying self-hosted model deployment
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Troubleshooting your self-managed GitLab Duo setup
|
||||
# Troubleshooting GitLab Duo Self-Hosted Models
|
||||
|
||||
This content tells administrators how to debug their self-managed GitLab Duo setup.
|
||||
When working with GitLab Duo Self-Hosted Models, you might encounter issues.
|
||||
|
||||
## Before you begin
|
||||
|
||||
Before you begin debugging, you should:
|
||||
Before you begin troubleshooting, you should:
|
||||
|
||||
- Be able to access open the [`gitlab-rails` console](../../administration/operations/rails_console.md).
|
||||
- Open a shell in the AI Gateway Docker image.
|
||||
- Know the endpoint where your:
|
||||
- AI Gateway is hosted.
|
||||
- Model is hosted.
|
||||
- Enable the feature flag `expanded_ai_logging` on the `gitlab-rails` console:
|
||||
|
||||
You should also enable the feature flag `expanded_ai_logging` on the `gitlab-rails` console:
|
||||
```ruby
|
||||
Feature.enable(:expanded_ai_logging)
|
||||
```
|
||||
|
||||
```ruby
|
||||
Feature.enable(:expanded_ai_logging)
|
||||
```
|
||||
Now, requests and responses from GitLab to the AI Gateway are logged to [`llm.log`](../logs/index.md#llmlog)
|
||||
|
||||
Now, requests and responses from GitLab to the AI Gateway are logged to [`llm.log`](../logs/index.md#llmlog)
|
||||
## Use debugging scripts
|
||||
|
||||
### Use debugging scripts
|
||||
We provide two debugging scripts to help administrators verify their self-hosted
|
||||
model configuration.
|
||||
|
||||
We provide two debugging scripts to help administrators verify their self-hosted setup.
|
||||
|
||||
1. Debug the GitLab to AI Gateway connection. From your GitLab instance, run the [Rake task](../../raketasks/index.md):
|
||||
1. Debug the GitLab to AI Gateway connection. From your GitLab instance, run the
|
||||
[Rake task](../../raketasks/index.md):
|
||||
|
||||
```shell
|
||||
gitlab-rake gitlab:duo:verify_self_hosted_setup
|
||||
|
|
@ -47,11 +46,13 @@ We provide two debugging scripts to help administrators verify their self-hosted
|
|||
|
||||
Verify the output of the commands, and fix accordingly.
|
||||
|
||||
If both commands are successful, but GitLab Duo Code Suggestions is still not working, raise an issue on the issue tracker.
|
||||
If both commands are successful, but GitLab Duo Code Suggestions is still not working,
|
||||
raise an issue on the issue tracker.
|
||||
|
||||
### Check if GitLab make a request to the model
|
||||
## Check if GitLab can make a request to the model
|
||||
|
||||
From the GitLab Rails console, verify that the model is reachable by running:
|
||||
From the GitLab Rails console, verify that GitLab can make a request to the model
|
||||
by running:
|
||||
|
||||
```ruby
|
||||
model_name = "<your_model_name>"
|
||||
|
|
@ -72,24 +73,26 @@ This should return a response from the model in the format:
|
|||
"timestamp"=>1723448920}}
|
||||
```
|
||||
|
||||
If that is not the case, this means that the:
|
||||
If that is not the case, this might means one of the following:
|
||||
|
||||
- User might not have access to Code Suggestions. To resolve, [check if a user can request Code Suggestions](#check-if-a-user-can-request-code-suggestions).
|
||||
- GitLab environment variables are not configured correctly. To resolve, [check that the GitLab environmental variables are set up correctly](#check-that-gitlab-environmental-variables-are-set-up-correctly).
|
||||
- GitLab instance is not configured to use self-hosted models. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
|
||||
- AI Gateway is not reachable. To resolve, [check if GitLab can make an HTTP request to the AI Gateway](#check-if-gitlab-can-make-an-http-request-to-ai-gateway).
|
||||
- The user might not have access to Code Suggestions. To resolve,
|
||||
[check if a user can request Code Suggestions](#check-if-a-user-can-request-code-suggestions).
|
||||
- The GitLab environment variables are not configured correctly. To resolve, [check that the GitLab environmental variables are set up correctly](#check-that-gitlab-environmental-variables-are-set-up-correctly).
|
||||
- The GitLab instance is not configured to use self-hosted models. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
|
||||
- The AI Gateway is not reachable. To resolve, [check if GitLab can make an HTTP request to the AI Gateway](#check-if-gitlab-can-make-an-http-request-to-ai-gateway).
|
||||
|
||||
## Check if a user can request Code Suggestions
|
||||
|
||||
In the GitLab Rails console, verify that a user can request Code Suggestions.
|
||||
In the GitLab Rails console, check if a user can request Code Suggestions by runnning:
|
||||
|
||||
```ruby
|
||||
User.find_by_id("<user_id>").can?(:access_code_suggestions)
|
||||
```
|
||||
|
||||
If this returns `false`, it means some configuration is missing that does not allow the user to access Code Suggestions.
|
||||
If this returns `false`, it means some configuration is missing, and the user
|
||||
cannot access Code Suggestions.
|
||||
|
||||
This missing configuration might be either of the following:
|
||||
This missing configuration might be because of either of the following:
|
||||
|
||||
- The license is not valid. To resolve, [check or update your license](../license_file.md#see-current-license-information).
|
||||
- GitLab Duo was not configured to use a self-hosted model. To resolve, [check if the GitLab instance is configured to use self-hosted models](#check-if-gitlab-instance-is-configured-to-use-self-hosted-models).
|
||||
|
|
@ -105,32 +108,36 @@ To check if GitLab Duo was configured correctly:
|
|||
|
||||
## Check that GitLab environmental variables are set up correctly
|
||||
|
||||
To check if the GitLab environmental variables are set up correctly, run the following on the GitLab Rails console:
|
||||
To check if the GitLab environmental variables are set up correctly, run the
|
||||
following on the GitLab Rails console:
|
||||
|
||||
```ruby
|
||||
ENV["AI_GATEWAY_URL"] == "<your-ai-gateway-endpoint>"
|
||||
```
|
||||
|
||||
If the environmental variables are not set up correctly, set them by following the [Linux package custom environment variables setting documentation](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
|
||||
If the environmental variables are not set up correctly, set them by following the
|
||||
[Linux package custom environment variables setting documentation](https://docs.gitlab.com/omnibus/settings/environment-variables.html).
|
||||
|
||||
## Check if GitLab can make an HTTP request to AI Gateway
|
||||
|
||||
In the GitLab Rails console, verify that the AI Gateway is reachable by running:
|
||||
In the GitLab Rails console, verify that GitLab can make an HTTP request to AI
|
||||
Gateway by running:
|
||||
|
||||
```ruby
|
||||
HTTParty.get('<your-aigateway-endpoint>/monitoring/healthz', headers: { 'accept' => 'application/json' }).code
|
||||
```
|
||||
|
||||
If the response is not `200`, this means either that:
|
||||
If the response is not `200`, this means either of the following:
|
||||
|
||||
- The network is not properly configured to allow GitLab to reach the AI Gateway container. Contact your network administrator to verify the setup.
|
||||
- AI Gateway is not able to process requests. To resolve this issue, [check if the AI Gateway can make a request to the model](#check-if-ai-gateway-can-make-a-request-to-the-model).
|
||||
|
||||
## Check if AI Gateway can make a request to the model
|
||||
|
||||
From the AI Gateway container, make an HTTP request to the AI Gateway API for a Code Suggestion. Replace:
|
||||
From the AI Gateway container, make an HTTP request to the AI Gateway API for a
|
||||
Code Suggestion. Replace:
|
||||
|
||||
- `<your_model_name>` with the name of the model you are using, for example `mistral` or `codegemma`.
|
||||
- `<your_model_name>` with the name of the model you are using. For example `mistral` or `codegemma`.
|
||||
- `<your_model_endpoint>` with the endpoint where the model is hosted.
|
||||
|
||||
```shell
|
||||
|
|
@ -141,11 +148,14 @@ curl --request POST "http://localhost:5052/v1/chat/agent" \
|
|||
--data '{ "prompt_components": [ { "type": "string", "metadata": { "source": "string", "version": "string" }, "payload": { "content": "Hello", "provider": "litellm", "model": "<your_model_name>", "model_endpoint": "<your_model_endpoint>" } } ], "stream": false }'
|
||||
```
|
||||
|
||||
If the request fails:
|
||||
If the request fails, the:
|
||||
|
||||
- AI-Gateway might not configured properly to use self-hosted models. To resolve this, [check that the AI Gateway environmental variables are set up correctly](#check-that-ai-gateway-environmental-variables-are-set-up-correctly).
|
||||
- AI-Gateway might not be able access the model. To resolve, [check if the model is reachable from the AI Gateway](#check-if-the-model-is-reachable-from-ai-gateway).
|
||||
- The model name or endpoint might be incorrect. Check the values, and correct them if necessary.
|
||||
- AI Gateway might not be configured properly to use self-hosted models. To resolve this,
|
||||
[check that the AI Gateway environmental variables are set up correctly](#check-that-ai-gateway-environmental-variables-are-set-up-correctly).
|
||||
- AI Gateway might not be able to access the model. To resolve,
|
||||
[check if the model is reachable from the AI Gateway](#check-if-the-model-is-reachable-from-ai-gateway).
|
||||
- Model name or endpoint might be incorrect. Check the values, and correct them
|
||||
if necessary.
|
||||
|
||||
## Check if AI Gateway can process requests
|
||||
|
||||
|
|
@ -158,7 +168,8 @@ If the response is not `200`, this means that AI Gateway is not installed correc
|
|||
|
||||
## Check that AI Gateway environmental variables are set up correctly
|
||||
|
||||
To check if the AI Gateway environmental variables are set up correctly, run the following in a console on the AI Gateway container:
|
||||
To check that the AI Gateway environmental variables are set up correctly, run the
|
||||
following in a console on the AI Gateway container:
|
||||
|
||||
```shell
|
||||
docker exec -it <ai-gateway-container> sh
|
||||
|
|
@ -166,13 +177,27 @@ echo $AIGW_AUTH__BYPASS_EXTERNAL # must be true
|
|||
echo $AIGW_CUSTOM_MODELS__ENABLED # must be true
|
||||
```
|
||||
|
||||
If the environmental variables are not set up correctly, set them by [creating a container](install_infrastructure.md#find-the-ai-gateway-release).
|
||||
If the environmental variables are not set up correctly, set them by
|
||||
[creating a container](install_infrastructure.md#find-the-ai-gateway-release).
|
||||
|
||||
## Check if the model is reachable from AI Gateway
|
||||
|
||||
Create a shell on the AI Gateway container and make a curl request to the model. If you find that the AI Gateway cannot make that request, this might be caused by the:
|
||||
Create a shell on the AI Gateway container and make a curl request to the model.
|
||||
If you find that the AI Gateway cannot make that request, this might be caused by the:
|
||||
|
||||
1. Model server not functioning correctly.
|
||||
1. Network settings around the container not being properly configured to allow requests to where the model is hosted.
|
||||
1. Network settings around the container not being properly configured to allow
|
||||
requests to where the model is hosted.
|
||||
|
||||
To resolve this, contact your network administrator.
|
||||
|
||||
## The image's platform does not match the host
|
||||
|
||||
When [finding the AI Gateway release](install_infrastructure.md#find-the-ai-gateway-release),
|
||||
you might get an error that states `The requested image’s platform (linux/amd64) does not match the detected host`.
|
||||
|
||||
To work around this error, add `--platform linux/amd64` to the `docker run` command:
|
||||
|
||||
```shell
|
||||
docker run --platform linux/amd64 -e AIGW_GITLAB_URL=<your-gitlab-endpoint> <image>
|
||||
```
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ PUT /application/plan_limits
|
|||
| `ci_active_jobs` | integer | no | Total number of jobs in currently active pipelines. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_project_subscriptions` | integer | no | Maximum number of pipeline subscriptions to and from a project. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_pipeline_schedules` | integer | no | Maximum number of pipeline schedules. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_needs_size_limit` | integer | no | Maximum number of [`needs`](../ci/directed_acyclic_graph/index.md) dependencies that a job can have. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_needs_size_limit` | integer | no | Maximum number of [`needs`](../ci/yaml/needs.md) dependencies that a job can have. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_registered_group_runners` | integer | no | Maximum number of runners registered per group. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `ci_registered_project_runners` | integer | no | Maximum number of runners registered per project. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/85895) in GitLab 15.0. |
|
||||
| `dotenv_size` | integer | no | Maximum size of a dotenv artifact in bytes. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/432529) in GitLab 17.1. |
|
||||
|
|
|
|||
|
|
@ -3019,6 +3019,60 @@ Example response:
|
|||
}
|
||||
```
|
||||
|
||||
## Real-time security scan
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Ultimate
|
||||
**Offering:** GitLab.com
|
||||
**Status:** Experiment
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/479210) in GitLab 17.6. This feature is an [experiment](../policy/experiment-beta-support.md).
|
||||
|
||||
Returns SAST scan results for a single file in real-time.
|
||||
|
||||
```plaintext
|
||||
POST /projects/:id/security_scans/sast/scan
|
||||
```
|
||||
|
||||
Supported attributes:
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-----------|-------------------|----------|-------------|
|
||||
| `id` | integer or string | Yes | The ID or [URL-encoded path of the project](rest/index.md#namespaced-path-encoding). |
|
||||
|
||||
Example request:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"file_path":"src/main.c",
|
||||
"content":"#include<string.h>\nint main(int argc, char **argv) {\n char buff[128];\n strcpy(buff, argv[1]);\n return 0;\n}\n"
|
||||
}' \
|
||||
--url "https://gitlab.example.com/api/v4/projects/:id/security_scans/sast/scan"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"vulnerabilities": [
|
||||
{
|
||||
"name": "Insecure string processing function (strcpy)",
|
||||
"description": "The `strcpy` family of functions do not provide the ability to limit or check buffer\nsizes before copying to a destination buffer. This can lead to buffer overflows. Consider\nusing more secure alternatives such as `strncpy` and provide the correct limit to the\ndestination buffer and ensure the string is null terminated.\n\nFor more information please see: https://linux.die.net/man/3/strncpy\n\nIf developing for C Runtime Library (CRT), more secure versions of these functions should be\nused, see:\nhttps://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/strncpy-s-strncpy-s-l-wcsncpy-s-wcsncpy-s-l-mbsncpy-s-mbsncpy-s-l?view=msvc-170\n",
|
||||
"severity": "High",
|
||||
"location": {
|
||||
"file": "src/main.c",
|
||||
"start_line": 5,
|
||||
"end_line": 5,
|
||||
"start_column": 3,
|
||||
"end_column": 23
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Get a project's pull mirror details
|
||||
|
||||
DETAILS:
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB |
|
|
@ -1,92 +1,11 @@
|
|||
---
|
||||
stage: Verify
|
||||
group: Pipeline Authoring
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
redirect_to: '../yaml/needs.md'
|
||||
remove_date: '2024-12-18'
|
||||
---
|
||||
|
||||
# Make jobs start earlier with `needs`
|
||||
This document was moved to [another location](../yaml/needs.md).
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
You can use the [`needs`](../yaml/index.md#needs) keyword to create dependencies between jobs
|
||||
in a pipeline. Jobs run as soon as their dependencies are met, regardless of the pipeline's `stages`
|
||||
configuration. You can even configure a pipeline with no stages defined (effectively one large stage)
|
||||
and jobs still run in the proper order. This pipeline structure is a kind of
|
||||
[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
|
||||
|
||||
For example, you may have a specific tool or separate website that is built
|
||||
as part of your main project. Using `needs`, you can specify dependencies between
|
||||
these jobs and GitLab executes the jobs as soon as possible instead of waiting
|
||||
for each stage to complete.
|
||||
|
||||
Unlike other solutions for CI/CD, GitLab does not require you to choose between staged
|
||||
or stageless execution flow. You can implement a hybrid combination of staged and stageless
|
||||
in a single pipeline, using only the `needs` keyword to enable the feature for any job.
|
||||
|
||||
Consider a monorepo as follows:
|
||||
|
||||
```plaintext
|
||||
./service_a
|
||||
./service_b
|
||||
./service_c
|
||||
./service_d
|
||||
```
|
||||
|
||||
This project could have a pipeline organized into three stages:
|
||||
|
||||
| build | test | deploy |
|
||||
|-----------|----------|--------|
|
||||
| `build_a` | `test_a` | `deploy_a` |
|
||||
| `build_b` | `test_b` | `deploy_b` |
|
||||
| `build_c` | `test_c` | `deploy_c` |
|
||||
| `build_d` | `test_d` | `deploy_d` |
|
||||
|
||||
You can improve job execution by using `needs` to relate the `a` jobs to each other
|
||||
separately from the `b`, `c`, and `d` jobs. `build_a` could take a very long time to build,
|
||||
but `test_b` doesn't need to wait, it can be configured to start as soon as `build_b` is finished,
|
||||
which could be much faster.
|
||||
|
||||
If desired, `c` and `d` jobs can be left to run in stage sequence.
|
||||
|
||||
The `needs` keyword also works with the [parallel](../yaml/index.md#parallel) keyword,
|
||||
giving you powerful options for parallelization in your pipeline.
|
||||
|
||||
## Use cases
|
||||
|
||||
You can use the [`needs`](../yaml/index.md#needs) keyword to define several different kinds of
|
||||
dependencies between jobs in a CI/CD pipeline. You can set dependencies to fan in or out,
|
||||
and even merge back together (diamond dependencies). These dependencies could be used for
|
||||
pipelines that:
|
||||
|
||||
- Handle multi-platform builds.
|
||||
- Have a complex web of dependencies like an operating system build.
|
||||
- Have a deployment graph of independently deployable but related microservices.
|
||||
|
||||
Additionally, `needs` can help improve the overall speed of pipelines and provide fast feedback.
|
||||
By creating dependencies that don't unnecessarily
|
||||
block each other, your pipelines run as quickly as possible regardless of
|
||||
pipeline stages, ensuring output (including errors) is available to developers
|
||||
as quickly as possible.
|
||||
|
||||
<!--- start_remove The following content will be removed on remove_date: '2024-12-19' -->
|
||||
## Needs dependency visualization (deprecated)
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/336560) in GitLab 17.1
|
||||
and is planned for removal in 17.4. View `needs` relationships in the
|
||||
[full pipeline graph](../pipelines/index.md#group-jobs-by-stage-or-needs-configuration) instead.
|
||||
|
||||
The needs dependency visualization makes it easier to visualize the dependencies
|
||||
between jobs in a pipeline. This graph displays all the jobs in a pipeline
|
||||
that need or are needed by other jobs. Jobs with no dependencies are not displayed in this view.
|
||||
|
||||
To see the needs visualization, select **Needs** when viewing a pipeline that uses the `needs` keyword.
|
||||
|
||||

|
||||
|
||||
Selecting a node highlights all the job paths it depends on.
|
||||
|
||||

|
||||
<!--- end_remove -->
|
||||
<!-- This redirect file can be deleted after <2024-12-18>. -->
|
||||
<!-- Redirects that point to other docs in the same project expire in three months. -->
|
||||
<!-- Redirects that point to docs in a different project or site (for example, link is not relative and starts with `https:`) expire in one year. -->
|
||||
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/redirects.html -->
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Pipelines can be configured in many different ways:
|
|||
|
||||
- [Basic pipelines](pipeline_architectures.md#basic-pipelines) run everything in each stage concurrently,
|
||||
followed by the next stage.
|
||||
- [Pipelines that use the `needs` keyword](../directed_acyclic_graph/index.md) run based on dependencies
|
||||
- [Pipelines that use the `needs` keyword](../yaml/needs.md) run based on dependencies
|
||||
between jobs and can run more quickly than basic pipelines.
|
||||
- [Merge request pipelines](../pipelines/merge_request_pipelines.md) run for merge
|
||||
requests only (rather than for every commit).
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ deploy_b:
|
|||
## Pipelines with the `needs` keyword
|
||||
|
||||
If efficiency is important and you want everything to run as quickly as possible,
|
||||
you can use the [`needs` keyword](../directed_acyclic_graph/index.md) to define dependencies
|
||||
you can use the [`needs` keyword](../yaml/needs.md) to define dependencies
|
||||
between your jobs. When GitLab knows the dependencies between your jobs,
|
||||
jobs can run as fast as possible, even starting earlier than other jobs in the same stage.
|
||||
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ shouldn't run, saving pipeline resources.
|
|||
|
||||
In a basic configuration, jobs always wait for all other jobs in earlier stages to complete
|
||||
before running. This is the simplest configuration, but it's also the slowest in most
|
||||
cases. [Pipelines with the `needs` keyword](../directed_acyclic_graph/index.md) and
|
||||
cases. [Pipelines with the `needs` keyword](../yaml/needs.md) and
|
||||
[parent/child pipelines](downstream_pipelines.md#parent-child-pipelines) are more flexible and can
|
||||
be more efficient, but can also make pipelines harder to understand and analyze.
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ For the complete `.gitlab-ci.yml` syntax, see the full [CI/CD YAML syntax refere
|
|||
- Each job contains a script section and belongs to a stage:
|
||||
- [`stage`](../yaml/index.md#stage) describes the sequential execution of jobs.
|
||||
If there are runners available, jobs in a single stage run in parallel.
|
||||
- Use the [`needs` keyword](../yaml/index.md#needs) to [run jobs out of stage order](../directed_acyclic_graph/index.md),
|
||||
- Use the [`needs` keyword](../yaml/index.md#needs) to [run jobs out of stage order](../yaml/needs.md),
|
||||
to increase pipeline speed and efficiency.
|
||||
- You can set additional configuration to customize how your jobs and stages perform:
|
||||
- Use the [`rules`](../yaml/index.md#rules) keyword to specify when to run or skip jobs.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ DETAILS:
|
|||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
If you have installed your own runners, you can configure and secure them in GitLab.
|
||||
This document describes how to configure runners in the GitLab UI.
|
||||
|
||||
If you need to configure runners on the machine where you installed GitLab Runner, see
|
||||
[the GitLab Runner documentation](https://docs.gitlab.com/runner/configuration/).
|
||||
|
|
|
|||
|
|
@ -2981,7 +2981,7 @@ In this example, a new pipeline causes a running pipeline to cancel `step-1` and
|
|||
### `needs`
|
||||
|
||||
Use `needs` to execute jobs out-of-order. Relationships between jobs
|
||||
that use `needs` can be visualized as a [directed acyclic graph](../directed_acyclic_graph/index.md).
|
||||
that use `needs` can be visualized as a [directed acyclic graph](../yaml/needs.md).
|
||||
|
||||
You can ignore stage ordering and run some jobs without waiting for others to complete.
|
||||
Jobs in multiple stages can run concurrently.
|
||||
|
|
@ -3173,8 +3173,8 @@ build_job:
|
|||
or the group/project must have public visibility.
|
||||
- You can't use `needs:project` in the same job as [`trigger`](#trigger).
|
||||
- When using `needs:project` to download artifacts from another pipeline, the job does not wait for
|
||||
the needed job to complete. [Directed acyclic graph](../directed_acyclic_graph/index.md)
|
||||
behavior is limited to jobs in the same pipeline. Make sure that the needed job in the other
|
||||
the needed job to complete. [Using `needs` to wait for jobs to complete](../yaml/needs.md)
|
||||
is limited to jobs in the same pipeline. Make sure that the needed job in the other
|
||||
pipeline completes before the job that needs it tries to download the artifacts.
|
||||
- You can't download artifacts from jobs that run in [`parallel`](#parallel).
|
||||
- Support [CI/CD variables](../variables/index.md) in `project`, `job`, and `ref`.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
---
|
||||
stage: Verify
|
||||
group: Pipeline Authoring
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://handbook.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# Make jobs start earlier with `needs`
|
||||
|
||||
DETAILS:
|
||||
**Tier:** Free, Premium, Ultimate
|
||||
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
|
||||
|
||||
You can use the [`needs`](../yaml/index.md#needs) keyword to create dependencies between jobs
|
||||
in a pipeline. Jobs run as soon as their dependencies are met, regardless of the pipeline's `stages`
|
||||
configuration. You can even configure a pipeline with no stages defined (effectively one large stage)
|
||||
and jobs still run in the proper order. This pipeline structure is a kind of
|
||||
[directed acyclic graph](https://en.wikipedia.org/wiki/Directed_acyclic_graph).
|
||||
|
||||
For example, you may have a specific tool or separate website that is built
|
||||
as part of your main project. Using `needs`, you can specify dependencies between
|
||||
these jobs and GitLab executes the jobs as soon as possible instead of waiting
|
||||
for each stage to complete.
|
||||
|
||||
Unlike other solutions for CI/CD, GitLab does not require you to choose between staged
|
||||
or stageless execution flow. You can implement a hybrid combination of staged and stageless
|
||||
in a single pipeline, using only the `needs` keyword to enable the feature for any job.
|
||||
|
||||
Consider a monorepo as follows:
|
||||
|
||||
```plaintext
|
||||
./service_a
|
||||
./service_b
|
||||
./service_c
|
||||
./service_d
|
||||
```
|
||||
|
||||
This project could have a pipeline organized into three stages:
|
||||
|
||||
| build | test | deploy |
|
||||
|-----------|----------|--------|
|
||||
| `build_a` | `test_a` | `deploy_a` |
|
||||
| `build_b` | `test_b` | `deploy_b` |
|
||||
| `build_c` | `test_c` | `deploy_c` |
|
||||
| `build_d` | `test_d` | `deploy_d` |
|
||||
|
||||
You can improve job execution by using `needs` to relate the `a` jobs to each other
|
||||
separately from the `b`, `c`, and `d` jobs. `build_a` could take a very long time to build,
|
||||
but `test_b` doesn't need to wait, it can be configured to start as soon as `build_b` is finished,
|
||||
which could be much faster.
|
||||
|
||||
If desired, `c` and `d` jobs can be left to run in stage sequence.
|
||||
|
||||
The `needs` keyword also works with the [parallel](../yaml/index.md#parallel) keyword,
|
||||
giving you powerful options for parallelization in your pipeline.
|
||||
|
||||
## Use cases
|
||||
|
||||
You can use the [`needs`](../yaml/index.md#needs) keyword to define several different kinds of
|
||||
dependencies between jobs in a CI/CD pipeline. You can set dependencies to fan in or out,
|
||||
and even merge back together (diamond dependencies). These dependencies could be used for
|
||||
pipelines that:
|
||||
|
||||
- Handle multi-platform builds.
|
||||
- Have a complex web of dependencies like an operating system build.
|
||||
- Have a deployment graph of independently deployable but related microservices.
|
||||
|
||||
Additionally, `needs` can help improve the overall speed of pipelines and provide fast feedback.
|
||||
By creating dependencies that don't unnecessarily
|
||||
block each other, your pipelines run as quickly as possible regardless of
|
||||
pipeline stages, ensuring output (including errors) is available to developers
|
||||
as quickly as possible.
|
||||
<!--- start_remove The following content will be removed on remove_date: '2024-12-19' -->
|
||||
|
||||
## Needs dependency visualization (deprecated)
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/336560) in GitLab 17.1
|
||||
and was [removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156207) in 17.4.
|
||||
View `needs` relationships in the [full pipeline graph](../pipelines/index.md#group-jobs-by-stage-or-needs-configuration)
|
||||
instead.
|
||||
|
||||
The needs dependency visualization makes it easier to visualize the dependencies
|
||||
between jobs in a pipeline. This graph displays all the jobs in a pipeline
|
||||
that need or are needed by other jobs. Jobs with no dependencies are not displayed in this view.
|
||||
|
||||
To see the needs visualization, select **Needs** when viewing a pipeline that uses the `needs` keyword.
|
||||
Selecting a node highlights all the job paths it depends on.
|
||||
<!--- end_remove -->
|
||||
|
|
@ -25,7 +25,7 @@ info: Any user with at least the Maintainer role can merge updates to this conte
|
|||
## Configure self-hosted models
|
||||
|
||||
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-the-self-hosted-model) to configure self-hosted models
|
||||
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-the-features-to-your-models) to configure features to use the models
|
||||
1. Follow the [instructions](../../administration/self_hosted_models/configure_duo_features.md#configure-gitlab-duo-features-to-use-self-hosted-models) to configure features to use the models
|
||||
|
||||
AI-powered features are now powered by self-hosted models.
|
||||
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ artifacts to the Google Artifact Registry from the GitLab project `gitlab-org/my
|
|||
The Google attribute `my_project_maintainer` is mapped to the GitLab claims
|
||||
`maintainer_access==true` and the `project_path=="gitlab-org/my-project"`.
|
||||
|
||||
1. In the Google Cloud Console, select [**IAM**](https://console.google.com/iam-admin/iam?supportedpurview=project).
|
||||
1. In the Google Cloud Console, go to the [**IAM** page](https://console.cloud.google.com/iam-admin/iam?supportedpurview=project).
|
||||
|
||||
1. Select **Grant access**.
|
||||
1. In the **New principals** text box, enter the principal set including the
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ To use GitLab Duo features in any project or group, you must assign the user to
|
|||
1. Select **Settings > GitLab Duo**.
|
||||
1. To the right of the user, turn on the toggle to assign a GitLab Duo seat.
|
||||
|
||||
The user is sent a confirmation email.
|
||||
|
||||
### For self-managed
|
||||
|
||||
Prerequisites:
|
||||
|
|
@ -64,6 +66,10 @@ Prerequisites:
|
|||
synchronize subscription (**{retry}**).
|
||||
1. To the right of the user, turn on the toggle to assign a GitLab Duo seat.
|
||||
|
||||
The user is sent a confirmation email.
|
||||
|
||||
To turn off these emails, an administrator can [disable the `duo_seat_assignment_email_for_sm` feature flag](../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
|
||||
|
||||
#### Configure network and proxy settings
|
||||
|
||||
For self-managed instances, to enable GitLab Duo features,
|
||||
|
|
|
|||
|
|
@ -17,5 +17,6 @@ GitLab can check your application for security vulnerabilities and that it meets
|
|||
| [Set up a merge request approval policy](scan_result_policy/index.md) | Learn how to configure a merge request approval policy that takes action based on scan results. | **{star}** |
|
||||
| [Set up a scan execution policy](scan_execution_policy/index.md) | Learn how to create a scan execution policy to enforce security scanning of your project. | **{star}** |
|
||||
| [Scan a Docker container for vulnerabilities](container_scanning/index.md) | Learn how to use container scanning templates to add container scanning to your projects. | **{star}** |
|
||||
| [Remove a secret from your commits](../user/application_security/secret_detection/remove_secrets_tutorial.md) | Learn how to remove a secret from your commit history. | **{star}** |
|
||||
| [Get started with GitLab application security](../user/application_security/get-started-security.md) | Follow recommended steps to set up security tools. | |
|
||||
| [GitLab Security Essentials](https://university.gitlab.com/courses/security-essentials) | Learn about the essential security capabilities of GitLab in this self-paced course. | |
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ These requirements are documented in the `Required permission` column in the fol
|
|||
|
||||
| Name | Required permission | Description | Introduced in | Feature flag | Enabled in |
|
||||
|:-----|:------------|:------------------|:---------|:--------------|:---------|
|
||||
| [`admin_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151825) | | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/442851) | `custom_ability_admin_runners` | |
|
||||
| [`admin_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/151825) | | Create, view, edit, and delete group or project Runners. Includes configuring Runner settings. | GitLab [17.1](https://gitlab.com/gitlab-org/gitlab/-/issues/442851) | | |
|
||||
| [`read_runners`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/156798) | | Allows read-only access to group or project runners, including the runner fleet dashboard. | GitLab [17.2](https://gitlab.com/gitlab-org/gitlab/-/issues/468202) | | |
|
||||
|
||||
## Secrets management
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ in your `.gitlab-ci.yml` file. The component supports these languages:
|
|||
the LSIF artifact for `golang`:
|
||||
|
||||
```yaml
|
||||
- component: ${CI_SERVER_FQDN}/components/code-intelligence/golang-code-intel@v0.0.2
|
||||
- component: ${CI_SERVER_FQDN}/components/code-intelligence/golang-code-intel@v0.0.3
|
||||
inputs:
|
||||
golang_version: ${GO_VERSION}
|
||||
```
|
||||
|
|
@ -78,11 +78,11 @@ To enable code intelligence for a project, add GitLab CI/CD jobs to your project
|
|||
|
||||
:::TabTitle With a SCIP indexer
|
||||
|
||||
1. Add two jobs to your `.gitlab-ci.yml` configuration. The first job (`code_navigation_generate`)
|
||||
generates the SCIP index. The second job (`code_navigation_convert`) converts the SCIP index to LSIF for use in GitLab:
|
||||
1. Add a job to your `.gitlab-ci.yml` configuration. This job generates the
|
||||
SCIP index and converts it to LSIF for use in GitLab:
|
||||
|
||||
```yaml
|
||||
code_navigation_generate:
|
||||
"code_navigation":
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH # the job only needs to run against the default branch
|
||||
image: node:latest
|
||||
|
|
@ -92,23 +92,15 @@ To enable code intelligence for a project, add GitLab CI/CD jobs to your project
|
|||
- npm install -g @sourcegraph/scip-typescript
|
||||
- npm install
|
||||
- scip-typescript index
|
||||
artifacts:
|
||||
paths:
|
||||
- index.scip
|
||||
|
||||
code_navigation_convert:
|
||||
rules:
|
||||
- if: $CI_COMMIT_REF_NAME == $CI_DEFAULT_BRANCH # the job only needs to run against the default branch
|
||||
image: golang
|
||||
stage: test
|
||||
allow_failure: true # recommended
|
||||
needs: ["code_navigation_generate"]
|
||||
script:
|
||||
- git clone --branch v0.4.0 --depth 1 https://github.com/sourcegraph/scip.git
|
||||
- cd scip
|
||||
- go build ./cmd/scip
|
||||
- |
|
||||
env \
|
||||
TAG="v0.4.0" \
|
||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')" \
|
||||
ARCH="$(uname -m | sed -e 's/x86_64/amd64/')" \
|
||||
bash -c 'curl --location "https://github.com/sourcegraph/scip/releases/download/$TAG/scip-$OS-$ARCH.tar.gz"' \
|
||||
| tar xzf - scip
|
||||
- chmod +x scip
|
||||
- ./scip convert --from ../index.scip --to ../dump.lsif
|
||||
- ./scip convert --from index.scip --to dump.lsif
|
||||
artifacts:
|
||||
reports:
|
||||
lsif: dump.lsif
|
||||
|
|
|
|||
|
|
@ -181,6 +181,13 @@ This API connection securely transmits a context window from your IDE/editor to
|
|||
- Algorithms or large code blocks might take more than 10 seconds to generate.
|
||||
- Streaming of code generation responses is supported in VS Code, leading to faster average response times. Other supported IDEs offer slower response times and will return the generated code in a single block.
|
||||
|
||||
### Use a self-hosted model
|
||||
|
||||
Instead of using the default model to manage Code Suggestions requests, you can
|
||||
[deploy a self-hosted model](../../../../administration/self_hosted_models/index.md).
|
||||
This maximizes security and privacy by making sure nothing is sent to an
|
||||
external model.
|
||||
|
||||
### Disable direct connections to the AI Gateway
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/462791) in GitLab 17.2 [with a flag](../../../../administration/feature_flags.md) named `code_suggestions_direct_access`. Disabled by default.
|
||||
|
|
|
|||
|
|
@ -768,7 +768,7 @@ module API
|
|||
requires :group_access, type: Integer, values: Gitlab::Access.all_values, as: :link_group_access, desc: 'The group access level'
|
||||
optional :expires_at, type: Date, desc: 'Share expiration date'
|
||||
end
|
||||
post ":id/share", feature_category: :groups_and_projects do
|
||||
post ":id/share", feature_category: :groups_and_projects, urgency: :low do
|
||||
authorize! :admin_project, user_project
|
||||
shared_with_group = Group.find_by_id(params[:group_id])
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ module Gitlab
|
|||
return false unless project_id
|
||||
|
||||
pipeline.update_column(:project_id, project_id)
|
||||
rescue StandardError # rubocop:disable BackgroundMigration/AvoidSilentRescueExceptions -- only rescuing in one method
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27192,9 +27192,6 @@ msgstr ""
|
|||
msgid "Hide host keys manual input"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Hide marketing-related entries from the Help page"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27818,9 +27815,6 @@ msgstr ""
|
|||
msgid "If using GitHub, you’ll see pipeline statuses on GitHub for your commits and pull requests. %{more_info_link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If you add %{codeStart}needs%{codeEnd} to jobs in your pipeline you'll be able to view the %{codeStart}needs%{codeEnd} dependencies between jobs in this tab."
|
||||
msgstr ""
|
||||
|
||||
msgid "If you are unable to sign in or recover your password, contact a GitLab administrator."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -31800,9 +31794,6 @@ msgstr ""
|
|||
msgid "Learn more about max seats used"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more about needs dependencies"
|
||||
msgstr ""
|
||||
|
||||
msgid "Learn more about seats owed"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35566,9 +35557,6 @@ msgstr ""
|
|||
msgid "Needs attention"
|
||||
msgstr ""
|
||||
|
||||
msgid "Needs visualization requires at least 3 dependent jobs."
|
||||
msgstr ""
|
||||
|
||||
msgid "Network"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40332,9 +40320,6 @@ msgstr ""
|
|||
msgid "Pipelines|The GitLab CI configuration could not be updated."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|The visualization in this tab will be %{linkStart}removed%{linkEnd}. Instead, view %{codeStart}needs%{codeEnd} relationships with the %{strongStart}Job dependency%{strongEnd} option in the %{strongStart}Pipeline%{strongEnd} tab."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|There are currently no finished pipelines."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -40407,9 +40392,6 @@ msgstr ""
|
|||
msgid "Pipelines|Unable to validate CI/CD configuration. See the %{linkStart}GitLab CI/CD troubleshooting guide%{linkEnd} for more details."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Upcoming visualization change"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pipelines|Update Trigger"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -51360,9 +51342,6 @@ msgstr ""
|
|||
msgid "Show less"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show list"
|
||||
msgstr ""
|
||||
|
||||
msgid "Show more"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -52490,9 +52469,6 @@ msgstr ""
|
|||
msgid "Specify an email address regex pattern to identify default internal users."
|
||||
msgstr ""
|
||||
|
||||
msgid "Speed up your pipelines with Needs relationships"
|
||||
msgstr ""
|
||||
|
||||
msgid "Spent at can't be a future date and time."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -55287,9 +55263,6 @@ msgstr ""
|
|||
msgid "There was an error loading users activity calendar."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error parsing the data for this graph."
|
||||
msgstr ""
|
||||
|
||||
msgid "There was an error removing the e-mail."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -59680,9 +59653,6 @@ msgstr ""
|
|||
msgid "Using required encryption strategy when encrypted field is missing!"
|
||||
msgstr ""
|
||||
|
||||
msgid "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."
|
||||
msgstr ""
|
||||
|
||||
msgid "Validate"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -60750,9 +60720,6 @@ msgstr ""
|
|||
msgid "We are currently unable to fetch data for the pipeline header."
|
||||
msgstr ""
|
||||
|
||||
msgid "We are currently unable to fetch data for this graph."
|
||||
msgstr ""
|
||||
|
||||
msgid "We could not determine the path to remove the epic"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@
|
|||
"swagger-cli": "^4.0.4",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"timezone-mock": "^1.0.8",
|
||||
"vite": "^5.4.5",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-ruby": "^5.1.0",
|
||||
"vue-loader-vue3": "npm:vue-loader@17.4.2",
|
||||
"vue-test-utils-compat": "0.0.14",
|
||||
|
|
|
|||
|
|
@ -303,8 +303,8 @@ module QA
|
|||
|
||||
# Print difference in the description
|
||||
#
|
||||
expected_body = expected_item[:body]
|
||||
actual_body = actual_item[:body]
|
||||
expected_body = remove_backticks(expected_item[:body])
|
||||
actual_body = remove_backticks(actual_item[:body])
|
||||
body_msg = "#{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)}"
|
||||
expect(actual_body).to eq(expected_body), body_msg
|
||||
|
||||
|
|
@ -317,8 +317,8 @@ module QA
|
|||
|
||||
# Print amount difference first
|
||||
#
|
||||
expected_comments = expected_item[:comments]
|
||||
actual_comments = actual_item[:comments]
|
||||
expected_comments = expected_item[:comments].map { |comment| remove_backticks(comment) }
|
||||
actual_comments = actual_item[:comments].map { |comment| remove_backticks(comment) }
|
||||
comment_count_msg = <<~MSG
|
||||
#{msg} same amount of comments. Source: #{expected_comments.length}, Target: #{actual_comments.length}
|
||||
MSG
|
||||
|
|
@ -429,6 +429,16 @@ module QA
|
|||
@created_by_pattern ||= /\n\n \*By .+ on \S+\*/
|
||||
end
|
||||
|
||||
# Remove backticks from string
|
||||
#
|
||||
# @param [String] text
|
||||
# @return [String] modified text
|
||||
def remove_backticks(text)
|
||||
return unless text.present?
|
||||
|
||||
text.delete('`')
|
||||
end
|
||||
|
||||
# Source project url
|
||||
#
|
||||
# @return [String]
|
||||
|
|
|
|||
|
|
@ -1,743 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||
<svg
|
||||
height="540"
|
||||
viewBox="0,0,1000,540"
|
||||
width="1000"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke-opacity="0.8"
|
||||
>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-0"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-1"
|
||||
x1="116"
|
||||
x2="361.3333333333333"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#e17223"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#83ab4a"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-2"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M100, 129
|
||||
V158
|
||||
H377.3333333333333
|
||||
V100
|
||||
H100
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip63)"
|
||||
d="M108,129L190,129L190,129L369.3333333333333,129"
|
||||
stroke="url(#dag-grad53)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-3"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-4"
|
||||
x1="377.3333333333333"
|
||||
x2="622.6666666666666"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#83ab4a"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#6f3500"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-5"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M361.3333333333333, 129.0000000000002
|
||||
V158.0000000000002
|
||||
H638.6666666666666
|
||||
V100
|
||||
H361.3333333333333
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip64)"
|
||||
d="M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002"
|
||||
stroke="url(#dag-grad54)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-6"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-7"
|
||||
x1="116"
|
||||
x2="622.6666666666666"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#5772ff"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#6f3500"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-8"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M100, 187.0000000000002
|
||||
V241.00000000000003
|
||||
H638.6666666666666
|
||||
V158.0000000000002
|
||||
H100
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip65)"
|
||||
d="M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002"
|
||||
stroke="url(#dag-grad55)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-9"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-10"
|
||||
x1="116"
|
||||
x2="361.3333333333333"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#b24800"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#006887"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-11"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M100, 269.9999999999998
|
||||
V324
|
||||
H377.3333333333333
|
||||
V240.99999999999977
|
||||
H100
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip66)"
|
||||
d="M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998"
|
||||
stroke="url(#dag-grad56)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-12"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-13"
|
||||
x1="116"
|
||||
x2="361.3333333333333"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#25d2d2"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#487900"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-14"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M100, 352.99999999999994
|
||||
V407.00000000000006
|
||||
H377.3333333333333
|
||||
V323.99999999999994
|
||||
H100
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip67)"
|
||||
d="M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994"
|
||||
stroke="url(#dag-grad57)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-15"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-16"
|
||||
x1="377.3333333333333"
|
||||
x2="622.6666666666666"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#006887"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#d84280"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-17"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M361.3333333333333, 270.0000000000001
|
||||
V299.0000000000001
|
||||
H638.6666666666666
|
||||
V240.99999999999977
|
||||
H361.3333333333333
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip68)"
|
||||
d="M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001"
|
||||
stroke="url(#dag-grad58)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-18"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-19"
|
||||
x1="377.3333333333333"
|
||||
x2="622.6666666666666"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#487900"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#d84280"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-20"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M361.3333333333333, 328.0000000000001
|
||||
V381.99999999999994
|
||||
H638.6666666666666
|
||||
V299.0000000000001
|
||||
H361.3333333333333
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip69)"
|
||||
d="M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001"
|
||||
stroke="url(#dag-grad59)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-21"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-22"
|
||||
x1="377.3333333333333"
|
||||
x2="622.6666666666666"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#487900"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#3547de"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-23"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M361.3333333333333, 411
|
||||
V440
|
||||
H638.6666666666666
|
||||
V381.99999999999994
|
||||
H361.3333333333333
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip70)"
|
||||
d="M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411"
|
||||
stroke="url(#dag-grad60)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-24"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-25"
|
||||
x1="638.6666666666666"
|
||||
x2="884"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#d84280"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#006887"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-26"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M622.6666666666666, 270.1890725105691
|
||||
V299.1890725105691
|
||||
H900
|
||||
V241.0000000000001
|
||||
H622.6666666666666
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip71)"
|
||||
d="M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691"
|
||||
stroke="url(#dag-grad61)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="dag-link gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke-opacity"
|
||||
id="reference-27"
|
||||
>
|
||||
<lineargradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id="reference-28"
|
||||
x1="638.6666666666666"
|
||||
x2="884"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stop-color="#3547de"
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stop-color="#275600"
|
||||
/>
|
||||
</lineargradient>
|
||||
<clippath
|
||||
id="reference-29"
|
||||
>
|
||||
<path
|
||||
d="
|
||||
M622.6666666666666, 411
|
||||
V440
|
||||
H900
|
||||
V382
|
||||
H622.6666666666666
|
||||
Z
|
||||
"
|
||||
/>
|
||||
</clippath>
|
||||
<path
|
||||
clip-path="url(#dag-clip72)"
|
||||
d="M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411"
|
||||
stroke="url(#dag-grad62)"
|
||||
stroke-width="56"
|
||||
style="stroke-linejoin: round;"
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-30"
|
||||
stroke="#e17223"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="108"
|
||||
x2="108"
|
||||
y1="104"
|
||||
y2="154.00000000000003"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-31"
|
||||
stroke="#83ab4a"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="369"
|
||||
x2="369"
|
||||
y1="104"
|
||||
y2="154"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-32"
|
||||
stroke="#5772ff"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="108"
|
||||
x2="108"
|
||||
y1="187.00000000000003"
|
||||
y2="237.00000000000003"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-33"
|
||||
stroke="#b24800"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="108"
|
||||
x2="108"
|
||||
y1="270"
|
||||
y2="320.00000000000006"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-34"
|
||||
stroke="#25d2d2"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="108"
|
||||
x2="108"
|
||||
y1="353.00000000000006"
|
||||
y2="403.0000000000001"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-35"
|
||||
stroke="#6f3500"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="630"
|
||||
x2="630"
|
||||
y1="104.0000000000002"
|
||||
y2="212.00000000000009"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-36"
|
||||
stroke="#006887"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="369"
|
||||
x2="369"
|
||||
y1="244.99999999999977"
|
||||
y2="294.99999999999994"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-37"
|
||||
stroke="#487900"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="369"
|
||||
x2="369"
|
||||
y1="327.99999999999994"
|
||||
y2="436"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-38"
|
||||
stroke="#d84280"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="630"
|
||||
x2="630"
|
||||
y1="245.00000000000009"
|
||||
y2="353"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-39"
|
||||
stroke="#3547de"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="630"
|
||||
x2="630"
|
||||
y1="386"
|
||||
y2="436"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-40"
|
||||
stroke="#006887"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="892"
|
||||
x2="892"
|
||||
y1="245.18907251056908"
|
||||
y2="295.1890725105691"
|
||||
/>
|
||||
<line
|
||||
class="dag-node gl-cursor-pointer gl-duration-slow gl-ease-ease gl-transition-stroke"
|
||||
id="reference-41"
|
||||
stroke="#275600"
|
||||
stroke-linecap="round"
|
||||
stroke-width="16"
|
||||
x1="892"
|
||||
x2="892"
|
||||
y1="386"
|
||||
y2="436"
|
||||
/>
|
||||
</g>
|
||||
<g
|
||||
class="gl-text-sm"
|
||||
>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58.00000000000003px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="8"
|
||||
y="100"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58.00000000000003px; text-align: right;"
|
||||
>
|
||||
build_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="369.3333333333333"
|
||||
y="75"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: left;"
|
||||
>
|
||||
test_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="8"
|
||||
y="183.00000000000003"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58px; text-align: right;"
|
||||
>
|
||||
test_b
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58.00000000000006px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="8"
|
||||
y="266"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58.00000000000006px; text-align: right;"
|
||||
>
|
||||
post_test_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58.00000000000006px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="8"
|
||||
y="349.00000000000006"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58.00000000000006px; text-align: right;"
|
||||
>
|
||||
post_test_b
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="630.6666666666666"
|
||||
y="75.0000000000002"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: right;"
|
||||
>
|
||||
post_test_c
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="369.3333333333333"
|
||||
y="215.99999999999977"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: left;"
|
||||
>
|
||||
staging_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="369.3333333333333"
|
||||
y="298.99999999999994"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: left;"
|
||||
>
|
||||
staging_b
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="630.6666666666666"
|
||||
y="216.00000000000009"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: right;"
|
||||
>
|
||||
canary_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="25px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="630.6666666666666"
|
||||
y="357"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 25px; text-align: right;"
|
||||
>
|
||||
canary_c
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="908"
|
||||
y="241.18907251056908"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58px; text-align: left;"
|
||||
>
|
||||
production_a
|
||||
</div>
|
||||
</foreignobject>
|
||||
<foreignobject
|
||||
class="gl-overflow-visible"
|
||||
height="58px"
|
||||
requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility"
|
||||
width="84"
|
||||
x="908"
|
||||
y="382"
|
||||
>
|
||||
<div
|
||||
class="gl-break-words gl-flex gl-flex-col gl-justify-center gl-pointer-events-none"
|
||||
style="height: 58px; text-align: left;"
|
||||
>
|
||||
production_d
|
||||
</div>
|
||||
</foreignobject>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import { GlButton } from '@gitlab/ui';
|
||||
import { shallowMount, mount } from '@vue/test-utils';
|
||||
import { nextTick } from 'vue';
|
||||
import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
|
||||
import { singleNote, multiNote } from '../mock_data';
|
||||
|
||||
describe('The DAG annotations', () => {
|
||||
let wrapper;
|
||||
|
||||
const getColorBlock = () => wrapper.find('[data-testid="dag-color-block"]');
|
||||
const getAllColorBlocks = () => wrapper.findAll('[data-testid="dag-color-block"]');
|
||||
const getTextBlock = () => wrapper.find('[data-testid="dag-note-text"]');
|
||||
const getAllTextBlocks = () => wrapper.findAll('[data-testid="dag-note-text"]');
|
||||
const getToggleButton = () => wrapper.findComponent(GlButton);
|
||||
|
||||
const createComponent = (propsData = {}, method = shallowMount) => {
|
||||
wrapper = method(DagAnnotations, {
|
||||
propsData,
|
||||
data() {
|
||||
return {
|
||||
showList: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('when there is one annotation', () => {
|
||||
const currentNote = singleNote['dag-link103'];
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ annotations: singleNote });
|
||||
});
|
||||
|
||||
it('displays the color block', () => {
|
||||
expect(getColorBlock().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays the text block', () => {
|
||||
expect(getTextBlock().exists()).toBe(true);
|
||||
expect(getTextBlock().text()).toBe(`${currentNote.source.name} → ${currentNote.target.name}`);
|
||||
});
|
||||
|
||||
it('does not display the list toggle link', () => {
|
||||
expect(getToggleButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there are multiple annoataions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ annotations: multiNote });
|
||||
});
|
||||
|
||||
it('displays a color block for each link', () => {
|
||||
expect(getAllColorBlocks().length).toBe(Object.keys(multiNote).length);
|
||||
});
|
||||
|
||||
it('displays a text block for each link', () => {
|
||||
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
|
||||
|
||||
Object.values(multiNote).forEach((item, idx) => {
|
||||
expect(getAllTextBlocks().at(idx).text()).toBe(`${item.source.name} → ${item.target.name}`);
|
||||
});
|
||||
});
|
||||
|
||||
it('displays the list toggle link', () => {
|
||||
expect(getToggleButton().exists()).toBe(true);
|
||||
expect(getToggleButton().text()).toBe('Hide list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('the list toggle', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ annotations: multiNote }, mount);
|
||||
});
|
||||
|
||||
describe('clicking hide', () => {
|
||||
it('hides listed items and changes text to show', async () => {
|
||||
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
|
||||
expect(getToggleButton().text()).toBe('Hide list');
|
||||
getToggleButton().trigger('click');
|
||||
await nextTick();
|
||||
expect(getAllTextBlocks().length).toBe(0);
|
||||
expect(getToggleButton().text()).toBe('Show list');
|
||||
});
|
||||
});
|
||||
|
||||
describe('clicking show', () => {
|
||||
it('shows listed items and changes text to hide', async () => {
|
||||
getToggleButton().trigger('click');
|
||||
getToggleButton().trigger('click');
|
||||
|
||||
await nextTick();
|
||||
expect(getAllTextBlocks().length).toBe(Object.keys(multiNote).length);
|
||||
expect(getToggleButton().text()).toBe('Hide list');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,209 +0,0 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { IS_HIGHLIGHTED, LINK_SELECTOR, NODE_SELECTOR } from '~/ci/pipeline_details/dag/constants';
|
||||
import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
|
||||
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
|
||||
import { highlightIn, highlightOut } from '~/ci/pipeline_details/dag/utils/interactions';
|
||||
import { removeOrphanNodes } from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import { parsedData } from '../mock_data';
|
||||
|
||||
describe('The DAG graph', () => {
|
||||
let wrapper;
|
||||
|
||||
const getGraph = () => wrapper.find('.dag-graph-container > svg');
|
||||
const getAllLinks = () => wrapper.findAll(`.${LINK_SELECTOR}`);
|
||||
const getAllNodes = () => wrapper.findAll(`.${NODE_SELECTOR}`);
|
||||
const getAllLabels = () => wrapper.findAll('foreignObject');
|
||||
|
||||
const createComponent = (propsData = {}) => {
|
||||
if (wrapper?.destroy) {
|
||||
wrapper.destroy();
|
||||
}
|
||||
|
||||
wrapper = shallowMount(DagGraph, {
|
||||
attachTo: document.body,
|
||||
propsData,
|
||||
data() {
|
||||
return {
|
||||
color: () => {},
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ graphData: parsedData });
|
||||
});
|
||||
|
||||
describe('in the basic case', () => {
|
||||
beforeEach(() => {
|
||||
/*
|
||||
The graph uses random to offset links. To keep the snapshot consistent,
|
||||
we mock Math.random. Wheeeee!
|
||||
*/
|
||||
const randomNumber = jest.spyOn(global.Math, 'random');
|
||||
randomNumber.mockImplementation(() => 0.2);
|
||||
createComponent({ graphData: parsedData });
|
||||
});
|
||||
|
||||
it('renders the graph svg', () => {
|
||||
expect(getGraph().exists()).toBe(true);
|
||||
expect(getGraph().html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('links', () => {
|
||||
it('renders the expected number of links', () => {
|
||||
expect(getAllLinks()).toHaveLength(parsedData.links.length);
|
||||
});
|
||||
|
||||
it('renders the expected number of gradients', () => {
|
||||
expect(wrapper.findAll('linearGradient')).toHaveLength(parsedData.links.length);
|
||||
});
|
||||
|
||||
it('renders the expected number of clip paths', () => {
|
||||
expect(wrapper.findAll('clipPath')).toHaveLength(parsedData.links.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodes and labels', () => {
|
||||
const sankeyNodes = createSankey()(parsedData).nodes;
|
||||
const processedNodes = removeOrphanNodes(sankeyNodes);
|
||||
|
||||
describe('nodes', () => {
|
||||
it('renders the expected number of nodes', () => {
|
||||
expect(getAllNodes()).toHaveLength(processedNodes.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('labels', () => {
|
||||
it('renders the expected number of labels as foreignObjects', () => {
|
||||
expect(getAllLabels()).toHaveLength(processedNodes.length);
|
||||
});
|
||||
|
||||
it('renders the title as text', () => {
|
||||
expect(getAllLabels().at(0).text()).toBe(parsedData.nodes[0].name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('interactions', () => {
|
||||
const strokeOpacity = (opacity) => `stroke-opacity: ${opacity};`;
|
||||
const baseOpacity = () => wrapper.vm.$options.viewOptions.baseOpacity;
|
||||
|
||||
describe('links', () => {
|
||||
const liveLink = () => getAllLinks().at(4);
|
||||
const otherLink = () => getAllLinks().at(1);
|
||||
|
||||
describe('on hover', () => {
|
||||
it('sets the link opacity to baseOpacity and background links to 0.2', () => {
|
||||
liveLink().trigger('mouseover');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
});
|
||||
|
||||
it('reverts the styles on mouseout', () => {
|
||||
liveLink().trigger('mouseover');
|
||||
liveLink().trigger('mouseout');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
});
|
||||
});
|
||||
|
||||
describe('on click', () => {
|
||||
describe('toggles link liveness', () => {
|
||||
it('turns link on', () => {
|
||||
liveLink().trigger('click');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
});
|
||||
|
||||
it('turns link off on second click', () => {
|
||||
liveLink().trigger('click');
|
||||
liveLink().trigger('click');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
});
|
||||
});
|
||||
|
||||
it('the link remains live even after mouseout', () => {
|
||||
liveLink().trigger('click');
|
||||
liveLink().trigger('mouseout');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
});
|
||||
|
||||
it('preserves state when multiple links are toggled on and off', () => {
|
||||
const anotherLiveLink = () => getAllLinks().at(2);
|
||||
|
||||
liveLink().trigger('click');
|
||||
anotherLiveLink().trigger('click');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
|
||||
anotherLiveLink().trigger('click');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
|
||||
liveLink().trigger('click');
|
||||
expect(liveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
expect(anotherLiveLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
expect(otherLink().attributes('style')).toBe(strokeOpacity(baseOpacity()));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('nodes', () => {
|
||||
const liveNode = () => getAllNodes().at(10);
|
||||
const anotherLiveNode = () => getAllNodes().at(5);
|
||||
const nodesNotHighlighted = () => getAllNodes().filter((n) => !n.classes(IS_HIGHLIGHTED));
|
||||
const linksNotHighlighted = () => getAllLinks().filter((n) => !n.classes(IS_HIGHLIGHTED));
|
||||
const nodesHighlighted = () => getAllNodes().filter((n) => n.classes(IS_HIGHLIGHTED));
|
||||
const linksHighlighted = () => getAllLinks().filter((n) => n.classes(IS_HIGHLIGHTED));
|
||||
|
||||
describe('on click', () => {
|
||||
it('highlights the clicked node and predecessors', () => {
|
||||
liveNode().trigger('click');
|
||||
|
||||
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
|
||||
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
|
||||
|
||||
linksHighlighted().wrappers.forEach((link) => {
|
||||
expect(link.attributes('style')).toBe(strokeOpacity(highlightIn));
|
||||
});
|
||||
|
||||
nodesHighlighted().wrappers.forEach((node) => {
|
||||
expect(node.attributes('stroke')).not.toBe('#f2f2f2');
|
||||
});
|
||||
|
||||
linksNotHighlighted().wrappers.forEach((link) => {
|
||||
expect(link.attributes('style')).toBe(strokeOpacity(highlightOut));
|
||||
});
|
||||
|
||||
nodesNotHighlighted().wrappers.forEach((node) => {
|
||||
expect(node.attributes('stroke')).toBe('#f2f2f2');
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles path off on second click', () => {
|
||||
liveNode().trigger('click');
|
||||
liveNode().trigger('click');
|
||||
|
||||
expect(nodesNotHighlighted().length).toBe(getAllNodes().length);
|
||||
expect(linksNotHighlighted().length).toBe(getAllLinks().length);
|
||||
});
|
||||
|
||||
it('preserves state when multiple nodes are toggled on and off', () => {
|
||||
anotherLiveNode().trigger('click');
|
||||
liveNode().trigger('click');
|
||||
anotherLiveNode().trigger('click');
|
||||
expect(nodesNotHighlighted().length < getAllNodes().length).toBe(true);
|
||||
expect(linksNotHighlighted().length < getAllLinks().length).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,187 +0,0 @@
|
|||
import { GlEmptyState } from '@gitlab/ui';
|
||||
import { nextTick } from 'vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { ADD_NOTE, REMOVE_NOTE, REPLACE_NOTES } from '~/ci/pipeline_details/dag/constants';
|
||||
import Dag from '~/ci/pipeline_details/dag/dag.vue';
|
||||
import DagAnnotations from '~/ci/pipeline_details/dag/components/dag_annotations.vue';
|
||||
import DagGraph from '~/ci/pipeline_details/dag/components/dag_graph.vue';
|
||||
|
||||
import { PARSE_FAILURE, UNSUPPORTED_DATA } from '~/ci/pipeline_details/constants';
|
||||
import {
|
||||
mockParsedGraphQLNodes,
|
||||
tooSmallGraph,
|
||||
unparseableGraph,
|
||||
graphWithoutDependencies,
|
||||
singleNote,
|
||||
multiNote,
|
||||
} from './mock_data';
|
||||
|
||||
describe('Pipeline DAG graph wrapper', () => {
|
||||
let wrapper;
|
||||
const getDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
|
||||
const getFailureAlert = () => wrapper.findByTestId('failure-alert');
|
||||
const getAllFailureAlerts = () => wrapper.findAllByTestId('failure-alert');
|
||||
const getGraph = () => wrapper.findComponent(DagGraph);
|
||||
const getNotes = () => wrapper.findComponent(DagAnnotations);
|
||||
const getErrorText = (type) => wrapper.vm.$options.errorTexts[type];
|
||||
const getEmptyState = () => wrapper.findComponent(GlEmptyState);
|
||||
|
||||
const createComponent = ({
|
||||
graphData = mockParsedGraphQLNodes,
|
||||
provideOverride = {},
|
||||
method = shallowMountExtended,
|
||||
} = {}) => {
|
||||
wrapper = method(Dag, {
|
||||
provide: {
|
||||
pipelineProjectPath: 'root/abc-dag',
|
||||
pipelineIid: '1',
|
||||
emptySvgPath: '/my-svg',
|
||||
dagDocPath: '/my-doc',
|
||||
...provideOverride,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
graphData,
|
||||
showFailureAlert: false,
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
describe('deprecation alert', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders the deprecation alert', () => {
|
||||
expect(getDeprecationAlert().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('dismisses the deprecation alert properly', async () => {
|
||||
getDeprecationAlert().vm.$emit('dismiss');
|
||||
await nextTick();
|
||||
|
||||
expect(getDeprecationAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a query argument is undefined', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
provideOverride: { pipelineProjectPath: undefined },
|
||||
graphData: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render the graph', () => {
|
||||
expect(getGraph().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the empty state', () => {
|
||||
expect(getEmptyState().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when all query variables are defined', () => {
|
||||
describe('but the parse fails', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
graphData: unparseableGraph,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the PARSE_FAILURE alert and not the graph', () => {
|
||||
expect(getFailureAlert().exists()).toBe(true);
|
||||
expect(getFailureAlert().text()).toBe(getErrorText(PARSE_FAILURE));
|
||||
expect(getGraph().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not render the empty state', () => {
|
||||
expect(getEmptyState().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse succeeds', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ method: mount });
|
||||
});
|
||||
|
||||
it('shows the graph', () => {
|
||||
expect(getGraph().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render the empty state', () => {
|
||||
expect(getEmptyState().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parse succeeds, but the resulting graph is too small', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
graphData: tooSmallGraph,
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
|
||||
expect(getFailureAlert().exists()).toBe(true);
|
||||
expect(getFailureAlert().text()).toBe(getErrorText(UNSUPPORTED_DATA));
|
||||
expect(getGraph().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show the empty dag graph state', () => {
|
||||
expect(getEmptyState().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the returned data is empty', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
method: shallowMountExtended,
|
||||
graphData: graphWithoutDependencies,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not render an error alert or the graph', () => {
|
||||
expect(getAllFailureAlerts().length).toBe(0);
|
||||
expect(getGraph().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('shows the empty dag graph state', () => {
|
||||
expect(getEmptyState().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('annotations', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('toggles on link mouseover and mouseout', async () => {
|
||||
const currentNote = singleNote['dag-link103'];
|
||||
|
||||
expect(getNotes().exists()).toBe(false);
|
||||
|
||||
getGraph().vm.$emit('update-annotation', { type: ADD_NOTE, data: currentNote });
|
||||
await nextTick();
|
||||
expect(getNotes().exists()).toBe(true);
|
||||
|
||||
getGraph().vm.$emit('update-annotation', { type: REMOVE_NOTE, data: currentNote });
|
||||
await nextTick();
|
||||
expect(getNotes().exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('toggles on node and link click', async () => {
|
||||
expect(getNotes().exists()).toBe(false);
|
||||
|
||||
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: multiNote });
|
||||
await nextTick();
|
||||
expect(getNotes().exists()).toBe(true);
|
||||
|
||||
getGraph().vm.$emit('update-annotation', { type: REPLACE_NOTES, data: {} });
|
||||
await nextTick();
|
||||
expect(getNotes().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
|
||||
import { createSankey } from '~/ci/pipeline_details/utils/drawing_utils';
|
||||
import { parseData } from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import { mockParsedGraphQLNodes } from '../mock_data';
|
||||
import { mockParsedGraphQLNodes } from './mock_data';
|
||||
|
||||
describe('DAG visualization drawing utilities', () => {
|
||||
const parsed = parseData(mockParsedGraphQLNodes);
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
|
||||
import { createSankey } from '~/ci/pipeline_details/dag/utils/drawing_utils';
|
||||
import {
|
||||
makeLinksFromNodes,
|
||||
filterByAncestors,
|
||||
|
|
@ -7,15 +6,14 @@ import {
|
|||
keepLatestDownstreamPipelines,
|
||||
listByLayers,
|
||||
parseData,
|
||||
removeOrphanNodes,
|
||||
getMaxNodes,
|
||||
} from '~/ci/pipeline_details/utils/parsing_utils';
|
||||
import { createNodeDict } from '~/ci/pipeline_details/utils';
|
||||
|
||||
import { mockDownstreamPipelinesRest } from '../../../vue_merge_request_widget/mock_data';
|
||||
import { mockDownstreamPipelinesGraphql } from '../../../commit/mock_data';
|
||||
import { mockParsedGraphQLNodes, missingJob } from '../dag/mock_data';
|
||||
import { generateResponse } from '../graph/mock_data';
|
||||
import { mockParsedGraphQLNodes, missingJob } from './mock_data';
|
||||
|
||||
describe('DAG visualization parsing utilities', () => {
|
||||
const nodeDict = createNodeDict(mockParsedGraphQLNodes);
|
||||
|
|
@ -82,27 +80,6 @@ describe('DAG visualization parsing utilities', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('removeOrphanNodes', () => {
|
||||
it('removes sankey nodes that have no needs and are not needed', () => {
|
||||
const layoutSettings = {
|
||||
width: 200,
|
||||
height: 200,
|
||||
nodeWidth: 10,
|
||||
nodePadding: 20,
|
||||
paddingForLabels: 100,
|
||||
};
|
||||
|
||||
const sankeyLayout = createSankey(layoutSettings)(parsed);
|
||||
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
|
||||
/*
|
||||
These lengths are determined by the mock data.
|
||||
If the data changes, the numbers may also change.
|
||||
*/
|
||||
expect(parsed.nodes).toHaveLength(mockParsedGraphQLNodes.length);
|
||||
expect(cleanedNodes).toHaveLength(12);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaxNodes', () => {
|
||||
it('returns the number of nodes in the most populous generation', () => {
|
||||
const layerNodes = [
|
||||
|
|
|
|||
|
|
@ -77,6 +77,21 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillOrDropCiPipelineOnProjectId,
|
|||
end
|
||||
end
|
||||
|
||||
context 'when backfill will create an invalid record' do
|
||||
before do
|
||||
table(:ci_pipelines, primary_key: :id, database: :ci)
|
||||
.create!(id: 100, iid: 100, partition_id: 100, project_id: 137)
|
||||
|
||||
pipeline_with_builds.update!(iid: 100)
|
||||
end
|
||||
|
||||
it 'deletes the pipeline instead' do
|
||||
migration.perform
|
||||
|
||||
expect { pipeline_with_builds.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when associations are invalid as well' do
|
||||
let!(:pipeline_with_bad_build) do
|
||||
table(:ci_pipelines, primary_key: :id, database: :ci).create!(id: 5, partition_id: 100)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FixNonNullableSnippets, migration: :gitlab_main, feature_category: :source_code_management do
|
||||
let(:migration) { described_class.new }
|
||||
let(:snippets) { table(:snippets) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:namespace) { namespaces.create!(name: 'foo', path: 'foo') }
|
||||
let!(:project) { projects.create!(namespace_id: namespace.id, project_namespace_id: namespace.id) }
|
||||
|
||||
let!(:personal_snippet) do
|
||||
snippets.create!(
|
||||
type: 'PersonalSnippet', author_id: 1, project_id: nil, title: 'Snippet1', organization_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
let!(:project_snippet_with_organization) do
|
||||
snippets.create!(
|
||||
type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet2', organization_id: 1
|
||||
)
|
||||
end
|
||||
|
||||
let!(:project_snippet_without_organization) do
|
||||
snippets.create!(
|
||||
type: 'ProjectSnippet', author_id: 1, project_id: project.id, title: 'Snippet3', organization_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
describe '#up' do
|
||||
context 'when GitLab.com', :saas do
|
||||
it 'nullfies organization_id for project snippets' do
|
||||
expect { migrate! }
|
||||
.to change { project_snippet_with_organization.reload.organization_id }.from(1).to(nil)
|
||||
.and not_change { personal_snippet.reload.organization_id }.from(1)
|
||||
.and not_change { project_snippet_without_organization.reload.organization_id }.from(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when self-managed' do
|
||||
it 'does not update organization_id for project snippets' do
|
||||
expect { migrate! }
|
||||
.to not_change { project_snippet_with_organization.reload.organization_id }.from(1)
|
||||
.and not_change { personal_snippet.reload.organization_id }.from(1)
|
||||
.and not_change { project_snippet_without_organization.reload.organization_id }.from(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require_migration!
|
||||
|
||||
RSpec.describe FinalizeNullifyOrganizationIdForSnippets, feature_category: :source_code_management, migration_version: 20240906133341 do
|
||||
describe '#up' do
|
||||
it 'ensures the migration is completed for self-managed instances' do
|
||||
QueueNullifyOrganizationIdForSnippets.new.up
|
||||
|
||||
migration = Gitlab::Database::BackgroundMigration::BatchedMigration.where(
|
||||
job_class_name: 'NullifyOrganizationIdForSnippets',
|
||||
table_name: 'snippets'
|
||||
).first
|
||||
|
||||
expect(migration.status).not_to eq(6)
|
||||
|
||||
migrate!
|
||||
|
||||
expect(migration.reload.status).to eq(6)
|
||||
QueueNullifyOrganizationIdForSnippets.new.down
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -61,6 +61,18 @@ RSpec.describe JwtController, feature_category: :system_access do
|
|||
end
|
||||
end
|
||||
|
||||
context 'POST /jwt/auth when in maintenance mode' do
|
||||
before do
|
||||
stub_maintenance_mode_setting(true)
|
||||
end
|
||||
|
||||
it 'returns 404' do
|
||||
post '/jwt/auth'
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'authenticating against container registry' do
|
||||
context 'existing service' do
|
||||
subject! { get '/jwt/auth', params: parameters }
|
||||
|
|
|
|||
|
|
@ -89,6 +89,18 @@ RSpec.describe Packages::CleanupPackageFileWorker, feature_category: :package_re
|
|||
.and change { Packages::Package.count }.by(-1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'removing the last package file in an ML model package' do
|
||||
let_it_be_with_reload(:package) { create(:ml_model_package) }
|
||||
let_it_be(:package_file) { create(:package_file, :pending_destruction, package: package) }
|
||||
|
||||
it 'deletes the package file but keeps the package' do
|
||||
expect(worker).to receive(:log_extra_metadata_on_done).twice
|
||||
|
||||
expect { subject }.to change { Packages::PackageFile.count }.by(-1)
|
||||
.and change { Packages::Package.count }.by(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#max_running_jobs' do
|
||||
|
|
|
|||
|
|
@ -14334,10 +14334,10 @@ vite-plugin-ruby@^5.1.0:
|
|||
debug "^4.3.4"
|
||||
fast-glob "^3.3.2"
|
||||
|
||||
vite@^5.4.5:
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.5.tgz#e4ab27709de46ff29bd8db52b0c51606acba893b"
|
||||
integrity sha512-pXqR0qtb2bTwLkev4SE3r4abCNioP3GkjvIDLlzziPpXtHgiJIjuKl+1GN6ESOT3wMjG3JTeARopj2SwYaHTOA==
|
||||
vite@^5.4.6:
|
||||
version "5.4.6"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.6.tgz#85a93a1228a7fb5a723ca1743e337a2588ed008f"
|
||||
integrity sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==
|
||||
dependencies:
|
||||
esbuild "^0.21.3"
|
||||
postcss "^8.4.43"
|
||||
|
|
|
|||
Loading…
Reference in New Issue