Commit cb21811b authored by Sean McGivern's avatar Sean McGivern

Merge branch 'master' into 582-toggle-overwrite

parents a6bd5fcd d0d27531
...@@ -61,3 +61,4 @@ eslint-report.html ...@@ -61,3 +61,4 @@ eslint-report.html
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
/webpack-report/ /webpack-report/
/locale/**/LC_MESSAGES /locale/**/LC_MESSAGES
/.rspec
--color
--format Fuubar
...@@ -265,7 +265,7 @@ gem 'base32', '~> 0.3.0' ...@@ -265,7 +265,7 @@ gem 'base32', '~> 0.3.0'
gem "gitlab-license", "~> 1.0" gem "gitlab-license", "~> 1.0"
# Sentry integration # Sentry integration
gem 'sentry-raven', '~> 2.4.0' gem 'sentry-raven', '~> 2.5.3'
gem 'premailer-rails', '~> 1.9.7' gem 'premailer-rails', '~> 1.9.7'
...@@ -295,6 +295,7 @@ group :metrics do ...@@ -295,6 +295,7 @@ group :metrics do
# Prometheus # Prometheus
gem 'prometheus-client-mmap', '~>0.7.0.beta5' gem 'prometheus-client-mmap', '~>0.7.0.beta5'
gem 'raindrops', '~> 0.18'
end end
group :development do group :development do
...@@ -394,6 +395,9 @@ gem 'health_check', '~> 2.6.0' ...@@ -394,6 +395,9 @@ gem 'health_check', '~> 2.6.0'
gem 'vmstat', '~> 2.3.0' gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# NTP client
gem 'net-ntp'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.9.0' gem 'gitaly', '~> 0.9.0'
......
...@@ -503,6 +503,7 @@ GEM ...@@ -503,6 +503,7 @@ GEM
mustermann (= 0.4.0) mustermann (= 0.4.0)
mysql2 (0.4.5) mysql2 (0.4.5)
net-ldap (0.12.1) net-ldap (0.12.1)
net-ntp (2.1.3)
net-ssh (3.0.1) net-ssh (3.0.1)
netrc (0.11.0) netrc (0.11.0)
nokogiri (1.6.8.1) nokogiri (1.6.8.1)
...@@ -627,8 +628,8 @@ GEM ...@@ -627,8 +628,8 @@ GEM
premailer-rails (1.9.7) premailer-rails (1.9.7)
actionmailer (>= 3, < 6) actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9) premailer (~> 1.7, >= 1.7.9)
prometheus-client-mmap (0.7.0.beta5) prometheus-client-mmap (0.7.0.beta8)
mmap2 (~> 2.2.6) mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4) pry (0.10.4)
coderay (~> 1.1.0) coderay (~> 1.1.0)
method_source (~> 0.8.1) method_source (~> 0.8.1)
...@@ -686,7 +687,7 @@ GEM ...@@ -686,7 +687,7 @@ GEM
thor (>= 0.18.1, < 2.0) thor (>= 0.18.1, < 2.0)
rainbow (2.2.2) rainbow (2.2.2)
rake rake
raindrops (0.17.0) raindrops (0.18.0)
rake (10.5.0) rake (10.5.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
...@@ -803,7 +804,7 @@ GEM ...@@ -803,7 +804,7 @@ GEM
activesupport (>= 3.1) activesupport (>= 3.1)
select2-rails (3.5.9.3) select2-rails (3.5.9.3)
thor (~> 0.14) thor (~> 0.14)
sentry-raven (2.4.0) sentry-raven (2.5.3)
faraday (>= 0.7.6, < 1.0) faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9) settingslogic (2.0.9)
sexp_processor (4.9.0) sexp_processor (4.9.0)
...@@ -1053,6 +1054,7 @@ DEPENDENCIES ...@@ -1053,6 +1054,7 @@ DEPENDENCIES
mousetrap-rails (~> 1.4.6) mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.4.5) mysql2 (~> 0.4.5)
net-ldap net-ldap
net-ntp
net-ssh (~> 3.0.1) net-ssh (~> 3.0.1)
nokogiri (~> 1.6.7, >= 1.6.7.2) nokogiri (~> 1.6.7, >= 1.6.7.2)
oauth2 (~> 1.4) oauth2 (~> 1.4)
...@@ -1098,6 +1100,7 @@ DEPENDENCIES ...@@ -1098,6 +1100,7 @@ DEPENDENCIES
rails-deprecated_sanitizer (~> 1.0.3) rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9) rails-i18n (~> 4.0.9)
rainbow (~> 2.2) rainbow (~> 2.2)
raindrops (~> 0.18)
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rdoc (~> 4.2) rdoc (~> 4.2)
recaptcha (~> 3.0) recaptcha (~> 3.0)
...@@ -1125,7 +1128,7 @@ DEPENDENCIES ...@@ -1125,7 +1128,7 @@ DEPENDENCIES
scss_lint (~> 0.47.0) scss_lint (~> 0.47.0)
seed-fu (~> 2.3.5) seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9) select2-rails (~> 3.5.9)
sentry-raven (~> 2.4.0) sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9) settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6) sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0) shoulda-matchers (~> 2.8.0)
......
...@@ -13,25 +13,21 @@ window.Build = (function () { ...@@ -13,25 +13,21 @@ window.Build = (function () {
this.options = options || $('.js-build-options').data(); this.options = options || $('.js-build-options').data();
this.pageUrl = this.options.pageUrl; this.pageUrl = this.options.pageUrl;
this.buildUrl = this.options.buildUrl;
this.buildStatus = this.options.buildStatus; this.buildStatus = this.options.buildStatus;
this.state = this.options.logState; this.state = this.options.logState;
this.buildStage = this.options.buildStage; this.buildStage = this.options.buildStage;
this.$document = $(document); this.$document = $(document);
this.logBytes = 0; this.logBytes = 0;
this.scrollOffsetPadding = 30;
this.hasBeenScrolled = false; this.hasBeenScrolled = false;
this.updateDropdown = this.updateDropdown.bind(this); this.updateDropdown = this.updateDropdown.bind(this);
this.getBuildTrace = this.getBuildTrace.bind(this); this.getBuildTrace = this.getBuildTrace.bind(this);
this.scrollToBottom = this.scrollToBottom.bind(this);
this.$body = $('body');
this.$buildTrace = $('#build-trace'); this.$buildTrace = $('#build-trace');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
this.$truncatedInfo = $('.js-truncated-info'); this.$truncatedInfo = $('.js-truncated-info');
this.$buildTraceOutput = $('.js-build-output'); this.$buildTraceOutput = $('.js-build-output');
this.$scrollContainer = $('.js-scroll-container'); this.$topBar = $('.js-top-bar');
// Scroll controllers // Scroll controllers
this.$scrollTopBtn = $('.js-scroll-up'); this.$scrollTopBtn = $('.js-scroll-up');
...@@ -63,13 +59,22 @@ window.Build = (function () { ...@@ -63,13 +59,22 @@ window.Build = (function () {
.off('click') .off('click')
.on('click', this.scrollToBottom.bind(this)); .on('click', this.scrollToBottom.bind(this));
const scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100); this.scrollThrottled = _.throttle(this.toggleScroll.bind(this), 100);
this.$scrollContainer $(window)
.off('scroll') .off('scroll')
.on('scroll', () => { .on('scroll', () => {
this.hasBeenScrolled = true; const contentHeight = this.$buildTraceOutput.prop('scrollHeight');
scrollThrottled(); if (contentHeight > this.windowSize) {
// means the user did not scroll, the content was updated.
this.windowSize = contentHeight;
} else {
// User scrolled
this.hasBeenScrolled = true;
this.toggleScrollAnimation(false);
}
this.scrollThrottled();
}); });
$(window) $(window)
...@@ -77,59 +82,73 @@ window.Build = (function () { ...@@ -77,59 +82,73 @@ window.Build = (function () {
.on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100)); .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.updateArtifactRemoveDate(); this.updateArtifactRemoveDate();
this.initAffixTopArea();
// eslint-disable-next-line this.getBuildTrace();
this.getBuildTrace()
.then(() => this.toggleScroll())
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
})
.then(() => this.verifyTopPosition());
} }
Build.prototype.initAffixTopArea = function () {
/**
If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix
**/
if (this.$topBar.css('position') !== 'static') return;
const offsetTop = this.$buildTrace.offset().top;
this.$topBar.affix({
offset: {
top: offsetTop,
},
});
};
Build.prototype.canScroll = function () { Build.prototype.canScroll = function () {
return (this.$scrollContainer.prop('scrollHeight') - this.scrollOffsetPadding) > this.$scrollContainer.height(); return document.body.scrollHeight > window.innerHeight;
}; };
/**
* | | Up | Down |
* |--------------------------|----------|----------|
* | on scroll bottom | active | disabled |
* | on scroll top | disabled | active |
* | no scroll | disabled | disabled |
* | on.('scroll') is on top | disabled | active |
* | on('scroll) is on bottom | active | disabled |
*
*/
Build.prototype.toggleScroll = function () { Build.prototype.toggleScroll = function () {
const currentPosition = this.$scrollContainer.scrollTop(); const currentPosition = document.body.scrollTop;
const bottomScroll = currentPosition + this.$scrollContainer.innerHeight(); const windowHeight = window.innerHeight;
if (this.canScroll()) { if (this.canScroll()) {
if (currentPosition === 0) { if (currentPosition > 0 &&
(document.body.scrollHeight - currentPosition !== windowHeight)) {
// User is in the middle of the log
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (currentPosition === 0) {
// User is at Top of Build Log
this.toggleDisableButton(this.$scrollTopBtn, true); this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, false); this.toggleDisableButton(this.$scrollBottomBtn, false);
} else if (bottomScroll === this.$scrollContainer.prop('scrollHeight')) { } else if (document.body.scrollHeight - currentPosition === windowHeight) {
// User is at the bottom of the build log.
this.toggleDisableButton(this.$scrollTopBtn, false); this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, true); this.toggleDisableButton(this.$scrollBottomBtn, true);
} else {
this.toggleDisableButton(this.$scrollTopBtn, false);
this.toggleDisableButton(this.$scrollBottomBtn, false);
} }
} else {
this.toggleDisableButton(this.$scrollTopBtn, true);
this.toggleDisableButton(this.$scrollBottomBtn, true);
} }
}; };
Build.prototype.scrollToTop = function () { Build.prototype.scrollDown = function () {
document.body.scrollTop = document.body.scrollHeight;
};
Build.prototype.scrollToBottom = function () {
this.scrollDown();
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(0);
this.toggleScroll(); this.toggleScroll();
}; };
Build.prototype.scrollToBottom = function () { Build.prototype.scrollToTop = function () {
document.body.scrollTop = 0;
this.hasBeenScrolled = true; this.hasBeenScrolled = true;
this.$scrollContainer.scrollTop(this.$scrollContainer.prop('scrollHeight'));
this.toggleScroll(); this.toggleScroll();
}; };
...@@ -142,47 +161,6 @@ window.Build = (function () { ...@@ -142,47 +161,6 @@ window.Build = (function () {
this.$scrollBottomBtn.toggleClass('animate', toggle); this.$scrollBottomBtn.toggleClass('animate', toggle);
}; };
/**
* Build trace top position depends on the space ocupied by the elments rendered before
*/
Build.prototype.verifyTopPosition = function () {
const $buildPage = $('.build-page');
const $flashError = $('.alert-wrapper');
const $header = $('.build-header', $buildPage);
const $runnersStuck = $('.js-build-stuck', $buildPage);
const $startsEnvironment = $('.js-environment-container', $buildPage);
const $erased = $('.js-build-erased', $buildPage);
const prependTopDefault = 20;
// header + navigation + margin
let topPostion = 168;
if ($header.length) {
topPostion += $header.outerHeight();
}
if ($runnersStuck.length) {
topPostion += $runnersStuck.outerHeight();
}
if ($startsEnvironment.length) {
topPostion += $startsEnvironment.outerHeight() + prependTopDefault;
}
if ($erased.length) {
topPostion += $erased.outerHeight() + prependTopDefault;
}
if ($flashError.length) {
topPostion += $flashError.outerHeight() + prependTopDefault;
}
this.$buildTrace.css({
top: topPostion,
});
};
Build.prototype.initSidebar = function () { Build.prototype.initSidebar = function () {
this.$sidebar = $('.js-build-sidebar'); this.$sidebar = $('.js-build-sidebar');
this.$sidebar.niceScroll(); this.$sidebar.niceScroll();
...@@ -200,6 +178,8 @@ window.Build = (function () { ...@@ -200,6 +178,8 @@ window.Build = (function () {
this.state = log.state; this.state = log.state;
} }
this.windowSize = this.$buildTraceOutput.prop('scrollHeight');
if (log.append) { if (log.append) {
this.$buildTraceOutput.append(log.html); this.$buildTraceOutput.append(log.html);
this.logBytes += log.size; this.logBytes += log.size;
...@@ -227,14 +207,7 @@ window.Build = (function () { ...@@ -227,14 +207,7 @@ window.Build = (function () {
} }
Build.timeout = setTimeout(() => { Build.timeout = setTimeout(() => {
//eslint-disable-next-line this.getBuildTrace();
this.getBuildTrace()
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollToBottom();
}
})
.then(() => this.verifyTopPosition());
}, 4000); }, 4000);
} else { } else {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
...@@ -247,7 +220,13 @@ window.Build = (function () { ...@@ -247,7 +220,13 @@ window.Build = (function () {
}) })
.fail(() => { .fail(() => {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
}); })
.then(() => {
if (!this.hasBeenScrolled) {
this.scrollDown();
}
})
.then(() => this.toggleScroll());
}; };
Build.prototype.shouldHideSidebarForViewport = function () { Build.prototype.shouldHideSidebarForViewport = function () {
...@@ -259,14 +238,11 @@ window.Build = (function () { ...@@ -259,14 +238,11 @@ window.Build = (function () {
const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined; const shouldShow = typeof shouldHide === 'boolean' ? !shouldHide : undefined;
const $toggleButton = $('.js-sidebar-build-toggle-header'); const $toggleButton = $('.js-sidebar-build-toggle-header');
this.$buildTrace
.toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide);
this.$sidebar this.$sidebar
.toggleClass('right-sidebar-expanded', shouldShow) .toggleClass('right-sidebar-expanded', shouldShow)
.toggleClass('right-sidebar-collapsed', shouldHide); .toggleClass('right-sidebar-collapsed', shouldHide);
$('.js-build-page') this.$topBar
.toggleClass('sidebar-expanded', shouldShow) .toggleClass('sidebar-expanded', shouldShow)
.toggleClass('sidebar-collapsed', shouldHide); .toggleClass('sidebar-collapsed', shouldHide);
...@@ -279,17 +255,10 @@ window.Build = (function () { ...@@ -279,17 +255,10 @@ window.Build = (function () {
Build.prototype.sidebarOnResize = function () { Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport()); this.toggleSidebar(this.shouldHideSidebarForViewport());
this.verifyTopPosition();
if (this.canScroll()) {
this.toggleScroll();
}
}; };
Build.prototype.sidebarOnClick = function () { Build.prototype.sidebarOnClick = function () {
if (this.shouldHideSidebarForViewport()) this.toggleSidebar(); if (this.shouldHideSidebarForViewport()) this.toggleSidebar();
this.verifyTopPosition();
}; };
Build.prototype.updateArtifactRemoveDate = function () { Build.prototype.updateArtifactRemoveDate = function () {
......
...@@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -26,14 +26,6 @@ document.addEventListener('DOMContentLoaded', () => {
mounted() { mounted() {
this.mediator.initBuildClass(); this.mediator.initBuildClass();
}, },
updated() {
// Wait for flash message to be appended
Vue.nextTick(() => {
if (this.mediator.build) {
this.mediator.build.verifyTopPosition();
}
});
},
render(createElement) { render(createElement) {
return createElement('job-header', { return createElement('job-header', {
props: { props: {
......
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
data() { data() {
return { return {
graphHeight: 500, graphHeight: 450,
graphWidth: 600, graphWidth: 600,
graphHeightOffset: 120, graphHeightOffset: 120,
xScale: {}, xScale: {},
...@@ -88,7 +88,9 @@ ...@@ -88,7 +88,9 @@
}, },
paddingBottomRootSvg() { paddingBottomRootSvg() {
return (Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0; return {
paddingBottom: `${(Math.ceil(this.graphHeight * 100) / this.graphWidth) || 0}%`,
};
}, },
}, },
...@@ -104,7 +106,7 @@ ...@@ -104,7 +106,7 @@
} }
this.data = query.result[0].values; this.data = query.result[0].values;
this.unitOfDisplay = query.unit || 'N/A'; this.unitOfDisplay = query.unit || 'N/A';
this.yAxisLabel = this.columnData.y_axis || 'Values'; this.yAxisLabel = this.columnData.y_label || 'Values';
this.legendTitle = query.legend || 'Average'; this.legendTitle = query.legend || '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;
...@@ -157,12 +159,12 @@ ...@@ -157,12 +159,12 @@
const xAxis = d3.svg.axis() const xAxis = d3.svg.axis()
.scale(axisXScale) .scale(axisXScale)
.ticks(measurements.ticks) .ticks(measurements.xTicks)
.orient('bottom'); .orient('bottom');
const yAxis = d3.svg.axis() const yAxis = d3.svg.axis()
.scale(this.yScale) .scale(this.yScale)
.ticks(measurements.ticks) .ticks(measurements.yTicks)
.orient('left'); .orient('left');
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis); d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
...@@ -170,8 +172,12 @@ ...@@ -170,8 +172,12 @@
const width = this.graphWidth; const width = this.graphWidth;
d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis) d3.select(this.$refs.baseSvg).select('.y-axis').call(yAxis)
.selectAll('.tick') .selectAll('.tick')
.each(function createTickLines() { .each(function createTickLines(d, i) {
d3.select(this).select('line').attr('x2', width); if (i > 0) {
d3.select(this).select('line')
.attr('x2', width)
.attr('class', 'axis-tick');
} // 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() this.xScale = d3.time.scale()
...@@ -198,7 +204,7 @@ ...@@ -198,7 +204,7 @@
watch: { watch: {
updateAspectRatio() { updateAspectRatio() {
if (this.updateAspectRatio) { if (this.updateAspectRatio) {
this.graphHeight = 500; this.graphHeight = 450;
this.graphWidth = 600; this.graphWidth = 600;
this.measurements = measurements.large; this.measurements = measurements.large;
this.draw(); this.draw();
...@@ -216,14 +222,14 @@ ...@@ -216,14 +222,14 @@
<div <div
:class="classType"> :class="classType">
<h5 <h5
class="text-center"> class="text-center graph-title">
{{columnData.title}} {{columnData.title}}
</h5> </h5>
<div <div
class="prometheus-svg-container"> class="prometheus-svg-container"
:style="paddingBottomRootSvg">
<svg <svg
:viewBox="outterViewBox" :viewBox="outterViewBox"
:style="{ 'padding-bottom': paddingBottomRootSvg }"
ref="baseSvg"> ref="baseSvg">
<g <g
class="x-axis" class="x-axis"
......
...@@ -87,14 +87,14 @@ ...@@ -87,14 +87,14 @@
</rect> </rect>
<text <text
class="text-metric text-metric-bold" class="text-metric text-metric-bold"
x="8" x="16"
y="35" y="35"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
{{formatTime}} {{formatTime}}
</text> </text>
<text <text
class="text-metric-date" class="text-metric"
x="8" x="16"
y="15" y="15"
transform="translate(-5, 20)"> transform="translate(-5, 20)">
{{formatDate}} {{formatDate}}
......
...@@ -109,13 +109,13 @@ ...@@ -109,13 +109,13 @@
</text> </text>
<rect <rect
class="rect-axis-text" class="rect-axis-text"
:x="xPosition + 50" :x="xPosition + 60"
:y="graphHeight - 80" :y="graphHeight - 80"
width="50" width="35"
height="50"> height="50">
</rect> </rect>
<text <text
class="label-axis-text" class="label-axis-text x-label-text"
:x="xPosition + 60" :x="xPosition + 60"
:y="yPosition" :y="yPosition"
dy=".35em"> dy=".35em">
...@@ -131,13 +131,13 @@ ...@@ -131,13 +131,13 @@
<text <text
class="text-metric-title" class="text-metric-title"
x="50" x="50"
:y="graphHeight - 40"> :y="graphHeight - 25">
{{legendTitle}} {{legendTitle}}
</text> </text>
<text <text
class="text-metric-usage" class="text-metric-usage"
x="50" x="50"
:y="graphHeight - 25"> :y="graphHeight - 10">
{{metricUsage}} {{metricUsage}}
</text> </text>
</g> </g>
......
...@@ -8,14 +8,14 @@ export default { ...@@ -8,14 +8,14 @@ export default {
}, },
legends: { legends: {
width: 15, width: 15,
height: 30, height: 25,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 50, height: 50,
}, },
axisLabelLineOffset: -20, axisLabelLineOffset: -20,
legendOffset: 52, legendOffset: 35,
}, },
large: { // This covers both md and lg screen sizes large: { // This covers both md and lg screen sizes
margin: { margin: {
...@@ -26,14 +26,15 @@ export default { ...@@ -26,14 +26,15 @@ export default {
}, },
legends: { legends: {
width: 20, width: 20,
height: 35, height: 30,
}, },
backgroundLegend: { backgroundLegend: {
width: 30, width: 30,
height: 150, height: 150,
}, },
axisLabelLineOffset: 20, axisLabelLineOffset: 20,
legendOffset: 55, legendOffset: 38,
}, },
ticks: 3, xTicks: 8,
yTicks: 3,
}; };
...@@ -2,56 +2,54 @@ ...@@ -2,56 +2,54 @@
/* eslint no-new: "off" */ /* eslint no-new: "off" */
import AccessorUtilities from './lib/utils/accessor'; import AccessorUtilities from './lib/utils/accessor';
((global) => { /**
/** * Memorize the last selected tab after reloading a page.
* Memorize the last selected tab after reloading a page. * Does that setting the current selected tab in the localStorage
* Does that setting the current selected tab in the localStorage */
*/ class ActiveTabMemoizer {
class ActiveTabMemoizer { constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) {
constructor({ currentTabKey = 'current_signin_tab', tabSelector = 'ul.nav-tabs' } = {}) { this.currentTabKey = currentTabKey;
this.currentTabKey = currentTabKey; this.tabSelector = tabSelector;
this.tabSelector = tabSelector; this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
this.bootstrap();
this.bootstrap(); }
}
bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
tabs[0].addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
}
});
}
this.showTab(); bootstrap() {
const tabs = document.querySelectorAll(this.tabSelector);
if (tabs.length > 0) {
tabs[0].addEventListener('click', (e) => {
if (e.target && e.target.nodeName === 'A') {
const anchorName = e.target.getAttribute('href');
this.saveData(anchorName);
}
});
} }
showTab() { this.showTab();
const anchorName = this.readData(); }
if (anchorName) {
const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`); showTab() {
if (tab) { const anchorName = this.readData();
tab.click(); if (anchorName) {
} const tab = document.querySelector(`${this.tabSelector} a[href="${anchorName}"]`);
if (tab) {
tab.click();
} }
} }
}
saveData(val) { saveData(val) {
if (!this.isLocalStorageAvailable) return undefined; if (!this.isLocalStorageAvailable) return undefined;
return window.localStorage.setItem(this.currentTabKey, val); return window.localStorage.setItem(this.currentTabKey, val);
} }
readData() { readData() {
if (!this.isLocalStorageAvailable) return null; if (!this.isLocalStorageAvailable) return null;
return window.localStorage.getItem(this.currentTabKey); return window.localStorage.getItem(this.currentTabKey);
}
} }
}
global.ActiveTabMemoizer = ActiveTabMemoizer; window.ActiveTabMemoizer = ActiveTabMemoizer;
})(window);
...@@ -2,99 +2,97 @@ ...@@ -2,99 +2,97 @@
import FilesCommentButton from './files_comment_button'; import FilesCommentButton from './files_comment_button';
(function() { window.SingleFileDiff = (function() {
window.SingleFileDiff = (function() { var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
WRAPPER = '<div class="diff-content"></div>'; WRAPPER = '<div class="diff-content"></div>';
LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>'; LOADING_HTML = '<i class="fa fa-spinner fa-spin"></i>';
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>'; ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>'; COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) { function SingleFileDiff(file) {
this.file = file; this.file = file;
this.toggleDiff = this.toggleDiff.bind(this); this.toggleDiff = this.toggleDiff.bind(this);
this.content = $('.diff-content', this.file); this.content = $('.diff-content', this.file);
this.$toggleIcon = $('.diff-toggle-caret', this.file); this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path'); this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath; this.isOpen = !this.diffForPath;
if (this.diffForPath) { if (this.diffForPath) {
this.collapsedContent = this.content; this.collapsedContent = this.content;
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide(); this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null; this.content = null;
this.collapsedContent.after(this.loadingContent); this.collapsedContent.after(this.loadingContent);
this.$toggleIcon.addClass('fa-caret-right'); this.$toggleIcon.addClass('fa-caret-right');
} else { } else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide(); this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent); this.content.after(this.collapsedContent);
this.$toggleIcon.addClass('fa-caret-down'); this.$toggleIcon.addClass('fa-caret-down');
} }
$('.js-file-title, .click-to-expand', this.file).on('click', (function (e) {
this.toggleDiff($(e.target));
}).bind(this));
}
$('.js-file-title, .click-to-expand', this.file).on('click', (function (e) { SingleFileDiff.prototype.toggleDiff = function($target, cb) {
this.toggleDiff($(e.target)); if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
}).bind(this)); this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
} }
};
SingleFileDiff.prototype.toggleDiff = function($target, cb) { SingleFileDiff.prototype.getContentHTML = function(cb) {
if (!$target.hasClass('js-file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return; this.collapsedContent.hide();
this.isOpen = !this.isOpen; this.loadingContent.show();
if (!this.isOpen && !this.hasError) { $.get(this.diffForPath, (function(_this) {
this.content.hide(); return function(data) {
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down'); _this.loadingContent.hide();
this.collapsedContent.show(); if (data.html) {
if (typeof gl.diffNotesCompileComponents !== 'undefined') { _this.content = $(data.html);
gl.diffNotesCompileComponents(); _this.content.syntaxHighlight();
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
} }
} else if (this.content) { _this.collapsedContent.after(_this.content);
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof gl.diffNotesCompileComponents !== 'undefined') { if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents(); gl.diffNotesCompileComponents();
} }
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML(cb);
}
};
SingleFileDiff.prototype.getContentHTML = function(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
return function(data) {
_this.loadingContent.hide();
if (data.html) {
_this.content = $(data.html);
_this.content.syntaxHighlight();
} else {
_this.hasError = true;
_this.content = $(ERROR_HTML);
}
_this.collapsedContent.after(_this.content);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
FilesCommentButton.init($(_this.file)); FilesCommentButton.init($(_this.file));
if (cb) cb(); if (cb) cb();
}; };
})(this)); })(this));
}; };
return SingleFileDiff; return SingleFileDiff;
})(); })();
$.fn.singleFileDiff = function() { $.fn.singleFileDiff = function() {
return this.each(function() { return this.each(function() {
if (!$.data(this, 'singleFileDiff')) { if (!$.data(this, 'singleFileDiff')) {
return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this)); return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this));
} }
}); });
}; };
}).call(window);
/* /**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable * Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API. * and controllable by a public API.
* */
* */
class SmartInterval {
(() => { /**
class SmartInterval { * @param { function } opts.callback Function to be called on each iteration (required)
/** * @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially
* @param { function } opts.callback Function to be called on each iteration (required) * @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this
* @param { milliseconds } opts.startingInterval `currentInterval` is set to this initially * @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this
* @param { milliseconds } opts.maxInterval `currentInterval` will be incremented to this * when the page is hidden
* @param { milliseconds } opts.hiddenInterval `currentInterval` is set to this * @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor
* when the page is hidden * @param { boolean } opts.lazyStart Configure if timer is initialized on
* @param { integer } opts.incrementByFactorOf `currentInterval` is incremented by this factor * instantiation or lazily
* @param { boolean } opts.lazyStart Configure if timer is initialized on * @param { boolean } opts.immediateExecution Configure if callback should
* instantiation or lazily * be executed before the first interval.
* @param { boolean } opts.immediateExecution Configure if callback should */
* be executed before the first interval. constructor(opts = {}) {
*/ this.cfg = {
constructor(opts = {}) { callback: opts.callback,
this.cfg = { startingInterval: opts.startingInterval,
callback: opts.callback, maxInterval: opts.maxInterval,
startingInterval: opts.startingInterval, hiddenInterval: opts.hiddenInterval,
maxInterval: opts.maxInterval, incrementByFactorOf: opts.incrementByFactorOf,
hiddenInterval: opts.hiddenInterval, lazyStart: opts.lazyStart,
incrementByFactorOf: opts.incrementByFactorOf, immediateExecution: opts.immediateExecution,
lazyStart: opts.lazyStart, };
immediateExecution: opts.immediateExecution,
}; this.state = {
intervalId: null,
this.state = { currentInterval: this.cfg.startingInterval,
intervalId: null, pageVisibility: 'visible',
currentInterval: this.cfg.startingInterval, };
pageVisibility: 'visible',
}; this.initInterval();
}
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
if (cfg.immediateExecution) {
cfg.immediateExecution = false;
cfg.callback();
}
state.intervalId = window.setInterval(() => { /* public */
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) { start() {
return; const cfg = this.cfg;
} const state = this.state;
this.incrementInterval(); if (cfg.immediateExecution) {
this.resume(); cfg.immediateExecution = false;
}, this.getCurrentInterval()); cfg.callback();
} }
// cancel the existing timer, setting the currentInterval back to startingInterval state.intervalId = window.setInterval(() => {
cancel() { cfg.callback();
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
onVisibilityHidden() { if (this.getCurrentInterval() === cfg.maxInterval) {
if (this.cfg.hiddenInterval) { return;
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel();
} }
}
// start a timer, using the existing interval this.incrementInterval();
resume() { this.resume();
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped }, this.getCurrentInterval());
this.start(); }
}
onVisibilityVisible() { // cancel the existing timer, setting the currentInterval back to startingInterval
this.cancel(); cancel() {
this.start(); this.setCurrentInterval(this.cfg.startingInterval);
} this.stopTimer();
}
destroy() { onVisibilityHidden() {
if (this.cfg.hiddenInterval) {
this.setCurrentInterval(this.cfg.hiddenInterval);
this.resume();
} else {
this.cancel(); this.cancel();
document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('beforeunload');
} }
}
/* private */ // start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
initInterval() { onVisibilityVisible() {
const cfg = this.cfg; this.cancel();
this.start();
}
if (!cfg.lazyStart) { destroy() {
this.start(); this.cancel();
} document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document).off('visibilitychange').off('beforeunload');
}
this.initVisibilityChangeHandling(); /* private */
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() { initInterval() {
// cancel interval when tab no longer shown (prevents cached pages from polling) const cfg = this.cfg;
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
initPageUnloadHandling() { if (!cfg.lazyStart) {
// TODO: Consider refactoring in light of turbolinks removal. this.start();
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('beforeunload', () => this.cancel());
} }
handleVisibilityChange(e) { this.initVisibilityChangeHandling();
this.state.pageVisibility = e.target.visibilityState; this.initPageUnloadHandling();
const intervalAction = this.isPageVisible() ? }
this.onVisibilityVisible :
this.onVisibilityHidden;
intervalAction.apply(this); initVisibilityChangeHandling() {
} // cancel interval when tab no longer shown (prevents cached pages from polling)
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
}
getCurrentInterval() { initPageUnloadHandling() {
return this.state.currentInterval; // TODO: Consider refactoring in light of turbolinks removal.
} // prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('beforeunload', () => this.cancel());
}
setCurrentInterval(newInterval) { handleVisibilityChange(e) {
this.state.currentInterval = newInterval; this.state.pageVisibility = e.target.visibilityState;
} const intervalAction = this.isPageVisible() ?
this.onVisibilityVisible :
this.onVisibilityHidden;
incrementInterval() { intervalAction.apply(this);
const cfg = this.cfg; }
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) { getCurrentInterval() {
nextInterval = cfg.maxInterval; return this.state.currentInterval;
} }
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
if (cfg.hiddenInterval && !this.isPageVisible()) return;
let nextInterval = currentInterval * cfg.incrementByFactorOf;
this.setCurrentInterval(nextInterval); if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
} }
isPageVisible() { return this.state.pageVisibility === 'visible'; } this.setCurrentInterval(nextInterval);
}
stopTimer() { isPageVisible() { return this.state.pageVisibility === 'visible'; }
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId); stopTimer() {
} const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
} }
gl.SmartInterval = SmartInterval; }
})(window.gl || (window.gl = {}));
window.gl.SmartInterval = SmartInterval;
/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */ /* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, max-len */
(global => { window.gl.SnippetsList = function() {
global.gl = global.gl || {}; var $holder = $('.snippets-list-holder');
gl.SnippetsList = function() { $holder.find('.pagination').on('ajax:success', (e, data) => {
var $holder = $('.snippets-list-holder'); $holder.replaceWith(data.html);
});
$holder.find('.pagination').on('ajax:success', (e, data) => { };
$holder.replaceWith(data.html);
});
};
})(window);
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */ /* global Flash */
(function() { window.Star = (function() {
this.Star = (function() { function Star() {
function Star() { $('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) { var $starIcon, $starSpan, $this, toggleStar;
var $starIcon, $starSpan, $this, toggleStar; $this = $(this);
$this = $(this); $starSpan = $this.find('span');
$starSpan = $this.find('span'); $starIcon = $this.find('i');
$starIcon = $this.find('i'); toggleStar = function(isStarred) {
toggleStar = function(isStarred) { $this.parent().find('.star-count').text(data.star_count);
$this.parent().find('.star-count').text(data.star_count); if (isStarred) {
if (isStarred) { $starSpan.removeClass('starred').text('Star');
$starSpan.removeClass('starred').text('Star'); $starIcon.removeClass('fa-star').addClass('fa-star-o');
$starIcon.removeClass('fa-star').addClass('fa-star-o'); } else {
} else { $starSpan.addClass('starred').text('Unstar');
$starSpan.addClass('starred').text('Unstar'); $starIcon.removeClass('fa-star-o').addClass('fa-star');
$starIcon.removeClass('fa-star-o').addClass('fa-star'); }
} };
}; toggleStar($starSpan.hasClass('starred'));
toggleStar($starSpan.hasClass('starred')); }).on('ajax:error', function(e, xhr, status, error) {
}).on('ajax:error', function(e, xhr, status, error) { new Flash('Star toggle failed. Try again later.', 'alert');
new Flash('Star toggle failed. Try again later.', 'alert'); });
}); }
}
return Star; return Star;
})(); })();
}).call(window);
(() => { class Subscription {
class Subscription { constructor(containerElm) {
constructor(containerElm) { this.containerElm = containerElm;
this.containerElm = containerElm;
const subscribeButton = containerElm.querySelector('.js-subscribe-button'); const subscribeButton = containerElm.querySelector('.js-subscribe-button');
if (subscribeButton) { if (subscribeButton) {
// remove class so we don't bind twice // remove class so we don't bind twice
subscribeButton.classList.remove('js-subscribe-button'); subscribeButton.classList.remove('js-subscribe-button');
subscribeButton.addEventListener('click', this.toggleSubscription.bind(this)); subscribeButton.addEventListener('click', this.toggleSubscription.bind(this));
}
} }
}
toggleSubscription(event) { toggleSubscription(event) {
const button = event.currentTarget; const button = event.currentTarget;
const buttonSpan = button.querySelector('span'); const buttonSpan = button.querySelector('span');
if (!buttonSpan || button.classList.contains('disabled')) { if (!buttonSpan || button.classList.contains('disabled')) {
return; return;
} }
button.classList.add('disabled'); button.classList.add('disabled');
const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe'; const isSubscribed = buttonSpan.innerHTML.trim().toLowerCase() !== 'subscribe';
const toggleActionUrl = this.containerElm.dataset.url; const toggleActionUrl = this.containerElm.dataset.url;
$.post(toggleActionUrl, () => { $.post(toggleActionUrl, () => {
button.classList.remove('disabled'); button.classList.remove('disabled');
// hack to allow this to work with the issue boards Vue object // hack to allow this to work with the issue boards Vue object
if (document.querySelector('html').classList.contains('issue-boards-page')) { if (document.querySelector('html').classList.contains('issue-boards-page')) {
gl.issueBoards.boardStoreIssueSet( gl.issueBoards.boardStoreIssueSet(
'subscribed', 'subscribed',
!gl.issueBoards.BoardsStore.detail.issue.subscribed, !gl.issueBoards.BoardsStore.detail.issue.subscribed,
); );
} else { } else {
buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe'; buttonSpan.innerHTML = isSubscribed ? 'Subscribe' : 'Unsubscribe';
} }
}); });
} }
static bindAll(selector) { static bindAll(selector) {
[].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm)); [].forEach.call(document.querySelectorAll(selector), elm => new Subscription(elm));
}
} }
}
window.gl = window.gl || {}; window.gl = window.gl || {};
window.gl.Subscription = Subscription; window.gl.Subscription = Subscription;
})();
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, object-shorthand, no-unused-vars, no-shadow, one-var, one-var-declaration-per-line, comma-dangle, max-len */
(function() {
this.SubscriptionSelect = (function() { window.SubscriptionSelect = (function() {
function SubscriptionSelect() { function SubscriptionSelect() {
$('.js-subscription-event').each(function(i, el) { $('.js-subscription-event').each(function(i, el) {
var fieldName; var fieldName;
fieldName = $(el).data("field-name"); fieldName = $(el).data("field-name");
return $(el).glDropdown({ return $(el).glDropdown({
selectable: true, selectable: true,
fieldName: fieldName, fieldName: fieldName,
toggleLabel: (function(_this) { toggleLabel: (function(_this) {
return function(selected, el, instance) { return function(selected, el, instance) {
var $item, label; var $item, label;
label = 'Subscription'; label = 'Subscription';
$item = instance.dropdown.find('.is-active'); $item = instance.dropdown.find('.is-active');
if ($item.length) { if ($item.length) {
label = $item.text(); label = $item.text();
} }
return label; return label;
}; };
})(this), })(this),
clicked: function(options) { clicked: function(options) {
return options.e.preventDefault(); return options.e.preventDefault();
}, },
id: function(obj, el) { id: function(obj, el) {
return $(el).data("id"); return $(el).data("id");
} }
});
}); });
} });
}
return SubscriptionSelect; return SubscriptionSelect;
})(); })();
}).call(window);
...@@ -9,19 +9,18 @@ ...@@ -9,19 +9,18 @@
// //
// <div class="js-syntax-highlight"></div> // <div class="js-syntax-highlight"></div>
// //
(function() {
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) { $.fn.syntaxHighlight = function() {
// Given the element itself, apply highlighting var $children;
return $(this).addClass(gon.user_color_scheme);
} else { if ($(this).hasClass('js-syntax-highlight')) {
// Given a parent element, recurse to any of its applicable children // Given the element itself, apply highlighting
$children = $(this).find('.js-syntax-highlight'); return $(this).addClass(gon.user_color_scheme);
if ($children.length) { } else {
return $children.syntaxHighlight(); // Given a parent element, recurse to any of its applicable children
} $children = $(this).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
} }
}; }
}).call(window); };
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */ /* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, max-len */
(function() { window.TreeView = (function() {
this.TreeView = (function() { function TreeView() {
function TreeView() { this.initKeyNav();
this.initKeyNav(); // Code browser tree slider
// Code browser tree slider // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
// Make the entire tree-item row clickable, but not if clicking another link (like a commit message) $(".tree-content-holder .tree-item").on('click', function(e) {
$(".tree-content-holder .tree-item").on('click', function(e) { var $clickedEl, path;
var $clickedEl, path; $clickedEl = $(e.target);
$clickedEl = $(e.target); path = $('.tree-item-file-name a', this).attr('href');
path = $('.tree-item-file-name a', this).attr('href'); if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) { if (e.metaKey || e.which === 2) {
if (e.metaKey || e.which === 2) { e.preventDefault();
e.preventDefault(); return window.open(path, '_blank');
return window.open(path, '_blank'); } else {
} else { return gl.utils.visitUrl(path);
return gl.utils.visitUrl(path);
}
} }
}); }
// Show the "Loading commit data" for only the first element });
$('span.log_loading:first').removeClass('hide'); // Show the "Loading commit data" for only the first element
} $('span.log_loading:first').removeClass('hide');
}
TreeView.prototype.initKeyNav = function() { TreeView.prototype.initKeyNav = function() {
var li, liSelected; var li, liSelected;
li = $("tr.tree-item"); li = $("tr.tree-item");
liSelected = null; liSelected = null;
return $('body').keydown(function(e) { return $('body').keydown(function(e) {
var next, path; var next, path;
if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) { if ($("input:focus").length > 0 && (e.which === 38 || e.which === 40)) {
return false; return false;
} }
if (e.which === 40) { if (e.which === 40) {
if (liSelected) { if (liSelected) {
next = liSelected.next(); next = liSelected.next();
if (next.length > 0) { if (next.length > 0) {
liSelected.removeClass("selected"); liSelected.removeClass("selected");
liSelected = next.addClass("selected"); liSelected = next.addClass("selected");
}
} else {
liSelected = li.eq(0).addClass("selected");
}
return $(liSelected).focus();
} else if (e.which === 38) {
if (liSelected) {
next = liSelected.prev();
if (next.length > 0) {
liSelected.removeClass("selected");
liSelected = next.addClass("selected");
}
} else {
liSelected = li.last().addClass("selected");
} }
return $(liSelected).focus(); } else {
} else if (e.which === 13) { liSelected = li.eq(0).addClass("selected");
path = $('.tree-item.selected .tree-item-file-name a').attr('href'); }
if (path) { return $(liSelected).focus();
return gl.utils.visitUrl(path); } else if (e.which === 38) {
if (liSelected) {
next = liSelected.prev();
if (next.length > 0) {
liSelected.removeClass("selected");
liSelected = next.addClass("selected");
} }
} else {
liSelected = li.last().addClass("selected");
}
return $(liSelected).focus();
} else if (e.which === 13) {
path = $('.tree-item.selected .tree-item-file-name a').attr('href');
if (path) {
return gl.utils.visitUrl(path);
} }
}); }
}; });
};
return TreeView; return TreeView;
})(); })();
}).call(window);
...@@ -2,34 +2,35 @@ ...@@ -2,34 +2,35 @@
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
((global) => { class User {
global.User = class { constructor({ action }) {
constructor({ action }) { this.action = action;
this.action = action; this.placeProfileAvatarsToTop();
this.placeProfileAvatarsToTop(); this.initTabs();
this.initTabs(); this.hideProjectLimitMessage();
this.hideProjectLimitMessage(); }
}
placeProfileAvatarsToTop() { placeProfileAvatarsToTop() {
$('.profile-groups-avatars').tooltip({ $('.profile-groups-avatars').tooltip({
placement: 'top' placement: 'top'
}); });
} }
initTabs() { initTabs() {
return new global.UserTabs({ return new window.gl.UserTabs({
parentEl: '.user-profile', parentEl: '.user-profile',
action: this.action action: this.action
}); });
} }
hideProjectLimitMessage() { hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', e => { $('.hide-project-limit-message').on('click', e => {
e.preventDefault(); e.preventDefault();
Cookies.set('hide_project_limit_message', 'false'); Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove(); $(this).parents('.project-limit-message').remove();
}); });
} }
}; }
})(window.gl || (window.gl = {}));
window.gl = window.gl || {};
window.gl.User = User;
...@@ -59,117 +59,118 @@ content on the Users#show page. ...@@ -59,117 +59,118 @@ content on the Users#show page.
</div> </div>
</div> </div>
*/ */
((global) => {
class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
}
this.activateTab(this.action); class UserTabs {
constructor ({ defaultAction, action, parentEl }) {
this.loaded = {};
this.defaultAction = defaultAction || 'activity';
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this._location = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
if (this.action === 'show') {
this.action = this.defaultAction;
} }
bindEvents() { this.activateTab(this.action);
this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this); }
this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]') bindEvents() {
.on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event)); this.changeProjectsPageWrapper = this.changeProjectsPage.bind(this);
this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper); this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
} .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
changeProjectsPage(e) { this.$parentEl.on('click', '.gl-pagination a', this.changeProjectsPageWrapper);
e.preventDefault(); }
$('.tab-pane.active').empty(); changeProjectsPage(e) {
const endpoint = $(e.target).attr('href'); e.preventDefault();
this.loadTab(this.getCurrentAction(), endpoint);
}
tabShown(event) { $('.tab-pane.active').empty();
const $target = $(event.target); const endpoint = $(e.target).attr('href');
const action = $target.data('action'); this.loadTab(this.getCurrentAction(), endpoint);
const source = $target.attr('href'); }
const endpoint = $target.data('endpoint');
this.setTab(action, endpoint);
return this.setCurrentAction(source);
}
activateTab(action) { tabShown(event) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`) const $target = $(event.target);
.tab('show'); const action = $target.data('action');
} const source = $target.attr('href');
const endpoint = $target.data('endpoint');
this.setTab(action, endpoint);
return this.setCurrentAction(source);
}
setTab(action, endpoint) { activateTab(action) {
if (this.loaded[action]) { return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
return; .tab('show');
} }
if (action === 'activity') {
this.loadActivities();
}
const loadableActions = ['groups', 'contributed', 'projects', 'snippets']; setTab(action, endpoint) {
if (loadableActions.indexOf(action) > -1) { if (this.loaded[action]) {
return this.loadTab(action, endpoint); return;
} }
if (action === 'activity') {
this.loadActivities();
} }
loadTab(action, endpoint) { const loadableActions = ['groups', 'contributed', 'projects', 'snippets'];
return $.ajax({ if (loadableActions.indexOf(action) > -1) {
beforeSend: () => this.toggleLoading(true), return this.loadTab(action, endpoint);
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
}
});
} }
}
loadActivities() { loadTab(action, endpoint) {
if (this.loaded['activity']) { return $.ajax({
return; beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
type: 'GET',
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
} }
const $calendarWrap = this.$parentEl.find('.user-calendar'); });
$calendarWrap.load($calendarWrap.data('href')); }
new gl.Activities();
return this.loaded['activity'] = true;
}
toggleLoading(status) { loadActivities() {
return this.$parentEl.find('.loading-status .loading') if (this.loaded['activity']) {
.toggle(status); return;
} }
const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href'));
new gl.Activities();
return this.loaded['activity'] = true;
}
setCurrentAction(source) { toggleLoading(status) {
let new_state = source; return this.$parentEl.find('.loading-status .loading')
new_state = new_state.replace(/\/+$/, ''); .toggle(status);
new_state += this._location.search + this._location.hash; }
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
}
getCurrentAction() { setCurrentAction(source) {
return this.$parentEl.find('.nav-links .active a').data('action'); let new_state = source;
} new_state = new_state.replace(/\/+$/, '');
new_state += this._location.search + this._location.hash;
history.replaceState({
url: new_state
}, document.title, new_state);
return new_state;
} }
global.UserTabs = UserTabs;
})(window.gl || (window.gl = {})); getCurrentAction() {
return this.$parentEl.find('.nav-links .active a').data('action');
}
}
window.gl = window.gl || {};
window.gl.UserTabs = UserTabs;
/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
((global) => { const debounceTimeoutDuration = 1000;
const debounceTimeoutDuration = 1000; const invalidInputClass = 'gl-field-error-outline';
const invalidInputClass = 'gl-field-error-outline'; const successInputClass = 'gl-field-success-outline';
const successInputClass = 'gl-field-success-outline'; const unavailableMessageSelector = '.username .validation-error';
const unavailableMessageSelector = '.username .validation-error'; const successMessageSelector = '.username .validation-success';
const successMessageSelector = '.username .validation-success'; const pendingMessageSelector = '.username .validation-pending';
const pendingMessageSelector = '.username .validation-pending'; const invalidMessageSelector = '.username .gl-field-error';
const invalidMessageSelector = '.username .gl-field-error';
class UsernameValidator {
class UsernameValidator { constructor() {
constructor() { this.inputElement = $('#new_user_username');
this.inputElement = $('#new_user_username'); this.inputDomElement = this.inputElement.get(0);
this.inputDomElement = this.inputElement.get(0); this.state = {
this.state = { available: false,
available: false, valid: false,
valid: false, pending: false,
pending: false, empty: true
empty: true };
};
const debounceTimeout = _.debounce((username) => {
const debounceTimeout = _.debounce((username) => { this.validateUsername(username);
this.validateUsername(username); }, debounceTimeoutDuration);
}, debounceTimeoutDuration);
this.inputElement.on('keyup.username_check', () => {
this.inputElement.on('keyup.username_check', () => { const username = this.inputElement.val();
const username = this.inputElement.val();
this.state.valid = this.inputDomElement.validity.valid;
this.state.valid = this.inputDomElement.validity.valid; this.state.empty = !username.length;
this.state.empty = !username.length;
if (this.state.valid) {
return debounceTimeout(username);
}
this.renderState();
});
// Override generic field validation
this.inputElement.on('invalid', this.interceptInvalid.bind(this));
}
renderState() { if (this.state.valid) {
// Clear all state return debounceTimeout(username);
this.clearFieldValidationState();
if (this.state.valid && this.state.available) {
return this.setSuccessState();
} }
if (this.state.empty) { this.renderState();
return this.clearFieldValidationState(); });
}
if (this.state.pending) { // Override generic field validation
return this.setPendingState(); this.inputElement.on('invalid', this.interceptInvalid.bind(this));
} }
if (!this.state.available) { renderState() {
return this.setUnavailableState(); // Clear all state
} this.clearFieldValidationState();
if (!this.state.valid) { if (this.state.valid && this.state.available) {
return this.setInvalidState(); return this.setSuccessState();
}
} }
interceptInvalid(event) { if (this.state.empty) {
event.preventDefault(); return this.clearFieldValidationState();
event.stopPropagation();
} }
validateUsername(username) { if (this.state.pending) {
if (this.state.valid) { return this.setPendingState();
this.state.pending = true;
this.state.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
}
} }
setAvailabilityState(usernameTaken) { if (!this.state.available) {
if (usernameTaken) { return this.setUnavailableState();
this.state.valid = false;
this.state.available = false;
} else {
this.state.available = true;
}
this.state.pending = false;
this.renderState();
} }
clearFieldValidationState() { if (!this.state.valid) {
this.inputElement.siblings('p').hide(); return this.setInvalidState();
this.inputElement.removeClass(invalidInputClass)
.removeClass(successInputClass);
} }
}
setUnavailableState() { interceptInvalid(event) {
const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector); event.preventDefault();
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); event.stopPropagation();
$usernameUnavailableMessage.show(); }
}
setSuccessState() { validateUsername(username) {
const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector); if (this.state.valid) {
this.inputElement.addClass(successInputClass).removeClass(invalidInputClass); this.state.pending = true;
$usernameSuccessMessage.show(); this.state.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
} }
}
setPendingState() { setAvailabilityState(usernameTaken) {
const $usernamePendingMessage = $(pendingMessageSelector); if (usernameTaken) {
if (this.state.pending) { this.state.valid = false;
$usernamePendingMessage.show(); this.state.available = false;
} else { } else {
$usernamePendingMessage.hide(); this.state.available = true;
}
} }
this.state.pending = false;
this.renderState();
}
clearFieldValidationState() {
this.inputElement.siblings('p').hide();
setInvalidState() { this.inputElement.removeClass(invalidInputClass)
const $inputErrorMessage = $(invalidMessageSelector); .removeClass(successInputClass);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass); }
$inputErrorMessage.show();
setUnavailableState() {
const $usernameUnavailableMessage = this.inputElement.siblings(unavailableMessageSelector);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
$usernameUnavailableMessage.show();
}
setSuccessState() {
const $usernameSuccessMessage = this.inputElement.siblings(successMessageSelector);
this.inputElement.addClass(successInputClass).removeClass(invalidInputClass);
$usernameSuccessMessage.show();
}
setPendingState() {
const $usernamePendingMessage = $(pendingMessageSelector);
if (this.state.pending) {
$usernamePendingMessage.show();
} else {
$usernamePendingMessage.hide();
} }
} }
global.UsernameValidator = UsernameValidator; setInvalidState() {
})(window); const $inputErrorMessage = $(invalidMessageSelector);
this.inputElement.addClass(invalidInputClass).removeClass(successInputClass);
$inputErrorMessage.show();
}
}
window.UsernameValidator = UsernameValidator;
...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) { ...@@ -206,8 +206,6 @@ function UsersSelect(currentUser, els) {
return $dropdown.glDropdown({ return $dropdown.glDropdown({
showMenuAbove: showMenuAbove, showMenuAbove: showMenuAbove,
data: function(term, callback) { data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
return _this.users(term, options, function(users) { return _this.users(term, options, function(users) {
// GitLabDropdownFilter returns this.instance // GitLabDropdownFilter returns this.instance
// GitLabDropdownRemote returns this.options.instance // GitLabDropdownRemote returns this.options.instance
......
(() => { class VisibilitySelect {
const gl = window.gl || (window.gl = {}); constructor(container) {
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1');
class VisibilitySelect { this.container = container;
constructor(container) { this.helpBlock = this.container.querySelector('.help-block');
if (!container) throw new Error('VisibilitySelect requires a container element as argument 1'); this.select = this.container.querySelector('select');
this.container = container; }
this.helpBlock = this.container.querySelector('.help-block');
this.select = this.container.querySelector('select');
}
init() { init() {
if (this.select) { if (this.select) {
this.updateHelpText(); this.updateHelpText();
this.select.addEventListener('change', this.updateHelpText.bind(this)); this.select.addEventListener('change', this.updateHelpText.bind(this));
} else { } else {
this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock; this.helpBlock.textContent = this.container.querySelector('.js-locked').dataset.helpBlock;
}
} }
}
updateHelpText() { updateHelpText() {
this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description; this.helpBlock.textContent = this.select.querySelector('option:checked').dataset.description;
}
} }
}
gl.VisibilitySelect = VisibilitySelect; window.gl = window.gl || {};
})(); window.gl.VisibilitySelect = VisibilitySelect;
...@@ -19,9 +19,6 @@ export default { ...@@ -19,9 +19,6 @@ export default {
return hasCI && !ciStatus; return hasCI && !ciStatus;
}, },
hasPipeline() {
return Object.keys(this.mr.pipeline || {}).length > 0;
},
svg() { svg() {
return statusIconEntityMap.icon_status_failed; return statusIconEntityMap.icon_status_failed;
}, },
...@@ -48,11 +45,7 @@ export default { ...@@ -48,11 +45,7 @@ export default {
template: ` template: `
<div class="mr-widget-heading"> <div class="mr-widget-heading">
<div class="ci-widget"> <div class="ci-widget">
<template v-if="!hasPipeline"> <template v-if="hasCIError">
<i class="fa fa-spinner fa-spin append-right-10" aria-hidden="true"></i>
Waiting for pipeline...
</template>
<template v-else-if="hasCIError">
<div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error"> <div class="ci-status-icon ci-status-icon-failed ci-error js-ci-error">
<span class="js-icon-link icon-link"> <span class="js-icon-link icon-link">
<span <span
......
...@@ -4,66 +4,65 @@ ...@@ -4,66 +4,65 @@
import 'vendor/jquery.nicescroll'; import 'vendor/jquery.nicescroll';
import './breakpoints'; import './breakpoints';
((global) => { class Wikis {
class Wikis { constructor() {
constructor() { this.bp = Breakpoints.get();
this.bp = Breakpoints.get(); this.sidebarEl = document.querySelector('.js-wiki-sidebar');
this.sidebarEl = document.querySelector('.js-wiki-sidebar'); this.sidebarExpanded = false;
this.sidebarExpanded = false; $(this.sidebarEl).niceScroll();
$(this.sidebarEl).niceScroll();
const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle'); const sidebarToggles = document.querySelectorAll('.js-sidebar-wiki-toggle');
for (let i = 0; i < sidebarToggles.length; i += 1) { for (let i = 0; i < sidebarToggles.length; i += 1) {
sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e)); sidebarToggles[i].addEventListener('click', e => this.handleToggleSidebar(e));
} }
this.newWikiForm = document.querySelector('form.new-wiki-page');
if (this.newWikiForm) {
this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
}
window.addEventListener('resize', () => this.renderSidebar()); this.newWikiForm = document.querySelector('form.new-wiki-page');
this.renderSidebar(); if (this.newWikiForm) {
this.newWikiForm.addEventListener('submit', e => this.handleNewWikiSubmit(e));
} }
handleNewWikiSubmit(e) { window.addEventListener('resize', () => this.renderSidebar());
if (!this.newWikiForm) return; this.renderSidebar();
}
const slugInput = this.newWikiForm.querySelector('#new_wiki_path'); handleNewWikiSubmit(e) {
const slug = gl.text.slugify(slugInput.value); if (!this.newWikiForm) return;
if (slug.length > 0) { const slugInput = this.newWikiForm.querySelector('#new_wiki_path');
const wikisPath = slugInput.getAttribute('data-wikis-path'); const slug = gl.text.slugify(slugInput.value);
window.location.href = `${wikisPath}/${slug}`;
e.preventDefault();
}
}
handleToggleSidebar(e) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
window.location.href = `${wikisPath}/${slug}`;
e.preventDefault(); e.preventDefault();
this.sidebarExpanded = !this.sidebarExpanded;
this.renderSidebar();
} }
}
sidebarCanCollapse() { handleToggleSidebar(e) {
const bootstrapBreakpoint = this.bp.getBreakpointSize(); e.preventDefault();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm'; this.sidebarExpanded = !this.sidebarExpanded;
} this.renderSidebar();
}
sidebarCanCollapse() {
const bootstrapBreakpoint = this.bp.getBreakpointSize();
return bootstrapBreakpoint === 'xs' || bootstrapBreakpoint === 'sm';
}
renderSidebar() { renderSidebar() {
if (!this.sidebarEl) return; if (!this.sidebarEl) return;
const { classList } = this.sidebarEl; const { classList } = this.sidebarEl;
if (this.sidebarExpanded || !this.sidebarCanCollapse()) { if (this.sidebarExpanded || !this.sidebarCanCollapse()) {
if (!classList.contains('right-sidebar-expanded')) { if (!classList.contains('right-sidebar-expanded')) {
classList.remove('right-sidebar-collapsed'); classList.remove('right-sidebar-collapsed');
classList.add('right-sidebar-expanded'); classList.add('right-sidebar-expanded');
}
} else if (classList.contains('right-sidebar-expanded')) {
classList.add('right-sidebar-collapsed');
classList.remove('right-sidebar-expanded');
} }
} else if (classList.contains('right-sidebar-expanded')) {
classList.add('right-sidebar-collapsed');
classList.remove('right-sidebar-expanded');
} }
} }
}
global.Wikis = Wikis; window.gl = window.gl || {};
})(window.gl || (window.gl = {})); window.gl.Wikis = Wikis;
...@@ -34,65 +34,64 @@ window.Dropzone = Dropzone; ...@@ -34,65 +34,64 @@ window.Dropzone = Dropzone;
// **Cancelable** No // **Cancelable** No
// **Target** a.js-zen-leave // **Target** a.js-zen-leave
// //
(function() {
this.ZenMode = (function() { window.ZenMode = (function() {
function ZenMode() { function ZenMode() {
this.active_backdrop = null; this.active_backdrop = null;
this.active_textarea = null; this.active_textarea = null;
$(document).on('click', '.js-zen-enter', function(e) { $(document).on('click', '.js-zen-enter', function(e) {
e.preventDefault(); e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:enter'); return $(e.currentTarget).trigger('zen_mode:enter');
}); });
$(document).on('click', '.js-zen-leave', function(e) { $(document).on('click', '.js-zen-leave', function(e) {
e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:leave');
});
$(document).on('zen_mode:enter', (function(_this) {
return function(e) {
return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop'));
};
})(this));
$(document).on('zen_mode:leave', (function(_this) {
return function(e) {
return _this.exit();
};
})(this));
$(document).on('keydown', function(e) {
// Esc
if (e.keyCode === 27) {
e.preventDefault(); e.preventDefault();
return $(e.currentTarget).trigger('zen_mode:leave'); return $(document).trigger('zen_mode:leave');
}); }
$(document).on('zen_mode:enter', (function(_this) { });
return function(e) { }
return _this.enter($(e.target).closest('.md-area').find('.zen-backdrop'));
};
})(this));
$(document).on('zen_mode:leave', (function(_this) {
return function(e) {
return _this.exit();
};
})(this));
$(document).on('keydown', function(e) {
// Esc
if (e.keyCode === 27) {
e.preventDefault();
return $(document).trigger('zen_mode:leave');
}
});
}
ZenMode.prototype.enter = function(backdrop) { ZenMode.prototype.enter = function(backdrop) {
Mousetrap.pause(); Mousetrap.pause();
this.active_backdrop = $(backdrop); this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen'); this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea'); this.active_textarea = this.active_backdrop.find('textarea');
// Prevent a user-resized textarea from persisting to fullscreen // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style'); this.active_textarea.removeAttr('style');
return this.active_textarea.focus(); return this.active_textarea.focus();
}; };
ZenMode.prototype.exit = function() { ZenMode.prototype.exit = function() {
if (this.active_textarea) { if (this.active_textarea) {
Mousetrap.unpause(); Mousetrap.unpause();
this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen'); this.active_textarea.closest('.zen-backdrop').removeClass('fullscreen');
this.scrollTo(this.active_textarea); this.scrollTo(this.active_textarea);
this.active_textarea = null; this.active_textarea = null;
this.active_backdrop = null; this.active_backdrop = null;
return Dropzone.forElement('.div-dropzone').enable(); return Dropzone.forElement('.div-dropzone').enable();
} }
}; };
ZenMode.prototype.scrollTo = function(zen_area) { ZenMode.prototype.scrollTo = function(zen_area) {
return $.scrollTo(zen_area, 0, { return $.scrollTo(zen_area, 0, {
offset: -150 offset: -150
}); });
}; };
return ZenMode; return ZenMode;
})(); })();
}).call(window);
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
@mixin maintain-sidebar-dimensions { @mixin maintain-sidebar-dimensions {
display: block; display: block;
width: $gutter-width; width: $gutter-width;
padding: 10px 20px; padding: 10px 0;
} }
.issues-bulk-update.right-sidebar { .issues-bulk-update.right-sidebar {
......
...@@ -37,65 +37,77 @@ ...@@ -37,65 +37,77 @@
} }
.build-page { .build-page {
.sticky { .build-trace-container {
position: absolute; position: relative;
left: 0;
right: 0;
} }
.build-trace-container { .build-trace {
position: absolute;
top: 225px;
left: 15px;
bottom: 10px;
background: $black; background: $black;
color: $gray-darkest; color: $gray-darkest;
font-family: $monospace_font; white-space: pre;
overflow-x: auto;
font-size: 12px; font-size: 12px;
border-radius: 0;
border: none;
&.sidebar-expanded { .bash {
right: 305px; display: block;
} }
}
&.sidebar-collapsed { .top-bar {
right: 16px; height: 35px;
display: flex;
justify-content: flex-end;
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
position: sticky;
position: -webkit-sticky;
top: 50px;
&.affix {
top: 50px;
} }
code { // with sidebar
background: $black; &.affix.sidebar-expanded {
color: $gray-darkest; right: 306px;
left: 16px;
} }
.top-bar { // without sidebar
top: 0; &.affix.sidebar-collapsed {
height: 35px; right: 16px;
display: flex; left: 16px;
justify-content: flex-end; }
background: $gray-light;
border: 1px solid $border-color;
color: $gl-text-color;
.truncated-info { &.affix-top {
margin: 0 auto; position: absolute;
align-self: center; right: 0;
left: 0;
}
.truncated-info-size { .truncated-info {
margin: 0 5px; margin: 0 auto;
} align-self: center;
.raw-link { .truncated-info-size {
color: $gl-text-color; margin: 0 5px;
margin-left: 5px; }
text-decoration: underline;
} .raw-link {
color: $gl-text-color;
margin-left: 5px;
text-decoration: underline;
} }
} }
.controllers { .controllers {
display: flex; display: flex;
align-self: center;
font-size: 15px; font-size: 15px;
margin-bottom: 4px; justify-content: center;
align-items: center;
svg { svg {
height: 15px; height: 15px;
...@@ -103,17 +115,9 @@ ...@@ -103,17 +115,9 @@
fill: $gl-text-color; fill: $gl-text-color;
} }
.controllers-buttons,
.btn-scroll {
color: $gl-text-color;
height: 15px;
vertical-align: middle;
padding: 0;
width: 12px;
}
.controllers-buttons { .controllers-buttons {
margin: 1px 10px; color: $gl-text-color;
margin: 0 10px;
} }
.btn-scroll.animate { .btn-scroll.animate {
...@@ -143,15 +147,6 @@ ...@@ -143,15 +147,6 @@
} }
} }
.bash {
top: 35px;
left: 10px;
bottom: 0;
padding: 10px 20px 20px 5px;
white-space: pre-wrap;
overflow: auto;
}
.environment-information { .environment-information {
border: 1px solid $border-color; border: 1px solid $border-color;
padding: 8px $gl-padding 12px; padding: 8px $gl-padding 12px;
......
...@@ -337,8 +337,7 @@ ...@@ -337,8 +337,7 @@
} }
.text-metric { .text-metric {
font-weight: 600; font-size: 12px;
font-size: 14px;
} }
.selected-metric-line { .selected-metric-line {
...@@ -382,10 +381,6 @@ ...@@ -382,10 +381,6 @@
width: 100%; width: 100%;
padding: 0; padding: 0;
padding-bottom: 100%; padding-bottom: 100%;
.text-metric-bold {
font-weight: 600;
}
} }
.prometheus-svg-container > svg { .prometheus-svg-container > svg {
...@@ -400,11 +395,15 @@ ...@@ -400,11 +395,15 @@
stroke-width: 0; stroke-width: 0;
} }
.text-metric-bold {
font-weight: 600;
}
.label-axis-text, .label-axis-text,
.text-metric-usage { .text-metric-usage {
fill: $black; fill: $black;
font-weight: 500; font-weight: 500;
font-size: 14px; font-size: 12px;
} }
.legend-axis-text { .legend-axis-text {
...@@ -412,7 +411,20 @@ ...@@ -412,7 +411,20 @@
} }
.tick > text { .tick > text {
font-size: 14px; font-size: 12px;
}
.text-metric-title {
font-size: 12px;
}
.y-label-text,
.x-label-text {
fill: $gray-darkest;
}
.axis-tick {
stroke: $gray-darker;
} }
@media (max-width: $screen-sm-max) { @media (max-width: $screen-sm-max) {
...@@ -427,3 +439,9 @@ ...@@ -427,3 +439,9 @@
} }
} }
} }
.prometheus-row {
h5 {
font-size: 16px;
}
}
...@@ -200,7 +200,6 @@ ...@@ -200,7 +200,6 @@
right: 0; right: 0;
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 0 20px;
z-index: 200; z-index: 200;
overflow: hidden; overflow: hidden;
...@@ -224,6 +223,10 @@ ...@@ -224,6 +223,10 @@
} }
} }
.issuable-sidebar {
padding: 0 20px;
}
.issuable-sidebar-header { .issuable-sidebar-header {
padding-top: 10px; padding-top: 10px;
} }
......
...@@ -72,6 +72,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -72,6 +72,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:disabled_oauth_sign_in_sources] = params[:application_setting][:disabled_oauth_sign_in_sources] =
AuthHelper.button_based_providers.map(&:to_s) - AuthHelper.button_based_providers.map(&:to_s) -
Array(enabled_oauth_sign_in_sources) Array(enabled_oauth_sign_in_sources)
params[:application_setting][:restricted_visibility_levels]&.delete("")
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit( params.require(:application_setting).permit(
......
...@@ -110,6 +110,8 @@ class ApplicationController < ActionController::Base ...@@ -110,6 +110,8 @@ class ApplicationController < ActionController::Base
end end
def log_exception(exception) def log_exception(exception)
Raven.capture_exception(exception) if sentry_enabled?
application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace application_trace = ActionDispatch::ExceptionWrapper.new(env, exception).application_trace
application_trace.map!{ |t| " #{t}\n" } application_trace.map!{ |t| " #{t}\n" }
logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}" logger.error "\n#{exception.class.name} (#{exception.message}):\n#{application_trace.join}"
......
...@@ -35,8 +35,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -35,8 +35,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
prompt_for_two_factor(@user) prompt_for_two_factor(@user)
else else
log_audit_event(@user, with: :ldap) log_audit_event(@user, with: :ldap)
flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\ # The counter only gets incremented in `sign_in_and_redirect`
'Refresh the page to see the changes.' show_ldap_sync_flash if @user.sign_in_count == 0
sign_in_and_redirect(@user) sign_in_and_redirect(@user)
end end
else else
...@@ -168,4 +168,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController ...@@ -168,4 +168,9 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
request_params = request.env['omniauth.params'] request_params = request.env['omniauth.params']
(request_params['remember_me'] == '1') if request_params.present? (request_params['remember_me'] == '1') if request_params.present?
end end
def show_ldap_sync_flash
flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\
'Refresh the page to see the changes.'
end
end end
...@@ -16,6 +16,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -16,6 +16,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { render json: {
environments: EnvironmentSerializer environments: EnvironmentSerializer
.new(project: @project, current_user: @current_user) .new(project: @project, current_user: @current_user)
......
...@@ -230,7 +230,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -230,7 +230,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue def issue
return @issue if defined?(@issue) return @issue if defined?(@issue)
# The Sortable default scope causes performance issues when used with find_by # The Sortable default scope causes performance issues when used with find_by
@noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take! @noteable = @issue ||= @project.issues.find_by!(iid: params[:id])
return render_404 unless can?(current_user, :read_issue, @issue) return render_404 unless can?(current_user, :read_issue, @issue)
......
...@@ -45,7 +45,7 @@ class Projects::PathLocksController < Projects::ApplicationController ...@@ -45,7 +45,7 @@ class Projects::PathLocksController < Projects::ApplicationController
private private
def check_license def check_license
unless @project.feature_available?(:file_lock) unless @project.feature_available?(:file_locks)
flash[:alert] = 'You need a different license to enable FileLocks feature' flash[:alert] = 'You need a different license to enable FileLocks feature'
redirect_to admin_license_path redirect_to admin_license_path
end end
......
...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update_params def update_params
params.require(:project).permit( params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds, :auto_cancel_pending_pipelines :public_builds, :auto_cancel_pending_pipelines, :ci_config_path
) )
end end
end end
...@@ -52,7 +52,7 @@ class Projects::RefsController < Projects::ApplicationController ...@@ -52,7 +52,7 @@ class Projects::RefsController < Projects::ApplicationController
contents.push(*tree.blobs) contents.push(*tree.blobs)
contents.push(*tree.submodules) contents.push(*tree.submodules)
show_path_locks = @project.feature_available?(:file_lock) && @project.path_locks.any? show_path_locks = @project.feature_available?(:file_locks) && @project.path_locks.any?
@logs = contents[@offset, @limit].to_a.map do |content| @logs = contents[@offset, @limit].to_a.map do |content|
file = @path ? File.join(@path, content.name) : content.name file = @path ? File.join(@path, content.name) : content.name
......
...@@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder ...@@ -83,7 +83,12 @@ class LabelsFinder < UnionFinder
def projects def projects
return @projects if defined?(@projects) return @projects if defined?(@projects)
@projects = skip_authorization ? Project.all : ProjectsFinder.new(current_user: current_user).execute @projects = if skip_authorization
Project.all
else
ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute
end
@projects = @projects.in_namespace(params[:group_id]) if group? @projects = @projects.in_namespace(params[:group_id]) if group?
@projects = @projects.where(id: params[:project_ids]) if projects? @projects = @projects.where(id: params[:project_ids]) if projects?
@projects = @projects.reorder(nil) @projects = @projects.reorder(nil)
......
...@@ -28,7 +28,14 @@ class ProjectsFinder < UnionFinder ...@@ -28,7 +28,14 @@ class ProjectsFinder < UnionFinder
end end
def execute def execute
collection = init_collection user = params.delete(:user)
collection =
if user
PersonalProjectsFinder.new(user).execute(current_user)
else
init_collection
end
collection = by_ids(collection) collection = by_ids(collection)
collection = by_personal(collection) collection = by_personal(collection)
collection = by_starred(collection) collection = by_starred(collection)
......
...@@ -302,10 +302,6 @@ module ApplicationHelper ...@@ -302,10 +302,6 @@ module ApplicationHelper
end end
end end
def can_toggle_new_nav?
Rails.env.development?
end
def show_new_nav? def show_new_nav?
cookies["new_nav"] == "true" cookies["new_nav"] == "true"
end end
......
...@@ -34,17 +34,17 @@ module ApplicationSettingsHelper ...@@ -34,17 +34,17 @@ module ApplicationSettingsHelper
# Return a group of checkboxes that use Bootstrap's button plugin for a # Return a group of checkboxes that use Bootstrap's button plugin for a
# toggle button effect. # toggle button effect.
def restricted_level_checkboxes(help_block_id) def restricted_level_checkboxes(help_block_id, checkbox_name)
Gitlab::VisibilityLevel.options.map do |name, level| Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level) checked = restricted_visibility_levels(true).include?(level)
css_class = checked ? 'active' : '' css_class = checked ? 'active' : ''
checkbox_name = "application_setting[restricted_visibility_levels][]" tag_name = "application_setting_visibility_level_#{level}"
label_tag(name, class: css_class) do label_tag(tag_name, class: css_class) do
check_box_tag(checkbox_name, level, checked, check_box_tag(checkbox_name, level, checked,
autocomplete: 'off', autocomplete: 'off',
'aria-describedby' => help_block_id, 'aria-describedby' => help_block_id,
id: name) + visibility_level_icon(level) + name id: tag_name) + visibility_level_icon(level) + name
end end
end end
end end
......
...@@ -64,6 +64,11 @@ module GroupsHelper ...@@ -64,6 +64,11 @@ module GroupsHelper
IssuesFinder.new(current_user, group_id: group.id).execute IssuesFinder.new(current_user, group_id: group.id).execute
end end
def remove_group_message(group)
_("You are going to remove %{group_name}.\nRemoved groups CANNOT be restored!\nAre you ABSOLUTELY sure?") %
{ group_name: group.name }
end
private private
def group_title_link(group, hidable: false) def group_title_link(group, hidable: false)
......
...@@ -109,7 +109,7 @@ module TreeHelper ...@@ -109,7 +109,7 @@ module TreeHelper
end end
def lock_file_link(project = @project, path = @path, html_options: {}) def lock_file_link(project = @project, path = @path, html_options: {})
return unless project.feature_available?(:file_lock) && current_user return unless project.feature_available?(:file_locks) && current_user
return if path.blank? return if path.blank?
path_lock = project.find_path_lock(path, downstream: true) path_lock = project.find_path_lock(path, downstream: true)
...@@ -169,7 +169,7 @@ module TreeHelper ...@@ -169,7 +169,7 @@ module TreeHelper
end end
def render_lock_icon(path) def render_lock_icon(path)
return unless @project.feature_available?(:file_lock) return unless @project.feature_available?(:file_locks)
return unless @project.root_ref?(@ref) return unless @project.root_ref?(@ref)
if file_lock = @project.find_path_lock(path, exact_match: true) if file_lock = @project.find_path_lock(path, exact_match: true)
......
...@@ -180,9 +180,12 @@ module Ci ...@@ -180,9 +180,12 @@ module Ci
# * Lowercased # * Lowercased
# * Anything not matching [a-z0-9-] is replaced with a - # * Anything not matching [a-z0-9-] is replaced with a -
# * Maximum length is 63 bytes # * Maximum length is 63 bytes
# * First/Last Character is not a hyphen
def ref_slug def ref_slug
slugified = ref.to_s.downcase ref.to_s
slugified.gsub(/[^a-z0-9]/, '-')[0..62] .downcase
.gsub(/[^a-z0-9]/, '-')[0..62]
.gsub(/(\A-+|-+\z)/, '')
end end
# Variables whose value does not depend on environment # Variables whose value does not depend on environment
...@@ -298,10 +301,6 @@ module Ci ...@@ -298,10 +301,6 @@ module Ci
artifacts_metadata? artifacts_metadata?
end end
def downloadable_single_artifacts_file?
artifacts_metadata? && artifacts_file.file_storage?
end
def artifacts_metadata? def artifacts_metadata?
artifacts? && artifacts_metadata.exists? artifacts? && artifacts_metadata.exists?
end end
......
...@@ -6,6 +6,8 @@ module Ci ...@@ -6,6 +6,8 @@ module Ci
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
prepend ::EE::Ci::Pipeline
belongs_to :project belongs_to :project
belongs_to :user belongs_to :user
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline' belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
...@@ -337,10 +339,24 @@ module Ci ...@@ -337,10 +339,24 @@ module Ci
end end
end end
def ci_yaml_file_path
if project.ci_config_path.blank?
'.gitlab-ci.yml'
else
project.ci_config_path
end
end
def ci_yaml_file def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file) return @ci_yaml_file if defined?(@ci_yaml_file)
@ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil @ci_yaml_file = begin
project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal
self.yaml_errors =
"Failed to load CI/CD config file at #{ci_yaml_file_path}"
nil
end
end end
def has_yaml_errors? def has_yaml_errors?
...@@ -389,7 +405,7 @@ module Ci ...@@ -389,7 +405,7 @@ module Ci
def predefined_variables def predefined_variables
[ [
{ key: 'CI_PIPELINE_ID', value: id.to_s, public: true }, { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
{ key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true } { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true }
] ]
end end
......
...@@ -78,7 +78,7 @@ module CacheMarkdownField ...@@ -78,7 +78,7 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field) def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field) html_field = cached_markdown_fields.html_field(markdown_field)
cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil? cached = cached_html_for(markdown_field).present? && __send__(markdown_field).present?
return false unless cached return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false markdown_changed = attribute_changed?(markdown_field) || false
......
...@@ -5,6 +5,25 @@ ...@@ -5,6 +5,25 @@
module Sortable module Sortable
extend ActiveSupport::Concern extend ActiveSupport::Concern
module DropDefaultScopeOnFinders
# Override these methods to drop the `ORDER BY id DESC` default scope.
# See http://dba.stackexchange.com/a/110919 for why we do this.
%i[find find_by find_by!].each do |meth|
define_method meth do |*args, &block|
return super(*args, &block) if block
unordered_relation = unscope(:order)
# We cannot simply call `meth` on `unscope(:order)`, since that is also
# an instance of the same relation class this module is included into,
# which means we'd get infinite recursion.
# We explicitly use the original implementation to prevent this.
original_impl = method(__method__).super_method.unbind
original_impl.bind(unordered_relation).call(*args)
end
end
end
included do included do
# By default all models should be ordered # By default all models should be ordered
# by created_at field starting from newest # by created_at field starting from newest
...@@ -18,6 +37,10 @@ module Sortable ...@@ -18,6 +37,10 @@ module Sortable
scope :order_updated_asc, -> { reorder(updated_at: :asc) } scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :order_name_asc, -> { reorder(name: :asc) } scope :order_name_asc, -> { reorder(name: :asc) }
scope :order_name_desc, -> { reorder(name: :desc) } scope :order_name_desc, -> { reorder(name: :desc) }
# All queries (relations) on this model are instances of this `relation_klass`.
relation_klass = relation_delegate_class(ActiveRecord::Relation)
relation_klass.prepend DropDefaultScopeOnFinders
end end
module ClassMethods module ClassMethods
......
module EE
module Ci
module Pipeline
def predefined_variables
result = super
result << { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
result
end
end
end
end
module EE module EE
module MergeRequest module MergeRequest
extend ActiveSupport::Concern
include ::Approvable include ::Approvable
included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approvers, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approver_groups, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
end
def ff_merge_possible? def ff_merge_possible?
project.repository.is_ancestor?(target_branch_sha, diff_head_sha) project.repository.is_ancestor?(target_branch_sha, diff_head_sha)
end end
......
...@@ -36,6 +36,10 @@ module EE ...@@ -36,6 +36,10 @@ module EE
has_many :remote_mirrors, inverse_of: :project has_many :remote_mirrors, inverse_of: :project
has_many :path_locks has_many :path_locks
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_project_id
has_many :source_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :project_id
scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only } scope :with_shared_runners_limit_enabled, -> { with_shared_runners.non_public_only }
scope :mirrors_to_sync, -> do scope :mirrors_to_sync, -> do
......
...@@ -18,6 +18,15 @@ module EE ...@@ -18,6 +18,15 @@ module EE
delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=, delegate :shared_runners_minutes_limit, :shared_runners_minutes_limit=,
to: :namespace to: :namespace
has_many :path_locks, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvers, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
# Protected Branch Access
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel # rubocop:disable Cop/ActiveRecordDependent
end end
module ClassMethods module ClassMethods
......
...@@ -2,18 +2,18 @@ class License < ActiveRecord::Base ...@@ -2,18 +2,18 @@ class License < ActiveRecord::Base
include ActionView::Helpers::NumberHelper include ActionView::Helpers::NumberHelper
AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze AUDITOR_USER_FEATURE = 'GitLab_Auditor_User'.freeze
BURNDOWN_CHARTS_FEATURE = 'BurndownCharts'.freeze BURNDOWN_CHARTS_FEATURE = 'GitLab_BurndownCharts'.freeze
CONTRIBUTION_ANALYTICS_FEATURE = 'ContributionAnalytics'.freeze CONTRIBUTION_ANALYTICS_FEATURE = 'GitLab_ContributionAnalytics'.freeze
DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze DEPLOY_BOARD_FEATURE = 'GitLab_DeployBoard'.freeze
ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze ELASTIC_SEARCH_FEATURE = 'GitLab_ElasticSearch'.freeze
EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze EXPORT_ISSUES_FEATURE = 'GitLab_ExportIssues'.freeze
FAST_FORWARD_MERGE_FEATURE = 'GitLab_FastForwardMerge'.freeze FAST_FORWARD_MERGE_FEATURE = 'GitLab_FastForwardMerge'.freeze
FILE_LOCK_FEATURE = 'GitLab_FileLocks'.freeze FILE_LOCKS_FEATURE = 'GitLab_FileLocks'.freeze
GEO_FEATURE = 'GitLab_Geo'.freeze GEO_FEATURE = 'GitLab_Geo'.freeze
GROUP_WEBHOOKS_FEATURE = 'GroupWebhooks'.freeze GROUP_WEBHOOKS_FEATURE = 'GitLab_GroupWebhooks'.freeze
ISSUABLE_DEFAULT_TEMPLATES_FEATURE = 'GitLab_IssuableDefaultTemplates'.freeze ISSUABLE_DEFAULT_TEMPLATES_FEATURE = 'GitLab_IssuableDefaultTemplates'.freeze
ISSUE_BOARDS_FOCUS_MODE_FEATURE = 'IssueBoardsFocusMode'.freeze ISSUE_BOARD_FOCUS_MODE_FEATURE = 'GitLab_IssueBoardFocusMode'.freeze
ISSUE_BOARD_MILESTONE_FEATURE = 'IssueBoardMilestone'.freeze ISSUE_BOARD_MILESTONE_FEATURE = 'GitLab_IssueBoardMilestone'.freeze
ISSUE_WEIGHTS_FEATURE = 'GitLab_IssueWeights'.freeze ISSUE_WEIGHTS_FEATURE = 'GitLab_IssueWeights'.freeze
MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze MERGE_REQUEST_APPROVERS_FEATURE = 'GitLab_MergeRequestApprovers'.freeze
MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze MERGE_REQUEST_REBASE_FEATURE = 'GitLab_MergeRequestRebase'.freeze
...@@ -21,9 +21,9 @@ class License < ActiveRecord::Base ...@@ -21,9 +21,9 @@ class License < ActiveRecord::Base
MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze MULTIPLE_ISSUE_ASSIGNEES_FEATURE = 'GitLab_MultipleIssueAssignees'.freeze
OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze OBJECT_STORAGE_FEATURE = 'GitLab_ObjectStorage'.freeze
PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze PUSH_RULES_FEATURE = 'GitLab_PushRules'.freeze
RELATED_ISSUES_FEATURE = 'RelatedIssues'.freeze RELATED_ISSUES_FEATURE = 'GitLab_RelatedIssues'.freeze
SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze SERVICE_DESK_FEATURE = 'GitLab_ServiceDesk'.freeze
VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'VariableEnvironmentScope'.freeze VARIABLE_ENVIRONMENT_SCOPE_FEATURE = 'GitLab_VariableEnvironmentScope'.freeze
FEATURE_CODES = { FEATURE_CODES = {
auditor_user: AUDITOR_USER_FEATURE, auditor_user: AUDITOR_USER_FEATURE,
...@@ -40,10 +40,10 @@ class License < ActiveRecord::Base ...@@ -40,10 +40,10 @@ class License < ActiveRecord::Base
deploy_board: DEPLOY_BOARD_FEATURE, deploy_board: DEPLOY_BOARD_FEATURE,
export_issues: EXPORT_ISSUES_FEATURE, export_issues: EXPORT_ISSUES_FEATURE,
fast_forward_merge: FAST_FORWARD_MERGE_FEATURE, fast_forward_merge: FAST_FORWARD_MERGE_FEATURE,
file_lock: FILE_LOCK_FEATURE, file_locks: FILE_LOCKS_FEATURE,
group_webhooks: GROUP_WEBHOOKS_FEATURE, group_webhooks: GROUP_WEBHOOKS_FEATURE,
issuable_default_templates: ISSUABLE_DEFAULT_TEMPLATES_FEATURE, issuable_default_templates: ISSUABLE_DEFAULT_TEMPLATES_FEATURE,
issue_board_focus_mode: ISSUE_BOARDS_FOCUS_MODE_FEATURE, issue_board_focus_mode: ISSUE_BOARD_FOCUS_MODE_FEATURE,
issue_board_milestone: ISSUE_BOARD_MILESTONE_FEATURE, issue_board_milestone: ISSUE_BOARD_MILESTONE_FEATURE,
issue_weights: ISSUE_WEIGHTS_FEATURE, issue_weights: ISSUE_WEIGHTS_FEATURE,
merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE, merge_request_approvers: MERGE_REQUEST_APPROVERS_FEATURE,
...@@ -66,7 +66,7 @@ class License < ActiveRecord::Base ...@@ -66,7 +66,7 @@ class License < ActiveRecord::Base
{ FAST_FORWARD_MERGE_FEATURE => 1 }, { FAST_FORWARD_MERGE_FEATURE => 1 },
{ GROUP_WEBHOOKS_FEATURE => 1 }, { GROUP_WEBHOOKS_FEATURE => 1 },
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, { ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 },
{ ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 }, { ISSUE_BOARD_FOCUS_MODE_FEATURE => 1 },
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 }, { ISSUE_BOARD_MILESTONE_FEATURE => 1 },
{ ISSUE_WEIGHTS_FEATURE => 1 }, { ISSUE_WEIGHTS_FEATURE => 1 },
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
...@@ -81,7 +81,7 @@ class License < ActiveRecord::Base ...@@ -81,7 +81,7 @@ class License < ActiveRecord::Base
*EES_FEATURES, *EES_FEATURES,
{ AUDITOR_USER_FEATURE => 1 }, { AUDITOR_USER_FEATURE => 1 },
{ DEPLOY_BOARD_FEATURE => 1 }, { DEPLOY_BOARD_FEATURE => 1 },
{ FILE_LOCK_FEATURE => 1 }, { FILE_LOCKS_FEATURE => 1 },
{ GEO_FEATURE => 1 }, { GEO_FEATURE => 1 },
{ OBJECT_STORAGE_FEATURE => 1 }, { OBJECT_STORAGE_FEATURE => 1 },
{ SERVICE_DESK_FEATURE => 1 }, { SERVICE_DESK_FEATURE => 1 },
...@@ -106,11 +106,11 @@ class License < ActiveRecord::Base ...@@ -106,11 +106,11 @@ class License < ActiveRecord::Base
{ DEPLOY_BOARD_FEATURE => 1 }, { DEPLOY_BOARD_FEATURE => 1 },
{ EXPORT_ISSUES_FEATURE => 1 }, { EXPORT_ISSUES_FEATURE => 1 },
{ FAST_FORWARD_MERGE_FEATURE => 1 }, { FAST_FORWARD_MERGE_FEATURE => 1 },
{ FILE_LOCK_FEATURE => 1 }, { FILE_LOCKS_FEATURE => 1 },
{ GEO_FEATURE => 1 }, { GEO_FEATURE => 1 },
{ GROUP_WEBHOOKS_FEATURE => 1 }, { GROUP_WEBHOOKS_FEATURE => 1 },
{ ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 }, { ISSUABLE_DEFAULT_TEMPLATES_FEATURE => 1 },
{ ISSUE_BOARDS_FOCUS_MODE_FEATURE => 1 }, { ISSUE_BOARD_FOCUS_MODE_FEATURE => 1 },
{ ISSUE_BOARD_MILESTONE_FEATURE => 1 }, { ISSUE_BOARD_MILESTONE_FEATURE => 1 },
{ ISSUE_WEIGHTS_FEATURE => 1 }, { ISSUE_WEIGHTS_FEATURE => 1 },
{ MERGE_REQUEST_APPROVERS_FEATURE => 1 }, { MERGE_REQUEST_APPROVERS_FEATURE => 1 },
......
...@@ -15,9 +15,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -15,9 +15,6 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approvers, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approver_groups, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_request_diffs has_many :merge_request_diffs
has_one :merge_request_diff, has_one :merge_request_diff,
-> { order('merge_request_diffs.id DESC') } -> { order('merge_request_diffs.id DESC') }
......
...@@ -188,9 +188,6 @@ class Project < ActiveRecord::Base ...@@ -188,9 +188,6 @@ class Project < ActiveRecord::Base
has_many :deployments has_many :deployments
has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule' has_many :pipeline_schedules, class_name: 'Ci::PipelineSchedule'
has_many :sourced_pipelines, class_name: Ci::Sources::Pipeline, foreign_key: :source_project_id
has_many :source_pipelines, class_name: Ci::Sources::Pipeline, foreign_key: :project_id
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :variables, allow_destroy: true
...@@ -206,6 +203,11 @@ class Project < ActiveRecord::Base ...@@ -206,6 +203,11 @@ class Project < ActiveRecord::Base
# Validations # Validations
validates :creator, presence: true, on: :create validates :creator, presence: true, on: :create
validates :description, length: { maximum: 2000 }, allow_blank: true validates :description, length: { maximum: 2000 }, allow_blank: true
validates :ci_config_path,
format: { without: /\.{2}/,
message: 'cannot include directory traversal.' },
length: { maximum: 255 },
allow_blank: true
validates :name, validates :name,
presence: true, presence: true,
length: { maximum: 255 }, length: { maximum: 255 },
...@@ -536,6 +538,11 @@ class Project < ActiveRecord::Base ...@@ -536,6 +538,11 @@ class Project < ActiveRecord::Base
import_data&.destroy import_data&.destroy
end end
def ci_config_path=(value)
# Strip all leading slashes so that //foo -> foo
super(value&.sub(%r{\A/+}, '')&.delete("\0"))
end
def import_url=(value) def import_url=(value)
return super(value) unless Gitlab::UrlSanitizer.valid?(value) return super(value) unless Gitlab::UrlSanitizer.valid?(value)
...@@ -830,7 +837,7 @@ class Project < ActiveRecord::Base ...@@ -830,7 +837,7 @@ class Project < ActiveRecord::Base
end end
def ci_service def ci_service
@ci_service ||= ci_services.reorder(nil).find_by(active: true) @ci_service ||= ci_services.find_by(active: true)
end end
def deployment_services def deployment_services
...@@ -838,7 +845,7 @@ class Project < ActiveRecord::Base ...@@ -838,7 +845,7 @@ class Project < ActiveRecord::Base
end end
def deployment_service def deployment_service
@deployment_service ||= deployment_services.reorder(nil).find_by(active: true) @deployment_service ||= deployment_services.find_by(active: true)
end end
def monitoring_services def monitoring_services
...@@ -846,7 +853,7 @@ class Project < ActiveRecord::Base ...@@ -846,7 +853,7 @@ class Project < ActiveRecord::Base
end end
def monitoring_service def monitoring_service
@monitoring_service ||= monitoring_services.reorder(nil).find_by(active: true) @monitoring_service ||= monitoring_services.find_by(active: true)
end end
def jira_tracker? def jira_tracker?
...@@ -1030,7 +1037,8 @@ class Project < ActiveRecord::Base ...@@ -1030,7 +1037,8 @@ class Project < ActiveRecord::Base
namespace: namespace.name, namespace: namespace.name,
visibility_level: visibility_level, visibility_level: visibility_level,
path_with_namespace: path_with_namespace, path_with_namespace: path_with_namespace,
default_branch: default_branch default_branch: default_branch,
ci_config_path: ci_config_path
} }
# Backward compatibility # Backward compatibility
......
...@@ -98,10 +98,13 @@ class KubernetesService < DeploymentService ...@@ -98,10 +98,13 @@ class KubernetesService < DeploymentService
end end
def predefined_variables def predefined_variables
config = YAML.dump(kubeconfig)
variables = [ variables = [
{ key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: actual_namespace, public: true } { key: 'KUBE_NAMESPACE', value: actual_namespace, public: true },
{ key: 'KUBECONFIG', value: config, public: false, file: true }
] ]
if ca_pem.present? if ca_pem.present?
...@@ -137,6 +140,14 @@ class KubernetesService < DeploymentService ...@@ -137,6 +140,14 @@ class KubernetesService < DeploymentService
private private
def kubeconfig
to_kubeconfig(
url: api_url,
namespace: actual_namespace,
token: token,
ca_pem: ca_pem)
end
def namespace_placeholder def namespace_placeholder
default_namespace || TEMPLATE_PLACEHOLDER default_namespace || TEMPLATE_PLACEHOLDER
end end
......
...@@ -1005,7 +1005,7 @@ class Repository ...@@ -1005,7 +1005,7 @@ class Repository
def is_ancestor?(ancestor_id, descendant_id) def is_ancestor?(ancestor_id, descendant_id)
return false if ancestor_id.nil? || descendant_id.nil? return false if ancestor_id.nil? || descendant_id.nil?
Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled| Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
if is_enabled if is_enabled
raw_repository.is_ancestor?(ancestor_id, descendant_id) raw_repository.is_ancestor?(ancestor_id, descendant_id)
...@@ -1158,8 +1158,8 @@ class Repository ...@@ -1158,8 +1158,8 @@ class Repository
blob_data_at(sha, '.gitlab/route-map.yml') blob_data_at(sha, '.gitlab/route-map.yml')
end end
def gitlab_ci_yml_for(sha) def gitlab_ci_yml_for(sha, path = '.gitlab-ci.yml')
blob_data_at(sha, '.gitlab-ci.yml') blob_data_at(sha, path)
end end
private private
......
...@@ -38,9 +38,7 @@ class Snippet < ActiveRecord::Base ...@@ -38,9 +38,7 @@ class Snippet < ActiveRecord::Base
validates :author, presence: true validates :author, presence: true
validates :title, presence: true, length: { maximum: 255 } validates :title, presence: true, length: { maximum: 255 }
validates :file_name, validates :file_name,
length: { maximum: 255 }, length: { maximum: 255 }
format: { with: Gitlab::Regex.file_name_regex,
message: Gitlab::Regex.file_name_regex_message }
validates :content, presence: true validates :content, presence: true
validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values } validates :visibility_level, inclusion: { in: Gitlab::VisibilityLevel.values }
......
...@@ -119,14 +119,6 @@ class User < ActiveRecord::Base ...@@ -119,14 +119,6 @@ class User < ActiveRecord::Base
has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :path_locks, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvals, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
has_many :approvers, dependent: :destroy # rubocop: disable Cop/ActiveRecordDependent
# Protected Branch Access
has_many :protected_branch_merge_access_levels, dependent: :destroy, class_name: ProtectedBranch::MergeAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :protected_branch_push_access_levels, dependent: :destroy, class_name: ProtectedBranch::PushAccessLevel # rubocop:disable Cop/ActiveRecordDependent
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
has_many :issue_assignees has_many :issue_assignees
......
...@@ -37,7 +37,7 @@ module Ci ...@@ -37,7 +37,7 @@ module Ci
unless pipeline.config_processor unless pipeline.config_processor
unless pipeline.ci_yaml_file unless pipeline.ci_yaml_file
return error('Missing .gitlab-ci.yml file') return error("Missing #{pipeline.ci_yaml_file_path} file")
end end
return error(pipeline.yaml_errors, save: save_on_errors) return error(pipeline.yaml_errors, save: save_on_errors)
end end
......
...@@ -10,6 +10,8 @@ class DeleteMergedBranchesService < BaseService ...@@ -10,6 +10,8 @@ class DeleteMergedBranchesService < BaseService
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) } branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
# Prevent deletion of branches relevant to open merge requests # Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names branches -= merge_request_branch_names
# Prevent deletion of protected branches
branches -= project.protected_branches.pluck(:name)
branches.each do |branch| branches.each do |branch|
DeleteBranchService.new(project, current_user).execute(branch) DeleteBranchService.new(project, current_user).execute(branch)
......
...@@ -90,8 +90,12 @@ module MergeRequests ...@@ -90,8 +90,12 @@ module MergeRequests
MergeRequests::PostMergeService.new(project, current_user).execute(merge_request) MergeRequests::PostMergeService.new(project, current_user).execute(merge_request)
if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch? if params[:should_remove_source_branch].present? || @merge_request.force_remove_source_branch?
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user) # Verify again that the source branch can be removed, since branch may be protected,
.execute(merge_request.source_branch) # or the source branch may have been updated.
if @merge_request.can_remove_source_branch?(branch_deletion_user)
DeleteBranchService.new(@merge_request.source_project, branch_deletion_user)
.execute(merge_request.source_branch)
end
end end
end end
......
...@@ -22,7 +22,9 @@ ...@@ -22,7 +22,9 @@
.form-group .form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2' = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
- restricted_level_checkboxes('restricted-visibility-help').each do |level| - checkbox_name = 'application_setting[restricted_visibility_levels][]'
= hidden_field_tag(checkbox_name)
- restricted_level_checkboxes('restricted-visibility-help', checkbox_name).each do |level|
.checkbox .checkbox
= level = level
%span.help-block#restricted-visibility-help %span.help-block#restricted-visibility-help
......
...@@ -72,6 +72,7 @@ ...@@ -72,6 +72,7 @@
= reply_email = reply_email
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled? = boolean_to_icon Gitlab::IncomingEmail.enabled?
- elastic = "Elasticsearch" - elastic = "Elasticsearch"
%p{ "aria-label" => "#{elastic}: status " + (current_application_settings.elasticsearch_search? ? "on" : "off") } %p{ "aria-label" => "#{elastic}: status " + (current_application_settings.elasticsearch_search? ? "on" : "off") }
= elastic = elastic
...@@ -82,6 +83,7 @@ ...@@ -82,6 +83,7 @@
= geo = geo
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab::Geo.enabled? = boolean_to_icon Gitlab::Geo.enabled?
- container_reg = "Container Registry" - container_reg = "Container Registry"
%p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") } %p{ "aria-label" => "#{container_reg}: status " + (Gitlab.config.registry.enabled ? "on" : "off") }
= container_reg = container_reg
...@@ -123,6 +125,7 @@ ...@@ -123,6 +125,7 @@
GitLab API GitLab API
%span.pull-right %span.pull-right
= API::API::version = API::API::version
- if Gitlab::Geo.enabled? - if Gitlab::Geo.enabled?
%p %p
Geo Geo
...@@ -131,6 +134,7 @@ ...@@ -131,6 +134,7 @@
= Gitlab::Geo.current_node.primary ? 'Primary node' : 'Secondary node' = Gitlab::Geo.current_node.primary ? 'Primary node' : 'Secondary node'
- else - else
Undefined Undefined
%p %p
Git Git
%span.pull-right %span.pull-right
......
...@@ -56,10 +56,13 @@ ...@@ -56,10 +56,13 @@
.panel.panel-danger .panel.panel-danger
.panel-heading Remove group .panel-heading Remove group
.panel-body .panel-body
%p = form_tag(@group, method: :delete) do
Removing group will cause all child projects and resources to be removed. %p
%br Removing group will cause all child projects and resources to be removed.
%strong Removed group can not be restored! %br
%strong Removed group can not be restored!
.form-actions .form-actions
= link_to 'Remove group', @group, data: {confirm: 'Removed group can not be restored! Are you sure?'}, method: :delete, class: "btn btn-remove" = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
= render 'shared/confirm_modal', phrase: @group.path
...@@ -86,9 +86,8 @@ ...@@ -86,9 +86,8 @@
= link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username } = link_to "Profile", current_user, class: 'profile-link', data: { user: current_user.username }
%li %li
= link_to "Settings", profile_path = link_to "Settings", profile_path
- if can_toggle_new_nav? %li
%li = link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation")
= link_to "Turn on new nav", profile_preferences_path(anchor: "new-navigation")
%li.divider %li.divider
%li %li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link" = link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link"
......
...@@ -16,25 +16,22 @@ ...@@ -16,25 +16,22 @@
.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
- if can_toggle_new_nav? .col-sm-12
.col-sm-12 %hr
%hr .col-lg-4.profile-settings-sidebar#new-navigation
.col-lg-3.profile-settings-sidebar#new-navigation %h4.prepend-top-0
%h4.prepend-top-0 New Navigation
New Navigation %p
%p This setting allows you to turn on or off the new upcoming navigation concept.
This setting allows you to turn on or off the new upcoming navigation concept. .col-lg-8.syntax-theme
= succeed '.' do = label_tag do
= link_to 'Learn more', '', target: '_blank' .preview= image_tag "old_nav.png"
.col-lg-9.syntax-theme %input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? }
= label_tag do Old
.preview= image_tag "old_nav.png" = label_tag do
%input.js-experiment-feature-toggle{ type: "radio", value: "false", name: "new_nav", checked: !show_new_nav? } .preview= image_tag "new_nav.png"
Old %input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? }
= label_tag do New
.preview= image_tag "new_nav.png"
%input.js-experiment-feature-toggle{ type: "radio", value: "true", name: "new_nav", checked: show_new_nav? }
New
.col-sm-12 .col-sm-12
%hr %hr
.col-lg-4.profile-settings-sidebar .col-lg-4.profile-settings-sidebar
......
- path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path) if @build.downloadable_single_artifacts_file? - path_to_file = file_project_job_artifacts_path(@project, @build, path: file.path)
%tr.tree-item{ 'data-link' => path_to_file } %tr.tree-item{ 'data-link' => path_to_file }
- blob = file.blob - blob = file.blob
%td.tree-item-file-name %td.tree-item-file-name
= tree_icon('file', blob.mode, blob.name) = tree_icon('file', blob.mode, blob.name)
%span.str-truncated %span.str-truncated
- if path_to_file = link_to file.name, path_to_file
= link_to file.name, path_to_file
- else
= file.name
%td %td
= number_to_human_size(blob.size, precision: 2) = number_to_human_size(blob.size, precision: 2)
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
= render 'projects/fork_suggestion' = render 'projects/fork_suggestion'
- if @project.feature_available?(:file_lock) - if @project.feature_available?(:file_locks)
:javascript :javascript
PathLocks.init( PathLocks.init(
'#{toggle_project_path_locks_path(@project)}', '#{toggle_project_path_locks_path(@project)}',
......
...@@ -12,4 +12,4 @@ ...@@ -12,4 +12,4 @@
- if hidden > 0 - if hidden > 0
%li.alert.alert-warning %li.alert.alert-warning
= n_('%d additional commit has been omitted to prevent performance issues.', '%d additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden) = n_('%s additional commit has been omitted to prevent performance issues.', '%s additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
...@@ -35,7 +35,7 @@ ...@@ -35,7 +35,7 @@
= link_to charts_project_graph_path(@project, current_ref) do = link_to charts_project_graph_path(@project, current_ref) do
#{ _('Charts') } #{ _('Charts') }
- if @project.feature_available?(:file_lock) - if @project.feature_available?(:file_locks)
= nav_link(controller: [:path_locks]) do = nav_link(controller: [:path_locks]) do
= link_to project_path_locks_path(@project) do = link_to project_path_locks_path(@project) do
Locked Files Locked Files
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
.top-area .top-area
.row .row
.col-sm-6 .col-sm-6
%h3.page-title %h3
Environment: Environment:
= link_to @environment.name, environment_path(@environment) = link_to @environment.name, environment_path(@environment)
......
...@@ -56,13 +56,14 @@ ...@@ -56,13 +56,14 @@
- else - else
Job has been erased #{time_ago_with_tooltip(@build.erased_at)} Job has been erased #{time_ago_with_tooltip(@build.erased_at)}
.build-trace-container#build-trace .build-trace-container.prepend-top-default
.top-bar.sticky .top-bar.js-top-bar
.js-truncated-info.truncated-info.hidden< .js-truncated-info.truncated-info.hidden<
Showing last Showing last
%span.js-truncated-info-size.truncated-info-size>< %span.js-truncated-info-size.truncated-info-size><
KiB of log - KiB of log -
%a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw %a.js-raw-link.raw-link{ href: raw_project_job_path(@project, @build) }>< Complete Raw
.controllers .controllers
- if @build.has_trace? - if @build.has_trace?
= link_to raw_project_job_path(@project, @build), = link_to raw_project_job_path(@project, @build),
...@@ -84,10 +85,12 @@ ...@@ -84,10 +85,12 @@
.has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} } .has-tooltip.controllers-buttons{ title: 'Scroll to bottom', data: { placement: 'top', container: 'body'} }
%button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true } %button.js-scroll-down.btn-scroll.btn-transparent.btn-blank{ type: 'button', disabled: true }
= custom_icon('scroll_down') = custom_icon('scroll_down')
.bash.sticky.js-scroll-container
%code.js-build-output %pre.build-trace#build-trace
%code.bash.js-build-output
.build-loader-animation.js-build-refresh .build-loader-animation.js-build-refresh
= render "sidebar" = render "sidebar"
.js-build-options{ data: javascript_build_options } .js-build-options{ data: javascript_build_options }
......
- @no_container = true - @no_container = true
- page_title "Charts", "Pipelines" - page_title _("Charts"), _("Pipelines")
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3') = page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs') = page_specific_javascript_bundle_tag('graphs')
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%div{ class: container_class } %div{ class: container_class }
.sub-header-block .sub-header-block
.oneline .oneline
A collection of graphs for Continuous Integration = _("A collection of graphs regarding Continuous Integration")
#charts.ci-charts #charts.ci-charts
.row .row
......
%h4 Overall stats %h4= s_("PipelineCharts|Overall statistics")
%ul %ul
%li %li
Total: = s_("PipelineCharts|Total:")
%strong= pluralize @counts[:total], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
%li %li
Successful: = s_("PipelineCharts|Successful:")
%strong= pluralize @counts[:success], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
%li %li
Failed: = s_("PipelineCharts|Failed:")
%strong= pluralize @counts[:failed], 'pipeline' %strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
%li %li
Success ratio: = s_("PipelineCharts|Success ratio:")
%strong %strong
#{success_ratio(@counts)}% #{success_ratio(@counts)}%
%div %div
%p.light %p.light
Commit duration in minutes for last 30 commits = _("Commit duration in minutes for last 30 commits")
%canvas#build_timesChart{ height: 200 } %canvas#build_timesChart{ height: 200 }
......
%h4 Pipelines charts %h4= _("Pipelines charts")
%p %p
&nbsp; &nbsp;
%span.cgreen %span.cgreen
= icon("circle") = icon("circle")
success = s_("Pipeline|success")
&nbsp; &nbsp;
%span.cgray %span.cgray
= icon("circle") = icon("circle")
all = s_("Pipeline|all")
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last week = _("Jobs for last week")
(#{date_from_to(Date.today - 7.days, Date.today)}) (#{date_from_to(Date.today - 7.days, Date.today)})
%canvas#weekChart{ height: 200 } %canvas#weekChart{ height: 200 }
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last month = _("Jobs for last month")
(#{date_from_to(Date.today - 30.days, Date.today)}) (#{date_from_to(Date.today - 30.days, Date.today)})
%canvas#monthChart{ height: 200 } %canvas#monthChart{ height: 200 }
.prepend-top-default .prepend-top-default
%p.light %p.light
Jobs for last year = _("Jobs for last year")
%canvas#yearChart.padded{ height: 250 } %canvas#yearChart.padded{ height: 250 }
- [:week, :month, :year].each do |scope| - [:week, :month, :year].each do |scope|
......
...@@ -45,6 +45,14 @@ ...@@ -45,6 +45,14 @@
Per job in minutes. If a job passes this threshold, it will be marked as failed Per job in minutes. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank' = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
.form-group
= f.label :ci_config_path, 'Custom CI config path', class: 'label-light'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.help-block
The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr %hr
.form-group .form-group
.checkbox .checkbox
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
= render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post = render 'projects/blob/upload', title: _('Upload New File'), placeholder: _('Upload New File'), button_title: _('Upload file'), form_path: project_create_blob_path(@project, @id), method: :post
= render 'projects/blob/new_dir' = render 'projects/blob/new_dir'
- if @project.feature_available?(:file_lock) - if @project.feature_available?(:file_locks)
:javascript :javascript
PathLocks.init( PathLocks.init(
'#{toggle_project_path_locks_path(@project)}', '#{toggle_project_path_locks_path(@project)}',
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
= auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits") = auto_discovery_link_tag(:atom, project_commits_url(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
= render "projects/commits/head" = render "projects/commits/head"
= render 'projects/last_push' %div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
%div{ class: container_class }
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref = render 'projects/files', commit: @last_commit, project: @project, ref: @ref
...@@ -10,17 +10,18 @@ class ExpirePipelineCacheWorker ...@@ -10,17 +10,18 @@ class ExpirePipelineCacheWorker
store = Gitlab::EtagCaching::Store.new store = Gitlab::EtagCaching::Store.new
store.touch(project_pipelines_path(project)) store.touch(project_pipelines_path(project))
store.touch(project_pipeline_path(pipeline)) store.touch(project_pipeline_path(project, pipeline))
store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
store.touch(new_merge_request_pipelines_path(project)) store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path| each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path) store.touch(path)
end end
store.touch(project_pipeline_path(pipeline.triggered_by_pipeline)) if pipeline.triggered_by_pipeline triggered_by = pipeline.triggered_by_pipeline
store.touch(project_pipeline_path(triggered_by.project, triggered_by)) if triggered_by
pipeline.triggered_pipelines.each do |triggered| pipeline.triggered_pipelines.each do |triggered|
store.touch(project_pipeline_path(triggered)) store.touch(project_pipeline_path(triggered.project, triggered))
end end
Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline) Gitlab::Cache::Ci::ProjectPipelineStatus.update_for_pipeline(pipeline)
...@@ -32,8 +33,8 @@ class ExpirePipelineCacheWorker ...@@ -32,8 +33,8 @@ class ExpirePipelineCacheWorker
Gitlab::Routing.url_helpers.project_pipelines_path(project, format: :json) Gitlab::Routing.url_helpers.project_pipelines_path(project, format: :json)
end end
def project_pipeline_path(pipeline) def project_pipeline_path(project, pipeline)
Gitlab::Routing.url_helpers.project_pipeline_path(pipeline.project, pipeline, format: :json) Gitlab::Routing.url_helpers.project_pipeline_path(project, pipeline, format: :json)
end end
def commit_pipelines_path(project, commit) def commit_pipelines_path(project, commit)
......
---
title: Add optional sha param when approving a merge request through the API
merge_request:
author:
---
title: Improve the performance of the project list API
merge_request: 12679
author:
---
title: All artifacts are now browsable
merge_request:
author:
---
title: 'Geo: Added extra SystemCheck checks'
merge_request: 2354
author:
---
title: Only show the LDAP sync banner on first login
merge_request:
author:
---
title: Fix creation of push rules via POST API
merge_request:
author:
---
title: Replace 'snippets/snippets.feature' spinach with rspec
merge_request: 12385
author: Alexander Randa @randaalex
---
title: Allow creation of files and directories with spaces through Web UI
merge_request: 12608
author:
---
title: Allow admins to disable all restricted visibility levels
merge_request: 12649
author:
---
title: Allow customize CI config path
merge_request: 12509
author: Keith Pope
---
title: Set default for Remove source branch to false.
merge_request: !12576
author:
---
title: "Remove group modal like remove project modal (requires typing + confirmation)"
merge_request: 12569
author: Diego Souza
---
title: Provide KUBECONFIG from KubernetesService for runners
merge_request: 12223
author:
---
title: Add user projects API
merge_request: 12596
author: Ivan Chernov
---
title: Introduce cache policies for CI jobs
merge_request: 12483
author:
---
title: Add Simplified Chinese translations of Commits Page
merge_request: 12405
author: Huang Tao
---
title: Add Traditional Chinese in HongKong translations of Commits Page
merge_request: 12406
author: Huang Tao
---
title: Add Traditional Chinese in Taiwan translations of Commits Page
merge_request: 12407
author: Huang Tao
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