Commit 82d20621 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'ce-to-ee-2017-09-13' into 'master'

CE upstream - Wednesday

Closes gitlab-ce#37652, gitaly#519, gitlab-ce#37653, #3389, gitlab-ce#37648 et gitlab-ce#35990

See merge request gitlab-org/gitlab-ee!2906
parents 5f9c1a0c bc84d069
...@@ -435,6 +435,7 @@ db:migrate:reset-mysql: ...@@ -435,6 +435,7 @@ db:migrate:reset-mysql:
.migration-paths: &migration-paths .migration-paths: &migration-paths
<<: *dedicated-runner <<: *dedicated-runner
<<: *pull-cache <<: *pull-cache
<<: *except-docs
stage: test stage: test
variables: variables:
SETUP_DB: "false" SETUP_DB: "false"
......
...@@ -645,7 +645,7 @@ Metrics/ClassLength: ...@@ -645,7 +645,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method. # of test cases needed to validate a method.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Enabled: true Enabled: true
Max: 14 Max: 13
# Limit lines to 80 characters. # Limit lines to 80 characters.
Metrics/LineLength: Metrics/LineLength:
...@@ -667,7 +667,7 @@ Metrics/ParameterLists: ...@@ -667,7 +667,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader. # A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Enabled: true Enabled: true
Max: 17 Max: 15
# Lint ######################################################################## # Lint ########################################################################
......
...@@ -422,7 +422,7 @@ request is as follows: ...@@ -422,7 +422,7 @@ request is as follows:
1. Fork the project into your personal space on GitLab.com 1. Fork the project into your personal space on GitLab.com
1. Create a feature branch, branch away from `master` 1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code 1. Write [tests](https://docs.gitlab.com/ee/development/rake_tasks.html#run-tests) and code
1. [Generate a changelog entry with `bin/changelog`][changelog] 1. [Generate a changelog entry with `bin/changelog`][changelog]
1. If you are writing documentation, make sure to follow the 1. If you are writing documentation, make sure to follow the
[documentation styleguide][doc-styleguide] [documentation styleguide][doc-styleguide]
......
...@@ -423,4 +423,4 @@ gem 'flipper-active_record', '~> 0.10.2' ...@@ -423,4 +423,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging # Structured logging
gem 'lograge', '~> 0.5' gem 'lograge', '~> 0.5'
gem 'grape_logging', '~> 1.6' gem 'grape_logging', '~> 1.7'
...@@ -380,7 +380,7 @@ GEM ...@@ -380,7 +380,7 @@ GEM
activesupport activesupport
grape (>= 0.16.0) grape (>= 0.16.0)
rake rake
grape_logging (1.6.0) grape_logging (1.7.0)
grape grape
grpc (1.4.5) grpc (1.4.5)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
...@@ -1073,7 +1073,7 @@ DEPENDENCIES ...@@ -1073,7 +1073,7 @@ DEPENDENCIES
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.6) grape_logging (~> 1.7)
gssapi gssapi
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
......
...@@ -12,4 +12,5 @@ import 'core-js/fn/symbol'; ...@@ -12,4 +12,5 @@ import 'core-js/fn/symbol';
// Browser polyfills // Browser polyfills
import './polyfills/custom_event'; import './polyfills/custom_event';
import './polyfills/element'; import './polyfills/element';
import './polyfills/event';
import './polyfills/nodelist'; import './polyfills/nodelist';
if (typeof window.CustomEvent !== 'function') { if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(event, params) { window.CustomEvent = function CustomEvent(event, params) {
const evt = document.createEvent('CustomEvent'); const evt = document.createEvent('CustomEvent');
const evtParams = params || { bubbles: false, cancelable: false, detail: undefined }; const evtParams = {
bubbles: false,
cancelable: false,
detail: undefined,
...params,
};
evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail); evt.initCustomEvent(event, evtParams.bubbles, evtParams.cancelable, evtParams.detail);
return evt; return evt;
}; };
......
/**
* Polyfill for IE11 support.
* new Event() is not supported by IE11.
* Although `initEvent` is deprecated for modern browsers it is the one supported by IE
*/
if (typeof window.Event !== 'function') {
window.Event = function Event(event, params) {
const evt = document.createEvent('Event');
const evtParams = {
bubbles: false,
cancelable: false,
...params,
};
evt.initEvent(event, evtParams.bubbles, evtParams.cancelable);
return evt;
};
window.Event.prototype = Event;
}
...@@ -148,7 +148,7 @@ export const documentMouseMove = (e) => { ...@@ -148,7 +148,7 @@ export const documentMouseMove = (e) => {
export const subItemsMouseLeave = (relatedTarget) => { export const subItemsMouseLeave = (relatedTarget) => {
clearTimeout(timeoutId); clearTimeout(timeoutId);
if (!relatedTarget.closest(`.${IS_OVER_CLASS}`)) { if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
hideMenu(currentOpenMenu); hideMenu(currentOpenMenu);
} }
}; };
......
...@@ -72,10 +72,6 @@ export default { ...@@ -72,10 +72,6 @@ export default {
required: false, required: false,
default: () => [], default: () => [],
}, },
isConfidential: {
type: Boolean,
required: true,
},
markdownPreviewPath: { markdownPreviewPath: {
type: String, type: String,
required: true, required: true,
...@@ -131,7 +127,6 @@ export default { ...@@ -131,7 +127,6 @@ export default {
this.showForm = true; this.showForm = true;
this.store.setFormState({ this.store.setFormState({
title: this.state.titleText, title: this.state.titleText,
confidential: this.isConfidential,
description: this.state.descriptionText, description: this.state.descriptionText,
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
...@@ -147,8 +142,6 @@ export default { ...@@ -147,8 +142,6 @@ export default {
.then((data) => { .then((data) => {
if (location.pathname !== data.web_url) { if (location.pathname !== data.web_url) {
gl.utils.visitUrl(data.web_url); gl.utils.visitUrl(data.web_url);
} else if (data.confidential !== this.isConfidential) {
gl.utils.visitUrl(location.pathname);
} }
return this.service.getData(); return this.service.getData();
......
<script>
export default {
props: {
formState: {
type: Object,
required: true,
},
},
};
</script>
<template>
<fieldset class="checkbox">
<label for="issue-confidential">
<input
type="checkbox"
value="1"
id="issue-confidential"
v-model="formState.confidential" />
This issue is confidential and should only be visible to team members with at least Reporter access.
</label>
</fieldset>
</template>
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
import descriptionField from './fields/description.vue'; import descriptionField from './fields/description.vue';
import editActions from './edit_actions.vue'; import editActions from './edit_actions.vue';
import descriptionTemplate from './fields/description_template.vue'; import descriptionTemplate from './fields/description_template.vue';
import confidentialCheckbox from './fields/confidential_checkbox.vue';
export default { export default {
props: { props: {
...@@ -44,7 +43,6 @@ ...@@ -44,7 +43,6 @@
descriptionField, descriptionField,
descriptionTemplate, descriptionTemplate,
editActions, editActions,
confidentialCheckbox,
}, },
computed: { computed: {
hasIssuableTemplates() { hasIssuableTemplates() {
...@@ -81,8 +79,6 @@ ...@@ -81,8 +79,6 @@
:form-state="formState" :form-state="formState"
:markdown-preview-path="markdownPreviewPath" :markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath" /> :markdown-docs-path="markdownDocsPath" />
<confidential-checkbox
:form-state="formState" />
<edit-actions <edit-actions
:form-state="formState" :form-state="formState"
:can-destroy="canDestroy" /> :can-destroy="canDestroy" />
......
...@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -35,7 +35,6 @@ document.addEventListener('DOMContentLoaded', () => {
initialDescriptionHtml: this.initialDescriptionHtml, initialDescriptionHtml: this.initialDescriptionHtml,
initialDescriptionText: this.initialDescriptionText, initialDescriptionText: this.initialDescriptionText,
issuableTemplates: this.issuableTemplates, issuableTemplates: this.issuableTemplates,
isConfidential: this.isConfidential,
markdownPreviewPath: this.markdownPreviewPath, markdownPreviewPath: this.markdownPreviewPath,
markdownDocsPath: this.markdownDocsPath, markdownDocsPath: this.markdownDocsPath,
projectPath: this.projectPath, projectPath: this.projectPath,
......
...@@ -3,7 +3,6 @@ export default class Store { ...@@ -3,7 +3,6 @@ export default class Store {
this.state = initialState; this.state = initialState;
this.formState = { this.formState = {
title: '', title: '',
confidential: false,
description: '', description: '',
lockedWarningVisible: false, lockedWarningVisible: false,
updateLoading: false, updateLoading: false,
......
...@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) { ...@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0; let relevantDigits = 0;
let formattedNumber = ''; let formattedNumber = '';
if (!isNaN(Number(number))) { if (!isNaN(Number(number))) {
digitsLeft = number.split('.')[0]; digitsLeft = number.toString().split('.')[0];
switch (digitsLeft.length) { switch (digitsLeft.length) {
case 1: case 1:
relevantDigits = 3; relevantDigits = 3;
......
<script> <script>
/* global Flash */ /* global Flash */
import _ from 'underscore'; import _ from 'underscore';
import statusCodes from '../../lib/utils/http_status';
import MonitoringService from '../services/monitoring_service'; import MonitoringService from '../services/monitoring_service';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import Graph from './graph.vue'; import Graph from './graph.vue';
...@@ -21,10 +20,9 @@ ...@@ -21,10 +20,9 @@
hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics), hasMetrics: gl.utils.convertPermissionToBoolean(metricsData.hasMetrics),
documentationPath: metricsData.documentationPath, documentationPath: metricsData.documentationPath,
settingsPath: metricsData.settingsPath, settingsPath: metricsData.settingsPath,
endpoint: metricsData.additionalMetrics, metricsEndpoint: metricsData.additionalMetrics,
deploymentEndpoint: metricsData.deploymentEndpoint, deploymentEndpoint: metricsData.deploymentEndpoint,
showEmptyState: true, showEmptyState: true,
backOffRequestCounter: 0,
updateAspectRatio: false, updateAspectRatio: false,
updatedAspectRatios: 0, updatedAspectRatios: 0,
resizeThrottled: {}, resizeThrottled: {},
...@@ -39,50 +37,16 @@ ...@@ -39,50 +37,16 @@
methods: { methods: {
getGraphsData() { getGraphsData() {
const maxNumberOfRequests = 3;
this.state = 'loading'; this.state = 'loading';
gl.utils.backOff((next, stop) => { Promise.all([
this.service.get().then((resp) => { this.service.getGraphsData()
if (resp.status === statusCodes.NO_CONTENT) { .then(data => this.store.storeMetrics(data)),
this.backOffRequestCounter = this.backOffRequestCounter += 1; this.service.getDeploymentData()
if (this.backOffRequestCounter < maxNumberOfRequests) { .then(data => this.store.storeDeploymentData(data))
next(); .catch(() => new Flash('Error getting deployment information.')),
} else { ])
stop(new Error('Failed to connect to the prometheus server')); .then(() => { this.showEmptyState = false; })
} .catch(() => { this.state = 'unableToConnect'; });
} else {
stop(resp);
}
}).catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.state = 'unableToConnect';
return false;
}
return resp.json();
})
.then((metricGroupsData) => {
if (!metricGroupsData) return false;
this.store.storeMetrics(metricGroupsData.data);
return this.getDeploymentData();
})
.then((deploymentData) => {
if (deploymentData !== false) {
this.store.storeDeploymentData(deploymentData.deployments);
this.showEmptyState = false;
}
return {};
})
.catch(() => {
this.state = 'unableToConnect';
});
},
getDeploymentData() {
return this.service.getDeploymentData(this.deploymentEndpoint)
.then(resp => resp.json())
.catch(() => new Flash('Error getting deployment information.'));
}, },
resize() { resize() {
...@@ -99,7 +63,10 @@ ...@@ -99,7 +63,10 @@
}, },
created() { created() {
this.service = new MonitoringService(this.endpoint); this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
deploymentEndpoint: this.deploymentEndpoint,
});
eventHub.$on('toggleAspectRatio', this.toggleAspectRatio); eventHub.$on('toggleAspectRatio', this.toggleAspectRatio);
}, },
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
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 GraphPath from './graph_path.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';
...@@ -40,8 +40,6 @@ ...@@ -40,8 +40,6 @@
graphHeightOffset: 120, graphHeightOffset: 120,
margin: {}, margin: {},
unitOfDisplay: '', unitOfDisplay: '',
areaColorRgb: '#8fbce8',
lineColorRgb: '#1f78d1',
yAxisLabel: '', yAxisLabel: '',
legendTitle: '', legendTitle: '',
reducedDeploymentData: [], reducedDeploymentData: [],
...@@ -63,7 +61,7 @@ ...@@ -63,7 +61,7 @@
GraphLegend, GraphLegend,
GraphFlag, GraphFlag,
GraphDeployment, GraphDeployment,
monitoringPaths, GraphPath,
}, },
computed: { computed: {
...@@ -143,7 +141,7 @@ ...@@ -143,7 +141,7 @@
}, },
renderAxesPaths() { renderAxesPaths() {
this.timeSeries = createTimeSeries(this.graphData.queries[0].result, this.timeSeries = createTimeSeries(this.graphData.queries[0],
this.graphWidth, this.graphWidth,
this.graphHeight, this.graphHeight,
this.graphHeightOffset); this.graphHeightOffset);
...@@ -162,7 +160,7 @@ ...@@ -162,7 +160,7 @@
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
.ticks(measurements.xTicks) .ticks(d3.time.minute, 60)
.tickFormat(timeScaleFormat) .tickFormat(timeScaleFormat)
.orient('bottom'); .orient('bottom');
...@@ -238,7 +236,7 @@ ...@@ -238,7 +236,7 @@
class="graph-data" class="graph-data"
:viewBox="innerViewBox" :viewBox="innerViewBox"
ref="graphData"> ref="graphData">
<monitoring-paths <graph-path
v-for="(path, index) in timeSeries" v-for="(path, index) in timeSeries"
:key="index" :key="index"
:generated-line-path="path.linePath" :generated-line-path="path.linePath"
...@@ -246,7 +244,7 @@ ...@@ -246,7 +244,7 @@
:line-color="path.lineColor" :line-color="path.lineColor"
:area-color="path.areaColor" :area-color="path.areaColor"
/> />
<monitoring-deployment <graph-deployment
:show-deploy-info="showDeployInfo" :show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData" :deployment-data="reducedDeploymentData"
:graph-height="graphHeight" :graph-height="graphHeight"
......
...@@ -81,6 +81,13 @@ ...@@ -81,6 +81,13 @@
formatMetricUsage(series) { formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`; return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
}, },
createSeriesString(index, series) {
if (series.metricTag) {
return `${series.metricTag} ${this.formatMetricUsage(series)}`;
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
}, },
mounted() { mounted() {
this.$nextTick(() => { this.$nextTick(() => {
...@@ -164,7 +171,7 @@ ...@@ -164,7 +171,7 @@
ref="legendTitleSvg" ref="legendTitleSvg"
x="38" x="38"
:y="graphHeight - 30"> :y="graphHeight - 30">
{{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}} {{createSeriesString(index, series)}}
</text> </text>
<text <text
v-else v-else
......
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource'; import VueResource from 'vue-resource';
import statusCodes from '../../lib/utils/http_status';
Vue.use(VueResource); Vue.use(VueResource);
const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) {
let requestCounter = 0;
return gl.utils.backOff((next, stop) => {
makeRequestCallback().then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
requestCounter += 1;
if (requestCounter < MAX_REQUESTS) {
next();
} else {
stop(new Error('Failed to connect to the prometheus server'));
}
} else {
stop(resp);
}
}).catch(stop);
});
}
export default class MonitoringService { export default class MonitoringService {
constructor(endpoint) { constructor({ metricsEndpoint, deploymentEndpoint }) {
this.graphs = Vue.resource(endpoint); this.metricsEndpoint = metricsEndpoint;
this.deploymentEndpoint = deploymentEndpoint;
} }
get() { getGraphsData() {
return this.graphs.get(); return backOffRequest(() => Vue.http.get(this.metricsEndpoint))
.then(resp => resp.json())
.then((response) => {
if (!response || !response.data) {
throw new Error('Unexpected metrics data response from prometheus endpoint');
}
return response.data;
});
} }
// eslint-disable-next-line class-methods-use-this getDeploymentData() {
getDeploymentData(endpoint) { return backOffRequest(() => Vue.http.get(this.deploymentEndpoint))
return Vue.http.get(endpoint); .then(resp => resp.json())
.then((response) => {
if (!response || !response.deployments) {
throw new Error('Unexpected deployment data response from prometheus endpoint');
}
return response.deployments;
});
} }
} }
import d3 from 'd3'; import d3 from 'd3';
import _ from 'underscore'; import _ from 'underscore';
export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) { const defaultColorPalette = {
const maxValues = seriesData.map((timeSeries, index) => { blue: ['#1f78d1', '#8fbce8'],
orange: ['#fc9403', '#feca81'],
red: ['#db3b21', '#ed9d90'],
green: ['#1aaa55', '#8dd5aa'],
purple: ['#6666c4', '#d1d1f0'],
};
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
let usedColors = [];
function pickColor(name) {
let pick;
if (name && defaultColorPalette[name]) {
pick = name;
} else {
const unusedColors = _.difference(defaultColorOrder, usedColors);
if (unusedColors.length > 0) {
pick = unusedColors[0];
} else {
usedColors = [];
pick = defaultColorOrder[0];
}
}
usedColors.push(pick);
return defaultColorPalette[pick];
}
const maxValues = queryData.result.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value)); const maxValue = d3.max(timeSeries.values.map(d => d.value));
return { return {
maxValue, maxValue,
...@@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr ...@@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
const maxValueFromSeries = _.max(maxValues, val => val.maxValue); const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
let timeSeriesNumber = 1; return queryData.result.map((timeSeries, timeSeriesNumber) => {
let lineColor = '#1f78d1'; let metricTag = '';
let areaColor = '#8fbce8'; let lineColor = '';
return seriesData.map((timeSeries) => { let areaColor = '';
const timeSeriesScaleX = d3.time.scale() const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]); .range([0, graphWidth - 70]);
...@@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr ...@@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
.range([graphHeight - graphHeightOffset, 0]); .range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time)); timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]); timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line() const lineFunction = d3.svg.line()
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value)); .y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area() const areaFunction = d3.svg.area()
.interpolate('linear')
.x(d => timeSeriesScaleX(d.time)) .x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset) .y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value)) .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) { const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
timeSeriesNumber = timeSeriesNumber += 1; const seriesCustomizationData = queryData.series != null &&
_.findWhere(queryData.series[0].when,
{ value: timeSeriesMetricLabel });
if (seriesCustomizationData != null) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else { } else {
timeSeriesNumber = 1; metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`;
[lineColor, areaColor] = pickColor();
} }
return { return {
...@@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr ...@@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
values: timeSeries.values, values: timeSeries.values,
lineColor, lineColor,
areaColor, areaColor,
metricTag,
}; };
}); });
} }
...@@ -18,6 +18,11 @@ export default class NewNavSidebar { ...@@ -18,6 +18,11 @@ export default class NewNavSidebar {
} }
bindEvents() { bindEvents() {
document.addEventListener('click', (e) => {
if (!e.target.closest('.nav-sidebar') && (bp.getBreakpointSize() === 'sm' || bp.getBreakpointSize() === 'md')) {
this.toggleCollapsedSidebar(true);
}
});
this.$openSidebar.on('click', () => this.toggleSidebarNav(true)); this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
this.$closeSidebar.on('click', () => this.toggleSidebarNav(false)); this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
this.$overlay.on('click', () => this.toggleSidebarNav(false)); this.$overlay.on('click', () => this.toggleSidebarNav(false));
...@@ -58,7 +63,7 @@ export default class NewNavSidebar { ...@@ -58,7 +63,7 @@ export default class NewNavSidebar {
if (breakpoint === 'sm' || breakpoint === 'md') { if (breakpoint === 'sm' || breakpoint === 'md') {
this.toggleCollapsedSidebar(true); this.toggleCollapsedSidebar(true);
} else if (breakpoint === 'lg') { } else if (breakpoint === 'lg') {
const collapse = this.$sidebar.hasClass('sidebar-icons-only'); const collapse = Cookies.get('sidebar_collapsed') === 'true';
this.toggleCollapsedSidebar(collapse); this.toggleCollapsedSidebar(collapse);
} }
} }
......
gl-emoji { gl-emoji {
font-style: normal;
display: inline-flex; display: inline-flex;
vertical-align: middle; vertical-align: middle;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
......
...@@ -17,8 +17,11 @@ ...@@ -17,8 +17,11 @@
max-width: $limited-layout-width-sm; max-width: $limited-layout-width-sm;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-top: 64px;
padding-bottom: 64px; @media (min-width: $screen-md-min) {
padding-top: 64px;
padding-bottom: 64px;
}
} }
} }
......
...@@ -431,6 +431,7 @@ header.navbar-gitlab-new { ...@@ -431,6 +431,7 @@ header.navbar-gitlab-new {
.breadcrumb-item-text { .breadcrumb-item-text {
@include str-truncated(128px); @include str-truncated(128px);
text-decoration: inherit;
} }
.breadcrumbs-list-angle { .breadcrumbs-list-angle {
......
...@@ -99,6 +99,13 @@ $new-sidebar-collapsed-width: 50px; ...@@ -99,6 +99,13 @@ $new-sidebar-collapsed-width: 50px;
box-shadow: inset -2px 0 0 $border-color; box-shadow: inset -2px 0 0 $border-color;
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
&:not(.sidebar-icons-only) {
@media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
box-shadow: inset -2px 0 0 $border-color,
2px 1px 3px $dropdown-shadow-color;
}
}
&.sidebar-icons-only { &.sidebar-icons-only {
width: $new-sidebar-collapsed-width; width: $new-sidebar-collapsed-width;
......
...@@ -608,7 +608,7 @@ ...@@ -608,7 +608,7 @@
+ .files, + .files,
+ .alert { + .alert {
margin-top: 30px; margin-top: 32px;
} }
} }
} }
......
...@@ -7,11 +7,11 @@ module Ci ...@@ -7,11 +7,11 @@ module Ci
def create def create
@content = params[:content] @content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content) @error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank? @status = @error.blank?
if @error.blank? if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content) @config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages @stages = @config_processor.stages
@builds = @config_processor.builds @builds = @config_processor.builds
@jobs = @config_processor.jobs @jobs = @config_processor.jobs
......
...@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController ...@@ -48,7 +48,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
ProjectsFinder ProjectsFinder
.new(params: finder_params, current_user: current_user) .new(params: finder_params, current_user: current_user)
.execute .execute
.includes(:route, :creator, namespace: :route) .includes(:route, :creator, namespace: [:route, :owner])
end end
def load_events def load_events
......
...@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts def charts
@charts = {} @charts = {}
@charts[:week] = Ci::Charts::WeekChart.new(project) @charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project) @charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project) @charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project) @charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {} @counts = {}
@counts[:total] = @project.pipelines.count(:all) @counts[:total] = @project.pipelines.count(:all)
......
module AutoDevopsHelper module AutoDevopsHelper
def show_auto_devops_callout?(project) def show_auto_devops_callout?(project)
show_callout?('auto_devops_settings_dismissed') && Feature.get(:auto_devops_banner_disabled).off? &&
show_callout?('auto_devops_settings_dismissed') &&
can?(current_user, :admin_pipeline, project) && can?(current_user, :admin_pipeline, project) &&
project.has_auto_devops_implicitly_disabled? project.has_auto_devops_implicitly_disabled? &&
!project.repository.gitlab_ci_yml &&
project.ci_services.active.none?
end end
end end
...@@ -30,7 +30,7 @@ module BuildsHelper ...@@ -30,7 +30,7 @@ module BuildsHelper
def build_failed_issue_options def build_failed_issue_options
{ {
title: "Build Failed ##{@build.id}", title: "Job Failed ##{@build.id}",
description: project_job_url(@project, @build) description: project_job_url(@project, @build)
} }
end end
......
...@@ -7,7 +7,8 @@ module GraphHelper ...@@ -7,7 +7,8 @@ module GraphHelper
refs << commit_refs.join(' ') refs << commit_refs.join(' ')
# append note count # append note count
refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0 notes_count = @graph.notes[commit.id]
refs << "[#{notes_count} #{pluralize(notes_count, 'note')}]" if notes_count > 0
refs refs
end end
......
...@@ -214,7 +214,6 @@ module IssuablesHelper ...@@ -214,7 +214,6 @@ module IssuablesHelper
canUpdate: can?(current_user, :update_issue, issuable), canUpdate: can?(current_user, :update_issue, issuable),
canDestroy: can?(current_user, :destroy_issue, issuable), canDestroy: can?(current_user, :destroy_issue, issuable),
issuableRef: issuable.to_reference, issuableRef: issuable.to_reference,
isConfidential: issuable.confidential,
markdownPreviewPath: preview_markdown_path(@project), markdownPreviewPath: preview_markdown_path(@project),
markdownDocsPath: help_page_path('user/markdown'), markdownDocsPath: help_page_path('user/markdown'),
issuableTemplates: issuable_templates(issuable), issuableTemplates: issuable_templates(issuable),
......
...@@ -137,15 +137,7 @@ module ProjectsHelper ...@@ -137,15 +137,7 @@ module ProjectsHelper
end end
def last_push_event def last_push_event
return unless current_user current_user&.recent_push(@project)
return current_user.recent_push unless @project
project_ids = [@project.id]
if fork = current_user.fork_of(@project)
project_ids << fork.id
end
current_user.recent_push(project_ids)
end end
def project_feature_access_select(field) def project_feature_access_select(field)
...@@ -338,7 +330,7 @@ module ProjectsHelper ...@@ -338,7 +330,7 @@ module ProjectsHelper
def git_user_name def git_user_name
if current_user if current_user
current_user.name current_user.name.gsub('"', '\"')
else else
_("Your name") _("Your name")
end end
......
...@@ -104,7 +104,7 @@ module TreeHelper ...@@ -104,7 +104,7 @@ module TreeHelper
subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path) subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
if subtree.count == 1 && subtree.first.dir? if subtree.count == 1 && subtree.first.dir?
return tree_join(tree.name, flatten_tree(subtree.first)) return tree_join(tree.name, flatten_tree(root_path, subtree.first))
else else
return tree.name return tree.name
end end
......
...@@ -150,11 +150,11 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -150,11 +150,11 @@ class ApplicationSetting < ActiveRecord::Base
validates :housekeeping_full_repack_period, validates :housekeeping_full_repack_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_incremental_repack_period } numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_incremental_repack_period }
validates :housekeeping_gc_period, validates :housekeeping_gc_period,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: :housekeeping_full_repack_period } numericality: { only_integer: true, greater_than_or_equal_to: :housekeeping_full_repack_period }
validates :terminal_max_session_time, validates :terminal_max_session_time,
presence: true, presence: true,
...@@ -260,7 +260,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -260,7 +260,7 @@ class ApplicationSetting < ActiveRecord::Base
housekeeping_full_repack_period: 50, housekeeping_full_repack_period: 50,
housekeeping_gc_period: 200, housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10, housekeeping_incremental_repack_period: 10,
import_sources: Gitlab::ImportSources.values, import_sources: Settings.gitlab['import_sources'],
koding_enabled: false, koding_enabled: false,
koding_url: nil, koding_url: nil,
max_artifacts_size: Settings.artifacts['max_size'], max_artifacts_size: Settings.artifacts['max_size'],
......
...@@ -13,7 +13,7 @@ module BlobViewer ...@@ -13,7 +13,7 @@ module BlobViewer
prepare! prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data) @validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end end
def valid? def valid?
......
...@@ -457,8 +457,8 @@ module Ci ...@@ -457,8 +457,8 @@ module Ci
return unless trace return unless trace
trace = trace.dup trace = trace.dup
Ci::MaskSecret.mask!(trace, project.runners_token) if project Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Ci::MaskSecret.mask!(trace, token) Gitlab::Ci::MaskSecret.mask!(trace, token)
trace trace
end end
......
module Ci module Ci
class GroupVariable < ActiveRecord::Base class GroupVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
include Presentable include Presentable
......
module Ci module Ci
class Pipeline < ActiveRecord::Base class Pipeline < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasStatus include HasStatus
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
...@@ -349,8 +349,8 @@ module Ci ...@@ -349,8 +349,8 @@ module Ci
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
@config_processor ||= begin @config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path) Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message self.yaml_errors = e.message
nil nil
rescue rescue
......
module Ci module Ci
class PipelineSchedule < ActiveRecord::Base class PipelineSchedule < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include Importable include Importable
acts_as_paranoid acts_as_paranoid
......
module Ci module Ci
class PipelineScheduleVariable < ActiveRecord::Base class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
belongs_to :pipeline_schedule belongs_to :pipeline_schedule
......
module Ci module Ci
class PipelineVariable < ActiveRecord::Base class PipelineVariable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
belongs_to :pipeline belongs_to :pipeline
......
module Ci module Ci
class Runner < ActiveRecord::Base class Runner < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
prepend EE::Ci::Runner prepend EE::Ci::Runner
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
......
module Ci module Ci
class RunnerProject < ActiveRecord::Base class RunnerProject < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
belongs_to :runner belongs_to :runner
belongs_to :project belongs_to :project
......
module Ci module Ci
class Stage < ActiveRecord::Base class Stage < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include Importable include Importable
include HasStatus include HasStatus
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
......
module Ci module Ci
class Trigger < ActiveRecord::Base class Trigger < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
acts_as_paranoid acts_as_paranoid
......
module Ci module Ci
class TriggerRequest < ActiveRecord::Base class TriggerRequest < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
belongs_to :trigger belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id belongs_to :pipeline, foreign_key: :commit_id
......
module Ci module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Ci::Model extend Gitlab::Ci::Model
include HasVariable include HasVariable
include Presentable include Presentable
prepend EE::Ci::Variable prepend EE::Ci::Variable
......
...@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base ...@@ -49,7 +49,7 @@ class Event < ActiveRecord::Base
belongs_to :author, class_name: "User" belongs_to :author, class_name: "User"
belongs_to :project belongs_to :project
belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations belongs_to :target, polymorphic: true # rubocop:disable Cop/PolymorphicAssociations
has_one :push_event_payload, foreign_key: :event_id has_one :push_event_payload
# Callbacks # Callbacks
after_create :reset_project_activity after_create :reset_project_activity
...@@ -247,13 +247,7 @@ class Event < ActiveRecord::Base ...@@ -247,13 +247,7 @@ class Event < ActiveRecord::Base
def action_name def action_name
if push? if push?
if new_ref? push_action_name
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
elsif closed? elsif closed?
"closed" "closed"
elsif merged? elsif merged?
...@@ -269,11 +263,7 @@ class Event < ActiveRecord::Base ...@@ -269,11 +263,7 @@ class Event < ActiveRecord::Base
elsif commented? elsif commented?
"commented on" "commented on"
elsif created_project? elsif created_project?
if project.external_import? created_project_action_name
"imported"
else
"created"
end
else else
"opened" "opened"
end end
...@@ -366,6 +356,24 @@ class Event < ActiveRecord::Base ...@@ -366,6 +356,24 @@ class Event < ActiveRecord::Base
private private
def push_action_name
if new_ref?
"pushed new"
elsif rm_ref?
"deleted"
else
"pushed to"
end
end
def created_project_action_name
if project.external_import?
"imported"
else
"created"
end
end
def recent_update? def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
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
......
...@@ -39,9 +39,6 @@ class Issue < ActiveRecord::Base ...@@ -39,9 +39,6 @@ class Issue < ActiveRecord::Base
has_many :issue_assignees has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees has_many :assignees, class_name: "User", through: :issue_assignees
has_many :issue_assignees
has_many :assignees, class_name: "User", through: :issue_assignees
validates :project, presence: true validates :project, presence: true
scope :in_projects, ->(project_ids) { where(project_id: project_ids) } scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
......
...@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base ...@@ -28,7 +28,7 @@ class PersonalAccessToken < ActiveRecord::Base
protected protected
def validate_scopes def validate_scopes
unless scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) } unless revoked || scopes.all? { |scope| Gitlab::Auth::AVAILABLE_SCOPES.include?(scope.to_sym) }
errors.add :scopes, "can only contain available scopes" errors.add :scopes, "can only contain available scopes"
end end
end end
......
class ProjectAutoDevops < ActiveRecord::Base class ProjectAutoDevops < ActiveRecord::Base
belongs_to :project belongs_to :project
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true } validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
def variables def variables
......
...@@ -30,6 +30,44 @@ class PushEvent < Event ...@@ -30,6 +30,44 @@ class PushEvent < Event
delegate :commit_count, to: :push_event_payload delegate :commit_count, to: :push_event_payload
alias_method :commits_count, :commit_count alias_method :commits_count, :commit_count
# Returns events of pushes that either pushed to an existing ref or created a
# new one.
def self.created_or_pushed
actions = [
PushEventPayload.actions[:pushed],
PushEventPayload.actions[:created]
]
joins(:push_event_payload)
.where(push_event_payloads: { action: actions })
end
# Returns events of pushes to a branch.
def self.branch_events
ref_type = PushEventPayload.ref_types[:branch]
joins(:push_event_payload)
.where(push_event_payloads: { ref_type: ref_type })
end
# Returns PushEvent instances for which no merge requests have been created.
def self.without_existing_merge_requests
existing_mrs = MergeRequest.except(:order)
.select(1)
.where('merge_requests.source_project_id = events.project_id')
.where('merge_requests.source_branch = push_event_payloads.ref')
# For reasons unknown the use of #eager_load will result in the
# "push_event_payload" association not being set. Because of this we're
# using "joins" here, which does mean an additional query needs to be
# executed in order to retrieve the "push_event_association" when the
# returned PushEvent is used.
joins(:push_event_payload)
.where('NOT EXISTS (?)', existing_mrs)
.created_or_pushed
.branch_events
end
def self.sti_name def self.sti_name
PUSHED PUSHED
end end
......
...@@ -670,20 +670,13 @@ class User < ActiveRecord::Base ...@@ -670,20 +670,13 @@ class User < ActiveRecord::Base
@personal_projects_count ||= personal_projects.count @personal_projects_count ||= personal_projects.count
end end
def recent_push(project_ids = nil) def recent_push(project = nil)
# Get push events not earlier than 2 hours ago service = Users::LastPushEventService.new(self)
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently if project
events.includes(:project).recent.find do |event| service.last_event_for_project(project)
next unless event.project.repository.branch_exists?(event.branch_name) else
service.last_event_for_user
merge_requests = MergeRequest.where("created_at >= ?", event.created_at)
.where(source_project_id: event.project.id,
source_branch: event.branch_name)
merge_requests.empty?
end end
end end
......
...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity ...@@ -32,8 +32,8 @@ class BuildDetailsEntity < JobEntity
private private
def build_failed_issue_options def build_failed_issue_options
{ title: "Build Failed ##{build.id}", { title: "Job Failed ##{build.id}",
description: project_job_path(project, build) } description: "Job [##{build.id}](#{project_job_path(project, build)}) failed for #{build.sha}:\n" }
end end
def current_user def current_user
......
...@@ -16,7 +16,7 @@ module Ci ...@@ -16,7 +16,7 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline| .execute(:trigger, ignore_skip_ci: true) do |pipeline|
trigger.trigger_requests.create!(pipeline: pipeline) pipeline.trigger_requests.create!(trigger: trigger)
create_pipeline_variables!(pipeline) create_pipeline_variables!(pipeline)
end end
......
...@@ -74,12 +74,19 @@ class EventCreateService ...@@ -74,12 +74,19 @@ class EventCreateService
# We're using an explicit transaction here so that any errors that may occur # We're using an explicit transaction here so that any errors that may occur
# when creating push payload data will result in the event creation being # when creating push payload data will result in the event creation being
# rolled back as well. # rolled back as well.
Event.transaction do event = Event.transaction do
event = create_event(project, current_user, Event::PUSHED) new_event = create_event(project, current_user, Event::PUSHED)
PushEventPayloadService.new(event, push_data).execute PushEventPayloadService
.new(new_event, push_data)
.execute
new_event
end end
Users::LastPushEventService.new(current_user)
.cache_last_push_event(event)
Users::ActivityService.new(current_user, 'push').execute Users::ActivityService.new(current_user, 'push').execute
end end
......
module Users
# Service class for caching and retrieving the last push event of a user.
class LastPushEventService
EXPIRATION = 2.hours
def initialize(user)
@user = user
end
# Caches the given push event for the current user in the Rails cache.
#
# event - An instance of PushEvent to cache.
def cache_last_push_event(event)
keys = [
project_cache_key(event.project),
user_cache_key
]
if event.project.forked?
keys << project_cache_key(event.project.forked_from_project)
end
keys.each { |key| set_key(key, event.id) }
end
# Returns the last PushEvent for the current user.
#
# This method will return nil if no event was found.
def last_event_for_user
find_cached_event(user_cache_key)
end
# Returns the last PushEvent for the current user and the given project.
#
# project - An instance of Project for which to retrieve the PushEvent.
#
# This method will return nil if no event was found.
def last_event_for_project(project)
find_cached_event(project_cache_key(project))
end
def find_cached_event(cache_key)
event_id = get_key(cache_key)
return unless event_id
unless (event = find_event_in_database(event_id))
# We don't want to keep querying the same data over and over when a
# merge request has been created, thus we remove the key if no event
# (meaning an MR was created) is returned.
Rails.cache.delete(cache_key)
end
event
end
private
def find_event_in_database(id)
PushEvent
.without_existing_merge_requests
.find_by(id: id)
end
def user_cache_key
"last-push-event/#{@user.id}"
end
def project_cache_key(project)
"last-push-event/#{@user.id}/#{project.id}"
end
def get_key(key)
Rails.cache.read(key, raw: true)
end
def set_key(key, value)
# We're using raw values here since this takes up less space and we don't
# store complex objects.
Rails.cache.write(key, value, raw: true, expires_in: EXPIRATION)
end
end
end
...@@ -126,6 +126,11 @@ ...@@ -126,6 +126,11 @@
GitLab API GitLab API
%span.pull-right %span.pull-right
= API::API::version = API::API::version
- if Gitlab.config.pages.enabled
%p
GitLab Pages
%span.pull-right
= Gitlab::Pages::VERSION
- if Gitlab::Geo.enabled? - if Gitlab::Geo.enabled?
%p %p
......
-# haml-lint:disable InlineJavaScript
:javascript
window.onload = function() {
var s=document.createElement("script");s.onload=function(){bootlint.showLintReportForCurrentDocument([], {hasProblems: false, problemFree: false});};s.src="https://maxcdn.bootstrapcdn.com/bootlint/latest/bootlint.min.js";document.body.appendChild(s);
}
...@@ -76,4 +76,3 @@ ...@@ -76,4 +76,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id') = render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id') = render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render 'layouts/bootlint' if Rails.env.development?
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
= icon('wrench') = icon('wrench')
.sidebar-context-title Admin Area .sidebar-context-title Admin Area
%ul.sidebar-top-level-items %ul.sidebar-top-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'shortcuts-tree' do = link_to admin_root_path, class: 'shortcuts-tree' do
.nav-icon-container .nav-icon-container
= custom_icon('overview') = custom_icon('overview')
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
Overview Overview
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts conversational_development_index), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_root_path do = link_to admin_root_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Overview') } #{ _('Overview') }
...@@ -52,7 +52,7 @@ ...@@ -52,7 +52,7 @@
%span %span
ConvDev Index ConvDev Index
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles audit_logs)) do = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles audit_logs)) do
= link_to admin_system_info_path do = link_to admin_system_info_path do
.nav-icon-container .nav-icon-container
= custom_icon('monitoring') = custom_icon('monitoring')
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
Monitoring Monitoring
%ul.sidebar-sub-level-items %ul.sidebar-sub-level-items
= nav_link(controller: %w(conversational_development_index system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles), html_options: { class: "fly-out-top-item" } ) do
= link_to admin_system_info_path do = link_to admin_system_info_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Monitoring') } #{ _('Monitoring') }
......
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
%span.badge.count.issue_counter.fly-out-badge %span.badge.count.issue_counter.fly-out-badge
= number_with_delimiter(@project.open_issues_count) = number_with_delimiter(@project.open_issues_count)
%li.divider.fly-out-top-item %li.divider.fly-out-top-item
= nav_link(controller: :issues) do = nav_link(controller: :issues, action: :index) do
= link_to project_issues_path(@project), title: 'Issues' do = link_to project_issues_path(@project), title: 'Issues' do
%span %span
List List
......
...@@ -36,10 +36,10 @@ ...@@ -36,10 +36,10 @@
.preview= image_tag "#{scheme.css_class}-scheme-preview.png" .preview= image_tag "#{scheme.css_class}-scheme-preview.png"
= f.radio_button :color_scheme_id, scheme.id = f.radio_button :color_scheme_id, scheme.id
= scheme.name = scheme.name
.col-sm-12 .col-sm-12
%hr %hr
.col-sm-12
%hr
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
%h4.prepend-top-0 %h4.prepend-top-0
Behavior Behavior
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
.commiter .commiter
- commit_author_link = commit_author_link(commit, avatar: false, size: 24) - commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- commit_timeago = time_ago_with_tooltip(commit.committed_date) - commit_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
- commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago } - commit_text = _('%{commit_author_link} committed %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
#{ commit_text.html_safe } #{ commit_text.html_safe }
- if show_project_name - if show_project_name
......
...@@ -41,10 +41,10 @@ ...@@ -41,10 +41,10 @@
.swipe.view.hide .swipe.view.hide
.swipe-frame .swipe-frame
.frame.deleted .frame.deleted
= image_tag(old_blob_raw_path, alt: diff_file.old_path) = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.swipe-wrap .swipe-wrap
.frame.added .frame.added
= image_tag(blob_raw_path, alt: diff_file.new_path) = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
%span.swipe-bar %span.swipe-bar
%span.top-handle %span.top-handle
%span.bottom-handle %span.bottom-handle
...@@ -52,9 +52,9 @@ ...@@ -52,9 +52,9 @@
.onion-skin.view.hide .onion-skin.view.hide
.onion-skin-frame .onion-skin-frame
.frame.deleted .frame.deleted
= image_tag(old_blob_raw_path, alt: diff_file.old_path) = image_tag(old_blob_raw_path, alt: diff_file.old_path, lazy: false)
.frame.added .frame.added
= image_tag(blob_raw_path, alt: diff_file.new_path) = image_tag(blob_raw_path, alt: diff_file.new_path, lazy: false)
.controls .controls
.transparent .transparent
.drag-track .drag-track
......
...@@ -39,6 +39,6 @@ ...@@ -39,6 +39,6 @@
Tags Tags
.col-sm-10 .col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
.help-block You can setup jobs to only use Runners with specific tags .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas.
.form-actions .form-actions
= f.submit 'Save changes', class: 'btn btn-save' = f.submit 'Save changes', class: 'btn btn-save'
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
"aria-hidden": "true" } "aria-hidden": "true" }
%span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"", %span.board-title-text.has-tooltip{ "v-if": "list.type !== \"label\"",
":title" => '(list.label ? list.label.description : "")' } ":title" => '(list.label ? list.label.description : "")', data: { container: "body" } }
{{ list.title }} {{ list.title }}
%span.has-tooltip{ "v-if": "list.type === \"label\"", %span.has-tooltip{ "v-if": "list.type === \"label\"",
......
...@@ -6,15 +6,15 @@ ...@@ -6,15 +6,15 @@
%script#js-register-u2f-setup{ type: "text/template" } %script#js-register-u2f-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled? - if current_user.two_factor_otp_enabled?
.row.append-bottom-10 .row.append-bottom-10
.col-md-3 .col-md-4
%button#js-setup-u2f-device.btn.btn-info Setup new U2F device %button#js-setup-u2f-device.btn.btn-info.btn-block Setup new U2F device
.col-md-9 .col-md-8
%p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left. %p Your U2F device needs to be set up. Plug it in (if not already) and click the button on the left.
- else - else
.row.append-bottom-10 .row.append-bottom-10
.col-md-3 .col-md-4
%button#js-setup-u2f-device.btn.btn-info{ disabled: true } Setup new U2F device %button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true } Setup new U2F device
.col-md-9 .col-md-8
%p.text-warning You need to register a two-factor authentication app before you can set up a U2F device. %p.text-warning You need to register a two-factor authentication app before you can set up a U2F device.
%script#js-register-u2f-in-progress{ type: "text/template" } %script#js-register-u2f-in-progress{ type: "text/template" }
......
---
title: Allow to use same periods for different housekeeping tasks (effectively
skipping the lesser task)
merge_request: 13711
author: @cernvcs
type: added
---
title: Escape quotes in git username
merge_request: 14020
author: Brandon Everett
type: fixed
---
title: Decrease Perceived Complexity threshold to 15
merge_request: 14160
author: Maxim Rydkin
type: other
---
title: Decrease Cyclomatic Complexity threshold to 13
merge_request: 14152
author: Maxim Rydkin
type: other
---
title: Add GitLab-Pages version to Admin Dashboard
merge_request: 14040
author: @travismiller
type: added
---
title: Fixed non-UTF-8 valid branch names from causing an error.
merge_request: 14090
type: fixed
---
title: 'Add help text to runner edit: tags should be separated by commas.'
merge_request:
author: Brendan O'Leary
type: added
---
title: Image attachments are properly displayed in notification emails again
merge_request: 14161
author:
type: fixed
---
title: Resolve Image onion skin + swipe does not work anymore
merge_request:
author:
type: fixed
---
title: Move `lib/ci` to `lib/gitlab/ci`
merge_request: 14078
author: Maxim Rydkin
type: other
---
title: Show notes number more user-friendly in the graph
merge_request: 13949
author: Vladislav Kaverin
type: changed
---
title: Fixed merge request changes bar jumping
merge_request:
author:
type: fixed
---
title: Tooltips in the commit info box now all face the same direction
merge_request:
author: Jedidiah Broadbent
type: fixed
---
title: Fix ConvDev Index nav item and Monitoring submenu regression
merge_request: !14124
author:
type: fixed
---
title: Eager load namespace owners for project dashboards
merge_request:
author:
type: other
---
title: Scripts to detect orphaned repositories
merge_request: 14204
author:
type: added
---
title: Fixes the 500 errors caused by a race condition in GPG's tmp directory handling
merge_request: 14194
author: Alexis Reigel
type: fixed
---
title: Fix Pipeline Triggers to show triggered label and predefined variables (e.g.
CI_PIPELINE_TRIGGERED)
merge_request: 14244
author:
type: fixed
---
title: Issue board tooltips are now the correct width when the column is collapsed
merge_request:
author: Jedidiah Broadbent
type: fixed
---
title: Hide read_registry scope when registry is disabled on instance
merge_request: 13314
author: Robin Bobbitt
---
title: Adds Event polyfill for IE11
merge_request:
author:
type: fixed
---
title: Read import sources from setting at first initialization
merge_request: 14141
author: Visay Keo
type: fixed
---
title: Update native unicode emojis to always render as normal text (previously could render italicized)
merge_request:
author: Branka Martinovic
type: fixed
---
title: Perform prometheus data endpoint requests in parallel
merge_request: 14003
author:
type: fixed
---
title: Replace the profile/emails.feature spinach test with an rspec analog
merge_request: 14172
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Replace project/group_links.feature spinach test with an rspec analog
merge_request: 14169
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Replace the project/milestone.feature spinach test with an rspec analog
merge_request: 14171
author: Vitaliy @blackst0ne Klachkov
type: other
---
title: Replace the 'profile/active_tab.feature' spinach test with an rspec analog
merge_request: 14239
author: Vitaliy @blackst0ne Klachkov
type: other
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.
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