Commit 738bf2c2 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/gb/manual-actions-protected-branches-permissions

* master: (103 commits)
  Include missing project attributes to Import/Export
  Create the rest of the wiki docs
  Fill in information about creating the wiki Home page
  Move wiki doc to its own index page
  Create initial file for Wiki documentation
  Default to null user when asignee is unselected
  Re-enable ref operations with gitaly after not-found fix
  #31560 Add repo parameter to gitaly:install and workhorse:install
  Remove N+1 queries when checking nodes visible to user
  Don't validate reserved words if the format doesn't match
  Revert "Shorten and improve some job names"
  Remove unused initializer
  DRY the `<<: *except-docs` a bit in `.gitlab-ci.yml`
  Make the static-analysis job be run for docs branches too
  Add download_snippet_path helper
  Refresh the markdown cache if it was `nil`
  Add some documentation for the new migration helpers
  Update comments
  Display comments for personal snippets
  Update docs on creating a project
  ...
parents eb45582f 40cc917a
...@@ -264,11 +264,25 @@ spinach mysql 9 10: *spinach-knapsack-mysql ...@@ -264,11 +264,25 @@ spinach mysql 9 10: *spinach-knapsack-mysql
static-analysis: static-analysis:
<<: *ruby-static-analysis <<: *ruby-static-analysis
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
stage: test stage: test
script: script:
- scripts/static-analysis - scripts/static-analysis
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
downtime_check: downtime_check:
<<: *rake-exec <<: *rake-exec
except: except:
...@@ -300,39 +314,38 @@ ee_compat_check: ...@@ -300,39 +314,38 @@ ee_compat_check:
.db-migrate-reset: &db-migrate-reset .db-migrate-reset: &db-migrate-reset
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:migrate:reset - bundle exec rake db:migrate:reset
db:migrate:reset pg: rake pg db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-pg <<: *use-pg
<<: *except-docs
db:migrate:reset mysql: rake mysql db:migrate:reset:
<<: *db-migrate-reset <<: *db-migrate-reset
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-rollback: &db-rollback .db-rollback: &db-rollback
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
script: script:
- bundle exec rake db:rollback STEP=120 - bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate - bundle exec rake db:migrate
db:rollback pg: rake pg db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-pg <<: *use-pg
<<: *except-docs
db:rollback mysql: rake mysql db:rollback:
<<: *db-rollback <<: *db-rollback
<<: *use-mysql <<: *use-mysql
<<: *except-docs
.db-seed_fu: &db-seed_fu .db-seed_fu: &db-seed_fu
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs
variables: variables:
SIZE: "1" SIZE: "1"
SETUP_DB: "false" SETUP_DB: "false"
...@@ -347,17 +360,15 @@ db:rollback mysql: ...@@ -347,17 +360,15 @@ db:rollback mysql:
paths: paths:
- log/development.log - log/development.log
db:seed_fu pg: rake pg db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-pg <<: *use-pg
<<: *except-docs
db:seed_fu mysql: rake mysql db:seed_fu:
<<: *db-seed_fu <<: *db-seed_fu
<<: *use-mysql <<: *use-mysql
<<: *except-docs
gitlab:assets:compile: rake gitlab:assets:compile:
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
<<: *except-docs <<: *except-docs
...@@ -377,7 +388,7 @@ gitlab:assets:compile: ...@@ -377,7 +388,7 @@ gitlab:assets:compile:
paths: paths:
- webpack-report/ - webpack-report/
karma: rake karma:
cache: cache:
paths: paths:
- vendor/ruby - vendor/ruby
...@@ -396,21 +407,6 @@ karma: ...@@ -396,21 +407,6 @@ karma:
paths: paths:
- coverage-javascript/ - coverage-javascript/
docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test
<<: *dedicated-runner
cache: {}
dependencies: []
before_script: []
script:
- mv doc/ /nanoc/content/
- cd /nanoc
# Build HTML from Markdown
- bundle exec nanoc
# Check the internal links
- bundle exec nanoc check internal_links
bundler:audit: bundler:audit:
stage: test stage: test
<<: *ruby-static-analysis <<: *ruby-static-analysis
...@@ -443,11 +439,11 @@ bundler:audit: ...@@ -443,11 +439,11 @@ bundler:audit:
- . scripts/prepare_build.sh - . scripts/prepare_build.sh
- bundle exec rake db:migrate - bundle exec rake db:migrate
migration path pg: migration pg paths:
<<: *migration-paths <<: *migration-paths
<<: *use-pg <<: *use-pg
migration path mysql: migration mysql paths:
<<: *migration-paths <<: *migration-paths
<<: *use-mysql <<: *use-mysql
...@@ -502,30 +498,14 @@ trigger_docs: ...@@ -502,30 +498,14 @@ trigger_docs:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee - master@gitlab-org/gitlab-ee
# Notify slack in the end
notify:slack:
stage: post-test
<<: *dedicated-runner
variables:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
- tags@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
- tags@gitlab-org/gitlab-ee
pages: pages:
before_script: [] before_script: []
stage: pages stage: pages
<<: *dedicated-runner <<: *dedicated-runner
dependencies: dependencies:
- coverage - coverage
- karma - rake karma
- gitlab:assets:compile - rake gitlab:assets:compile
- lint:javascript:report - lint:javascript:report
script: script:
- mv public/ .public/ - mv public/ .public/
......
...@@ -961,7 +961,7 @@ RSpec/DescribeSymbol: ...@@ -961,7 +961,7 @@ RSpec/DescribeSymbol:
# Checks that the second argument to top level describe is the tested method # Checks that the second argument to top level describe is the tested method
# name. # name.
RSpec/DescribedClass: RSpec/DescribedClass:
Enabled: false Enabled: true
# Checks for long example. # Checks for long example.
RSpec/ExampleLength: RSpec/ExampleLength:
......
app/assets/images/ci_favicons/favicon_status_canceled.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
app/assets/images/ci_favicons/favicon_status_canceled.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_created.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
app/assets/images/ci_favicons/favicon_status_created.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_failed.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
app/assets/images/ci_favicons/favicon_status_failed.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_manual.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
app/assets/images/ci_favicons/favicon_status_manual.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_not_found.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
app/assets/images/ci_favicons/favicon_status_not_found.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_pending.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
app/assets/images/ci_favicons/favicon_status_pending.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_running.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
app/assets/images/ci_favicons/favicon_status_running.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_skipped.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
app/assets/images/ci_favicons/favicon_status_skipped.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_success.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
app/assets/images/ci_favicons/favicon_status_success.ico
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/ci_favicons/favicon_status_warning.ico

5.3 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico

4.19 KB | W: | H:

app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
app/assets/images/ci_favicons/favicon_status_warning.ico
  • 2-up
  • Swipe
  • Onion skin
