Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f3e7bc8060
commit
2164573e45
54 changed files with 878 additions and 321 deletions
|
@ -51,6 +51,7 @@ const Api = {
|
||||||
pipelinesPath: '/api/:version/projects/:id/pipelines/',
|
pipelinesPath: '/api/:version/projects/:id/pipelines/',
|
||||||
environmentsPath: '/api/:version/projects/:id/environments',
|
environmentsPath: '/api/:version/projects/:id/environments',
|
||||||
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
|
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
|
||||||
|
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
|
||||||
|
|
||||||
group(groupId, callback) {
|
group(groupId, callback) {
|
||||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||||
|
@ -540,6 +541,22 @@ const Api = {
|
||||||
return axios.get(url, { params });
|
return axios.get(url, { params });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateIssue(project, issue, data = {}) {
|
||||||
|
const url = Api.buildUrl(Api.issuePath)
|
||||||
|
.replace(':id', encodeURIComponent(project))
|
||||||
|
.replace(':issue_iid', encodeURIComponent(issue));
|
||||||
|
|
||||||
|
return axios.put(url, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMergeRequest(project, mergeRequest, data = {}) {
|
||||||
|
const url = Api.buildUrl(Api.projectMergeRequestPath)
|
||||||
|
.replace(':id', encodeURIComponent(project))
|
||||||
|
.replace(':mrid', encodeURIComponent(mergeRequest));
|
||||||
|
|
||||||
|
return axios.put(url, data);
|
||||||
|
},
|
||||||
|
|
||||||
buildUrl(url) {
|
buildUrl(url) {
|
||||||
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
|
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
|
||||||
},
|
},
|
||||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
||||||
'btn-transparent comment-indicator': isNewNote,
|
'btn-transparent comment-indicator': isNewNote,
|
||||||
'js-image-badge badge badge-pill': !isNewNote,
|
'js-image-badge badge badge-pill': !isNewNote,
|
||||||
}"
|
}"
|
||||||
class="position-absolute"
|
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center"
|
||||||
type="button"
|
type="button"
|
||||||
@mousedown="$emit('mousedown', $event)"
|
@mousedown="$emit('mousedown', $event)"
|
||||||
@mouseup="$emit('mouseup', $event)"
|
@mouseup="$emit('mouseup', $event)"
|
||||||
|
|
|
@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import DagGraph from './dag_graph.vue';
|
import DagGraph from './dag_graph.vue';
|
||||||
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants';
|
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants';
|
||||||
import { parseData } from './utils';
|
import { parseData } from './parsing_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||||
|
|
|
@ -3,7 +3,8 @@ import * as d3 from 'd3';
|
||||||
import { uniqueId } from 'lodash';
|
import { uniqueId } from 'lodash';
|
||||||
import { PARSE_FAILURE } from './constants';
|
import { PARSE_FAILURE } from './constants';
|
||||||
|
|
||||||
import { createSankey, getMaxNodes, removeOrphanNodes } from './utils';
|
import { getMaxNodes, removeOrphanNodes } from './parsing_utils';
|
||||||
|
import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
viewOptions: {
|
viewOptions: {
|
||||||
|
@ -78,7 +79,7 @@ export default {
|
||||||
return (
|
return (
|
||||||
link
|
link
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', this.createLinkPath)
|
.attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth))
|
||||||
.attr('stroke', ({ gradId }) => `url(#${gradId})`)
|
.attr('stroke', ({ gradId }) => `url(#${gradId})`)
|
||||||
.style('stroke-linejoin', 'round')
|
.style('stroke-linejoin', 'round')
|
||||||
// minus two to account for the rounded nodes
|
// minus two to account for the rounded nodes
|
||||||
|
@ -89,7 +90,10 @@ export default {
|
||||||
|
|
||||||
appendLabelAsForeignObject(d, i, n) {
|
appendLabelAsForeignObject(d, i, n) {
|
||||||
const currentNode = n[i];
|
const currentNode = n[i];
|
||||||
const { height, wrapperWidth, width, x, y, textAlign } = this.labelPosition(d);
|
const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, {
|
||||||
|
...this.$options.viewOptions,
|
||||||
|
width: this.width,
|
||||||
|
});
|
||||||
|
|
||||||
const labelClasses = [
|
const labelClasses = [
|
||||||
'gl-display-flex',
|
'gl-display-flex',
|
||||||
|
@ -128,44 +132,13 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
createClip(link) {
|
createClip(link) {
|
||||||
/*
|
|
||||||
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 clip = ({ y0, y1, source, target, width }) => {
|
|
||||||
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 */
|
|
||||||
};
|
|
||||||
|
|
||||||
return link
|
return link
|
||||||
.append('clipPath')
|
.append('clipPath')
|
||||||
.attr('id', d => {
|
.attr('id', d => {
|
||||||
return this.createAndAssignId(d, 'clipId', 'dag-clip');
|
return this.createAndAssignId(d, 'clipId', 'dag-clip');
|
||||||
})
|
})
|
||||||
.append('path')
|
.append('path')
|
||||||
.attr('d', clip);
|
.attr('d', calculateClip);
|
||||||
},
|
},
|
||||||
|
|
||||||
createGradient(link) {
|
createGradient(link) {
|
||||||
|
@ -189,44 +162,6 @@ export default {
|
||||||
.attr('stop-color', ({ target }) => this.color(target));
|
.attr('stop-color', ({ target }) => this.color(target));
|
||||||
},
|
},
|
||||||
|
|
||||||
createLinkPath({ y0, y1, source, target, width }, idx) {
|
|
||||||
const { nodeWidth } = this.$options.viewOptions;
|
|
||||||
|
|
||||||
/*
|
|
||||||
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],
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
createLinks(svg, linksData) {
|
createLinks(svg, linksData) {
|
||||||
const link = this.generateLinks(svg, linksData);
|
const link = this.generateLinks(svg, linksData);
|
||||||
this.createGradient(link);
|
this.createGradient(link);
|
||||||
|
@ -322,42 +257,6 @@ export default {
|
||||||
return ({ name }) => colorFn(name);
|
return ({ name }) => colorFn(name);
|
||||||
},
|
},
|
||||||
|
|
||||||
labelPosition({ x0, x1, y0, y1 }) {
|
|
||||||
const { paddingForLabels, labelMargin, nodePadding } = this.$options.viewOptions;
|
|
||||||
|
|
||||||
const firstCol = x0 <= paddingForLabels;
|
|
||||||
const lastCol = x1 >= this.width - paddingForLabels;
|
|
||||||
|
|
||||||
if (firstCol) {
|
|
||||||
return {
|
|
||||||
x: 0 + labelMargin,
|
|
||||||
y: y0,
|
|
||||||
height: `${y1 - y0}px`,
|
|
||||||
width: paddingForLabels - 2 * labelMargin,
|
|
||||||
textAlign: 'right',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastCol) {
|
|
||||||
return {
|
|
||||||
x: this.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 < this.width / 2 ? 'left' : 'right',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
transformData(parsed) {
|
transformData(parsed) {
|
||||||
const baseLayout = createSankey()(parsed);
|
const baseLayout = createSankey()(parsed);
|
||||||
const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
|
const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
|
||||||
|
|
134
app/assets/javascripts/pipelines/components/dag/drawing_utils.js
Normal file
134
app/assets/javascripts/pipelines/components/dag/drawing_utils.js
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
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,4 +1,3 @@
|
||||||
import { sankey, sankeyLeft } from 'd3-sankey';
|
|
||||||
import { uniqWith, isEqual } from 'lodash';
|
import { uniqWith, isEqual } from 'lodash';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -136,34 +135,6 @@ export const parseData = data => {
|
||||||
return { nodes, links };
|
return { nodes, links };
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
|
||||||
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 })),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
The number of nodes in the most populous generation drives the height of the graph.
|
The number of nodes in the most populous generation drives the height of the graph.
|
||||||
*/
|
*/
|
|
@ -98,25 +98,27 @@ export default {
|
||||||
:has-issues="reports.length > 0"
|
:has-issues="reports.length > 0"
|
||||||
class="mr-widget-section grouped-security-reports mr-report"
|
class="mr-widget-section grouped-security-reports mr-report"
|
||||||
>
|
>
|
||||||
<div slot="body" class="mr-widget-grouped-section report-block">
|
<template #body>
|
||||||
<template v-for="(report, i) in reports">
|
<div class="mr-widget-grouped-section report-block">
|
||||||
<summary-row
|
<template v-for="(report, i) in reports">
|
||||||
:key="`summary-row-${i}`"
|
<summary-row
|
||||||
:summary="reportText(report)"
|
:key="`summary-row-${i}`"
|
||||||
:status-icon="getReportIcon(report)"
|
:summary="reportText(report)"
|
||||||
/>
|
:status-icon="getReportIcon(report)"
|
||||||
<issues-list
|
/>
|
||||||
v-if="shouldRenderIssuesList(report)"
|
<issues-list
|
||||||
:key="`issues-list-${i}`"
|
v-if="shouldRenderIssuesList(report)"
|
||||||
:unresolved-issues="unresolvedIssues(report)"
|
:key="`issues-list-${i}`"
|
||||||
:new-issues="newIssues(report)"
|
:unresolved-issues="unresolvedIssues(report)"
|
||||||
:resolved-issues="resolvedIssues(report)"
|
:new-issues="newIssues(report)"
|
||||||
:component="$options.componentNames.TestIssueBody"
|
:resolved-issues="resolvedIssues(report)"
|
||||||
class="report-block-group-list"
|
:component="$options.componentNames.TestIssueBody"
|
||||||
/>
|
class="report-block-group-list"
|
||||||
</template>
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
<modal :title="modalTitle" :modal-data="modalData" />
|
<modal :title="modalTitle" :modal-data="modalData" />
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</report-section>
|
</report-section>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -70,7 +70,7 @@ $avatar-sizes: (
|
||||||
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
|
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
|
||||||
$identicon-orange, $gray-darker;
|
$identicon-orange, $gray-darker;
|
||||||
|
|
||||||
.avatar-circle {
|
%avatar-circle {
|
||||||
float: left;
|
float: left;
|
||||||
margin-right: $gl-padding;
|
margin-right: $gl-padding;
|
||||||
border-radius: $avatar-radius;
|
border-radius: $avatar-radius;
|
||||||
|
@ -84,7 +84,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
@extend .avatar-circle;
|
@extend %avatar-circle;
|
||||||
transition-property: none;
|
transition-property: none;
|
||||||
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
|
@ -100,10 +100,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.s16 {
|
&.s16,
|
||||||
margin-right: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.s24 {
|
&.s24 {
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
@ -154,7 +151,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
@extend .avatar-circle;
|
@extend %avatar-circle;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,16 @@
|
||||||
top: 35px;
|
top: 35px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inactive {
|
.design-pin {
|
||||||
opacity: 0.5;
|
transition: opacity 0.5s ease;
|
||||||
|
|
||||||
|
&.inactive {
|
||||||
|
@include gl-opacity-5;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@include gl-opacity-10;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu-toggle,
|
.dropdown-menu-toggle,
|
||||||
.avatar-circle,
|
|
||||||
.header-user-avatar {
|
.header-user-avatar {
|
||||||
@include transition(border-color);
|
@include transition(border-color);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class Projects::ServicesController < Projects::ApplicationController
|
class Projects::ServicesController < Projects::ApplicationController
|
||||||
include ServiceParams
|
include ServiceParams
|
||||||
|
include InternalRedirect
|
||||||
|
|
||||||
# Authorize
|
# Authorize
|
||||||
before_action :authorize_admin_project!
|
before_action :authorize_admin_project!
|
||||||
|
@ -26,8 +27,8 @@ class Projects::ServicesController < Projects::ApplicationController
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html do
|
format.html do
|
||||||
if saved
|
if saved
|
||||||
redirect_to project_settings_integrations_path(@project),
|
target_url = safe_redirect_path(params[:redirect_to]).presence || project_settings_integrations_path(@project)
|
||||||
notice: success_message
|
redirect_to target_url, notice: success_message
|
||||||
else
|
else
|
||||||
render 'edit'
|
render 'edit'
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,7 +12,8 @@ module Types
|
||||||
description: 'Name of the link'
|
description: 'Name of the link'
|
||||||
field :url, GraphQL::STRING_TYPE, null: true,
|
field :url, GraphQL::STRING_TYPE, null: true,
|
||||||
description: 'URL of the link'
|
description: 'URL of the link'
|
||||||
|
field :link_type, Types::ReleaseLinkTypeEnum, null: true,
|
||||||
|
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
|
||||||
field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
|
field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
|
||||||
description: 'Indicates the link points to an external resource'
|
description: 'Indicates the link points to an external resource'
|
||||||
end
|
end
|
||||||
|
|
12
app/graphql/types/release_link_type_enum.rb
Normal file
12
app/graphql/types/release_link_type_enum.rb
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Types
|
||||||
|
class ReleaseLinkTypeEnum < BaseEnum
|
||||||
|
graphql_name 'ReleaseLinkType'
|
||||||
|
description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
|
||||||
|
|
||||||
|
::Releases::Link.link_types.keys.each do |link_type|
|
||||||
|
value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -688,6 +688,10 @@ module Ci
|
||||||
job_artifacts.any?
|
job_artifacts.any?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_test_reports?
|
||||||
|
job_artifacts.test_reports.exists?
|
||||||
|
end
|
||||||
|
|
||||||
def has_old_trace?
|
def has_old_trace?
|
||||||
old_trace.present?
|
old_trace.present?
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,5 +11,35 @@ module Ci
|
||||||
|
|
||||||
validates :build, :project, presence: true
|
validates :build, :project, presence: true
|
||||||
validates :data, json_schema: { filename: "build_report_result_data" }
|
validates :data, json_schema: { filename: "build_report_result_data" }
|
||||||
|
|
||||||
|
store_accessor :data, :tests
|
||||||
|
|
||||||
|
def tests_name
|
||||||
|
tests.dig("name")
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_duration
|
||||||
|
tests.dig("duration")
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_success
|
||||||
|
tests.dig("success").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_failed
|
||||||
|
tests.dig("failed").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_errored
|
||||||
|
tests.dig("errored").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_skipped
|
||||||
|
tests.dig("skipped").to_i
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_total
|
||||||
|
[tests_success, tests_failed, tests_errored, tests_skipped].sum
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
36
app/services/ci/build_report_result_service.rb
Normal file
36
app/services/ci/build_report_result_service.rb
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
class BuildReportResultService
|
||||||
|
def execute(build)
|
||||||
|
return unless Feature.enabled?(:build_report_summary, build.project)
|
||||||
|
return unless build.has_test_reports?
|
||||||
|
|
||||||
|
build.report_results.create!(
|
||||||
|
project_id: build.project_id,
|
||||||
|
data: tests_params(build)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def generate_test_suite_report(build)
|
||||||
|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
|
||||||
|
end
|
||||||
|
|
||||||
|
def tests_params(build)
|
||||||
|
test_suite = generate_test_suite_report(build)
|
||||||
|
|
||||||
|
{
|
||||||
|
tests: {
|
||||||
|
name: test_suite.name,
|
||||||
|
duration: test_suite.total_time,
|
||||||
|
failed: test_suite.failed_count,
|
||||||
|
errored: test_suite.error_count,
|
||||||
|
skipped: test_suite.skipped_count,
|
||||||
|
success: test_suite.success_count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -4,8 +4,6 @@
|
||||||
# Example:
|
# Example:
|
||||||
# ```
|
# ```
|
||||||
# class DummyService
|
# class DummyService
|
||||||
# prepend Measurable
|
|
||||||
#
|
|
||||||
# def execute
|
# def execute
|
||||||
# # ...
|
# # ...
|
||||||
# end
|
# end
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"coverage": { "type": "float" },
|
"coverage": { "type": "float" },
|
||||||
"junit": {
|
"tests": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"items": { "$ref": "./build_report_result_data_junit.json" }
|
"items": { "$ref": "./build_report_result_data_tests.json" }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"description": "Build report result data junit",
|
"description": "Build report result data tests",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": { "type": "string" },
|
"name": { "type": "string" },
|
|
@ -13,6 +13,7 @@
|
||||||
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
|
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
|
||||||
= render 'shared/service_settings', form: form, service: @service
|
= render 'shared/service_settings', form: form, service: @service
|
||||||
.footer-block.row-content-block
|
.footer-block.row-content-block
|
||||||
|
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
|
||||||
= service_save_button
|
= service_save_button
|
||||||
|
|
||||||
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
|
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
|
||||||
|
|
|
@ -803,6 +803,14 @@
|
||||||
:weight: 1
|
:weight: 1
|
||||||
:idempotent:
|
:idempotent:
|
||||||
:tags: []
|
:tags: []
|
||||||
|
- :name: pipeline_background:ci_build_report_result
|
||||||
|
:feature_category: :continuous_integration
|
||||||
|
:has_external_dependencies:
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent: true
|
||||||
|
:tags: []
|
||||||
- :name: pipeline_background:ci_build_trace_chunk_flush
|
- :name: pipeline_background:ci_build_trace_chunk_flush
|
||||||
:feature_category: :continuous_integration
|
:feature_category: :continuous_integration
|
||||||
:has_external_dependencies:
|
:has_external_dependencies:
|
||||||
|
|
|
@ -28,6 +28,7 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
|
||||||
# We execute these in sync to reduce IO.
|
# We execute these in sync to reduce IO.
|
||||||
BuildTraceSectionsWorker.new.perform(build.id)
|
BuildTraceSectionsWorker.new.perform(build.id)
|
||||||
BuildCoverageWorker.new.perform(build.id)
|
BuildCoverageWorker.new.perform(build.id)
|
||||||
|
Ci::BuildReportResultWorker.new.perform(build.id)
|
||||||
|
|
||||||
# We execute these async as these are independent operations.
|
# We execute these async as these are independent operations.
|
||||||
BuildHooksWorker.perform_async(build.id)
|
BuildHooksWorker.perform_async(build.id)
|
||||||
|
|
16
app/workers/ci/build_report_result_worker.rb
Normal file
16
app/workers/ci/build_report_result_worker.rb
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Ci
|
||||||
|
class BuildReportResultWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include PipelineBackgroundQueue
|
||||||
|
|
||||||
|
idempotent!
|
||||||
|
|
||||||
|
def perform(build_id)
|
||||||
|
Ci::Build.find_by_id(build_id).try do |build|
|
||||||
|
Ci::BuildReportResultService.new.execute(build)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
14
bin/secpick
14
bin/secpick
|
@ -21,10 +21,6 @@ module Secpick
|
||||||
@options = self.class.options
|
@options = self.class.options
|
||||||
end
|
end
|
||||||
|
|
||||||
def ee?
|
|
||||||
File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__))
|
|
||||||
end
|
|
||||||
|
|
||||||
def dry_run?
|
def dry_run?
|
||||||
@options[:try] == true
|
@options[:try] == true
|
||||||
end
|
end
|
||||||
|
@ -40,9 +36,7 @@ module Secpick
|
||||||
end
|
end
|
||||||
|
|
||||||
def stable_branch
|
def stable_branch
|
||||||
"#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name|
|
"#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze
|
||||||
name << "-ee" if ee?
|
|
||||||
end.freeze
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def git_commands
|
def git_commands
|
||||||
|
@ -64,11 +58,7 @@ module Secpick
|
||||||
end
|
end
|
||||||
|
|
||||||
def new_mr_url
|
def new_mr_url
|
||||||
if ee?
|
SECURITY_MR_URL
|
||||||
SECURITY_MR_URL
|
|
||||||
else
|
|
||||||
SECURITY_MR_URL.sub('/gitlab/', '/gitlab-foss/')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def create!
|
def create!
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add api.js methods to update issues and merge requests
|
||||||
|
merge_request: 32893
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Expose `release_links.type` via API
|
||||||
|
merge_request: 33154
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add `link_type` to `ReleaseLink` GraphQL type
|
||||||
|
merge_request: 33386
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add opacity transition to active design discussion pins
|
||||||
|
merge_request: 33493
|
||||||
|
author:
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Update deprecated slot syntax in app/assets/javascripts/reports/components/grouped_test_reports_app.vue
|
||||||
|
merge_request: 31975
|
||||||
|
author: Gilang Gumilar
|
||||||
|
type: other
|
|
@ -9648,6 +9648,11 @@ type ReleaseLink {
|
||||||
"""
|
"""
|
||||||
id: ID!
|
id: ID!
|
||||||
|
|
||||||
|
"""
|
||||||
|
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
|
||||||
|
"""
|
||||||
|
linkType: ReleaseLinkType
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Name of the link
|
Name of the link
|
||||||
"""
|
"""
|
||||||
|
@ -9694,6 +9699,31 @@ type ReleaseLinkEdge {
|
||||||
node: ReleaseLink
|
node: ReleaseLink
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"""
|
||||||
|
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
|
||||||
|
"""
|
||||||
|
enum ReleaseLinkType {
|
||||||
|
"""
|
||||||
|
Image link type
|
||||||
|
"""
|
||||||
|
IMAGE
|
||||||
|
|
||||||
|
"""
|
||||||
|
Other link type
|
||||||
|
"""
|
||||||
|
OTHER
|
||||||
|
|
||||||
|
"""
|
||||||
|
Package link type
|
||||||
|
"""
|
||||||
|
PACKAGE
|
||||||
|
|
||||||
|
"""
|
||||||
|
Runbook link type
|
||||||
|
"""
|
||||||
|
RUNBOOK
|
||||||
|
}
|
||||||
|
|
||||||
type ReleaseSource {
|
type ReleaseSource {
|
||||||
"""
|
"""
|
||||||
Format of the source
|
Format of the source
|
||||||
|
|
|
@ -28194,6 +28194,20 @@
|
||||||
"isDeprecated": false,
|
"isDeprecated": false,
|
||||||
"deprecationReason": null
|
"deprecationReason": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "linkType",
|
||||||
|
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
|
||||||
|
"args": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": {
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "ReleaseLinkType",
|
||||||
|
"ofType": null
|
||||||
|
},
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "name",
|
"name": "name",
|
||||||
"description": "Name of the link",
|
"description": "Name of the link",
|
||||||
|
@ -28342,6 +28356,41 @@
|
||||||
"enumValues": null,
|
"enumValues": null,
|
||||||
"possibleTypes": null
|
"possibleTypes": null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"kind": "ENUM",
|
||||||
|
"name": "ReleaseLinkType",
|
||||||
|
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
|
||||||
|
"fields": null,
|
||||||
|
"inputFields": null,
|
||||||
|
"interfaces": null,
|
||||||
|
"enumValues": [
|
||||||
|
{
|
||||||
|
"name": "OTHER",
|
||||||
|
"description": "Other link type",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RUNBOOK",
|
||||||
|
"description": "Runbook link type",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "PACKAGE",
|
||||||
|
"description": "Package link type",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "IMAGE",
|
||||||
|
"description": "Image link type",
|
||||||
|
"isDeprecated": false,
|
||||||
|
"deprecationReason": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"possibleTypes": null
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"kind": "OBJECT",
|
"kind": "OBJECT",
|
||||||
"name": "ReleaseSource",
|
"name": "ReleaseSource",
|
||||||
|
|
|
@ -1342,6 +1342,7 @@ Information about pagination in a connection.
|
||||||
| --- | ---- | ---------- |
|
| --- | ---- | ---------- |
|
||||||
| `external` | Boolean | Indicates the link points to an external resource |
|
| `external` | Boolean | Indicates the link points to an external resource |
|
||||||
| `id` | ID! | ID of the link |
|
| `id` | ID! | ID of the link |
|
||||||
|
| `linkType` | ReleaseLinkType | Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other` |
|
||||||
| `name` | String | Name of the link |
|
| `name` | String | Name of the link |
|
||||||
| `url` | String | URL of the link |
|
| `url` | String | URL of the link |
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ GitLab provides built-in tools to help improve performance and availability:
|
||||||
- [Request Profiling](../administration/monitoring/performance/request_profiling.md).
|
- [Request Profiling](../administration/monitoring/performance/request_profiling.md).
|
||||||
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
|
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
|
||||||
- [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
|
- [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
|
||||||
|
- [Service measurement](service_measurement.md) for measuring and logging service execution.
|
||||||
|
|
||||||
GitLab team members can use [GitLab.com's performance monitoring systems](https://about.gitlab.com/handbook/engineering/monitoring/) located at
|
GitLab team members can use [GitLab.com's performance monitoring systems](https://about.gitlab.com/handbook/engineering/monitoring/) located at
|
||||||
<https://dashboards.gitlab.net>, this requires you to log in using your
|
<https://dashboards.gitlab.net>, this requires you to log in using your
|
||||||
|
|
81
doc/development/service_measurement.md
Normal file
81
doc/development/service_measurement.md
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
# GitLab Developers Guide to service measurement
|
||||||
|
|
||||||
|
You can enable service measurement in order to debug any slow service's execution time, number of SQL calls, garbage collection stats, memory usage, etc.
|
||||||
|
|
||||||
|
## Measuring module
|
||||||
|
|
||||||
|
The measuring module is a tool that allows to measure a service's execution, and log:
|
||||||
|
|
||||||
|
- Service class name
|
||||||
|
- Execution time
|
||||||
|
- Number of sql calls
|
||||||
|
- Detailed gc stats and diffs
|
||||||
|
- RSS memory usage
|
||||||
|
- Server worker ID
|
||||||
|
|
||||||
|
The measuring module will log these measurements into a structured log called [`service_measurement.log`](../administration/logs.md#service_measurementlog),
|
||||||
|
as a single entry for each service execution.
|
||||||
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
For GitLab.com, `service_measurement.log` is ingested in Elasticsearch and Kibana as part of our monitoring solution.
|
||||||
|
|
||||||
|
## How to use it
|
||||||
|
|
||||||
|
The measuring module allows you to easily measure and log execution of any service,
|
||||||
|
by just prepending `Measurable` in any Service class, on the last line of the file that the class resides in.
|
||||||
|
|
||||||
|
For example, to prepend a module into the `DummyService` class, you would use the following approach:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class DummyService
|
||||||
|
def execute
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DummyService.prepend(Measurable)
|
||||||
|
```
|
||||||
|
|
||||||
|
In case when you are prepending a module from the `EE` namespace with EE features, you need to prepend Measurable after prepending the `EE` module.
|
||||||
|
|
||||||
|
This way, `Measurable` will be at the bottom of the ancestor chain, in order to measure execution of `EE` features as well:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
class DummyService
|
||||||
|
def execute
|
||||||
|
# ...
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
DummyService.prepend_if_ee('EE::DummyService')
|
||||||
|
DummyService.prepend(Measurable)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log additional attributes
|
||||||
|
|
||||||
|
In case you need to log some additional attributes, it is possible to define `extra_attributes_for_measurement` in the service class:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
def extra_attributes_for_measurement
|
||||||
|
{
|
||||||
|
project_path: @project.full_path,
|
||||||
|
user: current_user.name
|
||||||
|
}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
NOTE: **Note:**
|
||||||
|
Once the measurement module is injected in the service, it will be behind generic feature flag.
|
||||||
|
In order to actually use it, you need to enable measuring for the desired service by enabling the feature flag.
|
||||||
|
|
||||||
|
### Enabling measurement using feature flags
|
||||||
|
|
||||||
|
In the following example, the `:gitlab_service_measuring_projects_import_service`
|
||||||
|
[feature flag](feature_flags/development.md#enabling-a-feature-flag-in-development) is used to enable the measuring feature
|
||||||
|
for `Projects::ImportService`.
|
||||||
|
|
||||||
|
From chatops:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
/chatops run feature set gitlab_service_measuring_projects_import_service true
|
||||||
|
```
|
|
@ -9,6 +9,7 @@ module API
|
||||||
expose :url
|
expose :url
|
||||||
expose :direct_asset_url
|
expose :direct_asset_url
|
||||||
expose :external?, as: :external
|
expose :external?, as: :external
|
||||||
|
expose :link_type
|
||||||
|
|
||||||
def direct_asset_url
|
def direct_asset_url
|
||||||
return object.url unless object.filepath
|
return object.url unless object.filepath
|
||||||
|
|
|
@ -40,6 +40,7 @@ module API
|
||||||
requires :name, type: String, desc: 'The name of the link'
|
requires :name, type: String, desc: 'The name of the link'
|
||||||
requires :url, type: String, desc: 'The URL of the link'
|
requires :url, type: String, desc: 'The URL of the link'
|
||||||
optional :filepath, type: String, desc: 'The filepath of the link'
|
optional :filepath, type: String, desc: 'The filepath of the link'
|
||||||
|
optional :link_type, type: String, desc: 'The link type'
|
||||||
end
|
end
|
||||||
post 'links' do
|
post 'links' do
|
||||||
authorize! :create_release, release
|
authorize! :create_release, release
|
||||||
|
@ -75,6 +76,7 @@ module API
|
||||||
optional :name, type: String, desc: 'The name of the link'
|
optional :name, type: String, desc: 'The name of the link'
|
||||||
optional :url, type: String, desc: 'The URL of the link'
|
optional :url, type: String, desc: 'The URL of the link'
|
||||||
optional :filepath, type: String, desc: 'The filepath of the link'
|
optional :filepath, type: String, desc: 'The filepath of the link'
|
||||||
|
optional :link_type, type: String, desc: 'The link type'
|
||||||
at_least_one_of :name, :url
|
at_least_one_of :name, :url
|
||||||
end
|
end
|
||||||
put do
|
put do
|
||||||
|
|
|
@ -134,24 +134,50 @@ describe Projects::ServicesController do
|
||||||
describe 'PUT #update' do
|
describe 'PUT #update' do
|
||||||
describe 'as HTML' do
|
describe 'as HTML' do
|
||||||
let(:service_params) { { active: true } }
|
let(:service_params) { { active: true } }
|
||||||
|
let(:params) { project_params(service: service_params) }
|
||||||
|
|
||||||
|
let(:message) { 'Jira activated.' }
|
||||||
|
let(:redirect_url) { project_settings_integrations_path(project) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
put :update, params: project_params(service: service_params)
|
put :update, params: params
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'service update' do
|
||||||
|
it 'redirects to the correct url with a flash message' do
|
||||||
|
expect(response).to redirect_to(redirect_url)
|
||||||
|
expect(flash[:notice]).to eq(message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when param `active` is set to true' do
|
context 'when param `active` is set to true' do
|
||||||
it 'activates the service and redirects to integrations paths' do
|
let(:params) { project_params(service: service_params, redirect_to: redirect) }
|
||||||
expect(response).to redirect_to(project_settings_integrations_path(project))
|
|
||||||
expect(flash[:notice]).to eq 'Jira activated.'
|
context 'when redirect_to param is present' do
|
||||||
|
let(:redirect) { '/redirect_here' }
|
||||||
|
let(:redirect_url) { redirect }
|
||||||
|
|
||||||
|
it_behaves_like 'service update'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when redirect_to is an external domain' do
|
||||||
|
let(:redirect) { 'http://examle.com' }
|
||||||
|
|
||||||
|
it_behaves_like 'service update'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when redirect_to param is an empty string' do
|
||||||
|
let(:redirect) { '' }
|
||||||
|
|
||||||
|
it_behaves_like 'service update'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when param `active` is set to false' do
|
context 'when param `active` is set to false' do
|
||||||
let(:service_params) { { active: false } }
|
let(:service_params) { { active: false } }
|
||||||
|
let(:message) { 'Jira settings saved, but not activated.' }
|
||||||
|
|
||||||
it 'does not activate the service but saves the settings' do
|
it_behaves_like 'service update'
|
||||||
expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ FactoryBot.define do
|
||||||
project factory: :project
|
project factory: :project
|
||||||
data do
|
data do
|
||||||
{
|
{
|
||||||
junit: {
|
tests: {
|
||||||
name: "rspec",
|
name: "rspec",
|
||||||
duration: 0.42,
|
duration: 0.42,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
|
@ -20,7 +20,7 @@ FactoryBot.define do
|
||||||
trait :with_junit_success do
|
trait :with_junit_success do
|
||||||
data do
|
data do
|
||||||
{
|
{
|
||||||
junit: {
|
tests: {
|
||||||
name: "rspec",
|
name: "rspec",
|
||||||
duration: 0.42,
|
duration: 0.42,
|
||||||
failed: 0,
|
failed: 0,
|
||||||
|
|
|
@ -6,5 +6,6 @@ FactoryBot.define do
|
||||||
sequence(:name) { |n| "release-18.#{n}.dmg" }
|
sequence(:name) { |n| "release-18.#{n}.dmg" }
|
||||||
sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" }
|
sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" }
|
||||||
sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" }
|
sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" }
|
||||||
|
link_type { 'other' }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
3
spec/fixtures/api/schemas/release/link.json
vendored
3
spec/fixtures/api/schemas/release/link.json
vendored
|
@ -7,7 +7,8 @@
|
||||||
"filepath": { "type": "string" },
|
"filepath": { "type": "string" },
|
||||||
"url": { "type": "string" },
|
"url": { "type": "string" },
|
||||||
"direct_asset_url": { "type": "string" },
|
"direct_asset_url": { "type": "string" },
|
||||||
"external": { "type": "boolean" }
|
"external": { "type": "boolean" },
|
||||||
|
"link_type": { "type": "string" }
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -691,4 +691,38 @@ describe('Api', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateIssue', () => {
|
||||||
|
it('update an issue with the given payload', done => {
|
||||||
|
const projectId = 8;
|
||||||
|
const issue = 1;
|
||||||
|
const expectedArray = [1, 2, 3];
|
||||||
|
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`;
|
||||||
|
mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
|
||||||
|
|
||||||
|
Api.updateIssue(projectId, issue, { assigneeIds: expectedArray })
|
||||||
|
.then(({ data }) => {
|
||||||
|
expect(data.assigneeIds).toEqual(expectedArray);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateMergeRequest', () => {
|
||||||
|
it('update an issue with the given payload', done => {
|
||||||
|
const projectId = 8;
|
||||||
|
const mergeRequest = 1;
|
||||||
|
const expectedArray = [1, 2, 3];
|
||||||
|
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`;
|
||||||
|
mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
|
||||||
|
|
||||||
|
Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray })
|
||||||
|
.then(({ data }) => {
|
||||||
|
expect(data.assigneeIds).toEqual(expectedArray);
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Design discussions component should match the snapshot of note when repositioning 1`] = `
|
exports[`Design note pin component should match the snapshot of note when repositioning 1`] = `
|
||||||
<button
|
<button
|
||||||
aria-label="Comment form position"
|
aria-label="Comment form position"
|
||||||
class="position-absolute btn-transparent comment-indicator"
|
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
|
||||||
style="left: 10px; top: 10px; cursor: move;"
|
style="left: 10px; top: 10px; cursor: move;"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -14,10 +14,10 @@ exports[`Design discussions component should match the snapshot of note when rep
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Design discussions component should match the snapshot of note with index 1`] = `
|
exports[`Design note pin component should match the snapshot of note with index 1`] = `
|
||||||
<button
|
<button
|
||||||
aria-label="Comment '1' position"
|
aria-label="Comment '1' position"
|
||||||
class="position-absolute js-image-badge badge badge-pill"
|
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center js-image-badge badge badge-pill"
|
||||||
style="left: 10px; top: 10px;"
|
style="left: 10px; top: 10px;"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -27,10 +27,10 @@ exports[`Design discussions component should match the snapshot of note with ind
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Design discussions component should match the snapshot of note without index 1`] = `
|
exports[`Design note pin component should match the snapshot of note without index 1`] = `
|
||||||
<button
|
<button
|
||||||
aria-label="Comment form position"
|
aria-label="Comment form position"
|
||||||
class="position-absolute btn-transparent comment-indicator"
|
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
|
||||||
style="left: 10px; top: 10px;"
|
style="left: 10px; top: 10px;"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import DesignNotePin from '~/design_management/components/design_note_pin.vue';
|
import DesignNotePin from '~/design_management/components/design_note_pin.vue';
|
||||||
|
|
||||||
describe('Design discussions component', () => {
|
describe('Design note pin component', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
|
||||||
function createComponent(propsData = {}) {
|
function createComponent(propsData = {}) {
|
||||||
|
|
|
@ -10,12 +10,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip63\\">
|
<clipPath id=\\"dag-clip63\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M100, 129
|
M100, 129
|
||||||
V158
|
V158
|
||||||
H377.3333333333333
|
H377.3333333333333
|
||||||
V100
|
V100
|
||||||
H100
|
H100
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path>
|
<path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -26,12 +27,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip64\\">
|
<clipPath id=\\"dag-clip64\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M361.3333333333333, 129.0000000000002
|
M361.3333333333333, 129.0000000000002
|
||||||
V158.0000000000002
|
V158.0000000000002
|
||||||
H638.6666666666666
|
H638.6666666666666
|
||||||
V100
|
V100
|
||||||
H361.3333333333333
|
H361.3333333333333
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path>
|
<path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -42,12 +44,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip65\\">
|
<clipPath id=\\"dag-clip65\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M100, 187.0000000000002
|
M100, 187.0000000000002
|
||||||
V241.00000000000003
|
V241.00000000000003
|
||||||
H638.6666666666666
|
H638.6666666666666
|
||||||
V158.0000000000002
|
V158.0000000000002
|
||||||
H100
|
H100
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path>
|
<path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -58,12 +61,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip66\\">
|
<clipPath id=\\"dag-clip66\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M100, 269.9999999999998
|
M100, 269.9999999999998
|
||||||
V324
|
V324
|
||||||
H377.3333333333333
|
H377.3333333333333
|
||||||
V240.99999999999977
|
V240.99999999999977
|
||||||
H100
|
H100
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path>
|
<path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -74,12 +78,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip67\\">
|
<clipPath id=\\"dag-clip67\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M100, 352.99999999999994
|
M100, 352.99999999999994
|
||||||
V407.00000000000006
|
V407.00000000000006
|
||||||
H377.3333333333333
|
H377.3333333333333
|
||||||
V323.99999999999994
|
V323.99999999999994
|
||||||
H100
|
H100
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path>
|
<path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -90,12 +95,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip68\\">
|
<clipPath id=\\"dag-clip68\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M361.3333333333333, 270.0000000000001
|
M361.3333333333333, 270.0000000000001
|
||||||
V299.0000000000001
|
V299.0000000000001
|
||||||
H638.6666666666666
|
H638.6666666666666
|
||||||
V240.99999999999977
|
V240.99999999999977
|
||||||
H361.3333333333333
|
H361.3333333333333
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path>
|
<path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -106,12 +112,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip69\\">
|
<clipPath id=\\"dag-clip69\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M361.3333333333333, 328.0000000000001
|
M361.3333333333333, 328.0000000000001
|
||||||
V381.99999999999994
|
V381.99999999999994
|
||||||
H638.6666666666666
|
H638.6666666666666
|
||||||
V299.0000000000001
|
V299.0000000000001
|
||||||
H361.3333333333333
|
H361.3333333333333
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path>
|
<path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -122,12 +129,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip70\\">
|
<clipPath id=\\"dag-clip70\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M361.3333333333333, 411
|
M361.3333333333333, 411
|
||||||
V440
|
V440
|
||||||
H638.6666666666666
|
H638.6666666666666
|
||||||
V381.99999999999994
|
V381.99999999999994
|
||||||
H361.3333333333333
|
H361.3333333333333
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path>
|
<path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -138,12 +146,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip71\\">
|
<clipPath id=\\"dag-clip71\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M622.6666666666666, 270.1890725105691
|
M622.6666666666666, 270.1890725105691
|
||||||
V299.1890725105691
|
V299.1890725105691
|
||||||
H900
|
H900
|
||||||
V241.0000000000001
|
V241.0000000000001
|
||||||
H622.6666666666666
|
H622.6666666666666
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path>
|
<path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
@ -154,12 +163,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id=\\"dag-clip72\\">
|
<clipPath id=\\"dag-clip72\\">
|
||||||
<path d=\\"
|
<path d=\\"
|
||||||
M622.6666666666666, 411
|
M622.6666666666666, 411
|
||||||
V440
|
V440
|
||||||
H900
|
H900
|
||||||
V382
|
V382
|
||||||
H622.6666666666666
|
H622.6666666666666
|
||||||
Z\\"></path>
|
Z
|
||||||
|
\\"></path>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path>
|
<path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path>
|
||||||
</g>
|
</g>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { mount } from '@vue/test-utils';
|
import { mount } from '@vue/test-utils';
|
||||||
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
|
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
|
||||||
import { createSankey, removeOrphanNodes } from '~/pipelines/components/dag/utils';
|
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
|
||||||
|
import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils';
|
||||||
import { parsedData } from './mock_data';
|
import { parsedData } from './mock_data';
|
||||||
|
|
||||||
describe('The DAG graph', () => {
|
describe('The DAG graph', () => {
|
||||||
|
|
57
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
Normal file
57
spec/frontend/pipelines/components/dag/drawing_utils_spec.js
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
|
||||||
|
import { parseData } from '~/pipelines/components/dag/parsing_utils';
|
||||||
|
import { mockBaseData } from './mock_data';
|
||||||
|
|
||||||
|
describe('DAG visualization drawing utilities', () => {
|
||||||
|
const parsed = parseData(mockBaseData.stages);
|
||||||
|
|
||||||
|
const layoutSettings = {
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
nodeWidth: 10,
|
||||||
|
nodePadding: 20,
|
||||||
|
paddingForLabels: 100,
|
||||||
|
};
|
||||||
|
|
||||||
|
const sankeyLayout = createSankey(layoutSettings)(parsed);
|
||||||
|
|
||||||
|
describe('createSankey', () => {
|
||||||
|
it('returns a nodes data structure with expected d3-added properties', () => {
|
||||||
|
const exampleNode = sankeyLayout.nodes[0];
|
||||||
|
expect(exampleNode).toHaveProperty('sourceLinks');
|
||||||
|
expect(exampleNode).toHaveProperty('targetLinks');
|
||||||
|
expect(exampleNode).toHaveProperty('depth');
|
||||||
|
expect(exampleNode).toHaveProperty('layer');
|
||||||
|
expect(exampleNode).toHaveProperty('x0');
|
||||||
|
expect(exampleNode).toHaveProperty('x1');
|
||||||
|
expect(exampleNode).toHaveProperty('y0');
|
||||||
|
expect(exampleNode).toHaveProperty('y1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns a links data structure with expected d3-added properties', () => {
|
||||||
|
const exampleLink = sankeyLayout.links[0];
|
||||||
|
expect(exampleLink).toHaveProperty('source');
|
||||||
|
expect(exampleLink).toHaveProperty('target');
|
||||||
|
expect(exampleLink).toHaveProperty('width');
|
||||||
|
expect(exampleLink).toHaveProperty('y0');
|
||||||
|
expect(exampleLink).toHaveProperty('y1');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data structure integrity', () => {
|
||||||
|
const newObject = { name: 'bad-actor' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sankeyLayout.nodes.unshift(newObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sankey does not propagate changes back to the original', () => {
|
||||||
|
expect(sankeyLayout.nodes[0]).toBe(newObject);
|
||||||
|
expect(parsed.nodes[0]).not.toBe(newObject);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sankeyLayout.nodes.shift();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -3,11 +3,11 @@ import {
|
||||||
makeLinksFromNodes,
|
makeLinksFromNodes,
|
||||||
filterByAncestors,
|
filterByAncestors,
|
||||||
parseData,
|
parseData,
|
||||||
createSankey,
|
|
||||||
removeOrphanNodes,
|
removeOrphanNodes,
|
||||||
getMaxNodes,
|
getMaxNodes,
|
||||||
} from '~/pipelines/components/dag/utils';
|
} from '~/pipelines/components/dag/parsing_utils';
|
||||||
|
|
||||||
|
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
|
||||||
import { mockBaseData } from './mock_data';
|
import { mockBaseData } from './mock_data';
|
||||||
|
|
||||||
describe('DAG visualization parsing utilities', () => {
|
describe('DAG visualization parsing utilities', () => {
|
||||||
|
@ -105,44 +105,6 @@ describe('DAG visualization parsing utilities', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createSankey', () => {
|
|
||||||
it('returns a nodes data structure with expected d3-added properties', () => {
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('sourceLinks');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('targetLinks');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('depth');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('layer');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('x0');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('x1');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('y0');
|
|
||||||
expect(sankeyLayout.nodes[0]).toHaveProperty('y1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns a links data structure with expected d3-added properties', () => {
|
|
||||||
expect(sankeyLayout.links[0]).toHaveProperty('source');
|
|
||||||
expect(sankeyLayout.links[0]).toHaveProperty('target');
|
|
||||||
expect(sankeyLayout.links[0]).toHaveProperty('width');
|
|
||||||
expect(sankeyLayout.links[0]).toHaveProperty('y0');
|
|
||||||
expect(sankeyLayout.links[0]).toHaveProperty('y1');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('data structure integrity', () => {
|
|
||||||
const newObject = { name: 'bad-actor' };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
sankeyLayout.nodes.unshift(newObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('sankey does not propagate changes back to the original', () => {
|
|
||||||
expect(sankeyLayout.nodes[0]).toBe(newObject);
|
|
||||||
expect(parsed.nodes[0]).not.toBe(newObject);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
sankeyLayout.nodes.shift();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('removeOrphanNodes', () => {
|
describe('removeOrphanNodes', () => {
|
||||||
it('removes sankey nodes that have no needs and are not needed', () => {
|
it('removes sankey nodes that have no needs and are not needed', () => {
|
||||||
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
|
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
|
|
@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseLink'] do
|
||||||
|
|
||||||
it 'has the expected fields' do
|
it 'has the expected fields' do
|
||||||
expected_fields = %w[
|
expected_fields = %w[
|
||||||
id name url external
|
id name url external link_type
|
||||||
]
|
]
|
||||||
|
|
||||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||||
|
|
|
@ -29,4 +29,46 @@ describe Ci::BuildReportResult do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#tests_name' do
|
||||||
|
it 'returns the suite name' do
|
||||||
|
expect(build_report_result.tests_name).to eq("rspec")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_duration' do
|
||||||
|
it 'returns the suite duration' do
|
||||||
|
expect(build_report_result.tests_duration).to eq(0.42)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_success' do
|
||||||
|
it 'returns the success count' do
|
||||||
|
expect(build_report_result.tests_success).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_failed' do
|
||||||
|
it 'returns the failed count' do
|
||||||
|
expect(build_report_result.tests_failed).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_errored' do
|
||||||
|
it 'returns the errored count' do
|
||||||
|
expect(build_report_result.tests_errored).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_skipped' do
|
||||||
|
it 'returns the skipped count' do
|
||||||
|
expect(build_report_result.tests_skipped).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#tests_total' do
|
||||||
|
it 'returns the total count' do
|
||||||
|
expect(build_report_result.tests_total).to eq(2)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -875,6 +875,22 @@ describe Ci::Build do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#has_test_reports?' do
|
||||||
|
subject { build.has_test_reports? }
|
||||||
|
|
||||||
|
context 'when build has a test report' do
|
||||||
|
let(:build) { create(:ci_build, :test_reports) }
|
||||||
|
|
||||||
|
it { is_expected.to be_truthy }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when build does not have a test report' do
|
||||||
|
let(:build) { create(:ci_build) }
|
||||||
|
|
||||||
|
it { is_expected.to be_falsey }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#has_old_trace?' do
|
describe '#has_old_trace?' do
|
||||||
subject { build.has_old_trace? }
|
subject { build.has_old_trace? }
|
||||||
|
|
||||||
|
|
51
spec/services/ci/build_report_result_service_spec.rb
Normal file
51
spec/services/ci/build_report_result_service_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Ci::BuildReportResultService do
|
||||||
|
describe "#execute" do
|
||||||
|
subject(:build_report_result) { described_class.new.execute(build) }
|
||||||
|
|
||||||
|
context 'when build is finished' do
|
||||||
|
let(:build) { create(:ci_build, :success, :test_reports) }
|
||||||
|
|
||||||
|
it 'creates a build report result entry', :aggregate_failures do
|
||||||
|
expect(build_report_result.tests_name).to eq("test")
|
||||||
|
expect(build_report_result.tests_success).to eq(2)
|
||||||
|
expect(build_report_result.tests_failed).to eq(2)
|
||||||
|
expect(build_report_result.tests_errored).to eq(0)
|
||||||
|
expect(build_report_result.tests_skipped).to eq(0)
|
||||||
|
expect(build_report_result.tests_duration).to eq(0.010284)
|
||||||
|
expect(Ci::BuildReportResult.count).to eq(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when feature is disable' do
|
||||||
|
it 'does not persist the data' do
|
||||||
|
stub_feature_flags(build_report_summary: false)
|
||||||
|
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(Ci::BuildReportResult.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when data has already been persisted' do
|
||||||
|
it 'raises an error and do not persist the same data twice' do
|
||||||
|
expect { 2.times { described_class.new.execute(build) } }.to raise_error(ActiveRecord::RecordNotUnique)
|
||||||
|
|
||||||
|
expect(Ci::BuildReportResult.count).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when build is running and test report does not exist' do
|
||||||
|
let(:build) { create(:ci_build, :running) }
|
||||||
|
|
||||||
|
it 'does not persist data' do
|
||||||
|
subject
|
||||||
|
|
||||||
|
expect(Ci::BuildReportResult.count).to eq(0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
|
@ -15,7 +15,8 @@ describe 'projects/services/_form' do
|
||||||
|
|
||||||
allow(view).to receive_messages(current_user: user,
|
allow(view).to receive_messages(current_user: user,
|
||||||
can?: true,
|
can?: true,
|
||||||
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
|
current_application_settings: Gitlab::CurrentSettings.current_application_settings,
|
||||||
|
request: double(referrer: '/services'))
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'commit_events and merge_request_events' do
|
context 'commit_events and merge_request_events' do
|
||||||
|
@ -30,6 +31,7 @@ describe 'projects/services/_form' do
|
||||||
|
|
||||||
expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
|
expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
|
||||||
expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
|
expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
|
||||||
|
expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,7 +3,11 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe BuildFinishedWorker do
|
describe BuildFinishedWorker do
|
||||||
|
subject { described_class.new.perform(build.id) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
|
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline)) }
|
||||||
|
|
||||||
context 'when build exists' do
|
context 'when build exists' do
|
||||||
let!(:build) { create(:ci_build) }
|
let!(:build) { create(:ci_build) }
|
||||||
|
|
||||||
|
@ -18,8 +22,10 @@ describe BuildFinishedWorker do
|
||||||
expect(BuildHooksWorker).to receive(:perform_async)
|
expect(BuildHooksWorker).to receive(:perform_async)
|
||||||
expect(ArchiveTraceWorker).to receive(:perform_async)
|
expect(ArchiveTraceWorker).to receive(:perform_async)
|
||||||
expect(ExpirePipelineCacheWorker).to receive(:perform_async)
|
expect(ExpirePipelineCacheWorker).to receive(:perform_async)
|
||||||
|
expect(ChatNotificationWorker).not_to receive(:perform_async)
|
||||||
|
expect(Ci::BuildReportResultWorker).not_to receive(:perform)
|
||||||
|
|
||||||
described_class.new.perform(build.id)
|
subject
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -30,23 +36,26 @@ describe BuildFinishedWorker do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'schedules a ChatNotification job for a chat build' do
|
context 'when build has a chat' do
|
||||||
build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat))
|
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) }
|
||||||
|
|
||||||
expect(ChatNotificationWorker)
|
it 'schedules a ChatNotification job' do
|
||||||
.to receive(:perform_async)
|
expect(ChatNotificationWorker).to receive(:perform_async).with(build.id)
|
||||||
.with(build.id)
|
|
||||||
|
|
||||||
described_class.new.perform(build.id)
|
subject
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'does not schedule a ChatNotification job for a regular build' do
|
context 'when build has a test report' do
|
||||||
build = create(:ci_build, :success, pipeline: create(:ci_pipeline))
|
let(:build) { create(:ci_build, :test_reports) }
|
||||||
|
|
||||||
expect(ChatNotificationWorker)
|
it 'schedules a BuildReportResult job' do
|
||||||
.not_to receive(:perform_async)
|
expect_next_instance_of(Ci::BuildReportResultWorker) do |worker|
|
||||||
|
expect(worker).to receive(:perform).with(build.id)
|
||||||
|
end
|
||||||
|
|
||||||
described_class.new.perform(build.id)
|
subject
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
30
spec/workers/ci/build_report_result_worker_spec.rb
Normal file
30
spec/workers/ci/build_report_result_worker_spec.rb
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Ci::BuildReportResultWorker do
|
||||||
|
subject { described_class.new.perform(build_id) }
|
||||||
|
|
||||||
|
context 'when build exists' do
|
||||||
|
let(:build) { create(:ci_build) }
|
||||||
|
let(:build_id) { build.id }
|
||||||
|
|
||||||
|
it 'calls build report result service' do
|
||||||
|
expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service|
|
||||||
|
expect(build_report_result_service).to receive(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when build does not exist' do
|
||||||
|
let(:build_id) { -1 }
|
||||||
|
|
||||||
|
it 'does not call build report result service' do
|
||||||
|
expect(Ci::BuildReportResultService).not_to receive(:execute)
|
||||||
|
|
||||||
|
subject
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue