Commit f041c389 authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into breadcrumbs-improvements

parents a27c0013 6523ba1d
...@@ -3,11 +3,12 @@ ...@@ -3,11 +3,12 @@
import GraphLegend from './graph/legend.vue'; import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue'; import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue'; import GraphDeployment from './graph/deployment.vue';
import monitoringPaths from './monitoring_paths.vue';
import MonitoringMixin from '../mixins/monitoring_mixins'; import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import measurements from '../utils/measurements'; import measurements from '../utils/measurements';
import { formatRelevantDigits } from '../../lib/utils/number_utils';
import { timeScaleFormat } from '../utils/date_time_formatters'; import { timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints'; import bp from '../../breakpoints';
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
...@@ -36,32 +37,29 @@ ...@@ -36,32 +37,29 @@
data() { data() {
return { return {
baseGraphHeight: 450,
baseGraphWidth: 600,
graphHeight: 450, graphHeight: 450,
graphWidth: 600, graphWidth: 600,
graphHeightOffset: 120, graphHeightOffset: 120,
xScale: {},
yScale: {},
margin: {}, margin: {},
data: [],
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8', areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1', lineColorRgb: '#1f78d1',
yAxisLabel: '', yAxisLabel: '',
legendTitle: '', legendTitle: '',
reducedDeploymentData: [], reducedDeploymentData: [],
area: '',
line: '',
measurements: measurements.large, measurements: measurements.large,
currentData: { currentData: {
time: new Date(), time: new Date(),
value: 0, value: 0,
}, },
currentYCoordinate: 0, currentDataIndex: 0,
currentXCoordinate: 0, currentXCoordinate: 0,
currentFlagPosition: 0, currentFlagPosition: 0,
metricUsage: '',
showFlag: false, showFlag: false,
showDeployInfo: true, showDeployInfo: true,
timeSeries: [],
}; };
}, },
...@@ -69,16 +67,17 @@ ...@@ -69,16 +67,17 @@
GraphLegend, GraphLegend,
GraphFlag, GraphFlag,
GraphDeployment, GraphDeployment,
monitoringPaths,
}, },
computed: { computed: {
outterViewBox() { outterViewBox() {
return `0 0 ${this.graphWidth} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth} ${this.baseGraphHeight}`;
}, },
innerViewBox() { innerViewBox() {
if ((this.graphWidth - 150) > 0) { if ((this.baseGraphWidth - 150) > 0) {
return `0 0 ${this.graphWidth - 150} ${this.graphHeight}`; return `0 0 ${this.baseGraphWidth - 150} ${this.baseGraphHeight}`;
} }
return '0 0 0 0'; return '0 0 0 0';
}, },
...@@ -89,7 +88,7 @@ ...@@ -89,7 +88,7 @@
paddingBottomRootSvg() { paddingBottomRootSvg() {
return { return {
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`, paddingBottom: `${(Math.ceil(this.baseGraphHeight * 100) / this.baseGraphWidth) || 0}%`,
}; };
}, },
}, },
...@@ -104,17 +103,16 @@ ...@@ -104,17 +103,16 @@
this.margin = measurements.small.margin; this.margin = measurements.small.margin;
this.measurements = measurements.small; this.measurements = measurements.small;
} }
this.data = query.result[0].values;
this.unitOfDisplay = query.unit || ''; this.unitOfDisplay = query.unit || '';
this.yAxisLabel = this.graphData.y_label || 'Values'; this.yAxisLabel = this.graphData.y_label || 'Values';
this.legendTitle = query.label || 'Average'; this.legendTitle = query.label || 'Average';
this.graphWidth = this.$refs.baseSvg.clientWidth - this.graphWidth = this.$refs.baseSvg.clientWidth -
this.margin.left - this.margin.right; this.margin.left - this.margin.right;
this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom; this.graphHeight = this.graphHeight - this.margin.top - this.margin.bottom;
if (this.data !== undefined) { this.baseGraphHeight = this.graphHeight;
this.renderAxesPaths(); this.baseGraphWidth = this.graphWidth;
this.formatDeployments(); this.renderAxesPaths();
} this.formatDeployments();
}, },
handleMouseOverGraph(e) { handleMouseOverGraph(e) {
...@@ -123,16 +121,17 @@ ...@@ -123,16 +121,17 @@
point.y = e.clientY; point.y = e.clientY;
point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse()); point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
point.x = point.x += 7; point.x = point.x += 7;
const timeValueOverlay = this.xScale.invert(point.x); const firstTimeSeries = this.timeSeries[0];
const overlayIndex = bisectDate(this.data, timeValueOverlay, 1); const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
const d0 = this.data[overlayIndex - 1]; const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
const d1 = this.data[overlayIndex]; const d0 = firstTimeSeries.values[overlayIndex - 1];
const d1 = firstTimeSeries.values[overlayIndex];
if (d0 === undefined || d1 === undefined) return; if (d0 === undefined || d1 === undefined) return;
const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay; const evalTime = timeValueOverlay - d0[0] > d1[0] - timeValueOverlay;
this.currentData = evalTime ? d1 : d0; this.currentData = evalTime ? d1 : d0;
this.currentXCoordinate = Math.floor(this.xScale(this.currentData.time)); this.currentDataIndex = evalTime ? overlayIndex : (overlayIndex - 1);
this.currentXCoordinate = Math.floor(firstTimeSeries.timeSeriesScaleX(this.currentData.time));
const currentDeployXPos = this.mouseOverDeployInfo(point.x); const currentDeployXPos = this.mouseOverDeployInfo(point.x);
this.currentYCoordinate = this.yScale(this.currentData.value);
if (this.currentXCoordinate > (this.graphWidth - 200)) { if (this.currentXCoordinate > (this.graphWidth - 200)) {
this.currentFlagPosition = this.currentXCoordinate - 103; this.currentFlagPosition = this.currentXCoordinate - 103;
...@@ -145,17 +144,25 @@ ...@@ -145,17 +144,25 @@
} else { } else {
this.showFlag = true; this.showFlag = true;
} }
this.metricUsage = `${formatRelevantDigits(this.currentData.value)} ${this.unitOfDisplay}`;
}, },
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
if (this.timeSeries.length > 3) {
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
const axisXScale = d3.time.scale() const axisXScale = d3.time.scale()
.range([0, this.graphWidth]); .range([0, this.graphWidth]);
this.yScale = d3.scale.linear() const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]); .range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.data, d => d.time));
this.yScale.domain([0, d3.max(this.data.map(d => d.value))]); axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
...@@ -164,7 +171,7 @@ ...@@ -164,7 +171,7 @@
.orient('bottom'); .orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.svg.axis()
.scale(this.yScale) .scale(axisYScale)
.ticks(measurements.yTicks) .ticks(measurements.yTicks)
.orient('left'); .orient('left');
...@@ -180,25 +187,6 @@ ...@@ -180,25 +187,6 @@
.attr('class', 'axis-tick'); .attr('class', 'axis-tick');
} // Avoid adding the class to the first tick, to prevent coloring } // Avoid adding the class to the first tick, to prevent coloring
}); // This will select all of the ticks once they're rendered }); // This will select all of the ticks once they're rendered
this.xScale = d3.time.scale()
.range([0, this.graphWidth - 70]);
this.xScale.domain(d3.extent(this.data, d => d.time));
const areaFunction = d3.svg.area()
.x(d => this.xScale(d.time))
.y0(this.graphHeight - this.graphHeightOffset)
.y1(d => this.yScale(d.value))
.interpolate('linear');
const lineFunction = d3.svg.line()
.x(d => this.xScale(d.time))
.y(d => this.yScale(d.value));
this.line = lineFunction(this.data);
this.area = areaFunction(this.data);
}, },
}, },
...@@ -245,30 +233,25 @@ ...@@ -245,30 +233,25 @@
:graph-height="graphHeight" :graph-height="graphHeight"
:margin="margin" :margin="margin"
:measurements="measurements" :measurements="measurements"
:area-color-rgb="areaColorRgb"
:legend-title="legendTitle" :legend-title="legendTitle"
:y-axis-label="yAxisLabel" :y-axis-label="yAxisLabel"
:metric-usage="metricUsage" :time-series="timeSeries"
:unit-of-display="unitOfDisplay"
:current-data-index="currentDataIndex"
/> />
<svg <svg
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData"> ref="graphData">
<path <monitoring-paths
class="metric-area" v-for="(path, index) in timeSeries"
:d="area" :key="index"
:fill="areaColorRgb" :generated-line-path="path.linePath"
transform="translate(-5, 20)"> :generated-area-path="path.areaPath"
</path> :line-color="path.lineColor"
<path :area-color="path.areaColor"
class="metric-line" />
:d="line" <monitoring-deployment
:stroke="lineColorRgb"
fill="none"
stroke-width="2"
transform="translate(-5, 20)">
</path>
<graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-height="graphHeight" :graph-height="graphHeight"
...@@ -277,7 +260,6 @@ ...@@ -277,7 +260,6 @@
<graph-flag <graph-flag
v-if="showFlag" v-if="showFlag"
:current-x-coordinate="currentXCoordinate" :current-x-coordinate="currentXCoordinate"
:current-y-coordinate="currentYCoordinate"
:current-data="currentData" :current-data="currentData"
:current-flag-position="currentFlagPosition" :current-flag-position="currentFlagPosition"
:graph-height="graphHeight" :graph-height="graphHeight"
......
...@@ -7,10 +7,6 @@ ...@@ -7,10 +7,6 @@
type: Number, type: Number,
required: true, required: true,
}, },
currentYCoordinate: {
type: Number,
required: true,
},
currentFlagPosition: { currentFlagPosition: {
type: Number, type: Number,
required: true, required: true,
...@@ -60,16 +56,7 @@ ...@@ -60,16 +56,7 @@
:y2="calculatedHeight" :y2="calculatedHeight"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
</line> </line>
<circle <svg
class="circle-metric"
:fill="circleColorRgb"
stroke="#000"
:cx="currentXCoordinate"
:cy="currentYCoordinate"
r="5"
transform="translate(-5, 20)">
</circle>
<svg
class="rect-text-metric" class="rect-text-metric"
:x="currentFlagPosition" :x="currentFlagPosition"
y="0"> y="0">
......
<script> <script>
import { formatRelevantDigits } from '../../../lib/utils/number_utils';
export default { export default {
props: { props: {
graphWidth: { graphWidth: {
...@@ -17,10 +19,6 @@ ...@@ -17,10 +19,6 @@
type: Object, type: Object,
required: true, required: true,
}, },
areaColorRgb: {
type: String,
required: true,
},
legendTitle: { legendTitle: {
type: String, type: String,
required: true, required: true,
...@@ -29,15 +27,25 @@ ...@@ -29,15 +27,25 @@
type: String, type: String,
required: true, required: true,
}, },
metricUsage: { timeSeries: {
type: Array,
required: true,
},
unitOfDisplay: {
type: String, type: String,
required: true, required: true,
}, },
currentDataIndex: {
type: Number,
required: true,
},
}, },
data() { data() {
return { return {
yLabelWidth: 0, yLabelWidth: 0,
yLabelHeight: 0, yLabelHeight: 0,
seriesXPosition: 0,
metricUsageXPosition: 0,
}; };
}, },
computed: { computed: {
...@@ -63,10 +71,28 @@ ...@@ -63,10 +71,28 @@
yPosition() { yPosition() {
return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0; return ((this.graphHeight - this.margin.top) + this.measurements.axisLabelLineOffset) || 0;
}, },
},
methods: {
translateLegendGroup(index) {
return `translate(0, ${12 * (index)})`;
},
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
},
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
const bbox = this.$refs.ylabel.getBBox(); const bbox = this.$refs.ylabel.getBBox();
this.metricUsageXPosition = 0;
this.seriesXPosition = 0;
if (this.$refs.legendTitleSvg != null) {
this.seriesXPosition = this.$refs.legendTitleSvg[0].getBBox().width;
}
if (this.$refs.seriesTitleSvg != null) {
this.metricUsageXPosition = this.$refs.seriesTitleSvg[0].getBBox().width;
}
this.yLabelWidth = bbox.width + 10; // Added some padding this.yLabelWidth = bbox.width + 10; // Added some padding
this.yLabelHeight = bbox.height + 5; this.yLabelHeight = bbox.height + 5;
}); });
...@@ -121,24 +147,33 @@ ...@@ -121,24 +147,33 @@
dy=".35em"> dy=".35em">
Time Time
</text> </text>
<rect <g class="legend-group"
:fill="areaColorRgb" v-for="(series, index) in timeSeries"
:width="measurements.legends.width" :key="index"
:height="measurements.legends.height" :transform="translateLegendGroup(index)">
x="20" <rect
:y="graphHeight - measurements.legendOffset"> :fill="series.areaColor"
</rect> :width="measurements.legends.width"
<text :height="measurements.legends.height"
class="text-metric-title" x="20"
x="50" :y="graphHeight - measurements.legendOffset">
:y="graphHeight - 25"> </rect>
{{legendTitle}} <text
</text> v-if="timeSeries.length > 1"
<text class="legend-metric-title"
class="text-metric-usage" ref="legendTitleSvg"
x="50" x="38"
:y="graphHeight - 10"> :y="graphHeight - 30">
{{metricUsage}} {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
</text> </text>
<text
v-else
class="legend-metric-title"
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
{{legendTitle}} {{formatMetricUsage(series)}}
</text>
</g>
</g> </g>
</template> </template>
<script>
export default {
props: {
generatedLinePath: {
type: String,
required: true,
},
generatedAreaPath: {
type: String,
required: true,
},
lineColor: {
type: String,
required: true,
},
areaColor: {
type: String,
required: true,
},
},
};
</script>
<template>
<g>
<path
class="metric-area"
:d="generatedAreaPath"
:fill="areaColor"
transform="translate(-5, 20)">
</path>
<path
class="metric-line"
:d="generatedLinePath"
:stroke="lineColor"
fill="none"
stroke-width="1"
transform="translate(-5, 20)">
</path>
</g>
</template>
...@@ -21,9 +21,9 @@ const mixins = { ...@@ -21,9 +21,9 @@ const mixins = {
formatDeployments() { formatDeployments() {
this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => { this.reducedDeploymentData = this.deploymentData.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at); const time = new Date(deployment.created_at);
const xPos = Math.floor(this.xScale(time)); const xPos = Math.floor(this.timeSeries[0].timeSeriesScaleX(time));
time.setSeconds(this.data[0].time.getSeconds()); time.setSeconds(this.timeSeries[0].values[0].time.getSeconds());
if (xPos >= 0) { if (xPos >= 0) {
deploymentDataArray.push({ deploymentDataArray.push({
......
import _ from 'underscore'; import _ from 'underscore';
class MonitoringStore { function sortMetrics(metrics) {
return _.chain(metrics).sortBy('weight').sortBy('title').value();
}
function normalizeMetrics(metrics) {
return metrics.map(metric => ({
...metric,
queries: metric.queries.map(query => ({
...query,
result: query.result.map(result => ({
...result,
values: result.values.map(([timestamp, value]) => ({
time: new Date(timestamp * 1000),
value,
})),
})),
})),
}));
}
function collate(array, rows = 2) {
const collatedArray = [];
let row = [];
array.forEach((value, index) => {
row.push(value);
if ((index + 1) % rows === 0) {
collatedArray.push(row);
row = [];
}
});
if (row.length > 0) {
collatedArray.push(row);
}
return collatedArray;
}
export default class MonitoringStore {
constructor() { constructor() {
this.groups = []; this.groups = [];
this.deploymentData = []; this.deploymentData = [];
} }
// eslint-disable-next-line class-methods-use-this
createArrayRows(metrics = []) {
const currentMetrics = metrics;
const availableMetrics = [];
let metricsRow = [];
let index = 1;
Object.keys(currentMetrics).forEach((key) => {
const metricValues = currentMetrics[key].queries[0].result[0].values;
if (metricValues != null) {
const literalMetrics = metricValues.map(metric => ({
time: new Date(metric[0] * 1000),
value: metric[1],
}));
currentMetrics[key].queries[0].result[0].values = literalMetrics;
metricsRow.push(currentMetrics[key]);
if (index % 2 === 0) {
availableMetrics.push(metricsRow);
metricsRow = [];
}
index = index += 1;
}
});
if (metricsRow.length > 0) {
availableMetrics.push(metricsRow);
}
return availableMetrics;
}
storeMetrics(groups = []) { storeMetrics(groups = []) {
this.groups = groups.map((group) => { this.groups = groups.map(group => ({
const currentGroup = group; ...group,
currentGroup.metrics = _.chain(group.metrics).sortBy('weight').sortBy('title').value(); metrics: collate(normalizeMetrics(sortMetrics(group.metrics))),
currentGroup.metrics = this.createArrayRows(currentGroup.metrics); }));
return currentGroup;
});
} }
storeDeploymentData(deploymentData = []) { storeDeploymentData(deploymentData = []) {
...@@ -57,5 +63,3 @@ class MonitoringStore { ...@@ -57,5 +63,3 @@ class MonitoringStore {
return metricsCount; return metricsCount;
} }
} }
export default MonitoringStore;
...@@ -7,15 +7,15 @@ export default { ...@@ -7,15 +7,15 @@ export default {
left: 40, left: 40,
}, },
legends: { legends: {
width: 15, width: 10,
height: 25, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 50, height: 50,
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
legendOffset: 35, legendOffset: 33,
}, },
large: { // This covers both md and lg screen sizes large: { // This covers both md and lg screen sizes
margin: { margin: {
...@@ -25,15 +25,15 @@ export default { ...@@ -25,15 +25,15 @@ export default {
left: 80, left: 80,
}, },
legends: { legends: {
width: 20, width: 15,
height: 30, height: 3,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 150, height: 150,
}, },
axisLabelLineOffset: 20, axisLabelLineOffset: 20,
legendOffset: 38, legendOffset: 36,
}, },
xTicks: 8, xTicks: 8,
yTicks: 3, yTicks: 3,
......
import d3 from 'd3';
import _ from 'underscore';
export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
const maxValues = seriesData.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
index,
};
});
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
let timeSeriesNumber = 1;
let lineColor = '#1f78d1';
let areaColor = '#8fbce8';
return seriesData.map((timeSeries) => {
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line()
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value))
.interpolate('linear');
switch (timeSeriesNumber) {
case 1:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
case 2:
lineColor = '#fc9403';
areaColor = '#feca81';
break;
case 3:
lineColor = '#db3b21';
areaColor = '#ed9d90';
break;
case 4:
lineColor = '#1aaa55';
areaColor = '#8dd5aa';
break;
case 5:
lineColor = '#6666c4';
areaColor = '#d1d1f0';
break;
default:
lineColor = '#1f78d1';
areaColor = '#8fbce8';
break;
}
if (timeSeriesNumber <= 5) {
timeSeriesNumber = timeSeriesNumber += 1;
} else {
timeSeriesNumber = 1;
}
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
lineColor,
areaColor,
};
});
}
...@@ -272,6 +272,7 @@ body[data-page="projects:new"] #select2-drop, ...@@ -272,6 +272,7 @@ body[data-page="projects:new"] #select2-drop,
body[data-page="projects:merge_requests:edit"] #select2-drop, body[data-page="projects:merge_requests:edit"] #select2-drop,
body[data-page="projects:blob:new"] #select2-drop, body[data-page="projects:blob:new"] #select2-drop,
body[data-page="profiles:show"] #select2-drop, body[data-page="profiles:show"] #select2-drop,
body[data-page="projects:issues:show"] #select2-drop,
body[data-page="projects:blob:edit"] #select2-drop { body[data-page="projects:blob:edit"] #select2-drop {
&.select2-drop { &.select2-drop {
border: 1px solid $dropdown-border-color; border: 1px solid $dropdown-border-color;
......
...@@ -169,7 +169,7 @@ ...@@ -169,7 +169,7 @@
} }
.metric-area { .metric-area {
opacity: 0.8; opacity: 0.25;
} }
.prometheus-graph-overlay { .prometheus-graph-overlay {
...@@ -251,8 +251,14 @@ ...@@ -251,8 +251,14 @@
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
} }
.label-axis-text, .label-axis-text {
.text-metric-usage { fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage,
.legend-metric-title {
fill: $black; fill: $black;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
font-size: 12px; font-size: 12px;
......
...@@ -6,6 +6,10 @@ module Ci ...@@ -6,6 +6,10 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id belongs_to :pipeline, foreign_key: :commit_id
has_many :builds has_many :builds
# We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
# Ci::TriggerRequest doesn't save variables anymore.
validates :variables, absence: true
serialize :variables # rubocop:disable Cop/ActiveRecordSerialize serialize :variables # rubocop:disable Cop/ActiveRecordSerialize
def user_variables def user_variables
......
...@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base ...@@ -38,6 +38,14 @@ class CommitStatus < ActiveRecord::Base
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :after_stage, -> (index) { where('stage_idx > ?', index) }
enum failure_reason: {
unknown_failure: nil,
script_failure: 1,
api_failure: 2,
stuck_or_timeout_failure: 3,
runner_system_failure: 4
}
state_machine :status do state_machine :status do
event :process do event :process do
transition [:skipped, :manual] => :created transition [:skipped, :manual] => :created
...@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -79,6 +87,11 @@ class CommitStatus < ActiveRecord::Base
commit_status.finished_at = Time.now commit_status.finished_at = Time.now
end end
before_transition any => :failed do |commit_status, transition|
failure_reason = transition.args.first
commit_status.failure_reason = failure_reason
end
after_transition do |commit_status, transition| after_transition do |commit_status, transition|
next if transition.loopback? next if transition.loopback?
......
...@@ -17,5 +17,16 @@ module Ci ...@@ -17,5 +17,16 @@ module Ci
"Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}" "Job is redundant and is auto-canceled by Pipeline ##{auto_canceled_by_id}"
end end
end end
def trigger_variables
return [] unless trigger_request
@trigger_variables ||=
if pipeline.variables.any?
pipeline.variables.map(&:to_runner_variable)
else
trigger_request.user_variables
end
end
end end
end end
# This class is deprecated because we're closing Ci::TriggerRequest.
# New class is PipelineTriggerService (app/services/ci/pipeline_trigger_service.rb)
# which is integrated with Ci::PipelineVariable instaed of Ci::TriggerRequest.
# We remove this class after we removed v1 and v3 API. This class is still being
# referred by such legacy code.
module Ci
module CreateTriggerRequestService
Result = Struct.new(:trigger_request, :pipeline)
def self.execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables)
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref)
.execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
Result.new(trigger_request, pipeline)
end
end
end
...@@ -53,7 +53,7 @@ module Projects ...@@ -53,7 +53,7 @@ module Projects
log_error("Projects::UpdatePagesService: #{message}") log_error("Projects::UpdatePagesService: #{message}")
@status.allow_failure = !latest? @status.allow_failure = !latest?
@status.description = message @status.description = message
@status.drop @status.drop(:script_failure)
super super
end end
......
...@@ -46,14 +46,14 @@ ...@@ -46,14 +46,14 @@
%span.build-light-text Token: %span.build-light-text Token:
#{@build.trigger_request.trigger.short_token} #{@build.trigger_request.trigger.short_token}
- if @build.trigger_request.variables - if @build.trigger_variables.any?
%p %p
%button.btn.group.btn-group-justified.reveal-variables Reveal Variables %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide %dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_request.variables.each do |key, value| - @build.trigger_variables.each do |trigger_variable|
%dt.js-build-variable.trigger-build-variable= key %dt.js-build-variable.trigger-build-variable= trigger_variable[:key]
%dd.js-build-value.trigger-build-value= value %dd.js-build-value.trigger-build-value= trigger_variable[:value]
%div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") } %div{ class: (@build.pipeline.stages_count > 1 ? "block" : "block-last") }
%p %p
......
...@@ -53,7 +53,7 @@ class StuckCiJobsWorker ...@@ -53,7 +53,7 @@ class StuckCiJobsWorker
def drop_build(type, build, status, timeout) def drop_build(type, build, status, timeout)
Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})" Rails.logger.info "#{self.class}: Dropping #{type} build #{build.id} for runner #{build.runner_id} (status: #{status}, timeout: #{timeout})"
Gitlab::OptimisticLocking.retry_lock(build, 3) do |b| Gitlab::OptimisticLocking.retry_lock(build, 3) do |b|
b.drop b.drop(:stuck_or_timeout_failure)
end end
end end
end end
---
title: Added support the multiple time series for prometheus monitoring
merge_request: !36893
author:
type: changed
---
title: 'Extend API: Pipeline Schedule Variable'
merge_request: 13653
author:
type: added
---
title: Implement `failure_reason` on `ci_builds`
merge_request: 13937
author:
type: added
class AddFailureReasonToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_builds, :failure_reason, :integer
end
end
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20170824162758) do ActiveRecord::Schema.define(version: 20170830125940) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" enable_extension "plpgsql"
...@@ -247,6 +247,7 @@ ActiveRecord::Schema.define(version: 20170824162758) do ...@@ -247,6 +247,7 @@ ActiveRecord::Schema.define(version: 20170824162758) do
t.boolean "retried" t.boolean "retried"
t.integer "stage_id" t.integer "stage_id"
t.boolean "protected" t.boolean "protected"
t.integer "failure_reason"
end end
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
......
...@@ -84,7 +84,13 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/ ...@@ -84,7 +84,13 @@ curl --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/
"state": "active", "state": "active",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
"web_url": "https://gitlab.example.com/root" "web_url": "https://gitlab.example.com/root"
} },
"variables": [
{
"key": "TEST_VARIABLE_1",
"value": "TEST_1"
}
]
} }
``` ```
...@@ -271,3 +277,86 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi ...@@ -271,3 +277,86 @@ curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gi
} }
} }
``` ```
## Pipeline schedule variable
> [Introduced][ce-34518] in GitLab 10.0.
## Create a new pipeline schedule variable
Create a new variable of a pipeline schedule.
```
POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables
```
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
| `value` | string | yes | The `value` of a variable |
```sh
curl --request POST --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "key=NEW_VARIABLE" --form "value=new value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables"
```
```json
{
"key": "NEW_VARIABLE",
"value": "new value"
}
```
## Edit a pipeline schedule variable
Updates the variable of a pipeline schedule.
```
PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
```
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `key` | string | yes | The `key` of a variable |
| `value` | string | yes | The `value` of a variable |
```sh
curl --request PUT --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" --form "value=updated value" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables/NEW_VARIABLE"
```
```json
{
"key": "NEW_VARIABLE",
"value": "updated value"
}
```
## Delete a pipeline schedule variable
Delete the variable of a pipeline schedule.
```
DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key
```
| Attribute | Type | required | Description |
|------------------------|----------------|----------|--------------------------|
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
| `pipeline_schedule_id` | integer | yes | The pipeline schedule id |
| `key` | string | yes | The `key` of a variable |
```sh
curl --request DELETE --header "PRIVATE-TOKEN: k5ESFgWY2Qf5xEvDcFxZ" "https://gitlab.example.com/api/v4/projects/29/pipeline_schedules/13/variables/NEW_VARIABLE"
```
```json
{
"key": "NEW_VARIABLE",
"value": "updated value"
}
```
[ce-34518]: https://gitlab.com/gitlab-org/gitlab-ce/issues/34518
\ No newline at end of file
...@@ -130,7 +130,7 @@ There are also two edge cases worth mentioning: ...@@ -130,7 +130,7 @@ There are also two edge cases worth mentioning:
### types ### types
> Deprecated, and will be removed in 10.0. Use [stages](#stages) instead. > Deprecated, and could be removed in one of the future releases. Use [stages](#stages) instead.
Alias for [stages](#stages). Alias for [stages](#stages).
......
...@@ -103,7 +103,7 @@ module API ...@@ -103,7 +103,7 @@ module API
when 'success' when 'success'
status.success! status.success!
when 'failed' when 'failed'
status.drop! status.drop!(:api_failure)
when 'canceled' when 'canceled'
status.cancel! status.cancel!
else else
......
...@@ -819,7 +819,7 @@ module API ...@@ -819,7 +819,7 @@ module API
class Variable < Grape::Entity class Variable < Grape::Entity
expose :key, :value expose :key, :value
expose :protected?, as: :protected expose :protected?, as: :protected, if: -> (entity, _) { entity.respond_to?(:protected?) }
end end
class Pipeline < PipelineBasic class Pipeline < PipelineBasic
...@@ -840,6 +840,7 @@ module API ...@@ -840,6 +840,7 @@ module API
class PipelineScheduleDetails < PipelineSchedule class PipelineScheduleDetails < PipelineSchedule
expose :last_pipeline, using: Entities::PipelineBasic expose :last_pipeline, using: Entities::PipelineBasic
expose :variables, using: Entities::Variable
end end
class EnvironmentBasic < Grape::Entity class EnvironmentBasic < Grape::Entity
......
...@@ -31,10 +31,6 @@ module API ...@@ -31,10 +31,6 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
get ':id/pipeline_schedules/:pipeline_schedule_id' do get ':id/pipeline_schedules/:pipeline_schedule_id' do
authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule
present pipeline_schedule, with: Entities::PipelineScheduleDetails present pipeline_schedule, with: Entities::PipelineScheduleDetails
end end
...@@ -74,9 +70,6 @@ module API ...@@ -74,9 +70,6 @@ module API
optional :active, type: Boolean, desc: 'The activation of pipeline schedule' optional :active, type: Boolean, desc: 'The activation of pipeline schedule'
end end
put ':id/pipeline_schedules/:pipeline_schedule_id' do put ':id/pipeline_schedules/:pipeline_schedule_id' do
authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.update(declared_params(include_missing: false)) if pipeline_schedule.update(declared_params(include_missing: false))
...@@ -93,9 +86,6 @@ module API ...@@ -93,9 +86,6 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do post ':id/pipeline_schedules/:pipeline_schedule_id/take_ownership' do
authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :update_pipeline_schedule, pipeline_schedule authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule.own!(current_user) if pipeline_schedule.own!(current_user)
...@@ -112,21 +102,84 @@ module API ...@@ -112,21 +102,84 @@ module API
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id' requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
end end
delete ':id/pipeline_schedules/:pipeline_schedule_id' do delete ':id/pipeline_schedules/:pipeline_schedule_id' do
authorize! :read_pipeline_schedule, user_project
not_found!('PipelineSchedule') unless pipeline_schedule
authorize! :admin_pipeline_schedule, pipeline_schedule authorize! :admin_pipeline_schedule, pipeline_schedule
destroy_conditionally!(pipeline_schedule) destroy_conditionally!(pipeline_schedule)
end end
desc 'Create a new pipeline schedule variable' do
success Entities::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
end
post ':id/pipeline_schedules/:pipeline_schedule_id/variables' do
authorize! :update_pipeline_schedule, pipeline_schedule
variable_params = declared_params(include_missing: false)
variable = pipeline_schedule.variables.create(variable_params)
if variable.persisted?
present variable, with: Entities::Variable
else
render_validation_error!(variable)
end
end
desc 'Edit a pipeline schedule variable' do
success Entities::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
requires :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
end
put ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
authorize! :update_pipeline_schedule, pipeline_schedule
if pipeline_schedule_variable.update(declared_params(include_missing: false))
present pipeline_schedule_variable, with: Entities::Variable
else
render_validation_error!(pipeline_schedule_variable)
end
end
desc 'Delete a pipeline schedule variable' do
success Entities::Variable
end
params do
requires :pipeline_schedule_id, type: Integer, desc: 'The pipeline schedule id'
requires :key, type: String, desc: 'The key of the variable'
end
delete ':id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
authorize! :admin_pipeline_schedule, pipeline_schedule
status :accepted
present pipeline_schedule_variable.destroy, with: Entities::Variable
end
end end
helpers do helpers do
def pipeline_schedule def pipeline_schedule
@pipeline_schedule ||= @pipeline_schedule ||=
user_project.pipeline_schedules user_project
.preload(:owner, :last_pipeline) .pipeline_schedules
.find_by(id: params.delete(:pipeline_schedule_id)) .preload(:owner, :last_pipeline)
.find_by(id: params.delete(:pipeline_schedule_id)).tap do |pipeline_schedule|
unless can?(current_user, :read_pipeline_schedule, pipeline_schedule)
not_found!('Pipeline Schedule')
end
end
end
def pipeline_schedule_variable
@pipeline_schedule_variable ||=
pipeline_schedule.variables.find_by(key: params[:key]).tap do |pipeline_schedule_variable|
unless pipeline_schedule_variable
not_found!('Pipeline Schedule Variable')
end
end
end end
end end
end end
......
...@@ -114,6 +114,8 @@ module API ...@@ -114,6 +114,8 @@ module API
requires :id, type: Integer, desc: %q(Job's ID) requires :id, type: Integer, desc: %q(Job's ID)
optional :trace, type: String, desc: %q(Job's full trace) optional :trace, type: String, desc: %q(Job's full trace)
optional :state, type: String, desc: %q(Job's status: success, failed) optional :state, type: String, desc: %q(Job's status: success, failed)
optional :failure_reason, type: String, values: CommitStatus.failure_reasons.keys,
desc: %q(Job's failure_reason)
end end
put '/:id' do put '/:id' do
job = authenticate_job! job = authenticate_job!
...@@ -127,7 +129,7 @@ module API ...@@ -127,7 +129,7 @@ module API
when 'success' when 'success'
job.success job.success
when 'failed' when 'failed'
job.drop job.drop(params[:failure_reason] || :unknown_failure)
end end
end end
......
...@@ -16,25 +16,31 @@ module API ...@@ -16,25 +16,31 @@ module API
optional :variables, type: Hash, desc: 'The list of variables to be injected into build' optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
end end
post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
project = find_project(params[:id])
trigger = Ci::Trigger.find_by_token(params[:token].to_s)
not_found! unless project && trigger
unauthorized! unless trigger.project == project
# validate variables # validate variables
variables = params[:variables].to_h params[:variables] = params[:variables].to_h
unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) } unless params[:variables].all? { |key, value| key.is_a?(String) && value.is_a?(String) }
render_api_error!('variables needs to be a map of key-valued strings', 400) render_api_error!('variables needs to be a map of key-valued strings', 400)
end end
# create request and trigger builds project = find_project(params[:id])
result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables) not_found! unless project
pipeline = result.pipeline
result = Ci::PipelineTriggerService.new(project, nil, params).execute
not_found! unless result
if pipeline.persisted? if result[:http_status]
present result.trigger_request, with: ::API::V3::Entities::TriggerRequest render_api_error!(result[:message], result[:http_status])
else else
render_validation_error!(pipeline) pipeline = result[:pipeline]
# We switched to Ci::PipelineVariable from Ci::TriggerRequest.variables.
# Ci::TriggerRequest doesn't save variables anymore.
# Here is copying Ci::PipelineVariable to Ci::TriggerRequest.variables for presenting the variables.
# The same endpoint in v4 API pressents Pipeline instead of TriggerRequest, so it doesn't need such a process.
trigger_request = pipeline.trigger_requests.last
trigger_request.variables = params[:variables]
present trigger_request, with: ::API::V3::Entities::TriggerRequest
end end
end end
......
...@@ -107,7 +107,7 @@ FactoryGirl.define do ...@@ -107,7 +107,7 @@ FactoryGirl.define do
end end
trait :triggered do trait :triggered do
trigger_request factory: :ci_trigger_request_with_variables trigger_request factory: :ci_trigger_request
end end
after(:build) do |build, evaluator| after(:build) do |build, evaluator|
......
FactoryGirl.define do FactoryGirl.define do
factory :ci_trigger_request, class: Ci::TriggerRequest do factory :ci_trigger_request, class: Ci::TriggerRequest do
trigger factory: :ci_trigger trigger factory: :ci_trigger
factory :ci_trigger_request_with_variables do
variables do
{
TRIGGER_KEY_1: 'TRIGGER_VALUE_1',
TRIGGER_KEY_2: 'TRIGGER_VALUE_2'
}
end
end
end end
end end
...@@ -292,26 +292,44 @@ feature 'Jobs' do ...@@ -292,26 +292,44 @@ feature 'Jobs' do
end end
feature 'Variables' do feature 'Variables' do
let(:trigger_request) { create(:ci_trigger_request_with_variables) } let(:trigger_request) { create(:ci_trigger_request) }
let(:job) do let(:job) do
create :ci_build, pipeline: pipeline, trigger_request: trigger_request create :ci_build, pipeline: pipeline, trigger_request: trigger_request
end end
before do shared_examples 'expected variables behavior' do
visit project_job_path(project, job) it 'shows variable key and value after click', js: true do
expect(page).to have_css('.reveal-variables')
expect(page).not_to have_css('.js-build-variable')
expect(page).not_to have_css('.js-build-value')
click_button 'Reveal Variables'
expect(page).not_to have_css('.reveal-variables')
expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end
end end
it 'shows variable key and value after click', js: true do context 'when variables are stored in trigger_request' do
expect(page).to have_css('.reveal-variables') before do
expect(page).not_to have_css('.js-build-variable') trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
expect(page).not_to have_css('.js-build-value')
click_button 'Reveal Variables' visit project_job_path(project, job)
end
it_behaves_like 'expected variables behavior'
end
context 'when variables are stored in pipeline_variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
visit project_job_path(project, job)
end
expect(page).not_to have_css('.reveal-variables') it_behaves_like 'expected variables behavior'
expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
end end
end end
......
...@@ -31,6 +31,10 @@ ...@@ -31,6 +31,10 @@
"web_url": { "type": "uri" } "web_url": { "type": "uri" }
}, },
"additionalProperties": false "additionalProperties": false
},
"variables": {
"type": ["array", "null"],
"items": { "$ref": "pipeline_schedule_variable.json" }
} }
}, },
"required": [ "required": [
......
{
"type": ["object", "null"],
"properties": {
"key": { "type": "string" },
"value": { "type": "string" }
},
"additionalProperties": false
}
...@@ -32,10 +32,6 @@ describe('GraphFlag', () => { ...@@ -32,10 +32,6 @@ describe('GraphFlag', () => {
.toEqual(component.currentXCoordinate); .toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.selected-metric-line', 'x2')) expect(getCoordinate(component, '.selected-metric-line', 'x2'))
.toEqual(component.currentXCoordinate); .toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.circle-metric', 'cx'))
.toEqual(component.currentXCoordinate);
expect(getCoordinate(component, '.circle-metric', 'cy'))
.toEqual(component.currentYCoordinate);
}); });
it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => { it('has a SVG with the class rect-text-metric at the currentFlagPosition', () => {
......
import Vue from 'vue'; import Vue from 'vue';
import GraphLegend from '~/monitoring/components/graph/legend.vue'; import GraphLegend from '~/monitoring/components/graph/legend.vue';
import measurements from '~/monitoring/utils/measurements'; import measurements from '~/monitoring/utils/measurements';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from '../mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(GraphLegend); const Component = Vue.extend(GraphLegend);
...@@ -10,6 +12,28 @@ const createComponent = (propsData) => { ...@@ -10,6 +12,28 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const defaultValuesComponent = {
graphWidth: 500,
graphHeight: 300,
graphHeightOffset: 120,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
unitOfDisplay: 'Req/Sec',
currentDataIndex: 0,
};
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result,
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
defaultValuesComponent.timeSeries = timeSeries;
function getTextFromNode(component, selector) { function getTextFromNode(component, selector) {
return component.$el.querySelector(selector).firstChild.nodeValue.trim(); return component.$el.querySelector(selector).firstChild.nodeValue.trim();
} }
...@@ -17,95 +41,67 @@ function getTextFromNode(component, selector) { ...@@ -17,95 +41,67 @@ function getTextFromNode(component, selector) {
describe('GraphLegend', () => { describe('GraphLegend', () => {
describe('Computed props', () => { describe('Computed props', () => {
it('textTransform', () => { it('textTransform', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.textTransform).toContain('translate(15, 120) rotate(-90)'); expect(component.textTransform).toContain('translate(15, 120) rotate(-90)');
}); });
it('xPosition', () => { it('xPosition', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.xPosition).toEqual(180); expect(component.xPosition).toEqual(180);
}); });
it('yPosition', () => { it('yPosition', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.yPosition).toEqual(240); expect(component.yPosition).toEqual(240);
}); });
it('rectTransform', () => { it('rectTransform', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500,
graphHeight: 300,
margin: measurements.large.margin,
measurements: measurements.large,
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
});
expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)'); expect(component.rectTransform).toContain('translate(0, 120) rotate(-90)');
}); });
}); });
it('has 2 rect-axis-text rect svg elements', () => { describe('methods', () => {
const component = createComponent({ it('translateLegendGroup should only change Y direction', () => {
graphWidth: 500, const component = createComponent(defaultValuesComponent);
graphHeight: 300,
margin: measurements.large.margin, const translatedCoordinate = component.translateLegendGroup(1);
measurements: measurements.large, expect(translatedCoordinate.indexOf('translate(0, ')).not.toEqual(-1);
areaColorRgb: '#f0f0f0',
legendTitle: 'Title',
yAxisLabel: 'Values',
metricUsage: 'Value',
}); });
it('formatMetricUsage should contain the unit of display and the current value selected via "currentDataIndex"', () => {
const component = createComponent(defaultValuesComponent);
const formattedMetricUsage = component.formatMetricUsage(timeSeries[0]);
const valueFromSeries = timeSeries[0].values[component.currentDataIndex].value;
expect(formattedMetricUsage.indexOf(component.unitOfDisplay)).not.toEqual(-1);
expect(formattedMetricUsage.indexOf(valueFromSeries)).not.toEqual(-1);
});
});
it('has 2 rect-axis-text rect svg elements', () => {
const component = createComponent(defaultValuesComponent);
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2); expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
}); });
it('contains text to signal the usage, title and time', () => { it('contains text to signal the usage, title and time', () => {
const component = createComponent({ const component = createComponent(defaultValuesComponent);
graphWidth: 500, const titles = component.$el.querySelectorAll('.legend-metric-title');
graphHeight: 300,
margin: measurements.large.margin, expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1);
measurements: measurements.large, expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1);
areaColorRgb: '#f0f0f0', expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1);
legendTitle: 'Title', expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
yAxisLabel: 'Values', });
metricUsage: 'Value',
}); it('should contain the same number of legend groups as the timeSeries length', () => {
const component = createComponent(defaultValuesComponent);
expect(getTextFromNode(component, '.text-metric-title')).toEqual(component.legendTitle); expect(component.$el.querySelectorAll('.legend-group').length).toEqual(component.timeSeries.length);
expect(getTextFromNode(component, '.text-metric-usage')).toEqual(component.metricUsage);
expect(getTextFromNode(component, '.label-axis-text')).toEqual(component.yAxisLabel);
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import GraphRow from '~/monitoring/components/graph_row.vue'; import GraphRow from '~/monitoring/components/graph_row.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import { deploymentData, singleRowMetrics } from './mock_data'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(GraphRow); const Component = Vue.extend(GraphRow);
...@@ -11,15 +11,15 @@ const createComponent = (propsData) => { ...@@ -11,15 +11,15 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('GraphRow', () => { describe('GraphRow', () => {
beforeEach(() => { beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
}); });
describe('Computed props', () => { describe('Computed props', () => {
it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => { it('bootstrapClass is set to col-md-6 when rowData is higher/equal to 2', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -29,7 +29,7 @@ describe('GraphRow', () => { ...@@ -29,7 +29,7 @@ describe('GraphRow', () => {
it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => { it('bootstrapClass is set to col-md-12 when rowData is lower than 2', () => {
const component = createComponent({ const component = createComponent({
rowData: [singleRowMetrics[0]], rowData: [convertedMetrics[0]],
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -40,7 +40,7 @@ describe('GraphRow', () => { ...@@ -40,7 +40,7 @@ describe('GraphRow', () => {
it('has one column', () => { it('has one column', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
...@@ -51,7 +51,7 @@ describe('GraphRow', () => { ...@@ -51,7 +51,7 @@ describe('GraphRow', () => {
it('has two columns', () => { it('has two columns', () => {
const component = createComponent({ const component = createComponent({
rowData: singleRowMetrics, rowData: convertedMetrics,
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
}); });
......
import Vue from 'vue'; import Vue from 'vue';
import _ from 'underscore';
import Graph from '~/monitoring/components/graph.vue'; import Graph from '~/monitoring/components/graph.vue';
import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins'; import MonitoringMixins from '~/monitoring/mixins/monitoring_mixins';
import eventHub from '~/monitoring/event_hub'; import eventHub from '~/monitoring/event_hub';
import { deploymentData, singleRowMetrics } from './mock_data'; import { deploymentData, convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from './mock_data';
const createComponent = (propsData) => { const createComponent = (propsData) => {
const Component = Vue.extend(Graph); const Component = Vue.extend(Graph);
...@@ -13,6 +12,8 @@ const createComponent = (propsData) => { ...@@ -13,6 +12,8 @@ const createComponent = (propsData) => {
}).$mount(); }).$mount();
}; };
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
describe('Graph', () => { describe('Graph', () => {
beforeEach(() => { beforeEach(() => {
spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({}); spyOn(MonitoringMixins.methods, 'formatDeployments').and.returnValue({});
...@@ -20,7 +21,7 @@ describe('Graph', () => { ...@@ -20,7 +21,7 @@ describe('Graph', () => {
it('has a title', () => { it('has a title', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -29,29 +30,10 @@ describe('Graph', () => { ...@@ -29,29 +30,10 @@ describe('Graph', () => {
expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title); expect(component.$el.querySelector('.text-center').innerText.trim()).toBe(component.graphData.title);
}); });
it('creates a path for the line and area of the graph', (done) => {
const component = createComponent({
graphData: singleRowMetrics[0],
classType: 'col-md-6',
updateAspectRatio: false,
deploymentData,
});
Vue.nextTick(() => {
expect(component.area).toBeDefined();
expect(component.line).toBeDefined();
expect(typeof component.area).toEqual('string');
expect(typeof component.line).toEqual('string');
expect(_.isFunction(component.xScale)).toBe(true);
expect(_.isFunction(component.yScale)).toBe(true);
done();
});
});
describe('Computed props', () => { describe('Computed props', () => {
it('axisTransform translates an element Y position depending of its height', () => { it('axisTransform translates an element Y position depending of its height', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -64,7 +46,7 @@ describe('Graph', () => { ...@@ -64,7 +46,7 @@ describe('Graph', () => {
it('outterViewBox gets a width and height property based on the DOM size of the element', () => { it('outterViewBox gets a width and height property based on the DOM size of the element', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -79,7 +61,7 @@ describe('Graph', () => { ...@@ -79,7 +61,7 @@ describe('Graph', () => {
it('sends an event to the eventhub when it has finished resizing', (done) => { it('sends an event to the eventhub when it has finished resizing', (done) => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
...@@ -95,7 +77,7 @@ describe('Graph', () => { ...@@ -95,7 +77,7 @@ describe('Graph', () => {
it('has a title for the y-axis and the chart legend that comes from the backend', () => { it('has a title for the y-axis and the chart legend that comes from the backend', () => {
const component = createComponent({ const component = createComponent({
graphData: singleRowMetrics[0], graphData: convertedMetrics[1],
classType: 'col-md-6', classType: 'col-md-6',
updateAspectRatio: false, updateAspectRatio: false,
deploymentData, deploymentData,
......
This diff is collapsed.
import Vue from 'vue';
import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
const Component = Vue.extend(MonitoringPaths);
return new Component({
propsData,
}).$mount();
};
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
describe('Monitoring Paths', () => {
it('renders two paths to represent a line and the area underneath it', () => {
const component = createComponent({
generatedLinePath: timeSeries[0].linePath,
generatedAreaPath: timeSeries[0].areaPath,
lineColor: '#ccc',
areaColor: '#fff',
});
const metricArea = component.$el.querySelector('.metric-area');
const metricLine = component.$el.querySelector('.metric-line');
expect(metricArea.getAttribute('fill')).toBe('#fff');
expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath);
expect(metricLine.getAttribute('stroke')).toBe('#ccc');
expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath);
});
});
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
describe('Multiple time series', () => {
it('createTimeSeries returned array contains an object for each element', () => {
expect(typeof timeSeries[0].linePath).toEqual('string');
expect(typeof timeSeries[0].areaPath).toEqual('string');
expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function');
expect(typeof timeSeries[0].areaColor).toEqual('string');
expect(typeof timeSeries[0].lineColor).toEqual('string');
expect(timeSeries[0].values instanceof Array).toEqual(true);
});
it('createTimeSeries returns an array', () => {
expect(timeSeries instanceof Array).toEqual(true);
expect(timeSeries.length).toEqual(2);
});
});
...@@ -278,6 +278,7 @@ CommitStatus: ...@@ -278,6 +278,7 @@ CommitStatus:
- auto_canceled_by_id - auto_canceled_by_id
- retried - retried
- protected - protected
- failure_reason
Ci::Variable: Ci::Variable:
- id - id
- project_id - project_id
......
...@@ -1492,10 +1492,12 @@ describe Ci::Build do ...@@ -1492,10 +1492,12 @@ describe Ci::Build do
context 'when build is for triggers' do context 'when build is for triggers' do
let(:trigger) { create(:ci_trigger, project: project) } let(:trigger) { create(:ci_trigger, project: project) }
let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
let(:user_trigger_variable) do let(:user_trigger_variable) do
{ key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } { key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1', public: false }
end end
let(:predefined_trigger_variable) do let(:predefined_trigger_variable) do
{ key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true } { key: 'CI_PIPELINE_TRIGGERED', value: 'true', public: true }
end end
...@@ -1504,8 +1506,26 @@ describe Ci::Build do ...@@ -1504,8 +1506,26 @@ describe Ci::Build do
build.trigger_request = trigger_request build.trigger_request = trigger_request
end end
it { is_expected.to include(user_trigger_variable) } shared_examples 'returns variables for triggers' do
it { is_expected.to include(predefined_trigger_variable) } it { is_expected.to include(user_trigger_variable) }
it { is_expected.to include(predefined_trigger_variable) }
end
context 'when variables are stored in trigger_request' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
end
it_behaves_like 'returns variables for triggers'
end
context 'when variables are stored in pipeline_variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: 'TRIGGER_KEY_1', value: 'TRIGGER_VALUE_1')
end
it_behaves_like 'returns variables for triggers'
end
end end
context 'when pipeline has a variable' do context 'when pipeline has a variable' do
......
require 'spec_helper'
describe Ci::TriggerRequest do
describe 'validation' do
it 'be invalid if saving a variable' do
trigger = build(:ci_trigger_request, variables: { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
expect(trigger).not_to be_valid
end
it 'be valid if not saving a variable' do
trigger = build(:ci_trigger_request)
expect(trigger).to be_valid
end
end
end
...@@ -443,4 +443,25 @@ describe CommitStatus do ...@@ -443,4 +443,25 @@ describe CommitStatus do
end end
end end
end end
describe 'set failure_reason when drop' do
let(:commit_status) { create(:commit_status, :created) }
subject do
commit_status.drop!(reason)
commit_status
end
context 'when failure_reason is nil' do
let(:reason) { }
it { is_expected.to be_unknown_failure }
end
context 'when failure_reason is script_failure' do
let(:reason) { :script_failure }
it { is_expected.to be_script_failure }
end
end
end end
...@@ -100,4 +100,38 @@ describe Ci::BuildPresenter do ...@@ -100,4 +100,38 @@ describe Ci::BuildPresenter do
end end
end end
end end
describe '#trigger_variables' do
let(:build) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) }
let(:trigger) { create(:ci_trigger, project: project) }
let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) }
context 'when variable is stored in ci_pipeline_variables' do
let!(:pipeline_variable) { create(:ci_pipeline_variable, pipeline: pipeline) }
context 'when pipeline is triggered by trigger API' do
it 'returns variables' do
expect(presenter.trigger_variables).to eq([pipeline_variable.to_runner_variable])
end
end
context 'when pipeline is not triggered by trigger API' do
let(:build) { create(:ci_build, pipeline: pipeline) }
it 'does not return variables' do
expect(presenter.trigger_variables).to eq([])
end
end
end
context 'when variable is stored in ci_trigger_requests.variables' do
before do
trigger_request.update_attribute(:variables, { 'TRIGGER_KEY_1' => 'TRIGGER_VALUE_1' } )
end
it 'returns variables' do
expect(presenter.trigger_variables).to eq(trigger_request.user_variables)
end
end
end
end end
...@@ -142,6 +142,9 @@ describe API::CommitStatuses do ...@@ -142,6 +142,9 @@ describe API::CommitStatuses do
expect(json_response['ref']).not_to be_empty expect(json_response['ref']).not_to be_empty
expect(json_response['target_url']).to be_nil expect(json_response['target_url']).to be_nil
expect(json_response['description']).to be_nil expect(json_response['description']).to be_nil
if status == 'failed'
expect(CommitStatus.find(json_response['id'])).to be_api_failure
end
end end
end end
end end
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe API::PipelineSchedules do describe API::PipelineSchedules do
set(:developer) { create(:user) } set(:developer) { create(:user) }
set(:user) { create(:user) } set(:user) { create(:user) }
set(:project) { create(:project, :repository) } set(:project) { create(:project, :repository, public_builds: false) }
before do before do
project.add_developer(developer) project.add_developer(developer)
...@@ -110,6 +110,18 @@ describe API::PipelineSchedules do ...@@ -110,6 +110,18 @@ describe API::PipelineSchedules do
end end
end end
context 'authenticated user with insufficient permissions' do
before do
project.add_guest(user)
end
it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}", user)
expect(response).to have_http_status(:not_found)
end
end
context 'unauthenticated user' do context 'unauthenticated user' do
it 'does not return pipeline_schedules list' do it 'does not return pipeline_schedules list' do
get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}") get api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}")
...@@ -299,4 +311,150 @@ describe API::PipelineSchedules do ...@@ -299,4 +311,150 @@ describe API::PipelineSchedules do
end end
end end
end end
describe 'POST /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables' do
let(:params) { attributes_for(:ci_pipeline_schedule_variable) }
set(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
end
context 'authenticated user with valid permissions' do
context 'with required parameters' do
it 'creates pipeline_schedule_variable' do
expect do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
params
end.to change { pipeline_schedule.variables.count }.by(1)
expect(response).to have_http_status(:created)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['key']).to eq(params[:key])
expect(json_response['value']).to eq(params[:value])
end
end
context 'without required parameters' do
it 'does not create pipeline_schedule_variable' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer)
expect(response).to have_http_status(:bad_request)
end
end
context 'when key has validation error' do
it 'does not create pipeline_schedule_variable' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", developer),
params.merge('key' => '!?!?')
expect(response).to have_http_status(:bad_request)
expect(json_response['message']).to have_key('key')
end
end
end
context 'authenticated user with invalid permissions' do
it 'does not create pipeline_schedule_variable' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables", user), params
expect(response).to have_http_status(:not_found)
end
end
context 'unauthenticated user' do
it 'does not create pipeline_schedule_variable' do
post api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables"), params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
set(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
end
let(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule)
end
context 'authenticated user with valid permissions' do
it 'updates pipeline_schedule_variable' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer),
value: 'updated_value'
expect(response).to have_http_status(:ok)
expect(response).to match_response_schema('pipeline_schedule_variable')
expect(json_response['value']).to eq('updated_value')
end
end
context 'authenticated user with invalid permissions' do
it 'does not update pipeline_schedule_variable' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", user)
expect(response).to have_http_status(:not_found)
end
end
context 'unauthenticated user' do
it 'does not update pipeline_schedule_variable' do
put api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}")
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE /projects/:id/pipeline_schedules/:pipeline_schedule_id/variables/:key' do
let(:master) { create(:user) }
set(:pipeline_schedule) do
create(:ci_pipeline_schedule, project: project, owner: developer)
end
let!(:pipeline_schedule_variable) do
create(:ci_pipeline_schedule_variable, pipeline_schedule: pipeline_schedule)
end
before do
project.add_master(master)
end
context 'authenticated user with valid permissions' do
it 'deletes pipeline_schedule_variable' do
expect do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", master)
end.to change { Ci::PipelineScheduleVariable.count }.by(-1)
expect(response).to have_http_status(:accepted)
expect(response).to match_response_schema('pipeline_schedule_variable')
end
it 'responds with 404 Not Found if requesting non-existing pipeline_schedule_variable' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/____", master)
expect(response).to have_http_status(:not_found)
end
end
context 'authenticated user with invalid permissions' do
let!(:pipeline_schedule) { create(:ci_pipeline_schedule, project: project, owner: master) }
it 'does not delete pipeline_schedule_variable' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}", developer)
expect(response).to have_http_status(:forbidden)
end
end
context 'unauthenticated user' do
it 'does not delete pipeline_schedule_variable' do
delete api("/projects/#{project.id}/pipeline_schedules/#{pipeline_schedule.id}/variables/#{pipeline_schedule_variable.key}")
expect(response).to have_http_status(:unauthorized)
end
end
end
end end
...@@ -557,17 +557,36 @@ describe API::Runner do ...@@ -557,17 +557,36 @@ describe API::Runner do
{ 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }] { 'key' => 'TRIGGER_KEY_1', 'value' => 'TRIGGER_VALUE_1', 'public' => false }]
end end
let(:trigger) { create(:ci_trigger, project: project) }
let!(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, builds: [job], trigger: trigger) }
before do before do
trigger = create(:ci_trigger, project: project)
create(:ci_trigger_request_with_variables, pipeline: pipeline, builds: [job], trigger: trigger)
project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
end end
it 'returns variables for triggers' do shared_examples 'expected variables behavior' do
request_job it 'returns variables for triggers' do
request_job
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
expect(json_response['variables']).to include(*expected_variables) expect(json_response['variables']).to include(*expected_variables)
end
end
context 'when variables are stored in trigger_request' do
before do
trigger_request.update_attribute(:variables, { TRIGGER_KEY_1: 'TRIGGER_VALUE_1' } )
end
it_behaves_like 'expected variables behavior'
end
context 'when variables are stored in pipeline_variables' do
before do
create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1')
end
it_behaves_like 'expected variables behavior'
end end
end end
...@@ -626,13 +645,34 @@ describe API::Runner do ...@@ -626,13 +645,34 @@ describe API::Runner do
it 'mark job as succeeded' do it 'mark job as succeeded' do
update_job(state: 'success') update_job(state: 'success')
expect(job.reload.status).to eq 'success' job.reload
expect(job).to be_success
end end
it 'mark job as failed' do it 'mark job as failed' do
update_job(state: 'failed') update_job(state: 'failed')
expect(job.reload.status).to eq 'failed' job.reload
expect(job).to be_failed
expect(job).to be_unknown_failure
end
context 'when failure_reason is script_failure' do
before do
update_job(state: 'failed', failure_reason: 'script_failure')
job.reload
end
it { expect(job).to be_script_failure }
end
context 'when failure_reason is runner_system_failure' do
before do
update_job(state: 'failed', failure_reason: 'runner_system_failure')
job.reload
end
it { expect(job).to be_runner_system_failure }
end end
end end
......
...@@ -37,7 +37,7 @@ describe API::V3::Triggers do ...@@ -37,7 +37,7 @@ describe API::V3::Triggers do
it 'returns unauthorized if token is for different project' do it 'returns unauthorized if token is for different project' do
post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master') post v3_api("/projects/#{project2.id}/trigger/builds"), options.merge(ref: 'master')
expect(response).to have_http_status(401) expect(response).to have_http_status(404)
end end
end end
...@@ -80,7 +80,8 @@ describe API::V3::Triggers do ...@@ -80,7 +80,8 @@ describe API::V3::Triggers do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master') post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(variables: variables, ref: 'master')
expect(response).to have_http_status(201) expect(response).to have_http_status(201)
pipeline.builds.reload pipeline.builds.reload
expect(pipeline.builds.first.trigger_request.variables).to eq(variables) expect(pipeline.variables.map { |v| { v.key => v.value } }.first).to eq(variables)
expect(json_response['variables']).to eq(variables)
end end
end end
end end
......
require 'spec_helper'
describe Ci::CreateTriggerRequestService do
let(:service) { described_class }
let(:project) { create(:project, :repository) }
let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
let(:owner) { create(:user) }
before do
stub_ci_pipeline_to_return_yaml_file
project.add_developer(owner)
end
describe '#execute' do
context 'valid params' do
subject { service.execute(project, trigger, 'master') }
context 'without owner' do
it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(subject.pipeline).to be_trigger }
end
context 'with owner' do
it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
it { expect(subject.trigger_request.builds.first.user).to eq(owner) }
it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(subject.pipeline).to be_trigger }
it { expect(subject.pipeline.user).to eq(owner) }
end
end
context 'no commit for ref' do
subject { service.execute(project, trigger, 'other-branch') }
it { expect(subject.pipeline).not_to be_persisted }
end
context 'no builds created' do
subject { service.execute(project, trigger, 'master') }
before do
stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
end
it { expect(subject.pipeline).not_to be_persisted }
end
end
end
...@@ -22,7 +22,7 @@ describe Ci::RetryBuildService do ...@@ -22,7 +22,7 @@ describe Ci::RetryBuildService do
%i[type lock_version target_url base_tags %i[type lock_version target_url base_tags
commit_id deployments erased_by_id last_deployment project_id commit_id deployments erased_by_id last_deployment project_id
runner_id tag_taggings taggings tags trigger_request_id runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried].freeze user_id auto_canceled_by_id retried failure_reason].freeze
shared_examples 'build duplication' do shared_examples 'build duplication' do
let(:stage) do let(:stage) do
......
...@@ -116,6 +116,7 @@ describe Projects::UpdatePagesService do ...@@ -116,6 +116,7 @@ describe Projects::UpdatePagesService do
expect(deploy_status.description) expect(deploy_status.description)
.to match(/artifacts for pages are too large/) .to match(/artifacts for pages are too large/)
expect(deploy_status).to be_script_failure
end end
end end
......
...@@ -195,20 +195,4 @@ describe 'projects/jobs/show' do ...@@ -195,20 +195,4 @@ describe 'projects/jobs/show' do
text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) text: /\A\n#{Regexp.escape(commit_title)}\n\Z/)
end end
end end
describe 'shows trigger variables in sidebar' do
let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) }
before do
build.trigger_request = trigger_request
render
end
it 'shows trigger variables in separate lines' do
expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1')
expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2')
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1')
expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
end end
...@@ -6,27 +6,31 @@ describe StuckCiJobsWorker do ...@@ -6,27 +6,31 @@ describe StuckCiJobsWorker do
let(:worker) { described_class.new } let(:worker) { described_class.new }
let(:exclusive_lease_uuid) { SecureRandom.uuid } let(:exclusive_lease_uuid) { SecureRandom.uuid }
subject do
job.reload
job.status
end
before do before do
job.update!(status: status, updated_at: updated_at) job.update!(status: status, updated_at: updated_at)
allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid)
end end
shared_examples 'job is dropped' do shared_examples 'job is dropped' do
it 'changes status' do before do
worker.perform worker.perform
is_expected.to eq('failed') job.reload
end
it "changes status" do
expect(job).to be_failed
expect(job).to be_stuck_or_timeout_failure
end end
end end
shared_examples 'job is unchanged' do shared_examples 'job is unchanged' do
it "doesn't change status" do before do
worker.perform worker.perform
is_expected.to eq(status) job.reload
end
it "doesn't change status" do
expect(job.status).to eq(status)
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment