Commit 0434f38e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c27acb1d
...@@ -332,7 +332,6 @@ group :metrics do ...@@ -332,7 +332,6 @@ group :metrics do
end end
group :development do group :development do
gem 'listen', '~> 3.0'
gem 'brakeman', '~> 4.2', require: false gem 'brakeman', '~> 4.2', require: false
gem 'danger', '~> 6.0', require: false gem 'danger', '~> 6.0', require: false
...@@ -487,3 +486,5 @@ gem 'liquid', '~> 4.0' ...@@ -487,3 +486,5 @@ gem 'liquid', '~> 4.0'
# LRU cache # LRU cache
gem 'lru_redux' gem 'lru_redux'
gem 'erubi', '~> 1.9.0'
...@@ -1205,6 +1205,7 @@ DEPENDENCIES ...@@ -1205,6 +1205,7 @@ DEPENDENCIES
elasticsearch-rails (~> 6.1) elasticsearch-rails (~> 6.1)
email_reply_trimmer (~> 0.1) email_reply_trimmer (~> 0.1)
email_spec (~> 2.2.0) email_spec (~> 2.2.0)
erubi (~> 1.9.0)
escape_utils (~> 1.1) escape_utils (~> 1.1)
factory_bot_rails (~> 5.1.0) factory_bot_rails (~> 5.1.0)
faraday (~> 0.12) faraday (~> 0.12)
...@@ -1279,7 +1280,6 @@ DEPENDENCIES ...@@ -1279,7 +1280,6 @@ DEPENDENCIES
license_finder (~> 5.4) license_finder (~> 5.4)
licensee (~> 8.9) licensee (~> 8.9)
liquid (~> 4.0) liquid (~> 4.0)
listen (~> 3.0)
lograge (~> 0.5) lograge (~> 0.5)
loofah (~> 2.2) loofah (~> 2.2)
lru_redux lru_redux
......
...@@ -24,7 +24,7 @@ function MergeRequest(opts) { ...@@ -24,7 +24,7 @@ function MergeRequest(opts) {
this.initCommitMessageListeners(); this.initCommitMessageListeners();
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport(); this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
if ($('a.btn-close').length) { if ($('.description.js-task-list-container').length) {
this.taskList = new TaskList({ this.taskList = new TaskList({
dataType: 'merge_request', dataType: 'merge_request',
fieldName: 'description', fieldName: 'description',
......
import $ from 'jquery'; import $ from 'jquery';
import Chart from 'chart.js'; import Chart from 'chart.js';
import { barChartOptions, lineChartOptions } from '~/lib/utils/chart_utils'; import { lineChartOptions } from '~/lib/utils/chart_utils';
import initProjectPipelinesChartsApp from '~/projects/pipelines/charts/index';
const SUCCESS_LINE_COLOR = '#1aaa55'; const SUCCESS_LINE_COLOR = '#1aaa55';
...@@ -44,40 +46,13 @@ const buildChart = (chartScope, shouldAdjustFontSize) => { ...@@ -44,40 +46,13 @@ const buildChart = (chartScope, shouldAdjustFontSize) => {
}); });
}; };
const buildBarChart = (chartTimesData, shouldAdjustFontSize) => {
const data = {
labels: chartTimesData.labels,
datasets: [
{
backgroundColor: 'rgba(220,220,220,0.5)',
borderColor: 'rgba(220,220,220,1)',
borderWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartTimesData.values,
},
],
};
return new Chart(
$('#build_timesChart')
.get(0)
.getContext('2d'),
{
type: 'bar',
data,
options: barChartOptions(shouldAdjustFontSize),
},
);
};
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML); const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
// Scale fonts if window width lower than 768px (iPad portrait) // Scale fonts if window width lower than 768px (iPad portrait)
const shouldAdjustFontSize = window.innerWidth < 768; const shouldAdjustFontSize = window.innerWidth < 768;
buildBarChart(chartTimesData, shouldAdjustFontSize);
chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize)); chartsData.forEach(scope => buildChart(scope, shouldAdjustFontSize));
}); });
document.addEventListener('DOMContentLoaded', initProjectPipelinesChartsApp);
<script>
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import StatisticsList from './statistics_list.vue';
import {
CHART_CONTAINER_HEIGHT,
INNER_CHART_HEIGHT,
X_AXIS_LABEL_ROTATION,
X_AXIS_TITLE_OFFSET,
} from '../constants';
export default {
components: {
StatisticsList,
GlColumnChart,
},
props: {
counts: {
type: Object,
required: true,
},
timesChartData: {
type: Object,
required: true,
},
},
data() {
return {
timesChartTransformedData: {
full: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
},
};
},
methods: {
mergeLabelsAndValues(labels, values) {
return labels.map((label, index) => [label, values[index]]);
},
},
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
xAxis: {
axisLabel: {
rotate: X_AXIS_LABEL_ROTATION,
},
nameGap: X_AXIS_TITLE_OFFSET,
},
},
};
</script>
<template>
<div>
<h4 class="my-4">{{ s__('PipelineCharts|Overall statistics') }}</h4>
<div class="row">
<div class="col-md-6">
<statistics-list :counts="counts" />
</div>
<div class="col-md-6">
<strong>
{{ __('Duration for the last 30 commits') }}
</strong>
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
:data="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
/>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
counts: {
type: Object,
required: true,
},
},
};
</script>
<template>
<ul>
<li>
<span>{{ s__('PipelineCharts|Total:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.total) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Successful:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.success) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Failed:') }}</span>
<strong>{{ n__('1 pipeline', '%d pipelines', counts.failed) }}</strong>
</li>
<li>
<span>{{ s__('PipelineCharts|Success ratio:') }}</span>
<strong>{{ counts.successRatio }}%</strong>
</li>
</ul>
</template>
export const CHART_CONTAINER_HEIGHT = 300;
export const INNER_CHART_HEIGHT = 200;
export const X_AXIS_LABEL_ROTATION = 45;
export const X_AXIS_TITLE_OFFSET = 60;
import Vue from 'vue';
import ProjectPipelinesCharts from './components/app.vue';
export default () => {
const el = document.querySelector('#js-project-pipelines-charts-app');
const {
countsFailed,
countsSuccess,
countsTotal,
successRatio,
timesChartLabels,
timesChartValues,
} = el.dataset;
return new Vue({
el,
name: 'ProjectPipelinesChartsApp',
components: {
ProjectPipelinesCharts,
},
render: createElement =>
createElement(ProjectPipelinesCharts, {
props: {
counts: {
failed: countsFailed,
success: countsSuccess,
total: countsTotal,
successRatio,
},
timesChartData: {
labels: JSON.parse(timesChartLabels),
values: JSON.parse(timesChartValues),
},
},
}),
});
};
...@@ -74,7 +74,7 @@ export default { ...@@ -74,7 +74,7 @@ export default {
</script> </script>
<template> <template>
<section id="serverless-functions"> <section id="serverless-functions" class="flex-grow">
<gl-loading-icon <gl-loading-icon
v-if="checkingInstalled" v-if="checkingInstalled"
:size="2" :size="2"
......
/*
* A CSS cross-browser fix for Select2 failire to display HTML5 required warnings
* MR link https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22716
*/
.gl-select2-html5-required-fix div.select2-container+select.select2 {
display: block !important;
width: 1px;
height: 1px;
z-index: -1;
opacity: 0;
margin: -3px auto 0;
background-image: none;
background-color: transparent;
border: 0;
}
...@@ -63,6 +63,6 @@ module PageLimiter ...@@ -63,6 +63,6 @@ module PageLimiter
controller: params[:controller], controller: params[:controller],
action: params[:action], action: params[:action],
bot: dd.bot? bot: dd.bot?
) ).increment
end end
end end
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
class PrometheusMetric < ApplicationRecord class PrometheusMetric < ApplicationRecord
belongs_to :project, validate: true, inverse_of: :prometheus_metrics belongs_to :project, validate: true, inverse_of: :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :prometheus_metric
enum group: PrometheusMetricEnums.groups enum group: PrometheusMetricEnums.groups
...@@ -73,5 +74,3 @@ class PrometheusMetric < ApplicationRecord ...@@ -73,5 +74,3 @@ class PrometheusMetric < ApplicationRecord
PrometheusMetricEnums.group_details.fetch(group.to_sym) PrometheusMetricEnums.group_details.fetch(group.to_sym)
end end
end end
PrometheusMetric.prepend_if_ee('EE::PrometheusMetric')
...@@ -9,7 +9,8 @@ module PrometheusMetricEnums ...@@ -9,7 +9,8 @@ module PrometheusMetricEnums
aws_elb: -3, aws_elb: -3,
nginx: -4, nginx: -4,
kubernetes: -5, kubernetes: -5,
nginx_ingress: -6 nginx_ingress: -6,
cluster_health: -100
}.merge(custom_groups).freeze }.merge(custom_groups).freeze
end end
...@@ -54,6 +55,11 @@ module PrometheusMetricEnums ...@@ -54,6 +55,11 @@ module PrometheusMetricEnums
group_title: _('System metrics (Kubernetes)'), group_title: _('System metrics (Kubernetes)'),
required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total), required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
priority: 5 priority: 5
}.freeze,
cluster_health: {
group_title: _('Cluster Health'),
required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
priority: 10
}.freeze }.freeze
}.merge(custom_group_details).freeze }.merge(custom_group_details).freeze
end end
...@@ -76,5 +82,3 @@ module PrometheusMetricEnums ...@@ -76,5 +82,3 @@ module PrometheusMetricEnums
}.freeze }.freeze
end end
end end
PrometheusMetricEnums.prepend_if_ee('EE::PrometheusMetricEnums')
...@@ -19,6 +19,12 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -19,6 +19,12 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
project_tag_path(project, release.tag) project_tag_path(project, release.tag)
end end
def self_url
return unless ::Feature.enabled?(:release_show_page, project)
project_release_url(project, release)
end
def merge_requests_url def merge_requests_url
return unless release_mr_issue_urls_available? return unless release_mr_issue_urls_available?
......
- page_title _('CI / CD Charts') - page_title _('CI / CD Charts')
#js-project-pipelines-charts-app{ data: { counts: @counts, success_ratio: success_ratio(@counts), times_chart: { labels: @charts[:pipeline_times].labels, values: @charts[:pipeline_times].pipeline_times } } }
#charts.ci-charts #charts.ci-charts
= render 'projects/pipelines/charts/overall'
%hr %hr
= render 'projects/pipelines/charts/pipelines' = render 'projects/pipelines/charts/pipelines'
%h4.mt-4.mb-4= s_("PipelineCharts|Overall statistics")
.row
.col-md-6
= render 'projects/pipelines/charts/pipeline_statistics'
.col-md-6
= render 'projects/pipelines/charts/pipeline_times'
%ul
%li
= s_("PipelineCharts|Total:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:total]) % @counts[:total]
%li
= s_("PipelineCharts|Successful:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:success]) % @counts[:success]
%li
= s_("PipelineCharts|Failed:")
%strong= n_("1 pipeline", "%d pipelines", @counts[:failed]) % @counts[:failed]
%li
= s_("PipelineCharts|Success ratio:")
%strong
#{success_ratio(@counts)}%
%p.light
= _("Commit duration in minutes for last 30 commits")
%div
%canvas#build_timesChart{ height: 200 }
-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
- page_title _("Snippets") - page_title _("Snippets")
- new_project_snippet_link = new_project_snippet_path(@project) if can?(current_user, :create_snippet, @project)
- if @snippets.exists? - if @snippets.exists?
- if current_user - if current_user
...@@ -6,10 +7,10 @@ ...@@ -6,10 +7,10 @@
- include_private = @project.team.member?(current_user) || current_user.admin? - include_private = @project.team.member?(current_user) || current_user.admin?
= render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private } = render partial: 'snippets/snippets_scope_menu', locals: { subject: @project, include_private: include_private }
- if can?(current_user, :create_snippet, @project) - if new_project_snippet_link.present?
.nav-controls .nav-controls
= link_to _("New snippet"), new_project_snippet_path(@project), class: "btn btn-success", title: _("New snippet") = link_to _("New snippet"), new_project_snippet_link, class: "btn btn-success", title: _("New snippet")
= render 'shared/snippets/list' = render 'shared/snippets/list'
- else - else
= render 'shared/empty_states/snippets', button_path: new_namespace_project_snippet_path(@project.namespace, @project) = render 'shared/empty_states/snippets', button_path: new_project_snippet_link
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
- if secondary_button_link.present? - if secondary_button_link.present?
= link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted' = link_to secondary_button_label, secondary_button_link, class: 'btn btn-success btn-inverted'
- if primary_button_link.present?
= link_to primary_button_label, primary_button_link, class: 'btn btn-success' = link_to primary_button_label, primary_button_link, class: 'btn btn-success'
- else - else
%h5= visitor_empty_message %h5= visitor_empty_message
---
title: Migrate CI CD statistics + duration chart to VueJS
merge_request: 23840
author:
type: changed
---
title: Ensure New Snippet button is displayed based on the :create_snippet permission
in Project Snippets page and User profile > Snippets tab
merge_request: 55240
author:
type: fixed
---
title: Task lists work correctly again on closed MRs
merge_request: 23714
author:
type: fixed
---
title: Separate commit entities into own class files
merge_request: 24085
author: Rajendra Kadam
type: added
---
title: Fix database permission check for triggers on Amazon RDS
merge_request: 24035
author:
type: fixed
...@@ -50,8 +50,4 @@ Rails.application.configure do ...@@ -50,8 +50,4 @@ Rails.application.configure do
# BetterErrors live shell (REPL) on every stack frame # BetterErrors live shell (REPL) on every stack frame
BetterErrors::Middleware.allow_ip!("127.0.0.1/0") BetterErrors::Middleware.allow_ip!("127.0.0.1/0")
# Use an evented file watcher to asynchronously detect changes in source code,
# routes, locales, etc. This feature depends on the listen gem.
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
end end
...@@ -59,12 +59,12 @@ are recommended to get your merge request approved and merged by maintainer(s) ...@@ -59,12 +59,12 @@ are recommended to get your merge request approved and merged by maintainer(s)
from teams other than your own. from teams other than your own.
1. If your merge request includes backend changes [^1], it must be 1. If your merge request includes backend changes [^1], it must be
**approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_backend)**. **approved by a [backend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_backend)**.
1. If your merge request includes database migrations or changes to expensive queries [^2], it must be 1. If your merge request includes database migrations or changes to expensive queries [^2], it must be
**approved by a [database maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_database)**. **approved by a [database maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_database)**.
Read the [database review guidelines](database_review.md) for more details. Read the [database review guidelines](database_review.md) for more details.
1. If your merge request includes frontend changes [^1], it must be 1. If your merge request includes frontend changes [^1], it must be
**approved by a [frontend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab-ce_maintainers_frontend)**. **approved by a [frontend maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_frontend)**.
1. If your merge request includes UX changes [^1], it must be 1. If your merge request includes UX changes [^1], it must be
**approved by a [UX team member](https://about.gitlab.com/company/team/)**. **approved by a [UX team member](https://about.gitlab.com/company/team/)**.
1. If your merge request includes adding a new JavaScript library [^1], it must be 1. If your merge request includes adding a new JavaScript library [^1], it must be
......
...@@ -645,9 +645,8 @@ To indicate the steps of navigation through the UI: ...@@ -645,9 +645,8 @@ To indicate the steps of navigation through the UI:
- Images should have a specific, non-generic name that will - Images should have a specific, non-generic name that will
differentiate and describe them properly. differentiate and describe them properly.
- Always add to the end of the file name the GitLab release version - Always add to the end of the file name the GitLab release version
number corresponding to the release milestone the image was added to, corresponding to the version the screenshot was taken from, using the format
or corresponding to the release the screenshot was taken from, using the `image_name_vX_Y.png`.
format `image_name_vX_Y.png`.
([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/61027) in GitLab 12.1.) ([Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/61027) in GitLab 12.1.)
- For example, for a screenshot taken from the pipelines page of - For example, for a screenshot taken from the pipelines page of
GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're GitLab 11.1, a valid name is `pipelines_v11_1.png`. If you're
......
...@@ -98,30 +98,6 @@ members can join swiftly without requesting a link. ...@@ -98,30 +98,6 @@ members can join swiftly without requesting a link.
Read more how to [add or remove a zoom meeting](../project/issues/associate_zoom_meeting.md). Read more how to [add or remove a zoom meeting](../project/issues/associate_zoom_meeting.md).
### Alerting
You can let GitLab know of alerts that may be triggering in your applications and services. GitLab can react to these by automatically creating Issues, and alerting developers via Email.
#### Prometheus Alerts
Prometheus alerts can be setup in both GitLab-managed Prometheus installs and self-managed Prometheus installs.
Documentation for each method can be found here:
- [GitLab-managed Prometheus](../project/integrations/prometheus.md#setting-up-alerts-for-prometheus-metrics-ultimate)
- [Self-managed Prometheus](../project/integrations/prometheus.md#external-prometheus-instances)
#### Alert Endpoint
GitLab can accept alerts from any source via a generic webhook receiver. When you set up the generic alerts integration, a unique endpoint will
be created which can receive a payload in JSON format.
More information on setting this up, including how to customize the payload [can be found here](../project/integrations/generic_alerts.md).
#### Recovery Alerts
Coming soon: GitLab can automatically close Issues that have been automatically created when we receive notification that the alert is resolved.
### Configuring Incidents ### Configuring Incidents
Incident Management features can be easily enabled & disabled via the Project settings page. Head to Project -> Settings -> Operations -> Incidents. Incident Management features can be easily enabled & disabled via the Project settings page. Head to Project -> Settings -> Operations -> Incidents.
......
...@@ -96,9 +96,8 @@ to integrate with. ...@@ -96,9 +96,8 @@ to integrate with.
1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services) 1. Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
1. Click the **Prometheus** service 1. Click the **Prometheus** service
1. Provide the base URL of the your server, for example `http://prometheus.example.com/`. 1. Provide the base URL of your server, for example `http://prometheus.example.com/`
The **Test Settings** button can be used to confirm connectivity from GitLab 1. Click **Save changes**
to the Prometheus server.
![Configure Prometheus Service](img/prometheus_service_configuration.png) ![Configure Prometheus Service](img/prometheus_service_configuration.png)
...@@ -330,6 +329,33 @@ Note the following properties: ...@@ -330,6 +329,33 @@ Note the following properties:
![anomaly panel type](img/prometheus_dashboard_anomaly_panel_type.png) ![anomaly panel type](img/prometheus_dashboard_anomaly_panel_type.png)
#### Column
To add a column panel type to a dashboard, look at the following sample dashboard file:
```yaml
dashboard: 'Dashboard Title'
panel_groups:
- group: 'Group title'
panels:
- title: "Column"
type: "column"
metrics:
- id: 1024_memory
query: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
unit: MB
label: "Memory Usage"
```
Note the following properties:
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
| type | string | yes | Type of panel to be rendered. For column panel types, set to `column` |
| query_range | yes | yes | For column panel types, you must use a [range query](https://prometheus.io/docs/prometheus/latest/querying/api/#range-queries) |
![anomaly panel type](img/prometheus_dashboard_column_panel_type.png)
##### Single Stat ##### Single Stat
To add a single stat panel type to a dashboard, look at the following sample dashboard file: To add a single stat panel type to a dashboard, look at the following sample dashboard file:
......
...@@ -128,51 +128,6 @@ module API ...@@ -128,51 +128,6 @@ module API
end end
end end
class DiffRefs < Grape::Entity
expose :base_sha, :head_sha, :start_sha
end
class Commit < Grape::Entity
expose :id, :short_id, :created_at
expose :parent_ids
expose :full_title, as: :title
expose :safe_message, as: :message
expose :author_name, :author_email, :authored_date
expose :committer_name, :committer_email, :committed_date
end
class CommitStats < Grape::Entity
expose :additions, :deletions, :total
end
class CommitWithStats < Commit
expose :stats, using: Entities::CommitStats
end
class CommitDetail < Commit
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
expose :project_id
expose :last_pipeline do |commit, options|
pipeline = commit.last_pipeline if can_read_pipeline?
::API::Entities::PipelineBasic.represent(pipeline, options)
end
private
def can_read_pipeline?
Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
end
end
class CommitSignature < Grape::Entity
expose :gpg_key_id
expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email
expose :verification_status
expose :gpg_key_subkey_id
end
class BasicRef < Grape::Entity class BasicRef < Grape::Entity
expose :type, :name expose :type, :name
end end
...@@ -1101,6 +1056,7 @@ module API ...@@ -1101,6 +1056,7 @@ module API
expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? } expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end end
expose :_links do expose :_links do
expose :self_url, as: :self, expose_nil: false
expose :merge_requests_url, expose_nil: false expose :merge_requests_url, expose_nil: false
expose :issues_url, expose_nil: false expose :issues_url, expose_nil: false
expose :edit_url, expose_nil: false expose :edit_url, expose_nil: false
......
# frozen_string_literal: true
module API
module Entities
class Commit < Grape::Entity
expose :id, :short_id, :created_at
expose :parent_ids
expose :full_title, as: :title
expose :safe_message, as: :message
expose :author_name, :author_email, :authored_date
expose :committer_name, :committer_email, :committed_date
end
end
end
# frozen_string_literal: true
module API
module Entities
class CommitDetail < Commit
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
expose :project_id
expose :last_pipeline do |commit, options|
pipeline = commit.last_pipeline if can_read_pipeline?
::API::Entities::PipelineBasic.represent(pipeline, options)
end
private
def can_read_pipeline?
Ability.allowed?(options[:current_user], :read_pipeline, object.last_pipeline)
end
end
end
end
# frozen_string_literal: true
module API
module Entities
class CommitSignature < Grape::Entity
expose :gpg_key_id
expose :gpg_key_primary_keyid, :gpg_key_user_name, :gpg_key_user_email
expose :verification_status
expose :gpg_key_subkey_id
end
end
end
# frozen_string_literal: true
module API
module Entities
class CommitStats < Grape::Entity
expose :additions, :deletions, :total
end
end
end
# frozen_string_literal: true
module API
module Entities
class CommitWithStats < Commit
expose :stats, using: Entities::CommitStats
end
end
end
# frozen_string_literal: true
module API
module Entities
class DiffRefs < Grape::Entity
expose :base_sha, :head_sha, :start_sha
end
end
end
# frozen_string_literal: true
module Gitlab
module Alerting
class NotificationPayloadParser
BadPayloadError = Class.new(StandardError)
DEFAULT_TITLE = 'New: Incident'
def initialize(payload)
@payload = payload.to_h.with_indifferent_access
end
def self.call(payload)
new(payload).call
end
def call
{
'annotations' => annotations,
'startsAt' => starts_at
}.compact
end
private
attr_reader :payload
def title
payload[:title].presence || DEFAULT_TITLE
end
def annotations
primary_params
.reverse_merge(flatten_secondary_params)
.transform_values(&:presence)
.compact
end
def primary_params
{
'title' => title,
'description' => payload[:description],
'monitoring_tool' => payload[:monitoring_tool],
'service' => payload[:service],
'hosts' => hosts.presence
}
end
def hosts
Array(payload[:hosts]).reject(&:blank?)
end
def current_time
Time.current.change(usec: 0).rfc3339
end
def starts_at
Time.parse(payload[:start_time].to_s).rfc3339
rescue ArgumentError
current_time
end
def secondary_params
payload.except(:start_time)
end
def flatten_secondary_params
Gitlab::Utils::SafeInlineHash.merge_keys!(secondary_params)
rescue ArgumentError
raise BadPayloadError, 'The payload is too big'
end
end
end
end
...@@ -3,23 +3,18 @@ ...@@ -3,23 +3,18 @@
module Gitlab module Gitlab
module Database module Database
# Model that can be used for querying permissions of a SQL user. # Model that can be used for querying permissions of a SQL user.
class Grant < ActiveRecord::Base class Grant
include FromUnion
self.table_name = 'information_schema.role_table_grants'
# Returns true if the current user can create and execute triggers on the # Returns true if the current user can create and execute triggers on the
# given table. # given table.
def self.create_and_execute_trigger?(table) def self.create_and_execute_trigger?(table)
# We _must not_ use quote_table_name as this will produce double # We _must not_ use quote_table_name as this will produce double
# quotes on PostgreSQL and for "has_table_privilege" we need single # quotes on PostgreSQL and for "has_table_privilege" we need single
# quotes. # quotes.
connection = ActiveRecord::Base.connection
quoted_table = connection.quote(table) quoted_table = connection.quote(table)
begin begin
from(nil) connection.select_one("SELECT has_table_privilege(#{quoted_table}, 'TRIGGER')").present?
.pluck(Arel.sql("has_table_privilege(#{quoted_table}, 'TRIGGER')"))
.first
rescue ActiveRecord::StatementInvalid rescue ActiveRecord::StatementInvalid
# This error is raised when using a non-existing table name. In this # This error is raised when using a non-existing table name. In this
# case we just want to return false as a user technically can't # case we just want to return false as a user technically can't
......
...@@ -6,5 +6,3 @@ module Gitlab ...@@ -6,5 +6,3 @@ module Gitlab
end end
end end
end end
Gitlab::DatabaseImporters::CommonMetrics.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics')
...@@ -17,7 +17,9 @@ module Gitlab ...@@ -17,7 +17,9 @@ module Gitlab
# custom groups # custom groups
business: 0, business: 0,
response: 1, response: 1,
system: 2 system: 2,
cluster_health: -100
} }
end end
...@@ -31,12 +33,11 @@ module Gitlab ...@@ -31,12 +33,11 @@ module Gitlab
ha_proxy: _('Response metrics (HA Proxy)'), ha_proxy: _('Response metrics (HA Proxy)'),
aws_elb: _('Response metrics (AWS ELB)'), aws_elb: _('Response metrics (AWS ELB)'),
nginx: _('Response metrics (NGINX)'), nginx: _('Response metrics (NGINX)'),
kubernetes: _('System metrics (Kubernetes)') kubernetes: _('System metrics (Kubernetes)'),
cluster_health: _('Cluster Health')
} }
end end
end end
end end
end end
end end
::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums.prepend_if_ee('EE::Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums')
...@@ -4801,9 +4801,6 @@ msgstr "" ...@@ -4801,9 +4801,6 @@ msgstr ""
msgid "Commit deleted" msgid "Commit deleted"
msgstr "" msgstr ""
msgid "Commit duration in minutes for last 30 commits"
msgstr ""
msgid "Commit message" msgid "Commit message"
msgstr "" msgstr ""
...@@ -6813,6 +6810,9 @@ msgstr "" ...@@ -6813,6 +6810,9 @@ msgstr ""
msgid "Duration" msgid "Duration"
msgstr "" msgstr ""
msgid "Duration for the last 30 commits"
msgstr ""
msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below." msgid "During this process, you’ll be asked for URLs from GitLab’s side. Use the URLs shown below."
msgstr "" msgstr ""
......
...@@ -71,19 +71,23 @@ describe PageLimiter do ...@@ -71,19 +71,23 @@ describe PageLimiter do
describe "#default_page_out_of_bounds_response" do describe "#default_page_out_of_bounds_response" do
subject { instance.send(:default_page_out_of_bounds_response) } subject { instance.send(:default_page_out_of_bounds_response) }
after do
subject
end
it "returns a bad_request header" do it "returns a bad_request header" do
expect(instance).to receive(:head).with(:bad_request) expect(instance).to receive(:head).with(:bad_request)
subject
end end
end end
describe "#record_page_limit_interception" do describe "#record_page_limit_interception" do
subject { instance.send(:record_page_limit_interception) } subject { instance.send(:record_page_limit_interception) }
it "records a metric counter" do let(:counter) { double("counter", increment: true) }
before do
allow(Gitlab::Metrics).to receive(:counter) { counter }
end
it "creates a metric counter" do
expect(Gitlab::Metrics).to receive(:counter).with( expect(Gitlab::Metrics).to receive(:counter).with(
:gitlab_page_out_of_bounds, :gitlab_page_out_of_bounds,
controller: "explore/projects", controller: "explore/projects",
...@@ -93,5 +97,11 @@ describe PageLimiter do ...@@ -93,5 +97,11 @@ describe PageLimiter do
subject subject
end end
it "increments the counter" do
expect(counter).to receive(:increment)
subject
end
end end
end end
...@@ -90,8 +90,12 @@ describe Explore::ProjectsController do ...@@ -90,8 +90,12 @@ describe Explore::ProjectsController do
end end
describe "metrics recording" do describe "metrics recording" do
after do subject { get endpoint, params: { page: page_limit + 1 } }
get endpoint, params: { page: page_limit + 1 }
let(:counter) { double("counter", increment: true) }
before do
allow(Gitlab::Metrics).to receive(:counter) { counter }
end end
it "records the interception" do it "records the interception" do
...@@ -101,6 +105,8 @@ describe Explore::ProjectsController do ...@@ -101,6 +105,8 @@ describe Explore::ProjectsController do
action: endpoint.to_s, action: endpoint.to_s,
bot: false bot: false
) )
subject
end end
end end
end end
......
...@@ -85,7 +85,7 @@ describe 'Project Graph', :js do ...@@ -85,7 +85,7 @@ describe 'Project Graph', :js do
expect(page).to have_content 'Pipelines for last week' expect(page).to have_content 'Pipelines for last week'
expect(page).to have_content 'Pipelines for last month' expect(page).to have_content 'Pipelines for last month'
expect(page).to have_content 'Pipelines for last year' expect(page).to have_content 'Pipelines for last year'
expect(page).to have_content 'Commit duration in minutes for last 30 commits' expect(page).to have_content 'Duration for the last 30 commits'
end end
end end
end end
...@@ -3,17 +3,21 @@ ...@@ -3,17 +3,21 @@
require 'spec_helper' require 'spec_helper'
describe 'Projects > Snippets > User views snippets' do describe 'Projects > Snippets > User views snippets' do
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:user) { create(:user) }
def visit_project_snippets
visit(project_snippets_path(project))
end
context 'snippets list' do
let!(:project_snippet) { create(:project_snippet, project: project, author: user) } let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
let!(:snippet) { create(:snippet, author: user) } let!(:snippet) { create(:snippet, author: user) }
let(:snippets) { [project_snippet, snippet] } # Used by the shared examples let(:snippets) { [project_snippet, snippet] } # Used by the shared examples
let(:user) { create(:user) }
before do before do
project.add_maintainer(user) project.add_maintainer(user)
sign_in(user) sign_in(user)
visit(project_snippets_path(project))
end end
context 'pagination' do context 'pagination' do
...@@ -21,14 +25,75 @@ describe 'Projects > Snippets > User views snippets' do ...@@ -21,14 +25,75 @@ describe 'Projects > Snippets > User views snippets' do
create(:project_snippet, project: project, author: user) create(:project_snippet, project: project, author: user)
allow(Snippet).to receive(:default_per_page).and_return(1) allow(Snippet).to receive(:default_per_page).and_return(1)
visit project_snippets_path(project) visit_project_snippets
end end
it_behaves_like 'paginated snippets' it_behaves_like 'paginated snippets'
end end
it 'shows snippets' do it 'shows snippets' do
visit_project_snippets
expect(page).to have_link(project_snippet.title, href: project_snippet_path(project, project_snippet)) expect(page).to have_link(project_snippet.title, href: project_snippet_path(project, project_snippet))
expect(page).not_to have_content(snippet.title) expect(page).not_to have_content(snippet.title)
end end
end
context 'when current user is a guest' do
before do
project.add_guest(user)
sign_in(user)
end
context 'when snippets list is empty' do
it 'hides New Snippet button' do
visit_project_snippets
page.within(find('.empty-state')) do
expect(page).not_to have_link('New snippet')
end
end
end
context 'when project has snippets' do
let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
it 'hides New Snippet button' do
visit_project_snippets
page.within(find('.top-area')) do
expect(page).not_to have_link('New snippet')
end
end
end
end
context 'when current user is not a guest' do
before do
project.add_developer(user)
sign_in(user)
end
context 'when snippets list is empty' do
it 'shows New Snippet button' do
visit_project_snippets
page.within(find('.empty-state')) do
expect(page).to have_link('New snippet')
end
end
end
context 'when project has snippets' do
let!(:project_snippet) { create(:project_snippet, project: project, author: user) }
it 'shows New Snippet button' do
visit_project_snippets
page.within(find('.top-area')) do
expect(page).to have_link('New snippet')
end
end
end
end
end end
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe 'Task Lists' do describe 'Task Lists' do
include Warden::Test::Helpers include Warden::Test::Helpers
let(:project) { create(:project, :repository) } let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:user2) { create(:user) } let(:user2) { create(:user) }
...@@ -122,6 +122,7 @@ describe 'Task Lists' do ...@@ -122,6 +122,7 @@ describe 'Task Lists' do
it 'provides a summary on Issues#index' do it 'provides a summary on Issues#index' do
visit project_issues_path(project) visit project_issues_path(project)
expect(page).to have_content("2 of 6 tasks completed") expect(page).to have_content("2 of 6 tasks completed")
end end
end end
...@@ -191,6 +192,7 @@ describe 'Task Lists' do ...@@ -191,6 +192,7 @@ describe 'Task Lists' do
it 'is only editable by author', :js do it 'is only editable by author', :js do
visit_issue(project, issue) visit_issue(project, issue)
expect(page).to have_selector('.js-task-list-container') expect(page).to have_selector('.js-task-list-container')
gitlab_sign_out gitlab_sign_out
...@@ -237,10 +239,7 @@ describe 'Task Lists' do ...@@ -237,10 +239,7 @@ describe 'Task Lists' do
visit project_merge_request_path(project, merge) visit project_merge_request_path(project, merge)
end end
describe 'multiple tasks' do shared_examples 'multiple tasks' do
let(:project) { create(:project, :repository) }
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
it 'renders for description' do it 'renders for description' do
visit_merge_request(project, merge) visit_merge_request(project, merge)
...@@ -261,23 +260,40 @@ describe 'Task Lists' do ...@@ -261,23 +260,40 @@ describe 'Task Lists' do
expect(page).to have_selector('a.btn-close') expect(page).to have_selector('a.btn-close')
end end
it 'is only editable by author' do it 'is only editable by author', :js do
visit_merge_request(project, merge) visit_merge_request(project, merge)
expect(page).to have_selector('.js-task-list-container') expect(page).to have_selector('.js-task-list-container')
expect(page).to have_selector('li.task-list-item.enabled', count: 6)
logout(:user) logout(:user)
login_as(user2) login_as(user2)
visit current_path visit current_path
expect(page).not_to have_selector('.js-task-list-container') expect(page).not_to have_selector('.js-task-list-container')
expect(page).to have_selector('li.task-list-item.enabled', count: 0)
expect(page).to have_selector('li.task-list-item input[disabled]', count: 6)
end end
end
context 'when merge request is open' do
let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
it_behaves_like 'multiple tasks'
it 'provides a summary on MergeRequests#index' do it 'provides a summary on MergeRequests#index' do
visit project_merge_requests_path(project) visit project_merge_requests_path(project)
expect(page).to have_content("2 of 6 tasks completed") expect(page).to have_content("2 of 6 tasks completed")
end end
end end
context 'when merge request is closed' do
let!(:merge) { create(:merge_request, :closed, :simple, description: markdown, author: user, source_project: project) }
it_behaves_like 'multiple tasks'
end
describe 'single incomplete task' do describe 'single incomplete task' do
let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) } let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
...@@ -291,6 +307,7 @@ describe 'Task Lists' do ...@@ -291,6 +307,7 @@ describe 'Task Lists' do
it 'provides a summary on MergeRequests#index' do it 'provides a summary on MergeRequests#index' do
visit project_merge_requests_path(project) visit project_merge_requests_path(project)
expect(page).to have_content("0 of 1 task completed") expect(page).to have_content("0 of 1 task completed")
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatisticsList matches the snapshot 1`] = `
<ul>
<li>
<span>
Total:
</span>
<strong>
4 pipelines
</strong>
</li>
<li>
<span>
Successful:
</span>
<strong>
2 pipelines
</strong>
</li>
<li>
<span>
Failed:
</span>
<strong>
2 pipelines
</strong>
</li>
<li>
<span>
Success ratio:
</span>
<strong>
50%
</strong>
</li>
</ul>
`;
import { shallowMount } from '@vue/test-utils';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import Component from '~/projects/pipelines/charts/components/app';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list';
import { counts, timesChartData } from '../mock_data';
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
counts,
timesChartData,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('overall statistics', () => {
it('displays the statistics list', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBeTruthy();
expect(list.props('counts')).toBe(counts);
});
it('displays the commit duration chart', () => {
const chart = wrapper.find(GlColumnChart);
expect(chart.exists()).toBeTruthy();
expect(chart.props('yAxisTitle')).toBe('Minutes');
expect(chart.props('xAxisTitle')).toBe('Commit');
expect(chart.props('data')).toBe(wrapper.vm.timesChartTransformedData);
expect(chart.props('option')).toBe(wrapper.vm.$options.timesChartOptions);
});
});
});
import { shallowMount } from '@vue/test-utils';
import Component from '~/projects/pipelines/charts/components/statistics_list';
import { counts } from '../mock_data';
describe('StatisticsList', () => {
let wrapper;
beforeEach(() => {
wrapper = shallowMount(Component, {
propsData: {
counts,
},
});
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
export const counts = {
failed: 2,
success: 2,
total: 4,
successRatio: 50,
};
export const timesChartData = {
labels: ['as1234', 'kh423hy', 'ji56bvg', 'th23po'],
values: [5, 3, 7, 4],
};
# frozen_string_literal: true
require 'fast_spec_helper'
describe Gitlab::Alerting::NotificationPayloadParser do
describe '.call' do
let(:starts_at) { Time.current.change(usec: 0) }
let(:payload) do
{
'title' => 'alert title',
'start_time' => starts_at.rfc3339,
'description' => 'Description',
'monitoring_tool' => 'Monitoring tool name',
'service' => 'Service',
'hosts' => ['gitlab.com']
}
end
subject { described_class.call(payload) }
it 'returns Prometheus-like payload' do
is_expected.to eq(
{
'annotations' => {
'title' => 'alert title',
'description' => 'Description',
'monitoring_tool' => 'Monitoring tool name',
'service' => 'Service',
'hosts' => ['gitlab.com']
},
'startsAt' => starts_at.rfc3339
}
)
end
context 'when title is blank' do
before do
payload[:title] = ''
end
it 'sets a predefined title' do
expect(subject.dig('annotations', 'title')).to eq('New: Incident')
end
end
context 'when hosts attribute is a string' do
before do
payload[:hosts] = 'gitlab.com'
end
it 'returns hosts as an array of one element' do
expect(subject.dig('annotations', 'hosts')).to eq(['gitlab.com'])
end
end
context 'when the time is in unsupported format' do
before do
payload[:start_time] = 'invalid/date/format'
end
it 'sets startsAt to a current time in RFC3339 format' do
expect(subject['startsAt']).to eq(starts_at.rfc3339)
end
end
context 'when payload is blank' do
let(:payload) { {} }
it 'returns default parameters' do
is_expected.to eq(
'annotations' => { 'title' => 'New: Incident' },
'startsAt' => starts_at.rfc3339
)
end
end
context 'when payload attributes have blank lines' do
let(:payload) do
{
'title' => '',
'start_time' => '',
'description' => '',
'monitoring_tool' => '',
'service' => '',
'hosts' => ['']
}
end
it 'returns default parameters' do
is_expected.to eq(
'annotations' => { 'title' => 'New: Incident' },
'startsAt' => starts_at.rfc3339
)
end
end
context 'when payload has secondary params' do
let(:payload) do
{
'description' => 'Description',
'additional' => {
'params' => {
'1' => 'Some value 1',
'2' => 'Some value 2',
'blank' => ''
}
}
}
end
it 'adds secondary params to annotations' do
is_expected.to eq(
'annotations' => {
'title' => 'New: Incident',
'description' => 'Description',
'additional.params.1' => 'Some value 1',
'additional.params.2' => 'Some value 2'
},
'startsAt' => starts_at.rfc3339
)
end
end
context 'when secondary params hash is too big' do
before do
allow(Gitlab::Utils::SafeInlineHash).to receive(:merge_keys!).and_raise(ArgumentError)
end
it 'catches and re-raises an error' do
expect { subject }.to raise_error Gitlab::Alerting::NotificationPayloadParser::BadPayloadError, 'The payload is too big'
end
end
end
end
...@@ -67,6 +67,7 @@ describe PrometheusMetric do ...@@ -67,6 +67,7 @@ describe PrometheusMetric do
it_behaves_like 'group_title', :business, 'Business metrics (Custom)' it_behaves_like 'group_title', :business, 'Business metrics (Custom)'
it_behaves_like 'group_title', :response, 'Response metrics (Custom)' it_behaves_like 'group_title', :response, 'Response metrics (Custom)'
it_behaves_like 'group_title', :system, 'System metrics (Custom)' it_behaves_like 'group_title', :system, 'System metrics (Custom)'
it_behaves_like 'group_title', :cluster_health, 'Cluster Health'
end end
describe '#priority' do describe '#priority' do
...@@ -82,6 +83,7 @@ describe PrometheusMetric do ...@@ -82,6 +83,7 @@ describe PrometheusMetric do
:business | 0 :business | 0
:response | -5 :response | -5
:system | -10 :system | -10
:cluster_health | 10
end end
with_them do with_them do
...@@ -106,6 +108,7 @@ describe PrometheusMetric do ...@@ -106,6 +108,7 @@ describe PrometheusMetric do
:business | %w() :business | %w()
:response | %w() :response | %w()
:system | %w() :system | %w()
:cluster_health | %w(container_memory_usage_bytes container_cpu_usage_seconds_total)
end end
with_them do with_them do
......
...@@ -51,6 +51,22 @@ describe ReleasePresenter do ...@@ -51,6 +51,22 @@ describe ReleasePresenter do
end end
end end
describe '#self_url' do
subject { presenter.self_url }
it 'returns its own url' do
is_expected.to match /#{project_release_url(project, release)}/
end
context 'when release_show_page feature flag is disabled' do
before do
stub_feature_flags(release_show_page: false)
end
it { is_expected.to be_nil }
end
end
describe '#merge_requests_url' do describe '#merge_requests_url' do
subject { presenter.merge_requests_url } subject { presenter.merge_requests_url }
......
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