...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({ ...@@ -27,6 +27,9 @@ gl.issueBoards.BoardSidebar = Vue.extend({
computed: { computed: {
showSidebar () { showSidebar () {
return Object.keys(this.issue).length; return Object.keys(this.issue).length;
},
assigneeId() {
return this.issue.assignee ? this.issue.assignee.id : 0;
} }
}, },
watch: { watch: {
......
import d3 from 'd3';
export const dateFormat = d3.time.format('%b %d, %Y');
export const timeFormat = d3.time.format('%H:%M%p');
/* global Flash */
import d3 from 'd3';
import {
dateFormat,
timeFormat,
} from './constants';
export default class Deployments {
constructor(width, height) {
this.width = width;
this.height = height;
this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
this.createGradientDef();
}
init(chartData) {
this.chartData = chartData;
this.x = d3.time.scale().range([0, this.width]);
this.x.domain(d3.extent(this.chartData, d => d.time));
this.charts = d3.selectAll('.prometheus-graph');
this.getData();
}
getData() {
$.ajax({
url: this.endpoint,
dataType: 'JSON',
})
.fail(() => new Flash('Error getting deployment information.'))
.done((data) => {
this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
const time = new Date(deployment.created_at);
const xPos = Math.floor(this.x(time));
time.setSeconds(this.chartData[0].time.getSeconds());
if (xPos >= 0) {
deploymentDataArray.push({
id: deployment.id,
time,
sha: deployment.sha,
tag: deployment.tag,
ref: deployment.ref.name,
xPos,
});
}
return deploymentDataArray;
}, []);
this.plotData();
});
}
plotData() {
this.charts.each((d, i) => {
const svg = d3.select(this.charts[0][i]);
const chart = svg.select('.graph-container');
const key = svg.node().getAttribute('graph-type');
this.createLine(chart, key);
this.createDeployInfoBox(chart, key);
});
}
createGradientDef() {
const defs = d3.select('body')
.append('svg')
.attr({
height: 0,
width: 0,
})
.append('defs');
defs.append('linearGradient')
.attr({
id: 'shadow-gradient',
})
.append('stop')
.attr({
offset: '0%',
'stop-color': '#000',
'stop-opacity': 0.4,
})
.select(this.selectParentNode)
.append('stop')
.attr({
offset: '100%',
'stop-color': '#000',
'stop-opacity': 0,
});
}
createLine(chart, key) {
chart.append('g')
.attr({
class: 'deploy-info',
})
.selectAll('.deploy-info')
.data(this.data)
.enter()
.append('g')
.attr({
class: d => `deploy-info-${d.id}-${key}`,
transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
})
.append('rect')
.attr({
x: 1,
y: 0,
height: this.height + 1,
width: 3,
fill: 'url(#shadow-gradient)',
})
.select(this.selectParentNode)
.append('line')
.attr({
class: 'deployment-line',
x1: 0,
x2: 0,
y1: 0,
y2: this.height + 1,
});
}
createDeployInfoBox(chart, key) {
chart.selectAll('.deploy-info')
.selectAll('.js-deploy-info-box')
.data(this.data)
.enter()
.select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
.append('svg')
.attr({
class: 'js-deploy-info-box hidden',
x: 3,
y: 0,
width: 92,
height: 60,
})
.append('rect')
.attr({
class: 'rect-text-metric deploy-info-rect rect-metric',
x: 1,
y: 1,
rx: 2,
width: 90,
height: 58,
})
.select(this.selectParentNode)
.append('g')
.attr({
transform: 'translate(5, 2)',
})
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
})
.text(Deployments.refText)
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text',
y: 18,
})
.text(d => dateFormat(d.time))
.select(this.selectParentNode)
.append('text')
.attr({
class: 'deploy-info-text text-metric-bold',
y: 38,
})
.text(d => timeFormat(d.time));
}
static toggleDeployTextbox(deploy, key, showInfoBox) {
d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
.classed('hidden', !showInfoBox);
}
mouseOverDeployInfo(mouseXPos, key) {
if (!this.data) return false;
let dataFound = false;
this.data.forEach((d) => {
if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
dataFound = d.xPos + 1;
Deployments.toggleDeployTextbox(d, key, true);
} else {
Deployments.toggleDeployTextbox(d, key, false);
}
});
return dataFound;
}
/* `this` is bound to the D3 node */
selectParentNode() {
return this.parentNode;
}
static refText(d) {
return d.tag ? d.ref : d.sha.slice(0, 6);
}
}
...@@ -3,16 +3,20 @@ ...@@ -3,16 +3,20 @@
import d3 from 'd3'; import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import Deployments from './deployments';
import '../lib/utils/common_utils';
import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash'; import '../flash';
import {
dateFormat,
timeFormat,
} from './constants';
const prometheusContainer = '.prometheus-container'; const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs'; const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state'; const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left; const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
...@@ -36,6 +40,7 @@ class PrometheusGraph { ...@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right; this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom; this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0; this.backOffRequestCounter = 0;
this.deployments = new Deployments(this.width, this.height);
this.configureGraph(); this.configureGraph();
this.init(); this.init();
} else { } else {
...@@ -74,6 +79,12 @@ class PrometheusGraph { ...@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show(); $(prometheusParentGraphContainer).show();
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
const firstMetricData = this.graphSpecificProperties[
Object.keys(this.graphSpecificProperties)[0]
].data;
this.deployments.init(firstMetricData);
} }
}); });
} }
...@@ -96,6 +107,7 @@ class PrometheusGraph { ...@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right) .attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top) .attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g') .append('g')
.attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`); .attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer) const axisLabelContainer = d3.select(prometheusGraphContainer)
...@@ -116,6 +128,7 @@ class PrometheusGraph { ...@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y) .scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks) .ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width) .tickSize(-this.width)
.outerTickSize(0)
.orient('left'); .orient('left');
this.createAxisLabelContainers(axisLabelContainer, key); this.createAxisLabelContainers(axisLabelContainer, key);
...@@ -248,7 +261,8 @@ class PrometheusGraph { ...@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex]; const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay; const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0; const currentData = evalTime ? d1 : d0;
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time); const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`; const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value)); const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData); const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
...@@ -256,13 +270,12 @@ class PrometheusGraph { ...@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag // Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove(); d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g'); const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line') currentChart.append('line')
.attr('class', 'selected-metric-line')
.attr({ .attr({
class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate, x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0), y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate, x2: currentTimeCoordinate,
...@@ -272,33 +285,45 @@ class PrometheusGraph { ...@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle') currentChart.append('circle')
.attr('class', 'circle-metric') .attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color) .attr('fill', currentGraphProps.line_color)
.attr('cx', currentTimeCoordinate) .attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value)) .attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric); .attr('r', this.commonGraphProperties.circle_radius_metric);
if (currentDeployXPos) return;
// The little box with text // The little box with text
const rectTextMetric = currentChart.append('g') const rectTextMetric = currentChart.append('svg')
.attr('class', 'rect-text-metric') .attr({
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`); class: 'rect-text-metric',
x: currentTimeCoordinate,
y: 0,
});
rectTextMetric.append('rect') rectTextMetric.append('rect')
.attr('class', 'rect-metric') .attr({
.attr('x', currentTimeCoordinate + 10) class: 'rect-metric',
.attr('y', maxMetricValue) x: 4,
.attr('width', this.commonGraphProperties.rect_text_width) y: 1,
.attr('height', this.commonGraphProperties.rect_text_height); rx: 2,
width: this.commonGraphProperties.rect_text_width,
height: this.commonGraphProperties.rect_text_height,
});
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric') .attr({
.attr('x', currentTimeCoordinate + 35) class: 'text-metric text-metric-bold',
.attr('y', maxMetricValue + 35) x: 8,
y: 35,
})
.text(timeFormat(currentData.time)); .text(timeFormat(currentData.time));
rectTextMetric.append('text') rectTextMetric.append('text')
.attr('class', 'text-metric-date') .attr({
.attr('x', currentTimeCoordinate + 15) class: 'text-metric-date',
.attr('y', maxMetricValue + 15) x: 8,
.text(dayFormat(currentData.time)); y: 15,
})
.text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value); let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') { if (key === 'cpu_values') {
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
$els.each((function(_this) { $els.each((function(_this) {
return function(i, dropdown) { return function(i, dropdown) {
var options = {}; var options = {};
var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove; var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, defaultNullUser, firstUser, issueURL, selectedId, selectedIdDefault, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown); $dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id'); options.projectId = $dropdown.data('project-id');
options.groupId = $dropdown.data('group-id'); options.groupId = $dropdown.data('group-id');
...@@ -38,11 +38,11 @@ ...@@ -38,11 +38,11 @@
options.todoFilter = $dropdown.data('todo-filter'); options.todoFilter = $dropdown.data('todo-filter');
options.todoStateFilter = $dropdown.data('todo-state-filter'); options.todoStateFilter = $dropdown.data('todo-state-filter');
showNullUser = $dropdown.data('null-user'); showNullUser = $dropdown.data('null-user');
defaultNullUser = $dropdown.data('null-user-default');
showMenuAbove = $dropdown.data('showMenuAbove'); showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user'); showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user'); firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id'); options.authorId = $dropdown.data('author-id');
selectedId = $dropdown.data('selected');
defaultLabel = $dropdown.data('default-label'); defaultLabel = $dropdown.data('default-label');
issueURL = $dropdown.data('issueUpdate'); issueURL = $dropdown.data('issueUpdate');
$selectbox = $dropdown.closest('.selectbox'); $selectbox = $dropdown.closest('.selectbox');
...@@ -51,6 +51,8 @@ ...@@ -51,6 +51,8 @@
$value = $block.find('.value'); $value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user'); $collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
selectedIdDefault = (defaultNullUser && showNullUser) ? 0 : null;
selectedId = $dropdown.data('selected') || selectedIdDefault;
var updateIssueBoardsIssue = function () { var updateIssueBoardsIssue = function () {
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
...@@ -186,12 +188,14 @@ ...@@ -186,12 +188,14 @@
fieldName: $dropdown.data('field-name'), fieldName: $dropdown.data('field-name'),
toggleLabel: function(selected, el) { toggleLabel: function(selected, el) {
if (selected && 'id' in selected && $(el).hasClass('is-active')) { if (selected && 'id' in selected && $(el).hasClass('is-active')) {
$dropdown.find('.dropdown-toggle-text').removeClass('is-default');
if (selected.text) { if (selected.text) {
return selected.text; return selected.text;
} else { } else {
return selected.name; return selected.name;
} }
} else { } else {
$dropdown.find('.dropdown-toggle-text').addClass('is-default');
return defaultLabel; return defaultLabel;
} }
}, },
...@@ -204,13 +208,14 @@ ...@@ -204,13 +208,14 @@
}, },
vue: $dropdown.hasClass('js-issue-board-sidebar'), vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) { clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected; var isIssueIndex, isMRIndex, page, selected, isSelecting;
page = $('body').data('page'); page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index'; isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index'); isMRIndex = (page === page && page === 'projects:merge_requests:index');
isSelecting = (user.id !== selectedId);
selectedId = isSelecting ? user.id : selectedIdDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) { if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault(); e.preventDefault();
selectedId = user.id;
if (selectedId === gon.current_user_id) { if (selectedId === gon.current_user_id) {
$('.assign-to-me-link').hide(); $('.assign-to-me-link').hide();
} else { } else {
...@@ -221,12 +226,11 @@ ...@@ -221,12 +226,11 @@
if ($el.closest('.add-issues-modal').length) { if ($el.closest('.add-issues-modal').length) {
gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id; gl.issueBoards.ModalStore.store.filter[$dropdown.data('field-name')] = user.id;
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedId = user.id;
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) { } else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit(); return $dropdown.closest('form').submit();
} else if ($dropdown.hasClass('js-issue-board-sidebar')) { } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
if (user.id) { if (user.id && isSelecting) {
gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({ gl.issueBoards.boardStoreIssueSet('assignee', new ListUser({
id: user.id, id: user.id,
username: user.username, username: user.username,
...@@ -248,6 +252,9 @@ ...@@ -248,6 +252,9 @@
}, },
opened: function(e) { opened: function(e) {
const $el = $(e.currentTarget); const $el = $(e.currentTarget);
if ($dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = parseInt($dropdown[0].dataset.selected, 10) || selectedIdDefault;
}
$el.find('.is-active').removeClass('is-active'); $el.find('.is-active').removeClass('is-active');
$el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active'); $el.find(`li[data-user-id="${selectedId}"] .dropdown-menu-user-link`).addClass('is-active');
}, },
......
...@@ -390,7 +390,8 @@ ...@@ -390,7 +390,8 @@
&::before { &::before {
position: absolute; position: absolute;
left: 6px; left: 6px;
top: 6px; top: 50%;
transform: translateY(-50%);
font: normal normal normal 14px/1 FontAwesome; font: normal normal normal 14px/1 FontAwesome;
font-size: inherit; font-size: inherit;
text-rendering: auto; text-rendering: auto;
......
...@@ -157,7 +157,8 @@ ...@@ -157,7 +157,8 @@
.prometheus-graph { .prometheus-graph {
text { text {
fill: $stat-graph-axis-fill; fill: $gl-text-color;
stroke-width: 0;
} }
.label-axis-text, .label-axis-text,
...@@ -210,27 +211,33 @@ ...@@ -210,27 +211,33 @@
.rect-text-metric { .rect-text-metric {
fill: $white-light; fill: $white-light;
stroke-width: 1; stroke-width: 1;
stroke: $black; stroke: $gray-darkest;
} }
.rect-axis-text { .rect-axis-text {
fill: $white-light; fill: $white-light;
} }
.text-metric, .text-metric {
.text-median-metric, font-weight: 600;
.text-metric-usage,
.text-metric-date {
fill: $black;
} }
.text-metric-date { .selected-metric-line {
font-weight: 200; stroke: $gl-gray-dark;
stroke-width: 1;
} }
.selected-metric-line { .deployment-line {
stroke: $black; stroke: $black;
stroke-width: 1; stroke-width: 2;
}
.deploy-info-text {
dominant-baseline: text-before-edge;
}
.text-metric-bold {
font-weight: 600;
} }
.prometheus-state { .prometheus-state {
......
class Admin::HooksController < Admin::ApplicationController class Admin::HooksController < Admin::ApplicationController
before_action :hook, only: :edit
def index def index
@hooks = SystemHook.all @hooks = SystemHook.all
@hook = SystemHook.new @hook = SystemHook.new
...@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end end
end end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'System hook was successfully updated.'
redirect_to admin_hooks_path
else
render 'edit'
end
end
def destroy def destroy
@hook = SystemHook.find(params[:id]) hook.destroy
@hook.destroy
redirect_to admin_hooks_path redirect_to admin_hooks_path
end end
def test def test
@hook = SystemHook.find(params[:hook_id])
data = { data = {
event_name: "project_create", event_name: "project_create",
name: "Ruby", name: "Ruby",
...@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController ...@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone", owner_name: "Someone",
owner_email: "example@gitlabhq.com" owner_email: "example@gitlabhq.com"
} }
@hook.execute(data, 'system_hooks') hook.execute(data, 'system_hooks')
redirect_back_or_default redirect_back_or_default
end end
private
def hook
@hook ||= SystemHook.find(params[:id])
end
def hook_params def hook_params
params.require(:hook).permit( params.require(:hook).permit(
:enable_ssl_verification, :enable_ssl_verification,
......
module NotesActions
include RendersNotes
extend ActiveSupport::Concern
included do
before_action :authorize_admin_note!, only: [:update, :destroy]
end
def index
current_fetched_at = Time.now.to_i
notes_json = { notes: [], last_fetched_at: current_fetched_at }
@notes = notes_finder.execute.inc_relations_for_view
@notes = prepare_notes_for_rendering(@notes)
@notes.each do |note|
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create
create_params = note_params.merge(
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end
private
def note_json(note)
attrs = {
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, finder_params)
end
end
...@@ -10,6 +10,8 @@ module RendersNotes ...@@ -10,6 +10,8 @@ module RendersNotes
private private
def preload_max_access_for_authors(notes, project) def preload_max_access_for_authors(notes, project)
return nil unless project
user_ids = notes.map(&:author_id) user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids) project.team.max_member_access_for_user_ids(user_ids)
end end
......
...@@ -5,10 +5,12 @@ module SnippetsActions ...@@ -5,10 +5,12 @@ module SnippetsActions
end end
def raw def raw
disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
send_data( send_data(
convert_line_endings(@snippet.content), convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8', type: 'text/plain; charset=utf-8',
disposition: 'inline', disposition: disposition,
filename: @snippet.sanitized_file_name filename: @snippet.sanitized_file_name
) )
end end
......
...@@ -22,7 +22,8 @@ module ToggleAwardEmoji ...@@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable) def to_todoable(awardable)
case awardable case awardable
when Note when Note
awardable.noteable # we don't create todos for personal snippet comments for now
awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue when MergeRequest, Issue
awardable awardable
when Snippet when Snippet
......
class Projects::DeploymentsController < Projects::ApplicationController
before_action :authorize_read_environment!
before_action :authorize_read_deployment!
def index
deployments = environment.deployments.reorder(created_at: :desc)
deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
.represent_concise(deployments) }
end
private
def environment
@environment ||= project.environments.find(params[:environment_id])
end
end
class Projects::HooksController < Projects::ApplicationController class Projects::HooksController < Projects::ApplicationController
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
before_action :hook, only: :edit
respond_to :html respond_to :html
...@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController ...@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project) redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end end
def edit
end
def update
if hook.update_attributes(hook_params)
flash[:notice] = 'Hook was successfully updated.'
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
else
render 'edit'
end
end
def test def test
if !@project.empty_repo? if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user) status, message = TestHookService.new.execute(hook, current_user)
......
class Projects::NotesController < Projects::ApplicationController class Projects::NotesController < Projects::ApplicationController
include RendersNotes include NotesActions
include ToggleAwardEmoji include ToggleAwardEmoji
# Authorize
before_action :authorize_read_note! before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create] before_action :authorize_create_note!, only: [:create]
before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve] before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
def index #
current_fetched_at = Time.now.to_i # This is a fix to make spinach feature tests passing:
# Controller actions are returned from AbstractController::Base and methods of parent classes are
notes_json = { notes: [], last_fetched_at: current_fetched_at } # excluded in order to return only specific controller related methods.
# That is ok for the app (no :create method in ancestors)
@notes = notes_finder.execute.inc_relations_for_view # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
@notes = prepare_notes_for_rendering(@notes) #
# see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
@notes.each do |note| #
next if note.cross_reference_not_visible_for?(current_user)
notes_json[:notes] << note_json(note)
end
render json: notes_json
end
def create def create
create_params = note_params.merge( super
merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
)
@note = Notes::CreateService.new(project, current_user, create_params).execute
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def update
@note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
if @note.is_a?(Note)
Banzai::NoteRenderer.render([@note], @project, current_user)
end
respond_to do |format|
format.json { render json: note_json(@note) }
format.html { redirect_back_or_default }
end
end
def destroy
if note.editable?
Notes::DestroyService.new(project, current_user).execute(note)
end
respond_to do |format|
format.js { head :ok }
end
end end
def delete_attachment def delete_attachment
...@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note) def note_html(note)
render_to_string( render_to_string(
"projects/notes/_note", "shared/notes/_note",
layout: false, layout: false,
formats: [:html], formats: [:html],
locals: { note: note } locals: { note: note }
...@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController ...@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
) )
end end
def note_json(note) def finder_params
attrs = { params.merge(last_fetched_at: last_fetched_at)
commands_changes: note.commands_changes
}
if note.persisted?
attrs.merge!(
valid: true,
id: note.id,
discussion_id: note.discussion_id(noteable),
html: note_html(note),
note: note.note
)
discussion = note.to_discussion(noteable)
unless discussion.individual_note?
attrs.merge!(
discussion_resolvable: discussion.resolvable?,
diff_discussion_html: diff_discussion_html(discussion),
discussion_html: discussion_html(discussion)
)
end
else
attrs.merge!(
valid: false,
errors: note.errors
)
end
attrs
end
def authorize_admin_note!
return access_denied! unless can?(current_user, :admin_note, note)
end end
def authorize_resolve_note! def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note) return access_denied! unless can?(current_user, :resolve_note, note)
end end
def note_params
params.require(:note).permit(
:project_id,
:noteable_type,
:noteable_id,
:commit_id,
:noteable,
:type,
:note,
:attachment,
# LegacyDiffNote
:line_code,
# DiffNote
:position
)
end
def notes_finder
@notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
end
def noteable
@noteable ||= notes_finder.target
end
def last_fetched_at
request.headers['X-Last-Fetched-At']
end
end end
class Snippets::NotesController < ApplicationController
include NotesActions
include ToggleAwardEmoji
skip_before_action :authenticate_user!, only: [:index]
before_action :snippet
before_action :authorize_read_snippet!, only: [:show, :index, :create]
private
def note
@note ||= snippet.notes.find(params[:id])
end
alias_method :awardable, :note
def note_html(note)
render_to_string(
"shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
)
end
def project
nil
end
def snippet
PersonalSnippet.find_by(id: params[:snippet_id])
end
def note_params
super.merge(noteable_id: params[:snippet_id])
end
def finder_params
params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
end
def authorize_read_snippet!
return render_404 unless can?(current_user, :read_personal_snippet, snippet)
end
end
class SnippetsController < ApplicationController class SnippetsController < ApplicationController
include RendersNotes
include ToggleAwardEmoji include ToggleAwardEmoji
include SpammableActions include SpammableActions
include SnippetsActions include SnippetsActions
include MarkdownPreview include MarkdownPreview
include RendersBlob include RendersBlob
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download] before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet # Allow read snippet
before_action :authorize_read_snippet!, only: [:show, :raw, :download] before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet # Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update] before_action :authorize_update_snippet!, only: [:edit, :update]
...@@ -16,7 +17,7 @@ class SnippetsController < ApplicationController ...@@ -16,7 +17,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet # Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy] before_action :authorize_admin_snippet!, only: [:destroy]
skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download] skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets' layout 'snippets'
respond_to :html respond_to :html
...@@ -64,6 +65,11 @@ class SnippetsController < ApplicationController ...@@ -64,6 +65,11 @@ class SnippetsController < ApplicationController
blob = @snippet.blob blob = @snippet.blob
override_max_blob_size(blob) override_max_blob_size(blob)
@noteable = @snippet
@discussions = @snippet.discussions
@notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
respond_to do |format| respond_to do |format|
format.html do format.html do
render 'show' render 'show'
...@@ -83,14 +89,6 @@ class SnippetsController < ApplicationController ...@@ -83,14 +89,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path redirect_to snippets_path
end end
def download
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
filename: @snippet.sanitized_file_name
)
end
def preview_markdown def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true) render_markdown_preview(params[:text], skip_project_check: true)
end end
......
...@@ -68,6 +68,8 @@ class NotesFinder ...@@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet" when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project) SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
when "personal_snippet"
PersonalSnippet.all
else else
raise 'invalid target_type' raise 'invalid target_type'
end end
......
module AwardEmojiHelper module AwardEmojiHelper
def toggle_award_url(awardable) def toggle_award_url(awardable)
return url_for([:toggle_award_emoji, awardable]) unless @project return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note) if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x) # We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id) if awardable.for_personal_snippet?
toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
else
toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
end
else else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable]) url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end end
......
module MergeRequestsHelper module MergeRequestsHelper
def new_mr_path_from_push_event(event) def new_mr_path_from_push_event(event)
target_project = event.project.forked_from_project || event.project target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path( new_namespace_project_merge_request_path(
event.project.namespace, event.project.namespace,
event.project, event.project,
...@@ -127,6 +127,10 @@ module MergeRequestsHelper ...@@ -127,6 +127,10 @@ module MergeRequestsHelper
end end
end end
def target_projects(project)
[project, project.default_merge_request_target].uniq
end
def merge_request_button_visibility(merge_request, closed) def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork? return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end end
......
...@@ -8,6 +8,14 @@ module SnippetsHelper ...@@ -8,6 +8,14 @@ module SnippetsHelper
end end
end end
def download_snippet_path(snippet)
if snippet.project_id
raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
else
raw_snippet_path(snippet, inline: false)
end
end
# Return the path of a snippets index for a user or for a project # Return the path of a snippets index for a user or for a project
# #
# @returns String, path to snippet index # @returns String, path to snippet index
......
...@@ -78,6 +78,9 @@ module CacheMarkdownField ...@@ -78,6 +78,9 @@ 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?
return false unless cached
markdown_changed = attribute_changed?(markdown_field) || false markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false html_changed = attribute_changed?(html_field) || false
......
...@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing? validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?] validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork? validate :validate_fork, unless: :closed_without_fork?
validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name) where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
...@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base ...@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end end
end end
def validate_target_project
return true if target_project.merge_requests_enabled?
errors.add :base, 'Target project has disabled merge requests'
end
def validate_fork def validate_fork
return true unless target_project && source_project return true unless target_project && source_project
return true if target_project == source_project return true if target_project == source_project
......
...@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base ...@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base
validates :path, validates :path,
presence: true, presence: true,
length: { maximum: 255 }, length: { maximum: 255 },
namespace: true dynamic_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
...@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base ...@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path) Project.inside_path(full_path)
end end
def has_parent?
parent.present?
end
private private
def repository_storage_paths def repository_storage_paths
......
...@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base ...@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message } message: Gitlab::Regex.project_name_regex_message }
validates :path, validates :path,
presence: true, presence: true,
project_path: true, dynamic_path: true,
length: { maximum: 255 }, length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex, format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message } message: Gitlab::Regex.project_path_regex_message },
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id } validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import? validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?] validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
...@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base ...@@ -1270,6 +1271,9 @@ class Project < ActiveRecord::Base
else else
update_attribute(name, value) update_attribute(name, value)
end end
rescue ActiveRecord::RecordNotSaved => e
handle_update_attribute_error(e, value)
end end
def pushes_since_gc def pushes_since_gc
...@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base ...@@ -1314,6 +1318,14 @@ class Project < ActiveRecord::Base
namespace_id_changed? namespace_id_changed?
end end
def default_merge_request_target
if forked_from_project&.merge_requests_enabled?
forked_from_project
else
self
end
end
alias_method :name_with_namespace, :full_name alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path alias_method :path_with_namespace, :full_path
...@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base ...@@ -1383,4 +1395,16 @@ class Project < ActiveRecord::Base
ContainerRepository.build_root_repository(self).has_tags? ContainerRepository.build_root_repository(self).has_tags?
end end
def handle_update_attribute_error(ex, value)
if ex.message.start_with?('Failed to replace')
if value.respond_to?(:each)
invalid = value.detect(&:invalid?)
raise ex, ([ex.message] + invalid.errors.full_messages).join(' ') if invalid
end
end
raise ex
end
end end
...@@ -505,14 +505,8 @@ class Repository ...@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: [] cache_method :tag_names, fallback: []
def branch_count delegate :branch_count, :tag_count, to: :raw_repository
branches.size
end
cache_method :branch_count, fallback: 0 cache_method :branch_count, fallback: 0
def tag_count
raw_repository.rugged.tags.count
end
cache_method :tag_count, fallback: 0 cache_method :tag_count, fallback: 0
def avatar def avatar
......
...@@ -118,7 +118,7 @@ class User < ActiveRecord::Base ...@@ -118,7 +118,7 @@ class User < ActiveRecord::Base
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, validates :username,
namespace: true, dynamic_path: true,
presence: true, presence: true,
uniqueness: { case_sensitive: false } uniqueness: { case_sensitive: false }
......
...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity ...@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end end
end end
expose :created_at
expose :tag expose :tag
expose :last? expose :last?
expose :user, using: UserEntity expose :user, using: UserEntity
expose :commit, using: CommitEntity expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity expose :deployable, using: BuildEntity
......
class DeploymentSerializer < BaseSerializer
entity DeploymentEntity
def represent_concise(resource, opts = {})
opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
represent(resource, opts)
end
end
...@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity ...@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity
expose :details_path expose :details_path
expose :favicon do |status| expose :favicon do |status|
ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico")) dir = 'ci_favicons'
dir = File.join(dir, 'dev') if Rails.env.development?
ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end end
end end
...@@ -28,7 +28,7 @@ module MergeRequests ...@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project) return target_project if target_project.present? && can?(current_user, :read_project, target_project)
project.forked_from_project || project project.default_merge_request_target
end end
def find_target_branch def find_target_branch
......
...@@ -281,7 +281,7 @@ class TodoService ...@@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target) def attributes_for_target(target)
attributes = { attributes = {
project_id: target.project.id, project_id: target&.project&.id,
target_id: target.id, target_id: target.id,
target_type: target.class.name, target_type: target.class.name,
commit_id: nil commit_id: nil
......
# 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 reserved path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
# All routes that appear on the top level must be listed here.
# This will make sure that groups cannot be created with these names
# as these routes would be masked by the paths already in place.
#
# Example:
# /api/api-project
#
# the path `api` shouldn't be allowed because it would be masked by `api/*`
#
TOP_LEVEL_ROUTES = %w[
-
.well-known
abuse_reports
admin
all
api
assets
autocomplete
ci
dashboard
explore
files
groups
health_check
help
hooks
import
invites
issues
jwt
koding
member
merge_requests
new
notes
notification_settings
oauth
profile
projects
public
repository
robots.txt
s
search
sent_notifications
services
snippets
teams
u
unicorn_test
unsubscribes
uploads
users
].freeze
# This list should contain all words following `/*namespace_id/:project_id` in
# routes that contain a second wildcard.
#
# Example:
# /*namespace_id/:project_id/badges/*ref/build
#
# If `badges` was allowed as a project/group name, we would not be able to access the
# `badges` route for those projects:
#
# Consider a namespace with path `foo/bar` and a project called `badges`.
# The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
#
# When accessing this path the route would be matched to the `badges` path
# with the following params:
# - namespace_id: `foo`
# - project_id: `bar`
# - ref: `badges/master`
#
# Failing to find the project, this would result in a 404.
#
# By rejecting `badges` the router can _count_ on the fact that `badges` will
# be preceded by the `namespace/project`.
WILDCARD_ROUTES = %w[
badges
blame
blob
builds
commits
create
create_dir
edit
environments/folders
files
find_file
gitlab-lfs/objects
info/lfs/objects
new
preview
raw
refs
tree
update
wikis
].freeze
# These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
# We need to reject these because we have a `/groups/*id` page that is the same
# as the `/*id`.
#
# If we would allow a subgroup to be created with the name `activity` then
# this group would not be accessible through `/groups/parent/activity` since
# this would map to the activity-page of it's parent.
GROUP_ROUTES = %w[
activity
avatar
edit
group_members
issues
labels
merge_requests
milestones
projects
subgroups
].freeze
CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
def self.without_reserved_wildcard_paths_regex
@without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES)
end
def self.without_reserved_child_paths_regex
@without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES)
end
# This is used to validate a full path.
# It doesn't match paths
# - Starting with one of the top level words
# - Containing one of the child level words in the middle of a path
def self.regex_excluding_child_paths(child_routes)
reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES)
not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))}
reserved_child_level_words = Regexp.union(child_routes)
not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))}
%r{#{not_starting_in_reserved_word}
#{not_containing_reserved_child}
#{Gitlab::Regex.full_namespace_regex}}x
end
def self.valid?(path)
path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path)
end
def self.full_path_reserved?(path)
path = path.to_s.downcase
_project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse)
wildcard_reserved?(path) || child_reserved?(namespace_parts)
end
def self.child_reserved?(path)
return false unless path
path !~ without_reserved_child_paths_regex
end
def self.wildcard_reserved?(path)
return false unless path
path !~ without_reserved_wildcard_paths_regex
end
delegate :full_path_reserved?,
:child_reserved?,
to: :class
def path_reserved_for_record?(record, value)
full_path = record.respond_to?(:full_path) ? record.full_path : value
# For group paths the entire path cannot contain a reserved child word
# The path doesn't contain the last `_project_part` so we need to validate
# if the entire path.
# Example:
# A *group* with full path `parent/activity` is reserved.
# A *project* with full path `parent/activity` is allowed.
if record.is_a? Group
child_reserved?(full_path)
else
full_path_reserved?(full_path)
end
end
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
return
end
if path_reserved_for_record?(record, value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# NamespaceValidator
#
# Custom validator for GitLab namespace values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
RESERVED = %w[
.well-known
admin
all
assets
ci
dashboard
files
groups
help
hooks
issues
merge_requests
new
notes
profile
projects
public
repository
robots.txt
s
search
services
snippets
teams
u
unsubscribes
users
].freeze
WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
preview blob blame raw files create_dir find_file
artifacts graphs refs badges].freeze
STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
def self.valid?(value)
!reserved?(value) && follow_format?(value)
end
def self.reserved?(value, strict: false)
if strict
STRICT_RESERVED.include?(value)
else
RESERVED.include?(value)
end
end
def self.follow_format?(value)
value =~ Gitlab::Regex.namespace_regex
end
delegate :reserved?, :follow_format?, to: :class
def validate_each(record, attribute, value)
unless follow_format?(value)
record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
end
strict = record.is_a?(Group) && record.parent_id
if reserved?(value, strict: strict)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
# ProjectPathValidator
#
# Custom validator for GitLab project path values.
#
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class ProjectPathValidator < ActiveModel::EachValidator
# All project routes with wildcard argument must be listed here.
# Otherwise it can lead to routing issues when route considered as project name.
#
# Example:
# /group/project/tree/deploy_keys
#
# without tree as reserved name routing can match 'group/project' as group name,
# 'tree' as project name and 'deploy_keys' as route.
#
RESERVED = (NamespaceValidator::STRICT_RESERVED -
%w[dashboard help ci admin search notes services assets profile public]).freeze
def self.valid?(value)
!reserved?(value)
end
def self.reserved?(value)
RESERVED.include?(value)
end
delegate :reserved?, to: :class
def validate_each(record, attribute, value)
if reserved?(value)
record.errors.add(attribute, "#{value} is a reserved name")
end
end
end
...@@ -73,6 +73,12 @@ ...@@ -73,6 +73,12 @@
= container_reg = container_reg
%span.light.pull-right %span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled = boolean_to_icon Gitlab.config.registry.enabled
- gitlab_pages = 'GitLab Pages'
- gitlab_pages_enabled = Gitlab.config.pages.enabled
%p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
= gitlab_pages
%span.light.pull-right
= boolean_to_icon gitlab_pages_enabled
.col-md-4 .col-md-4
%h4 %h4
......
= form_errors(hook)
.form-group
= form.label :url, 'URL', class: 'control-label'
.col-sm-10
= form.text_field :url, class: 'form-control'
.form-group
= form.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= form.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= form.label :url, 'Trigger', class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= form.check_box :push_events, class: 'pull-left'
.prepend-left-20
= form.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= form.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
.col-sm-10
.checkbox
= form.label :enable_ssl_verification do
= form.check_box :enable_ssl_verification
%strong Enable SSL verification
- page_title 'Edit System Hook'
%h3.page-title
Edit System Hook
%p.light
#{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
= f.submit 'Save changes', class: 'btn btn-create'
- page_title "System Hooks" - page_title 'System Hooks'
%h3.page-title %h3.page-title
System hooks System hooks
%p.light %p.light
#{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project. used for binding events when GitLab creates a User or Project.
%hr %hr
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f| = form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
= form_errors(@hook) = render partial: 'form', locals: { form: f, hook: @hook }
.form-group
= f.label :url, 'URL', class: 'control-label'
.col-sm-10
= f.text_field :url, class: 'form-control'
.form-group
= f.label :token, 'Secret Token', class: 'control-label'
.col-sm-10
= f.text_field :token, class: 'form-control'
%p.help-block
Use this token to validate received payloads
.form-group
= f.label :url, "Trigger", class: 'control-label'
.col-sm-10.prepend-top-10
%div
System hook will be triggered on set of events like creating project
or adding ssh key. But you can also enable extra triggers like Push events.
.prepend-top-default
= f.check_box :push_events, class: 'pull-left'
.prepend-left-20
= f.label :push_events, class: 'list-label' do
%strong Push events
%p.light
This url will be triggered by a push to the repository
%div
= f.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20
= f.label :tag_push_events, class: 'list-label' do
%strong Tag push events
%p.light
This url will be triggered when a new tag is pushed to the repository
.form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
= f.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification
%strong Enable SSL verification
.form-actions .form-actions
= f.submit "Add system hook", class: "btn btn-create" = f.submit 'Add system hook', class: 'btn btn-create'
%hr %hr
- if @hooks.any? - if @hooks.any?
...@@ -62,11 +22,12 @@ ...@@ -62,11 +22,12 @@
- @hooks.each do |hook| - @hooks.each do |hook|
%li %li
.controls .controls
= link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm" = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm" = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url .monospace= hook.url
%div %div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger| - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger) - if hook.send(trigger)
%span.label.label-gray= trigger.titleize %span.label.label-gray= trigger.titleize
%span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
.discussion-notes .discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } } %ul.notes{ data: { discussion_id: discussion.id } }
= render partial: "projects/notes/note", collection: discussion.notes, as: :note = render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user - if current_user
.discussion-reply-holder .discussion-reply-holder
......
...@@ -28,8 +28,9 @@ ...@@ -28,8 +28,9 @@
":value" => "issue.assignee.id", ":value" => "issue.assignee.id",
"v-if" => "issue.assignee" } "v-if" => "issue.assignee" }
.dropdown .dropdown
%button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" }, %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true", null_user_default: "true" },
":data-issuable-id" => "issue.id", ":data-issuable-id" => "issue.id",
":data-selected" => "assigneeId",
":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" } ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
Select assignee Select assignee
= icon("chevron-down") = icon("chevron-down")
......
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
git init git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add . git add .
git commit git commit -m "Initial commit"
git push -u origin master git push -u origin master
%fieldset %fieldset
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring') = page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head" = render "projects/pipelines/head"
.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" } #js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.top-area .top-area
.row .row
.col-sm-6 .col-sm-6
......
= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project] .row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Add webhook', class: 'btn btn-create'
%hr
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
%ul.well-list
- @hooks.each do |hook|
= render 'project_hook', hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
= render 'projects/settings/head'
.row.prepend-top-default
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Save changes', class: 'btn btn-create'
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
.panel-heading .panel-heading
Target branch Target branch
.panel-body.clearfix .panel-body.clearfix
- projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project] - projects = target_projects(@project)
.merge-request-select.dropdown .merge-request-select.dropdown
= f.hidden_field :target_project_id = f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" } = dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
......
- access = note_max_access_for_user(note)
- if access
%span.note-role= access
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
%div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
#{note.note}
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
%ul#notes-list.notes.main-notes-list.timeline %ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes" = render "shared/notes/notes"
= render 'projects/notes/edit_form' = render 'projects/notes/edit_form'
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
%span %span
Members Members
- if can_edit - if can_edit
= nav_link(controller: [:integrations, :services]) do = nav_link(controller: [:integrations, :services, :hooks]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do = link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span %span
Integrations Integrations
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
.col-md-4.col-lg-5.text-right-lg.prepend-top-5 .col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline %span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"} SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
= link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm" = link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do = link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do
%span.sr-only Remove %span.sr-only Remove
......
...@@ -117,7 +117,7 @@ ...@@ -117,7 +117,7 @@
.issues_bulk_update.hide .issues_bulk_update.hide
= form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline .filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do = dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]", default_label: "Status" } } ) do
%ul %ul
%li %li
%a{ href: "#", data: { id: "reopen" } } Open %a{ href: "#", data: { id: "reopen" } } Open
...@@ -125,13 +125,13 @@ ...@@ -125,13 +125,13 @@
%a{ href: "#", data: { id: "close" } } Closed %a{ href: "#", data: { id: "close" } } Closed
.filter-item.inline .filter-item.inline
= dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable", = dropdown_tag("Assignee", options: { toggle_class: "js-user-search js-update-assignee js-filter-submit js-filter-bulk-update", title: "Assign to", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable",
placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]" } }) placeholder: "Search authors", data: { first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: @project.id, field_name: "update[assignee_id]", default_label: "Assignee" } })
.filter-item.inline .filter-item.inline
= dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true } }) = dropdown_tag("Milestone", options: { title: "Assign milestone", toggle_class: 'js-milestone-select js-extra-options js-filter-submit js-filter-bulk-update', filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone", placeholder: "Search milestones", data: { show_no: true, field_name: "update[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), use_id: true, default_label: "Milestone" } })
.filter-item.inline.labels-filter .filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true } = render "shared/issuable/label_dropdown", classes: ['js-filter-bulk-update', 'js-multiselect'], dropdown_title: 'Apply a label', show_create: false, show_footer: false, extra_options: false, filter_submit: false, data_options: { persist_when_hide: "true", field_name: "update[label_ids][]", show_no: false, show_any: false, use_id: true, default_label: "Labels" }
.filter-item.inline .filter-item.inline
= dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]" } } ) do = dropdown_tag("Subscription", options: { toggle_class: "js-subscription-event", title: "Change subscription", dropdown_class: "dropdown-menu-selectable", data: { field_name: "update[subscription_event]", default_label: "Subscription" } } ) do
%ul %ul
%li %li
%a{ href: "#", data: { id: "subscribe" } } Subscribe %a{ href: "#", data: { id: "subscribe" } } Subscribe
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
.selectbox.hide-collapsed .selectbox.hide-collapsed
= f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id' = f.hidden_field 'assignee_id', value: issuable.assignee_id, id: 'issue_assignee_id'
= dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true } }) = dropdown_tag('Select assignee', options: { toggle_class: 'js-user-search js-author-search', title: 'Assign to', filter: true, dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', placeholder: 'Search users', data: { first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), author_id: issuable.author_id, field_name: "#{issuable.to_ability_name}[assignee_id]", issue_update: issuable_json_path(issuable), ability_name: issuable.to_ability_name, null_user: true, null_user_default: true, selected: issuable.assignee_id } })
.block.milestone .block.milestone
.sidebar-collapsed-icon .sidebar-collapsed-icon
......
...@@ -29,58 +29,19 @@ ...@@ -29,58 +29,19 @@
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago') = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- unless note.system? - unless note.system?
.note-actions .note-actions
- access = note_max_access_for_user(note) - if note.for_personal_snippet?
- if access = render 'snippets/notes/actions', note: note, note_editable: note_editable
%span.note-role= access - else
= render 'projects/notes/actions', note: note, note_editable: note_editable
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
%resolve-btn{ "project-path" => project_path(note.project),
"discussion-id" => note.discussion_id(@noteable),
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
":author-name" => "'#{j(note.author.name)}'",
"author-avatar" => note.author.avatar_url,
":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"ref" => "note_#{note.id}" }
%button.note-action-button.line-resolve-btn{ type: "button",
class: ("is-disabled" unless can_resolve),
":class" => "{ 'is-active': isResolved }",
":aria-label" => "buttonText",
"@click" => "resolve",
":title" => "buttonText",
":ref" => "'button'" }
= icon("spin spinner", "v-show" => "loading", class: 'loading')
%div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
%span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
%span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
.note-body{ class: note_editable ? 'js-task-list-container' : '' } .note-body{ class: note_editable ? 'js-task-list-container' : '' }
.note-text.md .note-text.md
= note.redacted_note_html = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true) = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable - if note_editable
.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } } - if note.for_personal_snippet?
#{note.note} = render 'snippets/notes/edit', note: note
%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note - else
= render 'projects/notes/edit', note: note
.note-awards .note-awards
= render 'award_emoji/awards_block', awardable: note, inline: false = render 'award_emoji/awards_block', awardable: note, inline: false
- if note.system - if note.system
......
- if defined?(@discussions) - if defined?(@discussions)
- @discussions.each do |discussion| - @discussions.each do |discussion|
- if discussion.individual_note? - if discussion.individual_note?
= render partial: "projects/notes/note", collection: discussion.notes, as: :note = render partial: "shared/notes/note", collection: discussion.notes, as: :note
- else - else
= render 'discussions/discussion', discussion: discussion = render 'discussions/discussion', discussion: discussion
- else - else
= render partial: "projects/notes/note", collection: @notes, as: :note = render partial: "shared/notes/note", collection: @notes, as: :note
...@@ -18,7 +18,6 @@ ...@@ -18,7 +18,6 @@
= copy_blob_source_button(blob) = copy_blob_source_button(blob)
= open_raw_blob_button(blob) = open_raw_blob_button(blob)
- if defined?(download_path) && download_path = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
= link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
= render 'projects/blob/content', blob: blob = render 'projects/blob/content', blob: blob
.row.prepend-top-default = form_errors(hook)
.col-lg-3
%h4.prepend-top-0
= page_title
%p
#{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
= form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
= form_errors(hook)
.form-group .form-group
= f.label :url, "URL", class: 'label-light' = form.label :url, 'URL', class: 'label-light'
= f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json' = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json'
.form-group .form-group
= f.label :token, "Secret Token", class: 'label-light' = form.label :token, 'Secret Token', class: 'label-light'
= f.text_field :token, class: "form-control", placeholder: '' = form.text_field :token, class: 'form-control', placeholder: ''
%p.help-block %p.help-block
Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header. Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
.form-group .form-group
= f.label :url, "Trigger", class: 'label-light' = form.label :url, 'Trigger', class: 'label-light'
%ul.list-unstyled %ul.list-unstyled
%li %li
= f.check_box :push_events, class: 'pull-left' = form.check_box :push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :push_events, class: 'list-label' do = form.label :push_events, class: 'list-label' do
%strong Push events %strong Push events
%p.light %p.light
This URL will be triggered by a push to the repository This URL will be triggered by a push to the repository
%li %li
= f.check_box :tag_push_events, class: 'pull-left' = form.check_box :tag_push_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :tag_push_events, class: 'list-label' do = form.label :tag_push_events, class: 'list-label' do
%strong Tag push events %strong Tag push events
%p.light %p.light
This URL will be triggered when a new tag is pushed to the repository This URL will be triggered when a new tag is pushed to the repository
%li %li
= f.check_box :note_events, class: 'pull-left' = form.check_box :note_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :note_events, class: 'list-label' do = form.label :note_events, class: 'list-label' do
%strong Comments %strong Comments
%p.light %p.light
This URL will be triggered when someone adds a comment This URL will be triggered when someone adds a comment
%li %li
= f.check_box :issues_events, class: 'pull-left' = form.check_box :issues_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :issues_events, class: 'list-label' do = form.label :issues_events, class: 'list-label' do
%strong Issues events %strong Issues events
%p.light %p.light
This URL will be triggered when an issue is created/updated/merged This URL will be triggered when an issue is created/updated/merged
%li %li
= f.check_box :confidential_issues_events, class: 'pull-left' = form.check_box :confidential_issues_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :confidential_issues_events, class: 'list-label' do = form.label :confidential_issues_events, class: 'list-label' do
%strong Confidential Issues events %strong Confidential Issues events
%p.light %p.light
This URL will be triggered when a confidential issue is created/updated/merged This URL will be triggered when a confidential issue is created/updated/merged
%li %li
= f.check_box :merge_requests_events, class: 'pull-left' = form.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do = form.label :merge_requests_events, class: 'list-label' do
%strong Merge Request events %strong Merge Request events
%p.light %p.light
This URL will be triggered when a merge request is created/updated/merged This URL will be triggered when a merge request is created/updated/merged
%li %li
= f.check_box :build_events, class: 'pull-left' = form.check_box :build_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :build_events, class: 'list-label' do = form.label :build_events, class: 'list-label' do
%strong Jobs events %strong Jobs events
%p.light %p.light
This URL will be triggered when the job status changes This URL will be triggered when the job status changes
%li %li
= f.check_box :pipeline_events, class: 'pull-left' = form.check_box :pipeline_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :pipeline_events, class: 'list-label' do = form.label :pipeline_events, class: 'list-label' do
%strong Pipeline events %strong Pipeline events
%p.light %p.light
This URL will be triggered when the pipeline status changes This URL will be triggered when the pipeline status changes
%li %li
= f.check_box :wiki_page_events, class: 'pull-left' = form.check_box :wiki_page_events, class: 'pull-left'
.prepend-left-20 .prepend-left-20
= f.label :wiki_page_events, class: 'list-label' do = form.label :wiki_page_events, class: 'list-label' do
%strong Wiki Page events %strong Wiki Page events
%p.light %p.light
This URL will be triggered when a wiki page is created/updated This URL will be triggered when a wiki page is created/updated
.form-group .form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox' = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
.checkbox .checkbox
= f.label :enable_ssl_verification do = form.label :enable_ssl_verification do
= f.check_box :enable_ssl_verification = form.check_box :enable_ssl_verification
%strong Enable SSL verification %strong Enable SSL verification
= f.submit "Add webhook", class: "btn btn-create"
%hr
%h5.prepend-top-default
Webhooks (#{hooks.count})
- if hooks.any?
%ul.well-list
- hooks.each do |hook|
= render "project_hook", hook: hook
- else
%p.settings-message.text-center.append-bottom-0
No webhooks found, add one in the form above.
- if current_user
- if note.emoji_awardable?
- user_authored = note.user_authored?(current_user)
= link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
= icon('spinner spin')
%span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
%span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
%span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
= icon('pencil', class: 'link-highlight')
= link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
= icon('trash-o', class: 'danger-highlight')
%ul#notes-list.notes.main-notes-list.timeline
= render "projects/notes/notes"
...@@ -3,7 +3,10 @@ ...@@ -3,7 +3,10 @@
= render 'shared/snippets/header' = render 'shared/snippets/header'
%article.file-holder.snippet-file-content %article.file-holder.snippet-file-content
= render 'shared/snippets/blob', download_path: download_snippet_path(@snippet) = render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block .row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true = render 'award_emoji/awards_block', awardable: @snippet, inline: true
%ul#notes-list.notes.main-notes-list.timeline
#notes= render 'shared/notes/notes'
---
title: Display comments for personal snippets
merge_request:
author:
---
title: Implement ability to edit hooks
merge_request: 10816
author: Alexander Randa
---
title: Disallow merge requests from fork when source project have disabled merge requests
merge_request:
author: mhasbini
---
title: Improve validation of namespace & project paths
merge_request: 10413
author:
---
title: Display GitLab Pages status in Admin Dashboard
merge_request:
author:
---
title: Show checkmark on current assignee in assignee dropdown
merge_request: 10767
author:
---
title: Change Git commit command in Existing folder to git commit -m
merge_request: 10900
author: TM Lee
---
title: rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks
merge_request: 10979
author: M. Ricketts
---
title: Updated CI status favicons to include the tanuki
merge_request: 10923
author:
---
title: Add download button to project snippets
merge_request:
author:
---
title: Add missing project attributes to Import/Export
merge_request:
author:
---
title: Remove N+1 queries in processing MR references
merge_request:
author:
if ENV['ENABLE_QUERY_TRACE']
require 'active_record_query_trace'
ActiveRecordQueryTrace.enabled = 'true'
end
...@@ -50,8 +50,10 @@ namespace :admin do ...@@ -50,8 +50,10 @@ namespace :admin do
resources :deploy_keys, only: [:index, :new, :create, :destroy] resources :deploy_keys, only: [:index, :new, :create, :destroy]
resources :hooks, only: [:index, :create, :destroy] do resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
get :test member do
get :test
end
end end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
......
...@@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do member do
get 'raw' get :raw
post :mark_as_spam post :mark_as_spam
end end
end end
...@@ -138,6 +138,8 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -138,6 +138,8 @@ constraints(ProjectUrlConstrainer.new) do
collection do collection do
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ } get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end end
resources :deployments, only: [:index]
end end
resource :cycle_analytics, only: [:show] resource :cycle_analytics, only: [:show]
...@@ -185,7 +187,7 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -185,7 +187,7 @@ constraints(ProjectUrlConstrainer.new) do
end end
end end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do member do
get :test get :test
end end
......
resources :snippets, concerns: :awardable do resources :snippets, concerns: :awardable do
member do member do
get 'raw' get :raw
get 'download'
post :mark_as_spam post :mark_as_spam
post :preview_markdown post :preview_markdown
end end
scope module: :snippets do
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
end
end
end
end end
get '/s/:username', to: redirect('/u/%{username}/snippets'), get '/s/:username', to: redirect('/u/%{username}/snippets'),
......
class AddCreatedAtIndexToDeployments < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :deployments, :created_at
end
def down
remove_concurrent_index :deployments, :created_at
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.
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