Commit 4c6898bd authored by Douwe Maan's avatar Douwe Maan

Merge branch 'master' into '36860-migrate-issues-author'

# Conflicts:
#   db/schema.rb
parents ae9b7717 063e285e
...@@ -212,7 +212,6 @@ update-tests-metadata: ...@@ -212,7 +212,6 @@ update-tests-metadata:
flaky-examples-check: flaky-examples-check:
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
image: ruby:2.3-alpine image: ruby:2.3-alpine
services: [] services: []
before_script: [] before_script: []
...@@ -227,6 +226,7 @@ flaky-examples-check: ...@@ -227,6 +226,7 @@ flaky-examples-check:
- branches - branches
except: except:
- master - master
- /(^docs[\/-].*|.*-docs$)/
artifacts: artifacts:
expire_in: 30d expire_in: 30d
paths: paths:
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master) [![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines) [![Overall test coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg)](https://gitlab.com/gitlab-org/gitlab-ce/pipelines)
[![Dependency Status](https://gemnasium.com/gitlabhq/gitlabhq.svg)](https://gemnasium.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq) [![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42) [![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
[![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![Gitter](https://badges.gitter.im/gitlabhq/gitlabhq.svg)](https://gitter.im/gitlabhq/gitlabhq?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
......
...@@ -5,7 +5,7 @@ const Api = { ...@@ -5,7 +5,7 @@ const Api = {
groupPath: '/api/:version/groups/:id.json', groupPath: '/api/:version/groups/:id.json',
namespacesPath: '/api/:version/namespaces.json', namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json', groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json?simple=true', projectsPath: '/api/:version/projects.json',
labelsPath: '/:namespace_path/:project_path/labels', labelsPath: '/:namespace_path/:project_path/labels',
licensePath: '/api/:version/templates/licenses/:key', licensePath: '/api/:version/templates/licenses/:key',
gitignorePath: '/api/:version/templates/gitignores/:key', gitignorePath: '/api/:version/templates/gitignores/:key',
...@@ -58,6 +58,7 @@ const Api = { ...@@ -58,6 +58,7 @@ const Api = {
const defaults = { const defaults = {
search: query, search: query,
per_page: 20, per_page: 20,
simple: true,
}; };
if (gon.current_user_id) { if (gon.current_user_id) {
......
...@@ -2,3 +2,4 @@ import 'underscore'; ...@@ -2,3 +2,4 @@ import 'underscore';
import './polyfills'; import './polyfills';
import './jquery'; import './jquery';
import './bootstrap'; import './bootstrap';
import './vue';
import Vue from 'vue'; import Vue from 'vue';
import './vue_resource_interceptor';
if (process.env.NODE_ENV !== 'production') { if (process.env.NODE_ENV !== 'production') {
Vue.config.productionTip = false; Vue.config.productionTip = false;
......
...@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -21,11 +21,13 @@ const DiffNoteAvatars = Vue.extend({
}, },
template: ` template: `
<div class="diff-comment-avatar-holders" <div class="diff-comment-avatar-holders"
:class="discussionClassName"
v-show="notesCount !== 0"> v-show="notesCount !== 0">
<div v-if="!isVisible"> <div v-if="!isVisible">
<!-- FIXME: Pass an alt attribute here for accessibility --> <!-- FIXME: Pass an alt attribute here for accessibility -->
<user-avatar-image <user-avatar-image
v-for="note in notesSubset" v-for="note in notesSubset"
:key="note.id"
class="diff-comment-avatar js-diff-comment-avatar" class="diff-comment-avatar js-diff-comment-avatar"
@click.native="clickedAvatar($event)" @click.native="clickedAvatar($event)"
:img-src="note.authorAvatar" :img-src="note.authorAvatar"
...@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -68,7 +70,8 @@ const DiffNoteAvatars = Vue.extend({
}); });
}); });
}, },
destroyed() { beforeDestroy() {
this.addNoCommentClass();
$(document).off('toggle.comments'); $(document).off('toggle.comments');
}, },
watch: { watch: {
...@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({ ...@@ -85,6 +88,9 @@ const DiffNoteAvatars = Vue.extend({
}, },
}, },
computed: { computed: {
discussionClassName() {
return `js-diff-avatars-${this.discussionId}`;
},
notesSubset() { notesSubset() {
let notes = []; let notes = [];
......
...@@ -32,6 +32,10 @@ $(() => { ...@@ -32,6 +32,10 @@ $(() => {
const tmpApp = new tmp().$mount(); const tmpApp = new tmp().$mount();
$(this).replaceWith(tmpApp.$el); $(this).replaceWith(tmpApp.$el);
$(tmpApp.$el).one('remove.vue', () => {
tmpApp.$destroy();
tmpApp.$el.remove();
});
}); });
const $components = $(COMPONENT_SELECTOR).filter(function () { const $components = $(COMPONENT_SELECTOR).filter(function () {
......
...@@ -132,6 +132,7 @@ import './project_new'; ...@@ -132,6 +132,7 @@ import './project_new';
import './project_select'; import './project_select';
import './project_show'; import './project_show';
import './project_variables'; import './project_variables';
import './projects_dropdown';
import './projects_list'; import './projects_list';
import './syntax_highlight'; import './syntax_highlight';
import './render_math'; import './render_math';
...@@ -249,7 +250,10 @@ $(function () { ...@@ -249,7 +250,10 @@ $(function () {
// Initialize popovers // Initialize popovers
$body.popover({ $body.popover({
selector: '[data-toggle="popover"]', selector: '[data-toggle="popover"]',
trigger: 'focus' trigger: 'focus',
// set the viewport to the main content, excluding the navigation bar, so
// the navigation can't overlap the popover
viewport: '.page-with-sidebar'
}); });
$('.trigger-submit').on('change', function () { $('.trigger-submit').on('change', function () {
return $(this).parents('form').submit(); return $(this).parents('form').submit();
......
...@@ -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,
};
});
}
...@@ -464,7 +464,6 @@ export default class Notes { ...@@ -464,7 +464,6 @@ export default class Notes {
} }
renderDiscussionAvatar(diffAvatarContainer, noteEntity) { renderDiscussionAvatar(diffAvatarContainer, noteEntity) {
var commentButton = diffAvatarContainer.find('.js-add-diff-note-button');
var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders'); var avatarHolder = diffAvatarContainer.find('.diff-comment-avatar-holders');
if (!avatarHolder.length) { if (!avatarHolder.length) {
...@@ -475,10 +474,6 @@ export default class Notes { ...@@ -475,10 +474,6 @@ export default class Notes {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
if (commentButton.length) {
commentButton.remove();
}
} }
/** /**
...@@ -767,6 +762,7 @@ export default class Notes { ...@@ -767,6 +762,7 @@ export default class Notes {
var $note, $notes; var $note, $notes;
$note = $(el); $note = $(el);
$notes = $note.closest('.discussion-notes'); $notes = $note.closest('.discussion-notes');
const discussionId = $('.notes', $notes).data('discussion-id');
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteElId]) { if (gl.diffNoteApps[noteElId]) {
...@@ -783,6 +779,8 @@ export default class Notes { ...@@ -783,6 +779,8 @@ export default class Notes {
// "Discussions" tab // "Discussions" tab
$notes.closest('.timeline-entry').remove(); $notes.closest('.timeline-entry').remove();
$(`.js-diff-avatars-${discussionId}`).trigger('remove.vue');
// The notes tr can contain multiple lists of notes, like on the parallel diff // The notes tr can contain multiple lists of notes, like on the parallel diff
if (notesTr.find('.discussion-notes').length > 1) { if (notesTr.find('.discussion-notes').length > 1) {
$notes.remove(); $notes.remove();
......
...@@ -53,10 +53,6 @@ import Cookies from 'js-cookie'; ...@@ -53,10 +53,6 @@ import Cookies from 'js-cookie';
return _this.changeProject($(e.currentTarget).val()); return _this.changeProject($(e.currentTarget).val());
}; };
})(this)); })(this));
return $('.js-projects-dropdown-toggle').on('click', function(e) {
e.preventDefault();
return $('.js-projects-dropdown').select2('open');
});
}; };
Project.prototype.changeProject = function(url) { Project.prototype.changeProject = function(url) {
......
...@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button'; ...@@ -5,48 +5,6 @@ import ProjectSelectComboButton from './project_select_combo_button';
(function() { (function() {
this.ProjectSelect = (function() { this.ProjectSelect = (function() {
function ProjectSelect() { function ProjectSelect() {
$('.js-projects-dropdown-toggle').each(function(i, dropdown) {
var $dropdown;
$dropdown = $(dropdown);
return $dropdown.glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace']
},
data: function(term, callback) {
var finalCallback, projectsCallback;
var orderBy = $dropdown.data('order-by');
finalCallback = function(projects) {
return callback(projects);
};
if (this.includeGroups) {
projectsCallback = function(projects) {
var groupsCallback;
groupsCallback = function(groups) {
var data;
data = groups.concat(projects);
return finalCallback(data);
};
return Api.groups(term, {}, groupsCallback);
};
} else {
projectsCallback = finalCallback;
}
if (this.groupId) {
return Api.groupProjects(this.groupId, term, projectsCallback);
} else {
return Api.projects(term, { order_by: orderBy }, projectsCallback);
}
},
url: function(project) {
return project.web_url;
},
text: function(project) {
return project.name_with_namespace;
}
});
});
$('.ajax-project-select').each(function(i, select) { $('.ajax-project-select').each(function(i, select) {
var placeholder; var placeholder;
this.groupId = $(select).data('group-id'); this.groupId = $(select).data('group-id');
......
<script>
import bs from '../../breakpoints';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import projectsListFrequent from './projects_list_frequent.vue';
import projectsListSearch from './projects_list_search.vue';
import search from './search.vue';
export default {
components: {
search,
loadingIcon,
projectsListFrequent,
projectsListSearch,
},
props: {
currentProject: {
type: Object,
required: true,
},
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
isLoadingProjects: false,
isFrequentsListVisible: false,
isSearchListVisible: false,
isLocalStorageFailed: false,
isSearchFailed: false,
searchQuery: '',
};
},
computed: {
frequentProjects() {
return this.store.getFrequentProjects();
},
searchProjects() {
return this.store.getSearchedProjects();
},
},
methods: {
toggleFrequentProjectsList(state) {
this.isLoadingProjects = !state;
this.isSearchListVisible = !state;
this.isFrequentsListVisible = state;
},
toggleSearchProjectsList(state) {
this.isLoadingProjects = !state;
this.isFrequentsListVisible = !state;
this.isSearchListVisible = state;
},
toggleLoader(state) {
this.isFrequentsListVisible = !state;
this.isSearchListVisible = !state;
this.isLoadingProjects = state;
},
fetchFrequentProjects() {
const screenSize = bs.getBreakpointSize();
if (this.searchQuery && (screenSize !== 'sm' && screenSize !== 'xs')) {
this.toggleSearchProjectsList(true);
} else {
this.toggleLoader(true);
this.isLocalStorageFailed = false;
const projects = this.service.getFrequentProjects();
if (projects) {
this.toggleFrequentProjectsList(true);
this.store.setFrequentProjects(projects);
} else {
this.isLocalStorageFailed = true;
this.toggleFrequentProjectsList(true);
this.store.setFrequentProjects([]);
}
}
},
fetchSearchedProjects(searchQuery) {
this.searchQuery = searchQuery;
this.toggleLoader(true);
this.service.getSearchedProjects(this.searchQuery)
.then(res => res.json())
.then((results) => {
this.toggleSearchProjectsList(true);
this.store.setSearchedProjects(results);
})
.catch(() => {
this.isSearchFailed = true;
this.toggleSearchProjectsList(true);
});
},
logCurrentProjectAccess() {
this.service.logProjectAccess(this.currentProject);
},
handleSearchClear() {
this.searchQuery = '';
this.toggleFrequentProjectsList(true);
this.store.clearSearchedProjects();
},
handleSearchFailure() {
this.isSearchFailed = true;
this.toggleSearchProjectsList(true);
},
},
created() {
if (this.currentProject.id) {
this.logCurrentProjectAccess();
}
eventHub.$on('dropdownOpen', this.fetchFrequentProjects);
eventHub.$on('searchProjects', this.fetchSearchedProjects);
eventHub.$on('searchCleared', this.handleSearchClear);
eventHub.$on('searchFailed', this.handleSearchFailure);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.fetchFrequentProjects);
eventHub.$off('searchProjects', this.fetchSearchedProjects);
eventHub.$off('searchCleared', this.handleSearchClear);
eventHub.$off('searchFailed', this.handleSearchFailure);
},
};
</script>
<template>
<div>
<search/>
<loading-icon
class="loading-animation prepend-top-20"
size="2"
v-if="isLoadingProjects"
:label="s__('ProjectsDropdown|Loading projects')"
/>
<div
class="section-header"
v-if="isFrequentsListVisible"
>
{{ s__('ProjectsDropdown|Frequently visited') }}
</div>
<projects-list-frequent
v-if="isFrequentsListVisible"
:local-storage-failed="isLocalStorageFailed"
:projects="frequentProjects"
/>
<projects-list-search
v-if="isSearchListVisible"
:search-failed="isSearchFailed"
:matcher="searchQuery"
:projects="searchProjects"
/>
</div>
</template>
<script>
import { s__ } from '../../locale';
import projectsListItem from './projects_list_item.vue';
export default {
components: {
projectsListItem,
},
props: {
projects: {
type: Array,
required: true,
},
localStorageFailed: {
type: Boolean,
required: true,
},
},
computed: {
isListEmpty() {
return this.projects.length === 0;
},
listEmptyMessage() {
return this.localStorageFailed ?
s__('ProjectsDropdown|This feature requires browser localStorage support') :
s__('ProjectsDropdown|Projects you visit often will appear here');
},
},
};
</script>
<template>
<div
class="projects-list-frequent-container"
>
<ul
class="list-unstyled"
>
<li
class="section-empty"
v-if="isListEmpty"
>
{{listEmptyMessage}}
</li>
<projects-list-item
v-else
v-for="(project, index) in projects"
:key="index"
:project-id="project.id"
:project-name="project.name"
:namespace="project.namespace"
:web-url="project.webUrl"
:avatar-url="project.avatarUrl"
/>
</ul>
</div>
</template>
<script>
import identicon from '../../vue_shared/components/identicon.vue';
export default {
components: {
identicon,
},
props: {
matcher: {
type: String,
required: false,
},
projectId: {
type: Number,
required: true,
},
projectName: {
type: String,
required: true,
},
namespace: {
type: String,
required: true,
},
webUrl: {
type: String,
required: true,
},
avatarUrl: {
required: true,
validator(value) {
return value === null || typeof value === 'string';
},
},
},
computed: {
hasAvatar() {
return this.avatarUrl !== null;
},
highlightedProjectName() {
if (this.matcher) {
const matcherRegEx = new RegExp(this.matcher, 'gi');
const matches = this.projectName.match(matcherRegEx);
if (matches && matches.length > 0) {
return this.projectName.replace(matches[0], `<b>${matches[0]}</b>`);
}
}
return this.projectName;
},
},
};
</script>
<template>
<li
class="projects-list-item-container"
>
<a
class="clearfix"
:href="webUrl"
>
<div
class="project-item-avatar-container"
>
<img
v-if="hasAvatar"
class="avatar s32"
:src="avatarUrl"
/>
<identicon
v-else
size-class="s32"
:entity-id=projectId
:entity-name="projectName"
/>
</div>
<div
class="project-item-metadata-container"
>
<div
class="project-title"
:title="projectName"
v-html="highlightedProjectName"
>
</div>
<div
class="project-namespace"
:title="namespace"
>
{{namespace}}
</div>
</div>
</a>
</li>
</template>
<script>
import { s__ } from '../../locale';
import projectsListItem from './projects_list_item.vue';
export default {
components: {
projectsListItem,
},
props: {
matcher: {
type: String,
required: true,
},
projects: {
type: Array,
required: true,
},
searchFailed: {
type: Boolean,
required: true,
},
},
computed: {
isListEmpty() {
return this.projects.length === 0;
},
listEmptyMessage() {
return this.searchFailed ?
s__('ProjectsDropdown|Something went wrong on our end.') :
s__('ProjectsDropdown|No projects matched your query');
},
},
};
</script>
<template>
<div
class="projects-list-search-container"
>
<ul
class="list-unstyled"
>
<li
v-if="isListEmpty"
:class="{ 'section-failure': searchFailed }"
class="section-empty"
>
{{ listEmptyMessage }}
</li>
<projects-list-item
v-else
v-for="(project, index) in projects"
:key="index"
:project-id="project.id"
:project-name="project.name"
:namespace="project.namespace"
:web-url="project.webUrl"
:avatar-url="project.avatarUrl"
:matcher="matcher"
/>
</ul>
</div>
</template>
<script>
import _ from 'underscore';
import eventHub from '../event_hub';
export default {
data() {
return {
searchQuery: '',
};
},
watch: {
searchQuery() {
this.handleInput();
},
},
methods: {
setFocus() {
this.$refs.search.focus();
},
emitSearchEvents() {
if (this.searchQuery) {
eventHub.$emit('searchProjects', this.searchQuery);
} else {
eventHub.$emit('searchCleared');
}
},
/**
* Callback function within _.debounce is intentionally
* kept as ES5 `function() {}` instead of ES6 `() => {}`
* as it otherwise messes up function context
* and component reference is no longer accessible via `this`
*/
// eslint-disable-next-line func-names
handleInput: _.debounce(function () {
this.emitSearchEvents();
}, 500),
},
mounted() {
eventHub.$on('dropdownOpen', this.setFocus);
},
beforeDestroy() {
eventHub.$off('dropdownOpen', this.setFocus);
},
};
</script>
<template>
<div
class="search-input-container hidden-xs"
>
<input
type="search"
class="form-control"
ref="search"
v-model="searchQuery"
:placeholder="s__('ProjectsDropdown|Search projects')"
/>
<i
v-if="!searchQuery"
class="search-icon fa fa-fw fa-search"
aria-hidden="true"
/>
</div>
</template>
export const FREQUENT_PROJECTS = {
MAX_COUNT: 20,
LIST_COUNT_DESKTOP: 5,
LIST_COUNT_MOBILE: 3,
ELIGIBLE_FREQUENCY: 3,
};
export const HOUR_IN_MS = 3600000;
export const STORAGE_KEY = 'frequent-projects';
import Vue from 'vue';
export default new Vue();
import Vue from 'vue';
import Translate from '../vue_shared/translate';
import eventHub from './event_hub';
import ProjectsService from './service/projects_service';
import ProjectsStore from './store/projects_store';
import projectsDropdownApp from './components/app.vue';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-projects-dropdown');
const navEl = document.getElementById('nav-projects-dropdown');
// Don't do anything if element doesn't exist (No projects dropdown)
// This is for when the user accesses GitLab without logging in
if (!el || !navEl) {
return;
}
$(navEl).on('show.bs.dropdown', (e) => {
const dropdownEl = $(e.currentTarget).find('.projects-dropdown-menu');
dropdownEl.one('transitionend', () => {
eventHub.$emit('dropdownOpen');
});
});
// eslint-disable-next-line no-new
new Vue({
el,
components: {
projectsDropdownApp,
},
data() {
const dataset = this.$options.el.dataset;
const store = new ProjectsStore();
const service = new ProjectsService(dataset.userName);
const project = {
id: Number(dataset.projectId),
name: dataset.projectName,
namespace: dataset.projectNamespace,
webUrl: dataset.projectWebUrl,
avatarUrl: dataset.projectAvatarUrl || null,
lastAccessedOn: Date.now(),
};
return {
store,
service,
state: store.state,
currentUserName: dataset.userName,
currentProject: project,
};
},
render(createElement) {
return createElement('projects-dropdown-app', {
props: {
currentUserName: this.currentUserName,
currentProject: this.currentProject,
store: this.store,
service: this.service,
},
});
},
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
import bp from '../../breakpoints';
import Api from '../../api';
import AccessorUtilities from '../../lib/utils/accessor';
import { FREQUENT_PROJECTS, HOUR_IN_MS, STORAGE_KEY } from '../constants';
Vue.use(VueResource);
export default class ProjectsService {
constructor(currentUserName) {
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.currentUserName = currentUserName;
this.storageKey = `${this.currentUserName}/${STORAGE_KEY}`;
this.projectsPath = Vue.resource(Api.buildUrl(Api.projectsPath));
}
getSearchedProjects(searchQuery) {
return this.projectsPath.get({
simple: false,
per_page: 20,
membership: !!gon.current_user_id,
order_by: 'last_activity_at',
search: searchQuery,
});
}
getFrequentProjects() {
if (this.isLocalStorageAvailable) {
return this.getTopFrequentProjects();
}
return null;
}
logProjectAccess(project) {
let matchFound = false;
let storedFrequentProjects;
if (this.isLocalStorageAvailable) {
const storedRawProjects = localStorage.getItem(this.storageKey);
// Check if there's any frequent projects list set
if (!storedRawProjects) {
// No frequent projects list set, set one up.
storedFrequentProjects = [];
storedFrequentProjects.push({ ...project, frequency: 1 });
} else {
// Check if project is already present in frequents list
// When found, update metadata of it.
storedFrequentProjects = JSON.parse(storedRawProjects).map((projectItem) => {
if (projectItem.id === project.id) {
matchFound = true;
const diff = Math.abs(project.lastAccessedOn - projectItem.lastAccessedOn) / HOUR_IN_MS;
const updatedProject = {
...project,
frequency: projectItem.frequency,
lastAccessedOn: projectItem.lastAccessedOn,
};
// Check if duration since last access of this project
// is over an hour
if (diff > 1) {
return {
...updatedProject,
frequency: updatedProject.frequency + 1,
lastAccessedOn: Date.now(),
};
}
return {
...updatedProject,
};
}
return projectItem;
});
// Check whether currently logged project is present in frequents list
if (!matchFound) {
// We always keep size of frequents collection to 20 projects
// out of which only 5 projects with
// highest value of `frequency` and most recent `lastAccessedOn`
// are shown in projects dropdown
if (storedFrequentProjects.length === FREQUENT_PROJECTS.MAX_COUNT) {
storedFrequentProjects.shift(); // Remove an item from head of array
}
storedFrequentProjects.push({ ...project, frequency: 1 });
}
}
localStorage.setItem(this.storageKey, JSON.stringify(storedFrequentProjects));
}
}
getTopFrequentProjects() {
const storedFrequentProjects = JSON.parse(localStorage.getItem(this.storageKey));
let frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_DESKTOP;
if (!storedFrequentProjects) {
return [];
}
if (bp.getBreakpointSize() === 'sm' ||
bp.getBreakpointSize() === 'xs') {
frequentProjectsCount = FREQUENT_PROJECTS.LIST_COUNT_MOBILE;
}
const frequentProjects = storedFrequentProjects
.filter(project => project.frequency >= FREQUENT_PROJECTS.ELIGIBLE_FREQUENCY);
// Sort all frequent projects in decending order of frequency
// and then by lastAccessedOn with recent most first
frequentProjects.sort((projectA, projectB) => {
if (projectA.frequency < projectB.frequency) {
return 1;
} else if (projectA.frequency > projectB.frequency) {
return -1;
} else if (projectA.lastAccessedOn < projectB.lastAccessedOn) {
return 1;
} else if (projectA.lastAccessedOn > projectB.lastAccessedOn) {
return -1;
}
return 0;
});
return _.first(frequentProjects, frequentProjectsCount);
}
}
export default class ProjectsStore {
constructor() {
this.state = {};
this.state.frequentProjects = [];
this.state.searchedProjects = [];
}
setFrequentProjects(rawProjects) {
this.state.frequentProjects = rawProjects;
}
getFrequentProjects() {
return this.state.frequentProjects;
}
setSearchedProjects(rawProjects) {
this.state.searchedProjects = rawProjects.map(rawProject => ({
id: rawProject.id,
name: rawProject.name,
namespace: rawProject.name_with_namespace,
webUrl: rawProject.web_url,
avatarUrl: rawProject.avatar_url,
}));
}
getSearchedProjects() {
return this.state.searchedProjects;
}
clearSearchedProjects() {
this.state.searchedProjects = [];
}
}
...@@ -75,18 +75,20 @@ export default { ...@@ -75,18 +75,20 @@ export default {
class="btn btn-small inline"> class="btn btn-small inline">
Check out branch Check out branch
</a> </a>
<span class="dropdown inline prepend-left-10"> <span class="dropdown prepend-left-10">
<a <a
class="btn btn-xs dropdown-toggle" class="btn btn-small inline dropdown-toggle"
data-toggle="dropdown" data-toggle="dropdown"
aria-label="Download as" aria-label="Download as"
role="button"> role="button">
<i <i
class="fa fa-download" class="fa fa-download"
aria-hidden="true" /> aria-hidden="true">
</i>
<i <i
class="fa fa-caret-down" class="fa fa-caret-down"
aria-hidden="true" /> aria-hidden="true">
</i>
</a> </a>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li> <li>
......
...@@ -9,6 +9,11 @@ export default { ...@@ -9,6 +9,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
sizeClass: {
type: String,
required: false,
default: 's40',
},
}, },
computed: { computed: {
/** /**
...@@ -38,7 +43,8 @@ export default { ...@@ -38,7 +43,8 @@ export default {
<template> <template>
<div <div
class="avatar s40 identicon" class="avatar identicon"
:class="sizeClass"
:style="identiconStyles"> :style="identiconStyles">
{{identiconTitle}} {{identiconTitle}}
</div> </div>
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
.append-right-default { margin-right: $gl-padding; } .append-right-default { margin-right: $gl-padding; }
.append-right-20 { margin-right: 20px; } .append-right-20 { margin-right: 20px; }
.append-bottom-0 { margin-bottom: 0; } .append-bottom-0 { margin-bottom: 0; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-10 { margin-bottom: 10px; } .append-bottom-10 { margin-bottom: 10px; }
.append-bottom-15 { margin-bottom: 15px; } .append-bottom-15 { margin-bottom: 15px; }
.append-bottom-20 { margin-bottom: 20px; } .append-bottom-20 { margin-bottom: 20px; }
......
...@@ -737,6 +737,8 @@ ...@@ -737,6 +737,8 @@
@mixin new-style-dropdown($selector: '') { @mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu, #{$selector}.dropdown-menu,
#{$selector}.dropdown-menu-nav { #{$selector}.dropdown-menu-nav {
margin-bottom: 24px;
li { li {
display: block; display: block;
padding: 0 1px; padding: 0 1px;
...@@ -768,7 +770,7 @@ ...@@ -768,7 +770,7 @@
// make sure the text color is not overriden // make sure the text color is not overriden
&.text-danger { &.text-danger {
@extend .text-danger; color: $brand-danger;
} }
&.is-focused, &.is-focused,
...@@ -777,6 +779,11 @@ ...@@ -777,6 +779,11 @@
&:focus { &:focus {
background-color: $dropdown-item-hover-bg; background-color: $dropdown-item-hover-bg;
color: $gl-text-color; color: $gl-text-color;
// make sure the text color is not overriden
&.text-danger {
color: $brand-danger;
}
} }
&.is-active { &.is-active {
...@@ -822,3 +829,152 @@ ...@@ -822,3 +829,152 @@
} }
@include new-style-dropdown('.js-namespace-select + '); @include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
@media (max-width: $screen-xs-max) {
display: table;
left: -50px;
min-width: 300px;
}
}
.projects-dropdown-container {
display: flex;
flex-direction: row;
width: 500px;
height: 334px;
.project-dropdown-sidebar,
.project-dropdown-content {
padding: 8px 0;
}
.loading-animation {
color: $almost-black;
}
.project-dropdown-sidebar {
width: 30%;
border-right: 1px solid $border-color;
}
.project-dropdown-content {
position: relative;
width: 70%;
}
@media (max-width: $screen-xs-max) {
flex-direction: column;
width: 100%;
height: auto;
flex: 1;
.project-dropdown-sidebar,
.project-dropdown-content {
width: 100%;
}
.project-dropdown-sidebar {
border-bottom: 1px solid $border-color;
border-right: 0;
}
}
}
.projects-dropdown-container {
.projects-list-frequent-container,
.projects-list-search-container, {
padding: 8px 0;
overflow-y: auto;
}
.section-header,
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
padding: 0 15px;
}
.section-header,
.projects-list-frequent-container li.section-empty,
.projects-list-search-container li.section-empty {
color: $gl-text-color-secondary;
font-size: $gl-font-size;
}
.projects-list-frequent-container,
.projects-list-search-container {
li.section-empty.section-failure {
color: $callout-danger-color;
}
}
.search-input-container {
position: relative;
padding: 4px $gl-padding;
.search-icon {
position: absolute;
top: 13px;
right: 25px;
color: $md-area-border;
}
}
.section-header {
font-weight: 700;
margin-top: 8px;
}
.projects-list-search-container {
height: 284px;
}
@media (max-width: $screen-xs-max) {
.projects-list-frequent-container {
width: auto;
height: auto;
padding-bottom: 0;
}
}
}
.projects-list-item-container {
.project-item-avatar-container
.project-item-metadata-container {
float: left;
}
.project-title,
.project-namespace {
max-width: 250px;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover {
.project-item-avatar-container .avatar {
border-color: $md-area-border;
}
}
.project-title {
font-size: $gl-font-size;
font-weight: 400;
line-height: 16px;
}
.project-namespace {
margin-top: 4px;
font-size: 12px;
line-height: 12px;
color: $gl-text-color-secondary;
}
@media (max-width: $screen-xs-max) {
.project-item-metadata-container {
float: none;
}
}
}
...@@ -267,14 +267,26 @@ ...@@ -267,14 +267,26 @@
// TODO: change global style // TODO: change global style
.ajax-project-dropdown, .ajax-project-dropdown,
.ajax-users-dropdown,
body[data-page="projects:edit"] #select2-drop,
body[data-page="projects:new"] #select2-drop, body[data-page="projects:new"] #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="admin:groups: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-radius: $border-radius-base;
color: $gl-text-color; color: $gl-text-color;
} }
&.select2-drop-above {
border-top: none;
margin-top: -4px;
}
.select2-results { .select2-results {
.select2-no-results, .select2-no-results,
.select2-searching, .select2-searching,
......
...@@ -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;
......
...@@ -617,6 +617,8 @@ ...@@ -617,6 +617,8 @@
} }
.issuable-actions { .issuable-actions {
@include new-style-dropdown;
padding-top: 10px; padding-top: 10px;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
......
...@@ -143,8 +143,12 @@ ul.related-merge-requests > li { ...@@ -143,8 +143,12 @@ ul.related-merge-requests > li {
} }
} }
.issue-form .select2-container { .issue-form {
width: 250px !important; @include new-style-dropdown;
.select2-container {
width: 250px !important;
}
} }
.issues-footer { .issues-footer {
......
...@@ -23,6 +23,8 @@ ...@@ -23,6 +23,8 @@
.new-note, .new-note,
.note-edit-form { .note-edit-form {
.note-form-actions { .note-form-actions {
@include new-style-dropdown;
position: relative; position: relative;
margin: $gl-padding 0 0; margin: $gl-padding 0 0;
} }
......
...@@ -800,8 +800,10 @@ pre.light-well { ...@@ -800,8 +800,10 @@ pre.light-well {
} }
} }
.new_protected_branch, .new-protected-branch,
.new-protected-tag { .new-protected-tag {
@include new-style-dropdown;
label { label {
margin-top: 6px; margin-top: 6px;
font-weight: $gl-font-weight-normal; font-weight: $gl-font-weight-normal;
...@@ -821,19 +823,9 @@ pre.light-well { ...@@ -821,19 +823,9 @@ pre.light-well {
.protected-branches-list, .protected-branches-list,
.protected-tags-list { .protected-tags-list {
margin-bottom: 30px; @include new-style-dropdown;
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
&.is-active { margin-bottom: 30px;
font-weight: $gl-font-weight-bold;
}
}
.settings-message { .settings-message {
margin: 0; margin: 0;
......
...@@ -36,6 +36,34 @@ module IssuableCollections ...@@ -36,6 +36,34 @@ module IssuableCollections
@merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder) @merge_requests_finder ||= issuable_finder_for(MergeRequestsFinder)
end end
def redirect_out_of_range(relation, total_pages)
return false if total_pages.zero?
out_of_range = relation.current_page > total_pages
if out_of_range
redirect_to(url_for(params.merge(page: total_pages, only_path: true)))
end
out_of_range
end
def issues_page_count(relation)
page_count_for_relation(relation, issues_finder.row_count)
end
def merge_requests_page_count(relation)
page_count_for_relation(relation, merge_requests_finder.row_count)
end
def page_count_for_relation(relation, row_count)
limit = relation.limit_value.to_f
return 1 if limit.zero?
(row_count.to_f / limit).ceil
end
def issuable_finder_for(finder_class) def issuable_finder_for(finder_class)
finder_class.new(current_user, filter_params) finder_class.new(current_user, filter_params)
end end
......
...@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -27,10 +27,9 @@ class Projects::IssuesController < Projects::ApplicationController
@issues = issues_collection @issues = issues_collection
@issues = @issues.page(params[:page]) @issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
@total_pages = issues_page_count(@issues)
if @issues.out_of_range? && @issues.total_pages != 0 return if redirect_out_of_range(@issues, @total_pages)
return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end
if params[:label_name].present? if params[:label_name].present?
@labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute @labels = LabelsFinder.new(current_user, project_id: @project.id, title: params[:label_name]).execute
......
...@@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -18,10 +18,9 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
@merge_requests = @merge_requests.page(params[:page]) @merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(merge_request_diff: :merge_request) @merge_requests = @merge_requests.preload(merge_request_diff: :merge_request)
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
@total_pages = merge_requests_page_count(@merge_requests)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 return if redirect_out_of_range(@merge_requests, @total_pages)
return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end
if params[:label_name].present? if params[:label_name].present?
labels_params = { project_id: @project.id, title: params[:label_name] } labels_params = { project_id: @project.id, title: params[:label_name] }
......
...@@ -61,6 +61,10 @@ class IssuableFinder ...@@ -61,6 +61,10 @@ class IssuableFinder
execute.find_by(*params) execute.find_by(*params)
end end
def row_count
Gitlab::IssuablesCountForState.new(self).for_state_or_opened(params[:state])
end
# We often get counts for each state by running a query per state, and # We often get counts for each state by running a query per state, and
# counting those results. This is typically slower than running one query # counting those results. This is typically slower than running one query
# (even if that query is slower than any of the individual state queries) and # (even if that query is slower than any of the individual state queries) and
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
# search: string # search: string
# label_name: string # label_name: string
# sort: string # sort: string
# my_reaction_emoji: string
# #
class IssuesFinder < IssuableFinder class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
# label_name: string # label_name: string
# sort: string # sort: string
# non_archived: boolean # non_archived: boolean
# my_reaction_emoji: string
# #
class MergeRequestsFinder < IssuableFinder class MergeRequestsFinder < IssuableFinder
def klass def klass
......
...@@ -240,7 +240,8 @@ module IssuablesHelper ...@@ -240,7 +240,8 @@ module IssuablesHelper
def issuables_count_for_state(issuable_type, state) def issuables_count_for_state(issuable_type, state)
finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend finder = public_send("#{issuable_type}_finder") # rubocop:disable GitlabSecurity/PublicSend
finder.count_by_state[state]
Gitlab::IssuablesCountForState.new(finder)[state]
end end
def close_issuable_url(issuable) def close_issuable_url(issuable)
......
...@@ -72,12 +72,6 @@ module ProjectsHelper ...@@ -72,12 +72,6 @@ module ProjectsHelper
output.html_safe output.html_safe
end end
if current_user
project_link << button_tag(type: 'button', class: 'dropdown-toggle-caret js-projects-dropdown-toggle', aria: { label: 'Toggle switch project dropdown' }, data: { target: '.js-dropdown-menu-projects', toggle: 'dropdown', order_by: 'last_activity_at' }) do
icon("chevron-down")
end
end
"#{namespace_link} / #{project_link}".html_safe "#{namespace_link} / #{project_link}".html_safe
end end
......
...@@ -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
......
...@@ -405,6 +405,6 @@ class Commit ...@@ -405,6 +405,6 @@ class Commit
end end
def gpg_commit def gpg_commit
@gpg_commit ||= Gitlab::Gpg::Commit.for_commit(self) @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
end end
end end
...@@ -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?
......
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
# #
module Issuable module Issuable
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::SQL::Pattern
include CacheMarkdownField include CacheMarkdownField
include Participable include Participable
include Mentionable include Mentionable
...@@ -122,7 +123,9 @@ module Issuable ...@@ -122,7 +123,9 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def search(query) def search(query)
where(arel_table[:title].matches("%#{query}%")) title = to_fuzzy_arel(:title, query)
where(title)
end end
# Searches for records with a matching title or description. # Searches for records with a matching title or description.
...@@ -133,10 +136,10 @@ module Issuable ...@@ -133,10 +136,10 @@ module Issuable
# #
# Returns an ActiveRecord::Relation. # Returns an ActiveRecord::Relation.
def full_search(query) def full_search(query)
t = arel_table title = to_fuzzy_arel(:title, query)
pattern = "%#{query}%" description = to_fuzzy_arel(:description, query)
where(t[:title].matches(pattern).or(t[:description].matches(pattern))) where(title&.or(description))
end end
def sort(method, excluded_labels: []) def sort(method, excluded_labels: [])
......
...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base ...@@ -56,7 +56,7 @@ class GpgKey < ActiveRecord::Base
def verified_user_infos def verified_user_infos
user_infos.select do |user_info| user_infos.select do |user_info|
user_info[:email] == user.email user.verified_email?(user_info[:email])
end end
end end
...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base ...@@ -64,13 +64,17 @@ class GpgKey < ActiveRecord::Base
user_infos.map do |user_info| user_infos.map do |user_info|
[ [
user_info[:email], user_info[:email],
user_info[:email] == user.email user.verified_email?(user_info[:email])
] ]
end.to_h end.to_h
end end
def verified? def verified?
emails_with_verified_status.any? { |_email, verified| verified } emails_with_verified_status.values.any?
end
def verified_and_belongs_to_email?(email)
emails_with_verified_status.fetch(email, false)
end end
def update_invalid_gpg_signatures def update_invalid_gpg_signatures
...@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base ...@@ -78,11 +82,14 @@ class GpgKey < ActiveRecord::Base
end end
def revoke def revoke
GpgSignature.where(gpg_key: self, valid_signature: true).update_all( GpgSignature
gpg_key_id: nil, .where(gpg_key: self)
valid_signature: false, .where.not(verification_status: GpgSignature.verification_statuses[:unknown_key])
updated_at: Time.zone.now .update_all(
) gpg_key_id: nil,
verification_status: GpgSignature.verification_statuses[:unknown_key],
updated_at: Time.zone.now
)
destroy destroy
end end
......
class GpgSignature < ActiveRecord::Base class GpgSignature < ActiveRecord::Base
include ShaAttribute include ShaAttribute
include IgnorableColumn
ignore_column :valid_signature
sha_attribute :commit_sha sha_attribute :commit_sha
sha_attribute :gpg_key_primary_keyid sha_attribute :gpg_key_primary_keyid
enum verification_status: {
unverified: 0,
verified: 1,
same_user_different_email: 2,
other_user: 3,
unverified_key: 4,
unknown_key: 5
}
belongs_to :project belongs_to :project
belongs_to :gpg_key belongs_to :gpg_key
...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base ...@@ -20,6 +32,6 @@ class GpgSignature < ActiveRecord::Base
end end
def gpg_commit def gpg_commit
Gitlab::Gpg::Commit.new(project, commit_sha) Gitlab::Gpg::Commit.new(commit)
end end
end end
...@@ -166,32 +166,25 @@ class Repository ...@@ -166,32 +166,25 @@ class Repository
end end
def add_branch(user, branch_name, ref) def add_branch(user, branch_name, ref)
newrev = commit(ref).try(:sha) branch = raw_repository.add_branch(branch_name, committer: user, target: ref)
return false unless newrev
Gitlab::Git::OperationService.new(user, raw_repository).add_branch(branch_name, newrev)
after_create_branch after_create_branch
find_branch(branch_name)
branch
rescue Gitlab::Git::Repository::InvalidRef
false
end end
def add_tag(user, tag_name, target, message = nil) def add_tag(user, tag_name, target, message = nil)
newrev = commit(target).try(:id) raw_repository.add_tag(tag_name, committer: user, target: target, message: message)
options = { message: message, tagger: user_to_committer(user) } if message rescue Gitlab::Git::Repository::InvalidRef
false
return false unless newrev
Gitlab::Git::OperationService.new(user, raw_repository).add_tag(tag_name, newrev, options)
find_tag(tag_name)
end end
def rm_branch(user, branch_name) def rm_branch(user, branch_name)
before_remove_branch before_remove_branch
branch = find_branch(branch_name)
Gitlab::Git::OperationService.new(user, raw_repository).rm_branch(branch) raw_repository.rm_branch(branch_name, committer: user)
after_remove_branch after_remove_branch
true true
...@@ -199,9 +192,8 @@ class Repository ...@@ -199,9 +192,8 @@ class Repository
def rm_tag(user, tag_name) def rm_tag(user, tag_name)
before_remove_tag before_remove_tag
tag = find_tag(tag_name)
Gitlab::Git::OperationService.new(user, raw_repository).rm_tag(tag) raw_repository.rm_tag(tag_name, committer: user)
after_remove_tag after_remove_tag
true true
......
...@@ -1041,6 +1041,10 @@ class User < ActiveRecord::Base ...@@ -1041,6 +1041,10 @@ class User < ActiveRecord::Base
ensure_rss_token! ensure_rss_token!
end end
def verified_email?(email)
self.email == email
end
protected protected
# override, from Devise::Validatable # override, from Devise::Validatable
......
...@@ -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
......
%ul.list-unstyled.navbar-sub-nav %ul.list-unstyled.navbar-sub-nav
= nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "home"}) do = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do %a{ href: '#', title: 'Projects', data: { toggle: 'dropdown' } }
Projects Projects
= icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu.projects-dropdown-menu
= render "layouts/nav/projects_dropdown/show"
= nav_link(controller: ['dashboard/groups', 'explore/groups']) do = nav_link(controller: ['dashboard/groups', 'explore/groups']) do
= link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do = link_to dashboard_groups_path, class: 'dashboard-shortcuts-groups', title: 'Groups' do
Groups Groups
= nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm" }) do = nav_link(path: 'dashboard#activity', html_options: { class: "hidden-xs hidden-sm hidden-md" }) do
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
Activity Activity
...@@ -17,7 +20,7 @@ ...@@ -17,7 +20,7 @@
= icon("chevron-down", class: "dropdown-chevron") = icon("chevron-down", class: "dropdown-chevron")
.dropdown-menu .dropdown-menu
%ul %ul
= nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm" }) do = nav_link(path: 'dashboard#activity', html_options: { class: "visible-xs visible-sm visible-md" }) do
= link_to activity_dashboard_path, title: 'Activity' do = link_to activity_dashboard_path, title: 'Activity' do
Activity Activity
...@@ -31,3 +34,8 @@ ...@@ -31,3 +34,8 @@
%li.divider %li.divider
%li %li
= link_to "Help", help_path, title: 'About GitLab CE' = link_to "Help", help_path, title: 'About GitLab CE'
-# Shortcut to Dashboard > Projects
%li.hidden
= link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
Projects
- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: @project.web_url, avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar
%ul
= nav_link(path: 'dashboard/projects#index') do
= link_to dashboard_projects_path do
= _('Your projects')
= nav_link(path: 'projects#starred') do
= link_to starred_dashboard_projects_path do
= _('Starred projects')
= nav_link(path: 'projects#trending') do
= link_to explore_root_path do
= _('Explore projects')
.project-dropdown-content
#js-projects-dropdown{ data: { user_name: current_user.username, project: project_meta } }
...@@ -14,12 +14,4 @@ ...@@ -14,12 +14,4 @@
:javascript :javascript
window.uploads_path = "#{project_uploads_path(project)}"; window.uploads_path = "#{project_uploads_path(project)}";
- content_for :header_content do
.js-dropdown-menu-projects
.dropdown-menu.dropdown-select.dropdown-menu-projects
= dropdown_title("Go to a project")
= dropdown_filter("Search your projects")
= dropdown_content
= dropdown_loading
= render template: "layouts/application" = render template: "layouts/application"
- title = capture do - title = capture do
.gpg-popover-icon.invalid This commit was signed with a different user's verified signature.
= render 'shared/icons/icon_status_notfound_borderless.svg'
%div
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_classes: ['invalid'] } - locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals = render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a verified signature, but the committer email
is <strong>not verified</strong> to belong to the same user.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: ['invalid'], icon: 'icon_status_notfound_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
- if signature - if signature
- if signature.valid_signature? = render partial: "projects/commit/#{signature.verification_status}_signature_badge", locals: { signature: signature }
= render partial: 'projects/commit/valid_signature_badge', locals: { signature: signature }
- else
= render partial: 'projects/commit/invalid_signature_badge', locals: { signature: signature }
- css_classes = commit_signature_badge_classes(css_classes) - signature = local_assigns.fetch(:signature)
- title = local_assigns.fetch(:title)
- label = local_assigns.fetch(:label)
- css_class = local_assigns.fetch(:css_class)
- icon = local_assigns.fetch(:icon)
- show_user = local_assigns.fetch(:show_user, false)
- css_classes = commit_signature_badge_classes(css_class)
- title = capture do - title = capture do
.gpg-popover-status .gpg-popover-status
= title .gpg-popover-icon{ class: css_class }
= render "shared/icons/#{icon}.svg"
%div
= title
- content = capture do - content = capture do
.clearfix - if show_user
= content .clearfix
= render partial: 'projects/commit/signature_badge_user', locals: { signature: signature }
GPG Key ID: GPG Key ID:
%span.monospace= signature.gpg_key_primary_keyid %span.monospace= signature.gpg_key_primary_keyid
= link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link') = link_to('Learn more about signing commits', help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link')
%button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } } %button{ class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'auto top', title: title, content: content } }
......
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= user.name
%div= user.to_reference
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
= render partial: 'projects/commit/unverified_signature_badge', locals: { signature: signature }
- title = capture do
This commit was signed with an <strong>unverified</strong> signature.
- locals = { signature: signature, title: title, label: 'Unverified', css_class: 'invalid', icon: 'icon_status_notfound_borderless' }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
.gpg-popover-icon.valid
= render 'shared/icons/icon_status_success_borderless.svg'
%div
This commit was signed with a <strong>verified</strong> signature.
- content = capture do
- gpg_key = signature.gpg_key
- user = gpg_key&.user
- user_name = signature.gpg_key_user_name
- user_email = signature.gpg_key_user_email
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
%div
= user_avatar_without_link(user: user, size: 32)
%div
%strong= gpg_key.user.name
%div @#{gpg_key.user.username}
- else
= mail_to user_email do
%div
= user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
%div
%strong= user_name
%div= user_email
- locals = { signature: signature, title: title, content: content, label: 'Verified', css_classes: ['valid'] }
= render partial: 'projects/commit/signature_badge', locals: locals
- title = capture do
This commit was signed with a <strong>verified</strong> signature and the
committer email is verified to belong to the same user.
- locals = { signature: signature, title: title, label: 'Verified', css_class: 'valid', icon: 'icon_status_success_borderless', show_user: true }
= render partial: 'projects/commit/signature_badge', locals: locals
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
= render 'shared/empty_states/issues' = render 'shared/empty_states/issues'
- if @issues.present? - if @issues.present?
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab", total_pages: @total_pages
...@@ -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
......
...@@ -5,4 +5,4 @@ ...@@ -5,4 +5,4 @@
= render 'shared/empty_states/merge_requests' = render 'shared/empty_states/merge_requests'
- if @merge_requests.present? - if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab", total_pages: @total_pages
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
%template{ 'v-if' => 'isResolved' } %template{ 'v-if' => 'isResolved' }
= render 'shared/icons/icon_status_success_solid.svg' = render 'shared/icons/icon_status_success_solid.svg'
%template{ 'v-else' => '' } %template{ 'v-else' => '' }
= render 'shared/icons/icon_status_success.svg' = render 'shared/icons/icon_resolve_discussion.svg'
- if current_user - if current_user
- if note.emoji_awardable? - if note.emoji_awardable?
......
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg>
<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill-rule="evenodd"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></svg> <svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
...@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker ...@@ -6,7 +6,11 @@ class CreateGpgSignatureWorker
project = Project.find_by(id: project_id) project = Project.find_by(id: project_id)
return unless project return unless project
commit = project.commit(commit_sha)
return unless commit
# This calculates and caches the signature in the database # This calculates and caches the signature in the database
Gitlab::Gpg::Commit.new(project, commit_sha).signature Gitlab::Gpg::Commit.new(commit).signature
end end
end end
...@@ -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: Add dropdown to Projects nav item
merge_request: 13866
author:
type: added
---
title: Remove project select dropdown from breadcrumb
merge_request: 14010
author:
type: changed
---
title: Fix new navigation wrapping and causing height to grow
merge_request:
author:
type: fixed
---
title: Deprecate custom SSH client configuration for the git user
merge_request: 13930
author:
type: deprecated
---
title: Fix buttons with different height in merge request widget
merge_request:
author:
type: fixed
---
title: Fix broken svg in jobs dropdown for success status
merge_request:
author:
type: fixed
---
title: Added support the multiple time series for prometheus monitoring
merge_request: !36893
author:
type: changed
---
title: 'API: Add GPG key management'
merge_request: 13828
author: Robert Schilling
type: added
---
title: Add branch existence check to the APIv4 branches via HEAD request
merge_request: 13979
author: Vitaliy @blackst0ne Klachkov
type: added
---
title: Fixed add diff note button not showing after deleting a comment
merge_request:
author:
type: fixed
---
title: Add badge for dependency status
merge_request: 13588
author: Markus Koller
type: other
---
title: 'Update the GPG verification semantics: A GPG signature must additionally match
the committer in order to be verified'
merge_request: 13771
author: Alexis Reigel
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
---
title: Support a multi-word fuzzy seach issues/merge requests on search bar
merge_request: 13780
author: Hiroyuki Sato
type: changed
---
title: Add my_reaction_emoji param to /issues and /merge_requests API
merge_request: 14016
author: Hiroyuki Sato
type: added
---
title: Re-use issue/MR counts for the pagination system
merge_request:
author:
type: other
...@@ -30,7 +30,7 @@ var config = { ...@@ -30,7 +30,7 @@ var config = {
blob: './blob_edit/blob_bundle.js', blob: './blob_edit/blob_bundle.js',
boards: './boards/boards_bundle.js', boards: './boards/boards_bundle.js',
common: './commons/index.js', common: './commons/index.js',
common_vue: ['vue', './vue_shared/common_vue.js'], common_vue: './vue_shared/vue_resource_interceptor.js',
common_d3: ['d3'], common_d3: ['d3'],
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js', cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
......
class AddVerificationStatusToGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
include Gitlab::Database::MigrationHelpers
disable_ddl_transaction!
def up
# First we remove all signatures because we need to re-verify them all
# again anyway (because of the updated verification logic).
#
# This makes adding the column with default values faster
truncate(:gpg_signatures)
add_column_with_default(:gpg_signatures, :verification_status, :smallint, default: 0)
end
def down
remove_column(:gpg_signatures, :verification_status)
end
end
class AddFailureReasonToCiBuilds < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def change
add_column :ci_builds, :failure_reason, :integer
end
end
class DestroyGpgSignatures < ActiveRecord::Migration
DOWNTIME = false
def up
truncate(:gpg_signatures)
end
def down
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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