Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f3e7bc8060
commit
2164573e45
|
@ -51,6 +51,7 @@ const Api = {
|
|||
pipelinesPath: '/api/:version/projects/:id/pipelines/',
|
||||
environmentsPath: '/api/:version/projects/:id/environments',
|
||||
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
|
||||
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
|
||||
|
||||
group(groupId, callback) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
@ -540,6 +541,22 @@ const Api = {
|
|||
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) {
|
||||
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
|
||||
},
|
||||
|
|
|
@ -47,7 +47,7 @@ export default {
|
|||
'btn-transparent comment-indicator': 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"
|
||||
@mousedown="$emit('mousedown', $event)"
|
||||
@mouseup="$emit('mouseup', $event)"
|
||||
|
|
|
@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
|
|||
import { __ } from '~/locale';
|
||||
import DagGraph from './dag_graph.vue';
|
||||
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants';
|
||||
import { parseData } from './utils';
|
||||
import { parseData } from './parsing_utils';
|
||||
|
||||
export default {
|
||||
// eslint-disable-next-line @gitlab/require-i18n-strings
|
||||
|
|
|
@ -3,7 +3,8 @@ import * as d3 from 'd3';
|
|||
import { uniqueId } from 'lodash';
|
||||
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 {
|
||||
viewOptions: {
|
||||
|
@ -78,7 +79,7 @@ export default {
|
|||
return (
|
||||
link
|
||||
.append('path')
|
||||
.attr('d', this.createLinkPath)
|
||||
.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
|
||||
|
@ -89,7 +90,10 @@ export default {
|
|||
|
||||
appendLabelAsForeignObject(d, i, n) {
|
||||
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 = [
|
||||
'gl-display-flex',
|
||||
|
@ -128,44 +132,13 @@ export default {
|
|||
},
|
||||
|
||||
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
|
||||
.append('clipPath')
|
||||
.attr('id', d => {
|
||||
return this.createAndAssignId(d, 'clipId', 'dag-clip');
|
||||
})
|
||||
.append('path')
|
||||
.attr('d', clip);
|
||||
.attr('d', calculateClip);
|
||||
},
|
||||
|
||||
createGradient(link) {
|
||||
|
@ -189,44 +162,6 @@ export default {
|
|||
.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) {
|
||||
const link = this.generateLinks(svg, linksData);
|
||||
this.createGradient(link);
|
||||
|
@ -322,42 +257,6 @@ export default {
|
|||
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) {
|
||||
const baseLayout = createSankey()(parsed);
|
||||
const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
|
||||
|
|
|
@ -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';
|
||||
|
||||
/*
|
||||
|
@ -136,34 +135,6 @@ export const parseData = data => {
|
|||
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.
|
||||
*/
|
|
@ -98,25 +98,27 @@ export default {
|
|||
:has-issues="reports.length > 0"
|
||||
class="mr-widget-section grouped-security-reports mr-report"
|
||||
>
|
||||
<div slot="body" class="mr-widget-grouped-section report-block">
|
||||
<template v-for="(report, i) in reports">
|
||||
<summary-row
|
||||
:key="`summary-row-${i}`"
|
||||
:summary="reportText(report)"
|
||||
:status-icon="getReportIcon(report)"
|
||||
/>
|
||||
<issues-list
|
||||
v-if="shouldRenderIssuesList(report)"
|
||||
:key="`issues-list-${i}`"
|
||||
:unresolved-issues="unresolvedIssues(report)"
|
||||
:new-issues="newIssues(report)"
|
||||
:resolved-issues="resolvedIssues(report)"
|
||||
:component="$options.componentNames.TestIssueBody"
|
||||
class="report-block-group-list"
|
||||
/>
|
||||
</template>
|
||||
<template #body>
|
||||
<div class="mr-widget-grouped-section report-block">
|
||||
<template v-for="(report, i) in reports">
|
||||
<summary-row
|
||||
:key="`summary-row-${i}`"
|
||||
:summary="reportText(report)"
|
||||
:status-icon="getReportIcon(report)"
|
||||
/>
|
||||
<issues-list
|
||||
v-if="shouldRenderIssuesList(report)"
|
||||
:key="`issues-list-${i}`"
|
||||
:unresolved-issues="unresolvedIssues(report)"
|
||||
:new-issues="newIssues(report)"
|
||||
:resolved-issues="resolvedIssues(report)"
|
||||
:component="$options.componentNames.TestIssueBody"
|
||||
class="report-block-group-list"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<modal :title="modalTitle" :modal-data="modalData" />
|
||||
</div>
|
||||
<modal :title="modalTitle" :modal-data="modalData" />
|
||||
</div>
|
||||
</template>
|
||||
</report-section>
|
||||
</template>
|
||||
|
|
|
@ -70,7 +70,7 @@ $avatar-sizes: (
|
|||
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
|
||||
$identicon-orange, $gray-darker;
|
||||
|
||||
.avatar-circle {
|
||||
%avatar-circle {
|
||||
float: left;
|
||||
margin-right: $gl-padding;
|
||||
border-radius: $avatar-radius;
|
||||
|
@ -84,7 +84,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
|||
}
|
||||
|
||||
.avatar {
|
||||
@extend .avatar-circle;
|
||||
@extend %avatar-circle;
|
||||
transition-property: none;
|
||||
|
||||
width: 40px;
|
||||
|
@ -100,10 +100,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
|||
margin-left: 2px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.s16 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
&.s16,
|
||||
&.s24 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
@ -154,7 +151,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
|
|||
}
|
||||
|
||||
.avatar-container {
|
||||
@extend .avatar-circle;
|
||||
@extend %avatar-circle;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
|
||||
|
|
|
@ -9,8 +9,16 @@
|
|||
top: 35px;
|
||||
}
|
||||
|
||||
.inactive {
|
||||
opacity: 0.5;
|
||||
.design-pin {
|
||||
transition: opacity 0.5s ease;
|
||||
|
||||
&.inactive {
|
||||
@include gl-opacity-5;
|
||||
|
||||
&:hover {
|
||||
@include gl-opacity-10;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -93,7 +93,6 @@
|
|||
}
|
||||
|
||||
.dropdown-menu-toggle,
|
||||
.avatar-circle,
|
||||
.header-user-avatar {
|
||||
@include transition(border-color);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class Projects::ServicesController < Projects::ApplicationController
|
||||
include ServiceParams
|
||||
include InternalRedirect
|
||||
|
||||
# Authorize
|
||||
before_action :authorize_admin_project!
|
||||
|
@ -26,8 +27,8 @@ class Projects::ServicesController < Projects::ApplicationController
|
|||
respond_to do |format|
|
||||
format.html do
|
||||
if saved
|
||||
redirect_to project_settings_integrations_path(@project),
|
||||
notice: success_message
|
||||
target_url = safe_redirect_path(params[:redirect_to]).presence || project_settings_integrations_path(@project)
|
||||
redirect_to target_url, notice: success_message
|
||||
else
|
||||
render 'edit'
|
||||
end
|
||||
|
|
|
@ -12,7 +12,8 @@ module Types
|
|||
description: 'Name of the link'
|
||||
field :url, GraphQL::STRING_TYPE, null: true,
|
||||
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?,
|
||||
description: 'Indicates the link points to an external resource'
|
||||
end
|
||||
|
|
|
@ -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?
|
||||
end
|
||||
|
||||
def has_test_reports?
|
||||
job_artifacts.test_reports.exists?
|
||||
end
|
||||
|
||||
def has_old_trace?
|
||||
old_trace.present?
|
||||
end
|
||||
|
|
|
@ -11,5 +11,35 @@ module Ci
|
|||
|
||||
validates :build, :project, presence: true
|
||||
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
|
||||
|
|
|
@ -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:
|
||||
# ```
|
||||
# class DummyService
|
||||
# prepend Measurable
|
||||
#
|
||||
# def execute
|
||||
# # ...
|
||||
# end
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
"type": "object",
|
||||
"properties": {
|
||||
"coverage": { "type": "float" },
|
||||
"junit": {
|
||||
"tests": {
|
||||
"type": "object",
|
||||
"items": { "$ref": "./build_report_result_data_junit.json" }
|
||||
"items": { "$ref": "./build_report_result_data_tests.json" }
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"description": "Build report result data junit",
|
||||
"description": "Build report result data tests",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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|
|
||||
= render 'shared/service_settings', form: form, service: @service
|
||||
.footer-block.row-content-block
|
||||
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
|
||||
= service_save_button
|
||||
|
||||
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
|
||||
|
|
|
@ -803,6 +803,14 @@
|
|||
:weight: 1
|
||||
:idempotent:
|
||||
: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
|
||||
:feature_category: :continuous_integration
|
||||
:has_external_dependencies:
|
||||
|
|
|
@ -28,6 +28,7 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
|
|||
# We execute these in sync to reduce IO.
|
||||
BuildTraceSectionsWorker.new.perform(build.id)
|
||||
BuildCoverageWorker.new.perform(build.id)
|
||||
Ci::BuildReportResultWorker.new.perform(build.id)
|
||||
|
||||
# We execute these async as these are independent operations.
|
||||
BuildHooksWorker.perform_async(build.id)
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
||||
def ee?
|
||||
File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__))
|
||||
end
|
||||
|
||||
def dry_run?
|
||||
@options[:try] == true
|
||||
end
|
||||
|
@ -40,9 +36,7 @@ module Secpick
|
|||
end
|
||||
|
||||
def stable_branch
|
||||
"#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name|
|
||||
name << "-ee" if ee?
|
||||
end.freeze
|
||||
"#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze
|
||||
end
|
||||
|
||||
def git_commands
|
||||
|
@ -64,11 +58,7 @@ module Secpick
|
|||
end
|
||||
|
||||
def new_mr_url
|
||||
if ee?
|
||||
SECURITY_MR_URL
|
||||
else
|
||||
SECURITY_MR_URL.sub('/gitlab/', '/gitlab-foss/')
|
||||
end
|
||||
SECURITY_MR_URL
|
||||
end
|
||||
|
||||
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!
|
||||
|
||||
"""
|
||||
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
|
||||
"""
|
||||
linkType: ReleaseLinkType
|
||||
|
||||
"""
|
||||
Name of the link
|
||||
"""
|
||||
|
@ -9694,6 +9699,31 @@ type ReleaseLinkEdge {
|
|||
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 {
|
||||
"""
|
||||
Format of the source
|
||||
|
|
|
@ -28194,6 +28194,20 @@
|
|||
"isDeprecated": false,
|
||||
"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",
|
||||
"description": "Name of the link",
|
||||
|
@ -28342,6 +28356,41 @@
|
|||
"enumValues": 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",
|
||||
"name": "ReleaseSource",
|
||||
|
|
|
@ -1342,6 +1342,7 @@ Information about pagination in a connection.
|
|||
| --- | ---- | ---------- |
|
||||
| `external` | Boolean | Indicates the link points to an external resource |
|
||||
| `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 |
|
||||
| `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).
|
||||
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
|
||||
- [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
|
||||
<https://dashboards.gitlab.net>, this requires you to log in using your
|
||||
|
|
|
@ -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 :direct_asset_url
|
||||
expose :external?, as: :external
|
||||
expose :link_type
|
||||
|
||||
def direct_asset_url
|
||||
return object.url unless object.filepath
|
||||
|
|
|
@ -40,6 +40,7 @@ module API
|
|||
requires :name, type: String, desc: 'The name of the link'
|
||||
requires :url, type: String, desc: 'The URL of the link'
|
||||
optional :filepath, type: String, desc: 'The filepath of the link'
|
||||
optional :link_type, type: String, desc: 'The link type'
|
||||
end
|
||||
post 'links' do
|
||||
authorize! :create_release, release
|
||||
|
@ -75,6 +76,7 @@ module API
|
|||
optional :name, type: String, desc: 'The name of the link'
|
||||
optional :url, type: String, desc: 'The URL 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
|
||||
end
|
||||
put do
|
||||
|
|
|
@ -134,24 +134,50 @@ describe Projects::ServicesController do
|
|||
describe 'PUT #update' do
|
||||
describe 'as HTML' do
|
||||
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
|
||||
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
|
||||
|
||||
context 'when param `active` is set to true' do
|
||||
it 'activates the service and redirects to integrations paths' do
|
||||
expect(response).to redirect_to(project_settings_integrations_path(project))
|
||||
expect(flash[:notice]).to eq 'Jira activated.'
|
||||
let(:params) { project_params(service: service_params, redirect_to: redirect) }
|
||||
|
||||
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
|
||||
|
||||
context 'when param `active` is set to false' do
|
||||
let(:service_params) { { active: false } }
|
||||
let(:message) { 'Jira settings saved, but not activated.' }
|
||||
|
||||
it 'does not activate the service but saves the settings' do
|
||||
expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
|
||||
end
|
||||
it_behaves_like 'service update'
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ FactoryBot.define do
|
|||
project factory: :project
|
||||
data do
|
||||
{
|
||||
junit: {
|
||||
tests: {
|
||||
name: "rspec",
|
||||
duration: 0.42,
|
||||
failed: 0,
|
||||
|
@ -20,7 +20,7 @@ FactoryBot.define do
|
|||
trait :with_junit_success do
|
||||
data do
|
||||
{
|
||||
junit: {
|
||||
tests: {
|
||||
name: "rspec",
|
||||
duration: 0.42,
|
||||
failed: 0,
|
||||
|
|
|
@ -6,5 +6,6 @@ FactoryBot.define do
|
|||
sequence(:name) { |n| "release-18.#{n}.dmg" }
|
||||
sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" }
|
||||
sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" }
|
||||
link_type { 'other' }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
"filepath": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"direct_asset_url": { "type": "string" },
|
||||
"external": { "type": "boolean" }
|
||||
"external": { "type": "boolean" },
|
||||
"link_type": { "type": "string" }
|
||||
},
|
||||
"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
|
||||
|
||||
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
|
||||
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;"
|
||||
type="button"
|
||||
>
|
||||
|
@ -14,10 +14,10 @@ exports[`Design discussions component should match the snapshot of note when rep
|
|||
</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
|
||||
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;"
|
||||
type="button"
|
||||
>
|
||||
|
@ -27,10 +27,10 @@ exports[`Design discussions component should match the snapshot of note with ind
|
|||
</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
|
||||
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;"
|
||||
type="button"
|
||||
>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import DesignNotePin from '~/design_management/components/design_note_pin.vue';
|
||||
|
||||
describe('Design discussions component', () => {
|
||||
describe('Design note pin component', () => {
|
||||
let wrapper;
|
||||
|
||||
function createComponent(propsData = {}) {
|
||||
|
|
|
@ -10,12 +10,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip63\\">
|
||||
<path d=\\"
|
||||
M100, 129
|
||||
V158
|
||||
H377.3333333333333
|
||||
V100
|
||||
H100
|
||||
Z\\"></path>
|
||||
M100, 129
|
||||
V158
|
||||
H377.3333333333333
|
||||
V100
|
||||
H100
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -26,12 +27,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip64\\">
|
||||
<path d=\\"
|
||||
M361.3333333333333, 129.0000000000002
|
||||
V158.0000000000002
|
||||
H638.6666666666666
|
||||
V100
|
||||
H361.3333333333333
|
||||
Z\\"></path>
|
||||
M361.3333333333333, 129.0000000000002
|
||||
V158.0000000000002
|
||||
H638.6666666666666
|
||||
V100
|
||||
H361.3333333333333
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -42,12 +44,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip65\\">
|
||||
<path d=\\"
|
||||
M100, 187.0000000000002
|
||||
V241.00000000000003
|
||||
H638.6666666666666
|
||||
V158.0000000000002
|
||||
H100
|
||||
Z\\"></path>
|
||||
M100, 187.0000000000002
|
||||
V241.00000000000003
|
||||
H638.6666666666666
|
||||
V158.0000000000002
|
||||
H100
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -58,12 +61,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip66\\">
|
||||
<path d=\\"
|
||||
M100, 269.9999999999998
|
||||
V324
|
||||
H377.3333333333333
|
||||
V240.99999999999977
|
||||
H100
|
||||
Z\\"></path>
|
||||
M100, 269.9999999999998
|
||||
V324
|
||||
H377.3333333333333
|
||||
V240.99999999999977
|
||||
H100
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -74,12 +78,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip67\\">
|
||||
<path d=\\"
|
||||
M100, 352.99999999999994
|
||||
V407.00000000000006
|
||||
H377.3333333333333
|
||||
V323.99999999999994
|
||||
H100
|
||||
Z\\"></path>
|
||||
M100, 352.99999999999994
|
||||
V407.00000000000006
|
||||
H377.3333333333333
|
||||
V323.99999999999994
|
||||
H100
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -90,12 +95,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip68\\">
|
||||
<path d=\\"
|
||||
M361.3333333333333, 270.0000000000001
|
||||
V299.0000000000001
|
||||
H638.6666666666666
|
||||
V240.99999999999977
|
||||
H361.3333333333333
|
||||
Z\\"></path>
|
||||
M361.3333333333333, 270.0000000000001
|
||||
V299.0000000000001
|
||||
H638.6666666666666
|
||||
V240.99999999999977
|
||||
H361.3333333333333
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -106,12 +112,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip69\\">
|
||||
<path d=\\"
|
||||
M361.3333333333333, 328.0000000000001
|
||||
V381.99999999999994
|
||||
H638.6666666666666
|
||||
V299.0000000000001
|
||||
H361.3333333333333
|
||||
Z\\"></path>
|
||||
M361.3333333333333, 328.0000000000001
|
||||
V381.99999999999994
|
||||
H638.6666666666666
|
||||
V299.0000000000001
|
||||
H361.3333333333333
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -122,12 +129,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip70\\">
|
||||
<path d=\\"
|
||||
M361.3333333333333, 411
|
||||
V440
|
||||
H638.6666666666666
|
||||
V381.99999999999994
|
||||
H361.3333333333333
|
||||
Z\\"></path>
|
||||
M361.3333333333333, 411
|
||||
V440
|
||||
H638.6666666666666
|
||||
V381.99999999999994
|
||||
H361.3333333333333
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -138,12 +146,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip71\\">
|
||||
<path d=\\"
|
||||
M622.6666666666666, 270.1890725105691
|
||||
V299.1890725105691
|
||||
H900
|
||||
V241.0000000000001
|
||||
H622.6666666666666
|
||||
Z\\"></path>
|
||||
M622.6666666666666, 270.1890725105691
|
||||
V299.1890725105691
|
||||
H900
|
||||
V241.0000000000001
|
||||
H622.6666666666666
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
@ -154,12 +163,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
|
|||
</linearGradient>
|
||||
<clipPath id=\\"dag-clip72\\">
|
||||
<path d=\\"
|
||||
M622.6666666666666, 411
|
||||
V440
|
||||
H900
|
||||
V382
|
||||
H622.6666666666666
|
||||
Z\\"></path>
|
||||
M622.6666666666666, 411
|
||||
V440
|
||||
H900
|
||||
V382
|
||||
H622.6666666666666
|
||||
Z
|
||||
\\"></path>
|
||||
</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>
|
||||
</g>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
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';
|
||||
|
||||
describe('The DAG graph', () => {
|
||||
|
|
|
@ -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,
|
||||
filterByAncestors,
|
||||
parseData,
|
||||
createSankey,
|
||||
removeOrphanNodes,
|
||||
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';
|
||||
|
||||
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', () => {
|
||||
it('removes sankey nodes that have no needs and are not needed', () => {
|
||||
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
|
|
@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseLink'] do
|
|||
|
||||
it 'has the expected fields' do
|
||||
expected_fields = %w[
|
||||
id name url external
|
||||
id name url external link_type
|
||||
]
|
||||
|
||||
expect(described_class).to include_graphql_fields(*expected_fields)
|
||||
|
|
|
@ -29,4 +29,46 @@ describe Ci::BuildReportResult do
|
|||
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
|
||||
|
|
|
@ -875,6 +875,22 @@ describe Ci::Build do
|
|||
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
|
||||
subject { build.has_old_trace? }
|
||||
|
||||
|
|
|
@ -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,
|
||||
can?: true,
|
||||
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
|
||||
current_application_settings: Gitlab::CurrentSettings.current_application_settings,
|
||||
request: double(referrer: '/services'))
|
||||
end
|
||||
|
||||
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 merge request is created/updated/merged')
|
||||
expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe BuildFinishedWorker do
|
||||
subject { described_class.new.perform(build.id) }
|
||||
|
||||
describe '#perform' do
|
||||
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline)) }
|
||||
|
||||
context 'when build exists' do
|
||||
let!(:build) { create(:ci_build) }
|
||||
|
||||
|
@ -18,8 +22,10 @@ describe BuildFinishedWorker do
|
|||
expect(BuildHooksWorker).to receive(:perform_async)
|
||||
expect(ArchiveTraceWorker).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
|
||||
|
||||
|
@ -30,23 +36,26 @@ describe BuildFinishedWorker do
|
|||
end
|
||||
end
|
||||
|
||||
it 'schedules a ChatNotification job for a chat build' do
|
||||
build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat))
|
||||
context 'when build has a chat' do
|
||||
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) }
|
||||
|
||||
expect(ChatNotificationWorker)
|
||||
.to receive(:perform_async)
|
||||
.with(build.id)
|
||||
it 'schedules a ChatNotification job' do
|
||||
expect(ChatNotificationWorker).to receive(:perform_async).with(build.id)
|
||||
|
||||
described_class.new.perform(build.id)
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
||||
it 'does not schedule a ChatNotification job for a regular build' do
|
||||
build = create(:ci_build, :success, pipeline: create(:ci_pipeline))
|
||||
context 'when build has a test report' do
|
||||
let(:build) { create(:ci_build, :test_reports) }
|
||||
|
||||
expect(ChatNotificationWorker)
|
||||
.not_to receive(:perform_async)
|
||||
it 'schedules a BuildReportResult job' do
|
||||
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
|
||||
|
|
|
@ -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 New Issue