Commit a40bb176 authored by bikebilly's avatar bikebilly

Resolve conflicts

parents ad9c0bae e99ddb6f
......@@ -14,7 +14,7 @@ linters:
# Whether or not to prefer `border: 0` over `border: none`.
BorderZero:
enabled: false
enabled: true
# Reports when you define a rule set using a selector with chained classes
# (a.k.a. adjoining classes).
......
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import _ from 'underscore';
import { insertText, getSelectedFragment, nodeMatchesSelector } from './lib/utils/common_utils';
import { placeholderImage } from './lazy_loader';
import { insertText, getSelectedFragment, nodeMatchesSelector } from '../lib/utils/common_utils';
import { placeholderImage } from '../lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
......@@ -284,7 +285,7 @@ const gfmRules = {
},
};
class CopyAsGFM {
export class CopyAsGFM {
constructor() {
$(document).on('copy', '.md, .wiki', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection); });
$(document).on('copy', 'pre.code.highlight, .diff-content .line_content', (e) => { CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection); });
......@@ -469,7 +470,12 @@ class CopyAsGFM {
}
}
window.gl = window.gl || {};
window.gl.CopyAsGFM = CopyAsGFM;
// Export CopyAsGFM as a global for rspec to access
// see /spec/features/copy_as_gfm_spec.rb
if (process.env.NODE_ENV !== 'production') {
window.CopyAsGFM = CopyAsGFM;
}
new CopyAsGFM();
export default function initCopyAsGFM() {
return new CopyAsGFM();
}
import './autosize';
import './bind_in_out';
import initCopyAsGFM from './copy_as_gfm';
import './details_behavior';
import installGlEmojiElement from './gl_emoji';
import './quick_submit';
......@@ -7,3 +8,4 @@ import './requires_input';
import './toggler_behavior';
installGlEmojiElement();
initCopyAsGFM();
......@@ -46,7 +46,6 @@ import './commits';
import './compare';
import './compare_autocomplete';
import './confirm_danger_modal';
import './copy_as_gfm';
import './copy_to_clipboard';
import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
......
......@@ -138,7 +138,7 @@
renderAxesPaths() {
this.timeSeries = createTimeSeries(
this.graphData.queries[0],
this.graphData.queries,
this.graphWidth,
this.graphHeight,
this.graphHeightOffset,
......@@ -153,8 +153,9 @@
const axisYScale = d3.scale.linear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
axisXScale.domain(d3.extent(this.timeSeries[0].values, d => d.time));
axisYScale.domain([0, d3.max(this.timeSeries[0].values.map(d => d.value))]);
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
const xAxis = d3.svg.axis()
.scale(axisXScale)
......@@ -246,6 +247,7 @@
:key="index"
:generated-line-path="path.linePath"
:generated-area-path="path.areaPath"
:line-style="path.lineStyle"
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
......
......@@ -79,7 +79,8 @@
},
formatMetricUsage(series) {
const value = series.values[this.currentDataIndex].value;
const value = series.values[this.currentDataIndex] &&
series.values[this.currentDataIndex].value;
if (isNaN(value)) {
return '-';
}
......@@ -92,6 +93,12 @@
}
return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
},
strokeDashArray(type) {
if (type === 'dashed') return '6, 3';
if (type === 'dotted') return '3, 3';
return null;
},
},
mounted() {
this.$nextTick(() => {
......@@ -162,13 +169,15 @@
v-for="(series, index) in timeSeries"
:key="index"
:transform="translateLegendGroup(index)">
<rect
:fill="series.areaColor"
:width="measurements.legends.width"
:height="measurements.legends.height"
x="20"
:y="graphHeight - measurements.legendOffset">
</rect>
<line
:stroke="series.lineColor"
:stroke-width="measurements.legends.height"
:stroke-dasharray="strokeDashArray(series.lineStyle)"
:x1="measurements.legends.offsetX"
:x2="measurements.legends.offsetX + measurements.legends.width"
:y1="graphHeight - measurements.legends.offsetY"
:y2="graphHeight - measurements.legends.offsetY">
</line>
<text
v-if="timeSeries.length > 1"
class="legend-metric-title"
......
......@@ -9,6 +9,10 @@
type: String,
required: true,
},
lineStyle: {
type: String,
required: false,
},
lineColor: {
type: String,
required: true,
......@@ -18,6 +22,13 @@
required: true,
},
},
computed: {
strokeDashArray() {
if (this.lineStyle === 'dashed') return '3, 1';
if (this.lineStyle === 'dotted') return '1, 1';
return null;
},
},
};
</script>
<template>
......@@ -34,6 +45,7 @@
:stroke="lineColor"
fill="none"
stroke-width="1"
:stroke-dasharray="strokeDashArray"
transform="translate(-5, 20)">
</path>
</g>
......
......@@ -7,15 +7,16 @@ export default {
left: 40,
},
legends: {
width: 10,
width: 15,
height: 3,
offsetX: 20,
offsetY: 32,
},
backgroundLegend: {
width: 30,
height: 50,
},
axisLabelLineOffset: -20,
legendOffset: 33,
},
large: { // This covers both md and lg screen sizes
margin: {
......@@ -27,13 +28,14 @@ export default {
legends: {
width: 15,
height: 3,
offsetX: 20,
offsetY: 34,
},
backgroundLegend: {
width: 30,
height: 150,
},
axisLabelLineOffset: 20,
legendOffset: 36,
},
xTicks: 8,
yTicks: 3,
......
......@@ -11,7 +11,9 @@ const defaultColorPalette = {
const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
const defaultStyleOrder = ['solid', 'dashed', 'dotted'];
function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle) {
let usedColors = [];
function pickColor(name) {
......@@ -31,17 +33,7 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
return defaultColorPalette[pick];
}
const maxValues = queryData.result.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
index,
};
});
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
return queryData.result.map((timeSeries, timeSeriesNumber) => {
return query.result.map((timeSeries, timeSeriesNumber) => {
let metricTag = '';
let lineColor = '';
let areaColor = '';
......@@ -52,9 +44,9 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
const timeSeriesScaleY = d3.scale.linear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
timeSeriesScaleX.domain(xDom);
timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
......@@ -72,10 +64,10 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
.y1(d => timeSeriesScaleY(d.value));
const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
const seriesCustomizationData = queryData.series != null &&
_.findWhere(queryData.series[0].when,
{ value: timeSeriesMetricLabel });
if (seriesCustomizationData != null) {
const seriesCustomizationData = query.series != null &&
_.findWhere(query.series[0].when, { value: timeSeriesMetricLabel });
if (seriesCustomizationData) {
metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
[lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
......@@ -83,14 +75,35 @@ export default function createTimeSeries(queryData, graphWidth, graphHeight, gra
[lineColor, areaColor] = pickColor();
}
if (query.track) {
metricTag += ` - ${query.track}`;
}
return {
linePath: lineFunction(timeSeries.values),
areaPath: areaFunction(timeSeries.values),
timeSeriesScaleX,
values: timeSeries.values,
lineStyle,
lineColor,
areaColor,
metricTag,
};
});
}
export default function createTimeSeries(queries, graphWidth, graphHeight, graphHeightOffset) {
const allValues = queries.reduce((allQueryResults, query) => allQueryResults.concat(
query.result.reduce((allResults, result) => allResults.concat(result.values), []),
), []);
const xDom = d3.extent(allValues, d => d.time);
const yDom = [0, d3.max(allValues.map(d => d.value))];
return queries.reduce((series, query, index) => {
const lineStyle = defaultStyleOrder[index % defaultStyleOrder.length];
return series.concat(
queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom, yDom, lineStyle),
);
}, []);
}
......@@ -162,13 +162,19 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
items = [
{
header: "" + name
}, {
}
];
const issueItems = [
{
text: 'Issues assigned to me',
url: issuesPath + "/?assignee_username=" + userName
}, {
text: "Issues I've created",
url: issuesPath + "/?author_username=" + userName
}, 'separator', {
}
];
const mergeRequestItems = [
{
text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_username=" + userName
}, {
......@@ -176,6 +182,11 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
url: mrPath + "/?author_username=" + userName
}
];
if (options.issuesDisabled) {
items = items.concat(mergeRequestItems);
} else {
items = items.concat(...issueItems, 'separator', ...mergeRequestItems);
}
if (!name) {
items.splice(0, 1);
}
......@@ -408,6 +419,7 @@ import { isInGroupsPage, isInProjectPage, getGroupSlug, getProjectSlug } from '.
gl.projectOptions[projectPath] = {
name: $projectOptionsDataEl.data('name'),
issuesPath: $projectOptionsDataEl.data('issues-path'),
issuesDisabled: $projectOptionsDataEl.data('issues-disabled'),
mrPath: $projectOptionsDataEl.data('mr-path')
};
}
......
......@@ -4,6 +4,7 @@
import _ from 'underscore';
import 'mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
import { CopyAsGFM } from './behaviors/copy_as_gfm';
export default class ShortcutsIssuable extends ShortcutsNavigation {
constructor(isMergeRequest) {
......@@ -33,8 +34,8 @@ export default class ShortcutsIssuable extends ShortcutsNavigation {
return false;
}
const el = window.gl.CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = window.gl.CopyAsGFM.nodeToGFM(el);
const el = CopyAsGFM.transformGFMSelection(documentFragment.cloneNode(true));
const selected = CopyAsGFM.nodeToGFM(el);
if (selected.trim() === '') {
return false;
......
......@@ -47,8 +47,10 @@
},
},
methods: {
toggleMarkdownPreview() {
this.previewMarkdown = !this.previewMarkdown;
showPreviewTab() {
if (this.previewMarkdown) return;
this.previewMarkdown = true;
/*
Can't use `$refs` as the component is technically in the parent component
......@@ -56,20 +58,22 @@
*/
const text = this.$slots.textarea[0].elm.value;
if (!this.previewMarkdown) {
this.markdownPreview = '';
} else if (text) {
if (text) {
this.markdownPreviewLoading = true;
this.$http.post(this.markdownPreviewPath, { text })
.then(resp => resp.json())
.then((data) => {
this.renderMarkdown(data);
})
.then(data => this.renderMarkdown(data))
.catch(() => new Flash('Error loading markdown preview'));
} else {
this.renderMarkdown();
}
},
showWriteTab() {
this.markdownPreview = '';
this.previewMarkdown = false;
},
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || 'Nothing to preview.';
......@@ -106,7 +110,8 @@
ref="gl-form">
<markdown-header
:preview-markdown="previewMarkdown"
@toggle-markdown="toggleMarkdownPreview" />
@preview-markdown="showPreviewTab"
@write-markdown="showWriteTab" />
<div
class="md-write-holder"
v-show="!previewMarkdown">
......
......@@ -18,23 +18,31 @@
icon,
},
methods: {
toggleMarkdownPreview(e, form) {
if (form && !form.find('.js-vue-markdown-field').length) {
return;
} else if (e.target.blur) {
e.target.blur();
}
isMarkdownForm(form) {
return form && !form.find('.js-vue-markdown-field').length;
},
previewMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
this.$emit('preview-markdown');
},
writeMarkdownTab(event, form) {
if (event.target.blur) event.target.blur();
if (this.isMarkdownForm(form)) return;
this.$emit('toggle-markdown');
this.$emit('write-markdown');
},
},
mounted() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).on('markdown-preview:hide.vue', this.toggleMarkdownPreview);
$(document).on('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).on('markdown-preview:hide.vue', this.writeMarkdownTab);
},
beforeDestroy() {
$(document).on('markdown-preview:show.vue', this.toggleMarkdownPreview);
$(document).off('markdown-preview:hide.vue', this.toggleMarkdownPreview);
$(document).off('markdown-preview:show.vue', this.previewMarkdownTab);
$(document).off('markdown-preview:hide.vue', this.writeMarkdownTab);
},
};
</script>
......@@ -44,17 +52,19 @@
<ul class="nav-links clearfix">
<li :class="{ active: !previewMarkdown }">
<a
class="js-write-link"
href="#md-write-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
@click.prevent="writeMarkdownTab($event)">
Write
</a>
</li>
<li :class="{ active: previewMarkdown }">
<a
class="js-preview-link"
href="#md-preview-holder"
tabindex="-1"
@click.prevent="toggleMarkdownPreview($event)">
@click.prevent="previewMarkdownTab($event)">
Preview
</a>
</li>
......
......@@ -42,8 +42,7 @@
&.avatar-inline {
float: none;
display: inline-block;
margin-left: 4px;
margin-bottom: 2px;
margin-left: 2px;
flex-shrink: 0;
-webkit-flex-shrink: 0;
......@@ -59,7 +58,7 @@
&.avatar-tile {
border-radius: 0;
border: none;
border: 0;
}
&:not([href]):hover {
......@@ -96,7 +95,7 @@
.avatar {
border-radius: 0;
border: none;
border: 0;
height: auto;
width: 100%;
margin: 0;
......
......@@ -39,7 +39,7 @@
}
&.top-block {
border-top: none;
border-top: 0;
.container-fluid {
background-color: inherit;
......@@ -63,7 +63,7 @@
&.footer-block {
margin-top: 0;
border-bottom: none;
border-bottom: 0;
margin-bottom: -$gl-padding;
}
......@@ -100,7 +100,7 @@
&.build-content {
background-color: $white-light;
border-top: none;
border-top: 0;
}
}
......@@ -287,12 +287,12 @@
cursor: pointer;
color: $blue-300;
z-index: 1;
border: none;
border: 0;
background-color: transparent;
&:hover,
&:focus {
border: none;
border: 0;
color: $blue-400;
}
}
......
......@@ -304,7 +304,7 @@
}
.btn-clipboard {
border: none;
border: 0;
padding: 0 5px;
}
......
......@@ -28,7 +28,7 @@
pre {
&.clean {
background: none;
border: none;
border: 0;
margin: 0;
padding: 0;
}
......@@ -142,7 +142,7 @@ li.note {
img { max-width: 100%; }
.note-title {
li {
border-bottom: none !important;
border-bottom: 0 !important;
}
}
}
......@@ -187,7 +187,7 @@ li.note {
pre {
background: $white-light;
border: none;
border: 0;
font-size: 12px;
}
}
......@@ -386,7 +386,7 @@ img.emoji {
}
.hide-bottom-border {
border-bottom: none !important;
border-bottom: 0 !important;
}
.gl-accessibility {
......
......@@ -142,7 +142,7 @@
*/
&.blame {
table {
border: none;
border: 0;
margin: 0;
}
......@@ -150,20 +150,20 @@
border-bottom: 1px solid $blame-border;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
}
td {
border-top: none;
border-bottom: none;
border-top: 0;
border-bottom: 0;
&:first-child {
border-left: none;
border-left: 0;
}
&:last-child {
border-right: none;
border-right: 0;
}
&.blame-commit {
......
......@@ -255,7 +255,7 @@
.clear-search {
width: 35px;
background-color: $white-light;
border: none;
border: 0;
outline: none;
z-index: 1;
......@@ -418,7 +418,7 @@
.droplab-dropdown .dropdown-menu .filter-dropdown-item {
.btn {
border: none;
border: 0;
width: 100%;
text-align: left;
padding: 8px 16px;
......
......@@ -10,7 +10,7 @@
z-index: 1000;
margin-bottom: 0;
min-height: $header-height;
border: none;
border: 0;
border-bottom: 1px solid $border-color;
position: fixed;
top: 0;
......@@ -169,7 +169,7 @@
.navbar-collapse {
flex: 0 0 auto;
border-top: none;
border-top: 0;
padding: 0;
@media (max-width: $screen-xs-max) {
......@@ -352,77 +352,7 @@
.header-user .dropdown-menu-nav,
.header-new .dropdown-menu-nav {
margin-top: 4px;
}
.search {
margin: 4px 8px 0;
form {
height: 32px;
border: 0;
border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
box-shadow: none;
}
}
.search-input {
color: $white-light;
background: none;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
&.search-active {
form {
background-color: rgba($indigo-200, .3);
box-shadow: none;
.search-input {
color: $gl-text-color;
transition: color ease-in-out 0.15s;
}
.search-input::placeholder {
color: $gl-text-color-tertiary;
}
.search-input-wrap {
.search-icon,
.clear-icon {
color: $gl-text-color-tertiary;
transition: color ease-in-out 0.15s;
}
}
}
.location-badge {
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
.clear-icon {
color: $white-light;
}
}
}
margin-top: $dropdown-vertical-offset;
}
.breadcrumbs {
......
.file-content.code {
border: none;
border: 0;
box-shadow: none;
margin: 0;
padding: 0;
......@@ -7,7 +7,7 @@
pre {
padding: 10px 0;
border: none;
border: 0;
border-radius: 0;
font-family: $monospace_font;
font-size: $code_font_size;
......
......@@ -42,7 +42,7 @@
}
&:last-child {
border-bottom: none;
border-bottom: 0;
&.bottom {
background: $gray-light;
......@@ -92,7 +92,7 @@ ul.unstyled-list {
}
ul.unstyled-list > li {
border-bottom: none;
border-bottom: 0;
}
// Generic content list
......@@ -178,7 +178,7 @@ ul.content-list {
// When dragging a list item
&.ui-sortable-helper {
border-bottom: none;
border-bottom: 0;
}
&.list-placeholder {
......@@ -295,7 +295,7 @@ ul.indent-list {
}
> .group-list-tree > .group-row.has-children:first-child {
border-top: none;
border-top: 0;
}
}
......@@ -413,7 +413,7 @@ ul.indent-list {
padding: 0;
&.has-children {
border-top: none;
border-top: 0;
}
&:first-child {
......
......@@ -36,7 +36,7 @@
margin: 0;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
&.active {
......
......@@ -24,7 +24,7 @@
@media (min-width: $screen-md-min) {
margin: 0;
padding: $gl-padding 0;
border: none;
border: 0;
&:not(:last-child) {
border-bottom: 1px solid $white-normal;
......
......@@ -63,7 +63,7 @@
.nav-links {
margin-bottom: 0;
border-bottom: none;
border-bottom: 0;
float: left;
&.wide {
......@@ -335,69 +335,16 @@
border-bottom: 1px solid $border-color;
.nav-links {
border-bottom: none;
border-bottom: 0;
}
}
}
.page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3;
&.affix {
top: $header-height;
}
}
}
}
.with-performance-bar .page-with-layout-nav {
.right-sidebar {
top: ($header-height + 1) * 2 + $performance-bar-height;
}
&.page-with-sub-nav {
.right-sidebar {
top: ($header-height + 1) * 3 + $performance-bar-height;
&.affix {
top: $header-height + $performance-bar-height;
}
}
}
}
@media (max-width: $screen-xs-max) {
.top-area {
flex-flow: row wrap;
.nav-controls {
$controls-margin: $btn-xs-side-margin - 2px;
flex: 0 0 100%;
&.controls-flex {
display: flex;
flex-flow: row wrap;
align-items: center;
justify-content: center;
padding: 0 0 $gl-padding-top;
}
.controls-item,
.controls-item-full,
.controls-item:last-child {
flex: 1 1 35%;
display: block;
width: 100%;
margin: $controls-margin;
}
}
}
.project-item-select-holder.btn-group {
display: flex;
max-width: 350px;
overflow: hidden;
float: right;
.new-project-item-link {
white-space: nowrap;
......
......@@ -17,7 +17,7 @@
.select2-arrow {
background-image: none;
background-color: transparent;
border: none;
border: 0;
padding-top: 12px;
padding-right: 20px;
font-size: 10px;
......@@ -60,12 +60,17 @@
border-radius: $border-radius-base;
border: 1px solid $dropdown-border-color;
min-width: 175px;
color: $gl-grayish-blue;
color: $gl-text-color;
z-index: 999;
}
.select2-results .select2-result-label,
.select2-more-results {
padding: 10px 15px;
.select2-drop-mask {
z-index: 998;
}
.select2-drop.select2-drop-above.select2-drop-active {
border-top: 1px solid $dropdown-border-color;
margin-top: -6px;
}
.select2-container-active {
......@@ -158,18 +163,35 @@
}
}
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
.select2-results .select2-selection-limit {
background: $gray-light;
display: list-item;
padding: 10px 15px;
}
.select2-results {
margin: 0;
padding: 10px 0;
padding: #{$gl-padding / 2} 0;
.select2-no-results,
.select2-searching,
.select2-ajax-error,
.select2-selection-limit {
background: transparent;
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-result-label,
.select2-more-results {
padding: #{$gl-padding / 2} $gl-padding;
}
.select2-highlighted {
background: transparent;
color: $gl-text-color;
.select2-result-label {
background: $dropdown-item-hover-bg;
}
}
.select2-result {
padding: 0 1px;
}
li.select2-result-with-children > .select2-result-label {
font-weight: $gl-font-weight-bold;
......@@ -190,8 +212,6 @@
}
.select2-highlighted {
background: $gl-link-color !important;
.group-result {
.group-path {
color: $white-light;
......
......@@ -9,7 +9,7 @@
&.container-blank {
background: none;
padding: 0;
border: none;
border: 0;
}
}
}
......@@ -111,7 +111,7 @@
}
.block:last-of-type {
border: none;
border: 0;
}
}
......
......@@ -33,7 +33,7 @@ table {
th {
background-color: $gray-light;
font-weight: $gl-font-weight-normal;
border-bottom: none;
border-bottom: 0;
&.wide {
width: 55%;
......
......@@ -21,7 +21,7 @@
}
&.text-file .diff-file {
border-bottom: none;
border-bottom: 0;
}
}
......@@ -66,5 +66,5 @@
.discussion .timeline-entry {
margin: 0;
border-right: none;
border-right: 0;
}
......@@ -167,7 +167,7 @@
&.plain-readme {
background: none;
border: none;
border: 0;
padding: 0;
margin: 0;
font-size: 14px;
......
......@@ -9,7 +9,7 @@
z-index: 1031;
textarea {
border: none;
border: 0;
box-shadow: none;
border-radius: 0;
color: $black;
......
......@@ -48,7 +48,7 @@
overflow-x: auto;
font-size: 12px;
border-radius: 0;
border: none;
border: 0;
.bash {
display: block;
......
......@@ -36,7 +36,7 @@
pre.commit-message {
background: none;
padding: 0;
border: none;
border: 0;
margin: 20px 0;
border-radius: 0;
}
......
.commit-description {
background: none;
border: none;
border: 0;
padding: 0;
margin-top: 10px;
word-break: normal;
......@@ -247,7 +247,7 @@
word-break: normal;
pre {
border: none;
border: 0;
background: inherit;
padding: 0;
margin: 0;
......
......@@ -80,7 +80,7 @@
.panel {
.content-block {
padding: 24px 0;
border-bottom: none;
border-bottom: 0;
position: relative;
@media (max-width: $screen-xs-max) {
......@@ -222,11 +222,11 @@
}
&:first-child {
border-top: none;
border-top: 0;
}
&:last-child {
border-bottom: none;
border-bottom: 0;
}
.stage-nav-item-cell {
......@@ -290,7 +290,7 @@
border-bottom: 1px solid $gray-darker;
&:last-child {
border-bottom: none;
border-bottom: 0;
margin-bottom: 0;
}
......
......@@ -3,6 +3,7 @@
border-bottom: 1px solid $border-color;
color: $gl-text-color;
line-height: 34px;
display: flex;
a {
color: $gl-text-color;
......
......@@ -47,7 +47,7 @@
table {
width: 100%;
font-family: $monospace_font;
border: none;
border: 0;
border-collapse: separate;
margin: 0;
padding: 0;
......@@ -105,7 +105,7 @@
.new_line {
@include user-select(none);
margin: 0;
border: none;
border: 0;
padding: 0 5px;
border-right: 1px solid;
text-align: right;
......@@ -133,7 +133,7 @@
display: block;
margin: 0;
padding: 0 1.5em;
border: none;
border: 0;
position: relative;
&.parallel {
......@@ -359,7 +359,7 @@
cursor: pointer;
&:first-child {
border-left: none;
border-left: 0;
}
&:hover {
......@@ -388,7 +388,7 @@
.file-content .diff-file {
margin: 0;
border: none;
border: 0;
}
.diff-wrap-lines .line_content {
......@@ -400,7 +400,7 @@
}
.files-changed {
border-bottom: none;
border-bottom: 0;
}
.diff-stats-summary-toggler {
......
......@@ -3,13 +3,13 @@
border-top: 1px solid $border-color;
border-right: 1px solid $border-color;
border-left: 1px solid $border-color;
border-bottom: none;
border-bottom: 0;
border-radius: $border-radius-small $border-radius-small 0 0;
background: $gray-normal;
}
#editor {
border: none;
border: 0;
border-radius: 0;
height: 500px;
margin: 0;
......@@ -171,7 +171,7 @@
width: 100%;
margin: 5px 0;
padding: 0;
border-left: none;
border-left: 0;
}
}
......
......@@ -117,7 +117,7 @@
}
.no-btn {
border: none;
border: 0;
background: none;
outline: none;
width: 100%;
......@@ -133,11 +133,11 @@
}
.folder-row {
border-left: none;
border-right: none;
border-left: 0;
border-right: 0;
@media (min-width: $screen-sm-max) {
border-top: none;
border-top: 0;
}
}
......@@ -256,12 +256,6 @@
padding: 0;
padding-bottom: 100%;
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 10px;
}
.text-metric-usage,
.legend-metric-title {
fill: $black;
......@@ -276,19 +270,33 @@
left: 0;
top: 0;
.label-axis-text,
.text-metric-usage {
text {
fill: $gl-text-color;
stroke-width: 0;
}
.text-metric-bold {
font-weight: $gl-font-weight-bold;
}
.label-axis-text {
fill: $black;
font-weight: $gl-font-weight-normal;
font-size: 12px;
font-size: 10px;
}
.legend-axis-text {
fill: $black;
}
.tick > text {
font-size: 12px;
.tick {
> line {
stroke: $gray-darker;
}
> text {
font-size: 12px;
}
}
.text-metric-title {
......
......@@ -85,7 +85,7 @@
}
pre {
border: none;
border: 0;
background: $gray-light;
border-radius: 0;
color: $events-pre-color;
......@@ -128,14 +128,14 @@
}
}
&:last-child { border: none; }
&:last-child { border: 0; }
.event_commits {
li {
&.commit {
background: transparent;
padding: 0;
border: none;
border: 0;
.commit-row-title {
font-size: $gl-font-size;
......
......@@ -79,7 +79,7 @@
.title {
padding: 0;
margin-bottom: 16px;
border-bottom: none;
border-bottom: 0;
}
.btn-edit {
......@@ -131,12 +131,12 @@
top: $header-height;
bottom: 0;
right: 0;
transition: width .3s;
transition: width $right-sidebar-transition-duration;
background: $gray-light;
z-index: 200;
overflow: hidden;
a,
a:not(.btn-retry),
.btn-link {
color: inherit;
}
......@@ -164,7 +164,7 @@
}
&:last-child {
border: none;
border: 0;
}
span {
......@@ -338,7 +338,7 @@
.block {
width: $gutter_collapsed_width - 2px;
padding: 15px 0 0;
border-bottom: none;
border-bottom: 0;
overflow: hidden;
}
......@@ -399,7 +399,7 @@
}
.btn-clipboard {
border: none;
border: 0;
color: $issuable-sidebar-color;
&:hover {
......@@ -613,6 +613,8 @@
float: none;
display: inline-block;
margin-top: 0;
height: auto;
align-self: center;
@media (max-width: $screen-xs-max) {
position: absolute;
......@@ -626,6 +628,8 @@
padding-left: 45px;
padding-right: 45px;
line-height: 35px;
display: flex;
flex-grow: 1;
@media (min-width: $screen-sm-min) {
float: left;
......@@ -637,11 +641,12 @@
.issuable-actions {
@include new-style-dropdown;
padding-top: 10px;
align-self: center;
flex-shrink: 0;
flex: 0 0 auto;
@media (min-width: $screen-sm-min) {
float: right;
padding-top: 0;
}
}
......@@ -655,8 +660,9 @@
.issuable-meta {
display: inline-block;
line-height: 18px;
font-size: 14px;
line-height: 24px;
align-self: center;
}
.js-issuable-selector-wrap {
......
......@@ -134,11 +134,24 @@ ul.related-merge-requests > li {
}
@media (max-width: $screen-xs-max) {
.issue-btn-group {
width: 100%;
.detail-page-header,
.issuable-header {
display: block;
.issuable-meta {
line-height: 18px;
}
}
.btn {
.issuable-actions {
margin-top: 10px;
.issue-btn-group {
width: 100%;
.btn {
width: 100%;
}
}
}
}
......
......@@ -139,7 +139,7 @@
border-left: 1px solid $border-color;
&:first-of-type {
border-left: none;
border-left: 0;
border-top-left-radius: $border-radius-default;
}
......@@ -165,7 +165,7 @@
border-bottom: 1px solid $border-color;
a {
border: none;
border: 0;
border-bottom: 2px solid $link-underline-blue;
margin-right: 0;
color: $black;
......
......@@ -262,7 +262,7 @@ $colors: (
.editor {
pre {
height: 350px;
border: none;
border: 0;
border-radius: 0;
margin-bottom: 0;
}
......
......@@ -150,18 +150,6 @@
display: block;
}
.mr-widget-body {
@include clearfix;
&.media > *:first-child {
margin-right: 10px;
}
.approve-btn {
margin-right: 5px;
}
}
.mr-widget-pipeline-graph {
padding: 0 4px;
......@@ -169,9 +157,8 @@
z-index: 300;
}
.ci-action-icon-wrapper svg {
width: 16px;
height: 16px;
.ci-action-icon-wrapper {
line-height: 16px;
}
}
......@@ -195,10 +182,6 @@
overflow: hidden;
word-break: break-all;
&.media > *:first-child {
margin-right: 10px;
}
&.label-truncated {
position: relative;
display: inline-block;
......@@ -216,6 +199,18 @@
background-color: $gray-light;
}
}
}
.mr-widget-body {
@include clearfix;
&.media > *:first-child {
margin-right: 10px;
}
.approve-btn {
margin-right: 5px;
}
h4 {
float: left;
......@@ -239,10 +234,6 @@
margin-right: 7px;
}
.approve-btn {
margin-right: 5px;
}
label {
font-weight: $gl-font-weight-normal;
}
......@@ -342,17 +333,6 @@
}
}
.mini-pipeline-graph-dropdown-menu .mini-pipeline-graph-dropdown-item {
display: flex;
align-items: center;
.ci-status-text,
.ci-status-icon {
top: 0;
margin-right: 10px;
}
}
.mr-widget-help {
padding: 10px 16px 10px 48px;
font-style: italic;
......
......@@ -16,7 +16,7 @@
.discussion {
.new-note {
margin: 0;
border: none;
border: 0;
}
}
......@@ -106,15 +106,35 @@
background-color: $orange-100;
border-radius: $border-radius-default $border-radius-default 0 0;
border: 1px solid $border-gray-normal;
border-bottom: none;
border-bottom: 0;
padding: 3px 12px;
margin: auto;
align-items: center;
.icon {
margin-right: $issuable-warning-icon-margin;
}
+ .md-area {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.disabled-comment {
border: 0;
border-radius: $label-border-radius;
padding-top: $gl-vert-padding;
padding-bottom: $gl-vert-padding;
.icon svg {
position: relative;
top: 2px;
margin-right: $btn-xs-side-margin;
width: $gl-font-size;
height: $gl-font-size;
fill: $orange-600;
}
}
}
.sidebar-item-value {
......
......@@ -331,7 +331,7 @@ ul.notes {
td {
border: 1px solid $white-normal;
border-left: none;
border-left: 0;
&.notes_line {
vertical-align: middle;
......@@ -476,6 +476,10 @@ ul.notes {
float: none;
margin-left: 0;
}
.btn-group > .discussion-next-btn {
margin-left: -1px;
}
}
.note-actions {
......@@ -666,7 +670,7 @@ ul.notes {
.timeline-entry-inner {
padding-left: $gl-padding;
padding-right: $gl-padding;
border-bottom: none;
border-bottom: 0;
}
}
}
......@@ -679,7 +683,7 @@ ul.notes {
padding: 90px 0;
&.discussion-locked {
border: none;
border: 0;
background-color: $white-light;
}
......@@ -759,7 +763,7 @@ ul.notes {
top: 0;
padding: 0;
background-color: transparent;
border: none;
border: 0;
outline: 0;
color: $gray-darkest;
transition: color $general-hover-transition-duration $general-hover-transition-curve;
......
......@@ -179,7 +179,7 @@
* Play button with icon in dropdowns
*/
.no-btn {
border: none;
border: 0;
background: none;
outline: none;
width: 100%;
......@@ -288,7 +288,7 @@
.pipeline-actions {
@include new-style-dropdown;
border-bottom: none;
border-bottom: 0;
}
.tab-pane {
......@@ -318,7 +318,7 @@
}
.build-log {
border: none;
border: 0;
line-height: initial;
}
}
......@@ -386,13 +386,13 @@
// Remove right connecting horizontal line from first build in last stage
&:first-child {
&::after {
border: none;
border: 0;
}
}
// Remove right curved connectors from all builds in last stage
&:not(:first-child) {
&::after {
border: none;
border: 0;
}
}
// Remove opposite curve
......@@ -409,7 +409,7 @@
// Remove left curved connectors from all builds in first stage
&:not(:first-child) {
&::before {
border: none;
border: 0;
}
}
// Remove opposite curve
......@@ -518,7 +518,7 @@
.dropdown-menu-toggle {
background-color: transparent;
border: none;
border: 0;
padding: 0;
&:focus {
......@@ -823,6 +823,11 @@ button.mini-pipeline-graph-dropdown-toggle {
margin-left: 2px;
display: inline-block;
&::after {
content: '';
display: block;
}
@media (max-width: $screen-xs-max) {
max-width: 60%;
}
......@@ -951,7 +956,7 @@ button.mini-pipeline-graph-dropdown-toggle {
.terminal-container {
.content-block {
border-bottom: none;
border-bottom: 0;
}
#terminal {
......
......@@ -113,7 +113,7 @@
li {
padding: 3px 0;
border: none;
border: 0;
}
}
......
......@@ -80,7 +80,7 @@
.project-feature-settings {
background: $gray-lighter;
border-top: none;
border-top: 0;
margin-bottom: 16px;
}
......@@ -128,7 +128,7 @@
.project-feature-toggle {
position: relative;
border: none;
border: 0;
outline: 0;
display: block;
width: 100px;
......@@ -483,7 +483,7 @@ a.deploy-project-label {
flex: 1;
padding: 0;
background: transparent;
border: none;
border: 0;
line-height: 34px;
margin: 0;
......@@ -1012,7 +1012,7 @@ pre.light-well {
margin: 0;
border-radius: 0 0 1px 1px;
padding: 20px 0;
border: none;
border: 0;
}
.table-bordered {
......@@ -1165,7 +1165,7 @@ pre.light-well {
table-layout: fixed;
&.table-responsive {
border: none;
border: 0;
}
.variable-key {
......
......@@ -64,7 +64,7 @@
.monaco-editor.vs {
.current-line {
border: none;
border: 0;
background: $well-light-border;
}
......@@ -139,7 +139,7 @@
&.active {
background: $white-light;
border-bottom: none;
border-bottom: 0;
}
a {
......@@ -181,7 +181,7 @@
&.tabs-divider {
width: 100%;
background-color: $white-light;
border-right: none;
border-right: 0;
border-top-right-radius: 2px;
}
}
......
......@@ -5,7 +5,7 @@
margin-bottom: $gl-padding;
&:last-child {
border-bottom: none;
border-bottom: 0;
}
}
......@@ -57,7 +57,7 @@ input[type="checkbox"]:hover {
}
.search-input {
border: none;
border: 0;
font-size: 14px;
padding: 0 20px 0 0;
margin-left: 5px;
......@@ -78,10 +78,6 @@ input[type="checkbox"]:hover {
}
.search-input-wrap {
// Fallback if flexbox is not supported
display: inline-block;
width: 100%;
.search-icon,
.clear-icon {
position: absolute;
......
......@@ -141,7 +141,7 @@
}
pre {
border: none;
border: 0;
background: $gray-light;
border-radius: 0;
color: $todo-body-pre-color;
......
......@@ -252,7 +252,7 @@
margin-top: 20px;
padding: 0;
border-top: 1px solid $white-dark;
border-bottom: none;
border-bottom: 0;
}
.commit-stats li {
......
......@@ -39,7 +39,7 @@ module NotesActions
@note = Notes::CreateService.new(note_project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
......@@ -52,7 +52,7 @@ module NotesActions
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
Notes::RenderService.new(current_user).execute([@note], @project)
end
respond_to do |format|
......
......@@ -3,7 +3,7 @@ module RendersNotes
preload_noteable_for_regular_notes(notes)
preload_max_access_for_authors(notes, @project)
preload_first_time_contribution_for_authors(noteable, notes)
Banzai::NoteRenderer.render(notes, @project, current_user)
Notes::RenderService.new(current_user).execute(notes, @project)
notes
end
......
......@@ -57,5 +57,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
end
......@@ -32,6 +32,8 @@ class DashboardController < Dashboard::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: @event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events)
end
def set_show_full_reference
......
......@@ -155,6 +155,8 @@ class GroupsController < Groups::ApplicationController
@events = EventCollection
.new(@projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def user_actions
......
......@@ -3,10 +3,16 @@ class MetricsController < ActionController::Base
protect_from_forgery with: :exception
before_action :validate_prometheus_metrics
def index
render text: metrics_service.metrics_text, content_type: 'text/plain; version=0.0.4'
response = if Gitlab::Metrics.prometheus_metrics_enabled?
metrics_service.metrics_text
else
help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics',
anchor: 'gitlab-prometheus-metrics'
)
"# Metrics are disabled, see: #{help_page}\n"
end
render text: response, content_type: 'text/plain; version=0.0.4'
end
private
......@@ -14,8 +20,4 @@ class MetricsController < ActionController::Base
def metrics_service
@metrics_service ||= MetricsService.new
end
def validate_prometheus_metrics
render_404 unless Gitlab::Metrics.prometheus_metrics_enabled?
end
end
......@@ -27,11 +27,13 @@ class Projects::ClustersController < Projects::ApplicationController
end
def new
@cluster = project.build_cluster
@cluster = Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end
end
def create
@cluster = Ci::CreateClusterService
@cluster = Clusters::CreateService
.new(project, current_user, create_params)
.execute(token_in_session)
......@@ -58,7 +60,7 @@ class Projects::ClustersController < Projects::ApplicationController
end
def update
Ci::UpdateClusterService
Clusters::UpdateService
.new(project, current_user, update_params)
.execute(cluster)
......@@ -88,19 +90,19 @@ class Projects::ClustersController < Projects::ApplicationController
def create_params
params.require(:cluster).permit(
:gcp_project_id,
:gcp_cluster_zone,
:gcp_cluster_name,
:gcp_cluster_size,
:gcp_machine_type,
:project_namespace,
:enabled)
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
end
def update_params
params.require(:cluster).permit(
:project_namespace,
:enabled)
params.require(:cluster).permit(:enabled)
end
def authorize_google_api
......
......@@ -2,7 +2,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
before_action :check_merge_requests_available!
before_action :merge_request
before_action :authorize_read_merge_request!
before_action :ensure_ref_fetched
private
......@@ -10,12 +9,6 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
@issuable = @merge_request ||= @project.merge_requests.find_by!(iid: params[:id])
end
# Make sure merge requests created before 8.0
# have head file in refs/merge-requests/
def ensure_ref_fetched
@merge_request.ensure_ref_fetched if Gitlab::Database.read_write?
end
def merge_request_params
params.require(:merge_request).permit(merge_request_params_attributes)
end
......
......@@ -4,7 +4,6 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include RendersCommits
skip_before_action :merge_request
skip_before_action :ensure_ref_fetched
before_action :authorize_create_merge_request!
before_action :apply_diff_view_cookie!, only: [:diffs, :diff_for_path]
before_action :build_merge_request, except: [:create]
......
......@@ -7,7 +7,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include IssuableCollections
skip_before_action :merge_request, only: [:index, :bulk_update]
skip_before_action :ensure_ref_fetched, only: [:index, :bulk_update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
......@@ -52,7 +51,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
def show
validates_merge_request
ensure_ref_fetched
close_merge_request_without_source_project
check_if_can_be_merged
......
......@@ -300,6 +300,8 @@ class ProjectsController < Projects::ApplicationController
@events = EventCollection
.new(projects, offset: params[:offset].to_i, filter: event_filter)
.to_a
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def project_params
......
......@@ -108,6 +108,8 @@ class UsersController < ApplicationController
.references(:project)
.with_associations
.limit_recent(20, params[:offset])
Events::RenderService.new(current_user).execute(@events, atom_request: request.format.atom?)
end
def load_projects
......
......@@ -172,16 +172,6 @@ module EventsHelper
end
end
def event_note(text, options = {})
text = first_line_in_markdown(text, 150, options)
sanitize(
text,
tags: %w(a img gl-emoji b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
end
def event_commit_title(message)
message ||= ''
(message.split("\n").first || "").truncate(70)
......
......@@ -69,10 +69,16 @@ module MarkupHelper
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
def first_line_in_markdown(text, max_chars = nil, options = {})
md = markdown(text, options).strip
def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
md = markdown_field(object, attribute, options)
truncate_visible(md, max_chars || md.length) if md.present?
text = truncate_visible(md, max_chars || md.length) if md.present?
sanitize(
text,
tags: %w(a img gl-emoji b pre code p span),
attributes: Rails::Html::WhiteListSanitizer.allowed_attributes + ['style', 'data-src', 'data-name', 'data-unicode-version']
)
end
def markdown(text, context = {})
......@@ -83,15 +89,17 @@ module MarkupHelper
prepare_for_rendering(html, context)
end
def markdown_field(object, field)
def markdown_field(object, field, context = {})
object = object.for_display if object.respond_to?(:for_display)
redacted_field_html = object.try(:"redacted_#{field}_html")
return '' unless object.present?
return redacted_field_html if redacted_field_html
html = Banzai.render_field(object, field)
prepare_for_rendering(html, object.banzai_render_context(field))
html = Banzai.render_field(object, field, context)
context.reverse_merge!(object.banzai_render_context(field)) if object.respond_to?(:banzai_render_context)
prepare_for_rendering(html, context)
end
def markup(file_name, text, context = {})
......
module Clusters
class Cluster < ActiveRecord::Base
include Presentable
self.table_name = 'clusters'
belongs_to :user
has_many :cluster_projects, class_name: 'Clusters::Project'
has_many :projects, through: :cluster_projects, class_name: '::Project'
# we force autosave to happen when we save `Cluster` model
has_one :provider_gcp, class_name: 'Clusters::Providers::Gcp', autosave: true
# We have to ":destroy" it today to ensure that we clean also the Kubernetes Integration
has_one :platform_kubernetes, class_name: 'Clusters::Platforms::Kubernetes', autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
validates :name, cluster_name: true
validate :restrict_modification, on: :update
# TODO: Move back this into Clusters::Platforms::Kubernetes in 10.3
# We need callback here because `enabled` belongs to Clusters::Cluster
# Callbacks in Clusters::Platforms::Kubernetes will not be called after update
after_save :update_kubernetes_integration!
delegate :status, to: :provider, allow_nil: true
delegate :status_reason, to: :provider, allow_nil: true
delegate :status_name, to: :provider, allow_nil: true
delegate :on_creation?, to: :provider, allow_nil: true
delegate :update_kubernetes_integration!, to: :platform, allow_nil: true
enum platform_type: {
kubernetes: 1
}
enum provider_type: {
user: 0,
gcp: 1
}
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
def provider
return provider_gcp if gcp?
end
def platform
return platform_kubernetes if kubernetes?
end
def first_project
return @first_project if defined?(@first_project)
@first_project = projects.first
end
alias_method :project, :first_project
private
def restrict_modification
if provider&.on_creation?
errors.add(:base, "cannot modify during creation")
return false
end
true
end
end
end
module Clusters
module Platforms
class Kubernetes < ActiveRecord::Base
self.table_name = 'cluster_platforms_kubernetes'
belongs_to :cluster, inverse_of: :platform_kubernetes, class_name: 'Clusters::Cluster'
attr_encrypted :password,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
before_validation :enforce_namespace_to_lower_case
validates :namespace,
allow_blank: true,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
# We expect to be `active?` only when enabled and cluster is created (the api_url is assigned)
validates :api_url, url: true, presence: true
validates :token, presence: true
# TODO: Glue code till we migrate Kubernetes Integration into Platforms::Kubernetes
after_destroy :destroy_kubernetes_integration!
alias_attribute :ca_pem, :ca_cert
delegate :project, to: :cluster, allow_nil: true
delegate :enabled?, to: :cluster, allow_nil: true
class << self
def namespace_for_project(project)
"#{project.path}-#{project.id}"
end
end
def actual_namespace
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
self.class.namespace_for_project(project) if project
end
def update_kubernetes_integration!
raise 'Kubernetes service already configured' unless manages_kubernetes_service?
# This is neccesary, otheriwse enabled? returns true even though cluster updated with enabled: false
cluster.reload
ensure_kubernetes_service&.update!(
active: enabled?,
api_url: api_url,
namespace: namespace,
token: token,
ca_pem: ca_cert
)
end
private
def enforce_namespace_to_lower_case
self.namespace = self.namespace&.downcase
end
# TODO: glue code till we migrate Kubernetes Service into Platforms::Kubernetes class
def manages_kubernetes_service?
return true unless kubernetes_service&.active?
kubernetes_service.api_url == api_url
end
def destroy_kubernetes_integration!
return unless manages_kubernetes_service?
kubernetes_service&.destroy!
end
def kubernetes_service
@kubernetes_service ||= project&.kubernetes_service
end
def ensure_kubernetes_service
@kubernetes_service ||= kubernetes_service || project&.build_kubernetes_service
end
end
end
end
module Clusters
class Project < ActiveRecord::Base
self.table_name = 'cluster_projects'
belongs_to :cluster, class_name: 'Clusters::Cluster'
belongs_to :project, class_name: '::Project'
end
end
module Clusters
module Providers
class Gcp < ActiveRecord::Base
self.table_name = 'cluster_providers_gcp'
belongs_to :cluster, inverse_of: :provider_gcp, class_name: 'Clusters::Cluster'
default_value_for :zone, 'us-central1-a'
default_value_for :num_nodes, 3
default_value_for :machine_type, 'n1-standard-4'
attr_encrypted :access_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :zone, presence: true
validates :num_nodes,
presence: true,
numericality: {
only_integer: true,
greater_than: 0
}
state_machine :status, initial: :scheduled do
state :scheduled, value: 1
state :creating, value: 2
state :created, value: 3
state :errored, value: 4
event :make_creating do
transition any - [:creating] => :creating
end
event :make_created do
transition any - [:created] => :created
end
event :make_errored do
transition any - [:errored] => :errored
end
before_transition any => [:errored, :created] do |provider|
provider.access_token = nil
provider.operation_id = nil
end
before_transition any => [:creating] do |provider, transition|
operation_id = transition.args.first
raise ArgumentError.new('operation_id is required') unless operation_id.present?
provider.operation_id = operation_id
end
before_transition any => [:errored] do |provider, transition|
status_reason = transition.args.first
provider.status_reason = status_reason if status_reason
end
end
def on_creation?
scheduled? || creating?
end
def api_client
return unless access_token
@api_client ||= GoogleApi::CloudPlatform::Client.new(access_token, nil)
end
end
end
end
......@@ -21,8 +21,8 @@ module IgnorableColumn
@ignored_columns ||= Set.new
end
def ignore_column(name)
ignored_columns << name.to_s
def ignore_column(*names)
ignored_columns.merge(names.map(&:to_s))
end
end
end
module Gcp
class Cluster < ActiveRecord::Base
extend Gitlab::Gcp::Model
include Presentable
belongs_to :project, inverse_of: :cluster
belongs_to :user
belongs_to :service
scope :enabled, -> { where(enabled: true) }
scope :disabled, -> { where(enabled: false) }
default_value_for :gcp_cluster_zone, 'us-central1-a'
default_value_for :gcp_cluster_size, 3
default_value_for :gcp_machine_type, 'n1-standard-2'
attr_encrypted :password,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :kubernetes_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
attr_encrypted :gcp_token,
mode: :per_attribute_iv,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
validates :gcp_project_id,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :gcp_cluster_name,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
validates :gcp_cluster_zone, presence: true
validates :gcp_cluster_size,
presence: true,
numericality: {
only_integer: true,
greater_than: 0
}
validates :project_namespace,
allow_blank: true,
length: 1..63,
format: {
with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message
}
# if we do not do status transition we prevent change
validate :restrict_modification, on: :update, unless: :status_changed?
state_machine :status, initial: :scheduled do
state :scheduled, value: 1
state :creating, value: 2
state :created, value: 3
state :errored, value: 4
event :make_creating do
transition any - [:creating] => :creating
end
event :make_created do
transition any - [:created] => :created
end
event :make_errored do
transition any - [:errored] => :errored
end
before_transition any => [:errored, :created] do |cluster|
cluster.gcp_token = nil
cluster.gcp_operation_id = nil
end
before_transition any => [:errored] do |cluster, transition|
status_reason = transition.args.first
cluster.status_reason = status_reason if status_reason
end
end
def project_namespace_placeholder
"#{project.path}-#{project.id}"
end
def on_creation?
scheduled? || creating?
end
def api_url
'https://' + endpoint if endpoint
end
def restrict_modification
if on_creation?
errors.add(:base, "cannot modify during creation")
return false
end
true
end
end
end
......@@ -8,7 +8,8 @@ class MergeRequest < ActiveRecord::Base
include CreatedAtFilterable
include TimeTrackable
ignore_column :locked_at
ignore_column :locked_at,
:ref_fetched
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
......@@ -426,7 +427,7 @@ class MergeRequest < ActiveRecord::Base
end
def create_merge_request_diff
fetch_ref
fetch_ref!
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37435
Gitlab::GitalyClient.allow_n_plus_1_calls do
......@@ -811,29 +812,14 @@ class MergeRequest < ActiveRecord::Base
end
end
def fetch_ref
write_ref
update_column(:ref_fetched, true)
def fetch_ref!
target_project.repository.fetch_source_branch!(source_project.repository, source_branch, ref_path)
end
def ref_path
"refs/#{Repository::REF_MERGE_REQUEST}/#{iid}/head"
end
def ref_fetched?
super ||
begin
computed_value = project.repository.ref_exists?(ref_path)
update_column(:ref_fetched, true) if computed_value
computed_value
end
end
def ensure_ref_fetched
fetch_ref unless ref_fetched?
end
def in_locked_state
begin
lock_mr
......@@ -975,10 +961,4 @@ class MergeRequest < ActiveRecord::Base
project.merge_requests.merged.where(author_id: author_id).empty?
end
private
def write_ref
target_project.repository.fetch_source_branch(source_project.repository, source_branch, ref_path)
end
end
......@@ -36,7 +36,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
dynamic_path: true
namespace_path: true
validate :nesting_level_allowed
......
......@@ -186,7 +186,9 @@ class Project < ActiveRecord::Base
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster, class_name: 'Gcp::Cluster', inverse_of: :project
has_one :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry,
# which is not managed by the DB. Hence we're still using dependent: :destroy
......@@ -240,10 +242,8 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
dynamic_path: true,
project_path: true,
length: { maximum: 255 },
format: { with: Gitlab::PathRegex.project_path_format_regex,
message: Gitlab::PathRegex.project_path_format_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true
......
......@@ -969,8 +969,8 @@ class Repository
gitlab_shell.fetch_remote(raw_repository, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags)
end
def fetch_source_branch(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch(source_repository.raw_repository, source_branch, local_ref)
def fetch_source_branch!(source_repository, source_branch, local_ref)
raw_repository.fetch_source_branch!(source_repository.raw_repository, source_branch, local_ref)
end
def compare_source_branch(target_branch_name, source_repository, source_branch_name, straight:)
......
......@@ -146,7 +146,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
dynamic_path: true,
user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
......@@ -164,7 +164,7 @@ class User < ActiveRecord::Base
before_validation :set_notification_email, if: :email_changed?
before_validation :set_public_email, if: :public_email_changed?
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: :external_changed?
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
......@@ -1139,8 +1139,9 @@ class User < ActiveRecord::Base
self.can_create_group = false
self.projects_limit = 0
else
self.can_create_group = gitlab_config.default_can_create_group
self.projects_limit = current_application_settings.default_projects_limit
# Only revert these back to the default if they weren't specifically changed in this update.
self.can_create_group = gitlab_config.default_can_create_group unless can_create_group_changed?
self.projects_limit = current_application_settings.default_projects_limit unless projects_limit_changed?
end
end
......
module Gcp
module Clusters
class ClusterPolicy < BasePolicy
alias_method :cluster, :subject
delegate { @subject.project }
delegate { cluster.project }
rule { can?(:master_access) }.policy do
enable :update_cluster
......
module Gcp
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
presents :cluster
def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{gcp_cluster_zone}/#{gcp_cluster_name}"
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end
end
end
class BaseRenderer
attr_reader :current_user
def initialize(current_user = nil)
@current_user = current_user
end
end
module Ci
class CreateClusterService < BaseService
def execute(access_token)
params['gcp_machine_type'] ||= GoogleApi::CloudPlatform::Client::DEFAULT_MACHINE_TYPE
cluster_params =
params.merge(user: current_user,
gcp_token: access_token)
project.create_cluster(cluster_params).tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
end
end
end
module Ci
class FetchGcpOperationService
def execute(cluster)
api_client =
GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
operation = api_client.projects_zones_operations(
cluster.gcp_project_id,
cluster.gcp_cluster_zone,
cluster.gcp_operation_id)
yield(operation) if block_given?
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
end
end
module Ci
class FinalizeClusterCreationService
def execute(cluster)
api_client =
GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
begin
gke_cluster = api_client.projects_zones_clusters_get(
cluster.gcp_project_id,
cluster.gcp_cluster_zone,
cluster.gcp_cluster_name)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
endpoint = gke_cluster.endpoint
api_url = 'https://' + endpoint
ca_cert = Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate)
username = gke_cluster.master_auth.username
password = gke_cluster.master_auth.password
kubernetes_token = Ci::FetchKubernetesTokenService.new(
api_url, ca_cert, username, password).execute
unless kubernetes_token
return cluster.make_errored!('Failed to get a default token of kubernetes')
end
Ci::IntegrateClusterService.new.execute(
cluster, endpoint, ca_cert, kubernetes_token, username, password)
end
end
end
module Ci
class IntegrateClusterService
def execute(cluster, endpoint, ca_cert, token, username, password)
Gcp::Cluster.transaction do
cluster.update!(
enabled: true,
endpoint: endpoint,
ca_cert: ca_cert,
kubernetes_token: token,
username: username,
password: password,
service: cluster.project.find_or_initialize_service('kubernetes'),
status_event: :make_created)
cluster.service.update!(
active: true,
api_url: cluster.api_url,
ca_pem: ca_cert,
namespace: cluster.project_namespace,
token: token)
end
rescue ActiveRecord::RecordInvalid => e
cluster.make_errored!("Failed to integrate cluster into kubernetes_service: #{e.message}")
end
end
end
module Ci
class ProvisionClusterService
def execute(cluster)
api_client =
GoogleApi::CloudPlatform::Client.new(cluster.gcp_token, nil)
begin
operation = api_client.projects_zones_clusters_create(
cluster.gcp_project_id,
cluster.gcp_cluster_zone,
cluster.gcp_cluster_name,
cluster.gcp_cluster_size,
machine_type: cluster.gcp_machine_type)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
return cluster.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
unless operation.status == 'RUNNING' || operation.status == 'PENDING'
return cluster.make_errored!("Operation status is unexpected; #{operation.status_message}")
end
cluster.gcp_operation_id = api_client.parse_operation_id(operation.self_link)
unless cluster.gcp_operation_id
return cluster.make_errored!('Can not find operation_id from self_link')
end
if cluster.make_creating
WaitForClusterCreationWorker.perform_in(
WaitForClusterCreationWorker::INITIAL_INTERVAL, cluster.id)
else
return cluster.make_errored!("Failed to update cluster record; #{cluster.errors}")
end
end
end
end
module Ci
class UpdateClusterService < BaseService
def execute(cluster)
Gcp::Cluster.transaction do
cluster.update!(params)
if params['enabled'] == 'true'
cluster.service.update!(
active: true,
api_url: cluster.api_url,
ca_pem: cluster.ca_cert,
namespace: cluster.project_namespace,
token: cluster.kubernetes_token)
else
cluster.service.update!(active: false)
end
end
rescue ActiveRecord::RecordInvalid => e
cluster.errors.add(:base, e.message)
end
end
end
module Clusters
class CreateService < BaseService
attr_reader :access_token
def execute(access_token)
@access_token = access_token
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
end
private
def create_cluster
Clusters::Cluster.create(cluster_params)
end
def cluster_params
return @cluster_params if defined?(@cluster_params)
params[:provider_gcp_attributes].try do |provider|
provider[:access_token] = access_token
end
@cluster_params = params.merge(user: current_user, projects: [project])
end
end
end
module Clusters
module Gcp
class FetchOperationService
def execute(provider)
operation = provider.api_client.projects_zones_operations(
provider.gcp_project_id,
provider.zone,
provider.operation_id)
yield(operation) if block_given?
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
end
end
end
module Clusters
module Gcp
class FinalizeCreationService
attr_reader :provider
def execute(provider)
@provider = provider
configure_provider
configure_kubernetes
cluster.save!
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
rescue ActiveRecord::RecordInvalid => e
provider.make_errored!("Failed to configure GKE Cluster: #{e.message}")
end
private
def configure_provider
provider.endpoint = gke_cluster.endpoint
provider.status_event = :make_created
end
def configure_kubernetes
cluster.platform_type = :kubernetes
cluster.build_platform_kubernetes(
api_url: 'https://' + gke_cluster.endpoint,
ca_cert: Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
username: gke_cluster.master_auth.username,
password: gke_cluster.master_auth.password,
token: request_kuberenetes_token)
end
def request_kuberenetes_token
Ci::FetchKubernetesTokenService.new(
'https://' + gke_cluster.endpoint,
Base64.decode64(gke_cluster.master_auth.cluster_ca_certificate),
gke_cluster.master_auth.username,
gke_cluster.master_auth.password).execute
end
def gke_cluster
@gke_cluster ||= provider.api_client.projects_zones_clusters_get(
provider.gcp_project_id,
provider.zone,
cluster.name)
end
def cluster
@cluster ||= provider.cluster
end
end
end
end
module Clusters
module Gcp
class ProvisionService
attr_reader :provider
def execute(provider)
@provider = provider
get_operation_id do |operation_id|
if provider.make_creating(operation_id)
WaitForClusterCreationWorker.perform_in(
Clusters::Gcp::VerifyProvisionStatusService::INITIAL_INTERVAL,
provider.cluster_id)
else
provider.make_errored!("Failed to update provider record; #{provider.errors}")
end
end
end
private
def get_operation_id
operation = provider.api_client.projects_zones_clusters_create(
provider.gcp_project_id,
provider.zone,
provider.cluster.name,
provider.num_nodes,
machine_type: provider.machine_type)
unless operation.status == 'PENDING' || operation.status == 'RUNNING'
return provider.make_errored!("Operation status is unexpected; #{operation.status_message}")
end
operation_id = provider.api_client.parse_operation_id(operation.self_link)
unless operation_id
return provider.make_errored!('Can not find operation_id from self_link')
end
yield(operation_id)
rescue Google::Apis::ServerError, Google::Apis::ClientError, Google::Apis::AuthorizationError => e
provider.make_errored!("Failed to request to CloudPlatform; #{e.message}")
end
end
end
end
module Clusters
module Gcp
class VerifyProvisionStatusService
attr_reader :provider
INITIAL_INTERVAL = 2.minutes
EAGER_INTERVAL = 10.seconds
TIMEOUT = 20.minutes
def execute(provider)
@provider = provider
request_operation do |operation|
case operation.status
when 'PENDING', 'RUNNING'
continue_creation(operation)
when 'DONE'
finalize_creation
else
return provider.make_errored!("Unexpected operation status; #{operation.status} #{operation.status_message}")
end
end
end
private
def continue_creation(operation)
if elapsed_time_from_creation(operation) < TIMEOUT
WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
else
provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
end
end
def elapsed_time_from_creation(operation)
Time.now.utc - operation.start_time.to_time.utc
end
def finalize_creation
Clusters::Gcp::FinalizeCreationService.new.execute(provider)
end
def request_operation(&blk)
Clusters::Gcp::FetchOperationService.new.execute(provider, &blk)
end
end
end
end
module Clusters
class UpdateService < BaseService
def execute(cluster)
cluster.update(params)
end
end
end
module Events
class RenderService < BaseRenderer
def execute(events, atom_request: false)
events.map(&:note).compact.group_by(&:project).each do |project, notes|
render_notes(notes, project, atom_request)
end
end
private
def render_notes(notes, project, atom_request)
Notes::RenderService.new(current_user).execute(notes, project, render_options(atom_request))
end
def render_options(atom_request)
return {} unless atom_request
{ only_path: false, xhtml: true }
end
end
end
module Banzai
module NoteRenderer
module Notes
class RenderService < BaseRenderer
# Renders a collection of Note instances.
#
# notes - The notes to render.
# project - The project to use for redacting.
# user - The user viewing the notes.
# path - The request path.
# wiki - The project's wiki.
# git_ref - The current Git reference.
def self.render(notes, project, user = nil, path = nil, wiki = nil, git_ref = nil)
renderer = ObjectRenderer.new(project,
user,
requested_path: path,
project_wiki: wiki,
ref: git_ref)
# Possible options:
# requested_path - The request path.
# project_wiki - The project's wiki.
# ref - The current Git reference.
# only_path - flag to turn relative paths into absolute ones.
# xhtml - flag to save the html in XHTML
def execute(notes, project, **opts)
renderer = Banzai::ObjectRenderer.new(project, current_user, **opts)
renderer.render(notes, :note)
end
......
class AbstractPathValidator < ActiveModel::EachValidator
extend Gitlab::EncodingHelper
def self.path_regex
raise NotImplementedError
end
def self.format_regex
raise NotImplementedError
end
def self.format_error_message
raise NotImplementedError
end
def self.full_path(record, value)
value
end
def self.valid_path?(path)
encode!(path)
"#{path}/" =~ path_regex
end
def validate_each(record, attribute, value)
unless value =~ self.class.format_regex
record.errors.add(attribute, self.class.format_error_message)
return
end
full_path = self.class.full_path(record, value)
return unless full_path
unless self.class.valid_path?(full_path)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# ClusterNameValidator
#
# Custom validator for ClusterName.
class ClusterNameValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
if record.user?
unless value.present?
record.errors.add(attribute, " has to be present")
end
elsif record.gcp?
if record.persisted? && record.name_changed?
record.errors.add(attribute, " can not be changed because it's synchronized with provider")
end
unless value.length >= 1 && value.length <= 63
record.errors.add(attribute, " is invalid syntax")
end
unless value =~ Gitlab::Regex.kubernetes_namespace_regex
record.errors.add(attribute, Gitlab::Regex.kubernetes_namespace_regex_message)
end
end
end
end
# DynamicPathValidator
#
# Custom validator for GitLab path values.
# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
#
# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
extend Gitlab::EncodingHelper
class << self
def valid_user_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.root_namespace_path_regex
end
def valid_group_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_namespace_path_regex
end
def valid_project_path?(path)
encode!(path)
"#{path}/" =~ Gitlab::PathRegex.full_project_path_regex
end
end
def path_valid_for_record?(record, value)
full_path = record.respond_to?(:build_full_path) ? record.build_full_path : value
return true unless full_path
case record
when Project
self.class.valid_project_path?(full_path)
when Group
self.class.valid_group_path?(full_path)
else # User or non-Group Namespace
self.class.valid_user_path?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::PathRegex.namespace_format_regex
record.errors.add(attribute, Gitlab::PathRegex.namespace_format_message)
return
end
unless path_valid_for_record?(record, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
class NamespacePathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
def self.path_regex
Gitlab::PathRegex.full_namespace_path_regex
end
def self.format_regex
Gitlab::PathRegex.namespace_format_regex
end
def self.format_error_message
Gitlab::PathRegex.namespace_format_message
end
def self.full_path(record, value)
record.build_full_path
end
end
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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