summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/assets/javascripts/main.js1
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue27
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue17
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue28
-rw-r--r--app/assets/javascripts/monitoring/monitoring_bundle.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js8
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/controllers/concerns/invisible_captcha.rb51
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/registrations_controller.rb1
-rw-r--r--app/helpers/sessions_helper.rb7
-rw-r--r--app/views/devise/sessions/_new_base.html.haml17
-rw-r--r--app/views/devise/shared/_signup_box.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml8
-rw-r--r--changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml5
-rw-r--r--changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml5
-rw-r--r--changelogs/unreleased/65483-add-a-resend-confirmation-link.yml5
-rw-r--r--config/initializers/invisible_captcha.rb7
-rw-r--r--config/locales/invisible_captcha.en.yml4
-rw-r--r--doc/development/documentation/styleguide.md3
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/update/upgrading_from_source.md4
-rw-r--r--doc/user/analytics/productivity_analytics.md69
-rw-r--r--doc/user/project/cycle_analytics.md28
-rw-r--r--doc/user/project/integrations/mattermost.md2
-rw-r--r--doc/user/project/issues/issue_data_and_actions.md7
-rw-r--r--lib/tasks/services.rake2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/registrations_controller_spec.rb86
-rw-r--r--spec/features/invites_spec.rb1
-rw-r--r--spec/features/profiles/user_visits_profile_preferences_page_spec.rb4
-rw-r--r--spec/features/users/login_spec.rb36
-rw-r--r--spec/features/users/signup_spec.rb4
-rw-r--r--spec/helpers/sessions_helper_spec.rb17
-rw-r--r--spec/javascripts/monitoring/charts/area_spec.js29
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js35
42 files changed, 445 insertions, 100 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 79e06439ac2..b75c63e1f58 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -8,7 +8,7 @@ require:
- rubocop-rspec
AllCops:
- TargetRubyVersion: 2.5
+ TargetRubyVersion: 2.6
TargetRailsVersion: 5.0
Exclude:
- 'vendor/**/*'
diff --git a/Gemfile b/Gemfile
index ae9ee1cb333..a91399ab3ad 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,6 +51,7 @@ gem 'jwt', '~> 2.1.0'
# Spam and anti-bot protection
gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails'
gem 'akismet', '~> 2.0'
+gem 'invisible_captcha', '~> 0.12.1'
# Two-factor authentication
gem 'devise-two-factor', '~> 3.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 918115b3b01..16d7f63cb66 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -438,6 +438,8 @@ GEM
influxdb (0.2.3)
cause
json
+ invisible_captcha (0.12.1)
+ rails (>= 3.2.0)
ipaddress (0.8.3)
jaeger-client (0.10.0)
opentracing (~> 0.3)
@@ -1129,6 +1131,7 @@ DEPENDENCIES
httparty (~> 0.16.4)
icalendar
influxdb (~> 0.2)
+ invisible_captcha (~> 0.12.1)
jira-ruby (~> 1.4)
js_regex (~> 3.1)
json-schema (~> 2.8.0)
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 9e97f345717..ba33d72b1f3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -107,6 +107,7 @@ function deferredInitialisation() {
.then(() => {
$('select.select2').select2({
width: 'resolve',
+ minimumResultsForSearch: 10,
dropdownAutoWidth: true,
});
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 5b950f8c966..838447e6c75 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -1,7 +1,6 @@
<script>
import { __ } from '~/locale';
-import { mapState } from 'vuex';
-import { GlLink, GlButton } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils';
@@ -16,7 +15,6 @@ let debouncedResize;
export default {
components: {
GlAreaChart,
- GlButton,
GlChartSeriesLabel,
GlLink,
Icon,
@@ -69,7 +67,6 @@ export default {
};
},
computed: {
- ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']),
chartData() {
// Transforms & supplements query data to render appropriate labels & styles
// Input: [{ queryAttributes1 }, { queryAttributes2 }]
@@ -179,18 +176,6 @@ export default {
yAxisLabel() {
return `${this.graphData.y_label}`;
},
- csvText() {
- const chartData = this.chartData[0].data;
- const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
- return chartData.reduce((csv, data) => {
- const row = data.join(',');
- return `${csv}${row}\r\n`;
- }, header);
- },
- downloadLink() {
- const data = new Blob([this.csvText], { type: 'text/plain' });
- return window.URL.createObjectURL(data);
- },
},
watch: {
containerWidth: 'onResize',
@@ -259,16 +244,6 @@ export default {
<div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
<div class="prometheus-graph-header">
<h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
- <gl-button
- v-if="exportMetricsToCsvEnabled"
- :href="downloadLink"
- :title="__('Download CSV')"
- :aria-label="__('Download CSV')"
- style="margin-left: 200px;"
- download="chart_metrics.csv"
- >
- {{ __('Download CSV') }}
- </gl-button>
<div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
</div>
<gl-area-chart
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 782e4310f3e..587392adbc3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -235,6 +235,19 @@ export default {
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
+ csvText(graphData) {
+ const chartData = graphData.queries[0].result[0].values;
+ const yLabel = graphData.y_label;
+ const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadCsv(graphData) {
+ const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
// TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
// Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845
getGraphAlerts(queries) {
@@ -448,7 +461,6 @@ export default {
@setAlerts="setAlerts"
/>
<gl-dropdown
- v-if="alertWidgetAvailable"
v-gl-tooltip
class="mx-2"
toggle-class="btn btn-transparent border-0"
@@ -459,6 +471,9 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
+ <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv">
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`"
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index 295c0851f12..3fbac71f3d7 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -1,7 +1,14 @@
<script>
import { mapState } from 'vuex';
import _ from 'underscore';
-import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import Icon from '~/vue_shared/components/icon.vue';
import MonitorAreaChart from './charts/area.vue';
import MonitorSingleStatChart from './charts/single_stat.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
@@ -11,12 +18,14 @@ export default {
MonitorAreaChart,
MonitorSingleStatChart,
MonitorEmptyChart,
+ Icon,
GlDropdown,
GlDropdownItem,
GlModal,
},
directives: {
GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
props: {
graphData: {
@@ -41,6 +50,19 @@ export default {
graphDataHasMetrics() {
return this.graphData.queries[0].result.length > 0;
},
+ csvText() {
+ const chartData = this.graphData.queries[0].result[0].values;
+ const yLabel = this.graphData.y_label;
+ const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
+ return chartData.reduce((csv, data) => {
+ const row = data.join(',');
+ return `${csv}${row}\r\n`;
+ }, header);
+ },
+ downloadCsv() {
+ const data = new Blob([this.csvText], { type: 'text/plain' });
+ return window.URL.createObjectURL(data);
+ },
},
methods: {
getGraphAlerts(queries) {
@@ -81,7 +103,6 @@ export default {
@setAlerts="setAlerts"
/>
<gl-dropdown
- v-if="alertWidgetAvailable"
v-gl-tooltip
class="mx-2"
toggle-class="btn btn-transparent border-0"
@@ -92,6 +113,9 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
+ <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv">
+ {{ __('Download CSV') }}
+ </gl-dropdown-item>
<gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`">
{{ __('Alerts') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
index 366034becd0..c0fee1ebb99 100644
--- a/app/assets/javascripts/monitoring/monitoring_bundle.js
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -13,7 +13,6 @@ export default (props = {}) => {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
- exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled,
});
}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a9c491c7c6c..0cbad179f17 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => {
export const setFeatureFlags = (
{ commit },
- {
- prometheusEndpointEnabled,
- multipleDashboardsEnabled,
- additionalPanelTypesEnabled,
- exportMetricsToCsvEnabled,
- },
+ { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled },
) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
- commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 9ec8214b167..4b1aadbcf05 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
-export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index a2dceb21fc0..b19520d6638 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -99,7 +99,4 @@ export default {
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
- [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) {
- state.exportMetricsToCsvEnabled = enabled;
- },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index a14a25e3a20..440bdc951e0 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -10,7 +10,6 @@ export default () => ({
useDashboardEndpoint: false,
multipleDashboardsEnabled: false,
additionalPanelTypesEnabled: false,
- exportMetricsToCsvEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb
new file mode 100644
index 00000000000..c9f66e5c194
--- /dev/null
+++ b/app/controllers/concerns/invisible_captcha.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+module InvisibleCaptcha
+ extend ActiveSupport::Concern
+
+ included do
+ invisible_captcha only: :create, on_spam: :on_honeypot_spam_callback, on_timestamp_spam: :on_timestamp_spam_callback
+ end
+
+ def on_honeypot_spam_callback
+ return unless Feature.enabled?(:invisible_captcha)
+
+ invisible_captcha_honeypot_counter.increment
+ log_request('Invisible_Captcha_Honeypot_Request')
+
+ head(200)
+ end
+
+ def on_timestamp_spam_callback
+ return unless Feature.enabled?(:invisible_captcha)
+
+ invisible_captcha_timestamp_counter.increment
+ log_request('Invisible_Captcha_Timestamp_Request')
+
+ redirect_to new_user_session_path, alert: InvisibleCaptcha.timestamp_error_message
+ end
+
+ def invisible_captcha_honeypot_counter
+ @invisible_captcha_honeypot_counter ||=
+ Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_honeypot,
+ 'Counter of blocked sign up attempts with filled honeypot')
+ end
+
+ def invisible_captcha_timestamp_counter
+ @invisible_captcha_timestamp_counter ||=
+ Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_timestamp,
+ 'Counter of blocked sign up attempts with invalid timestamp')
+ end
+
+ def log_request(message)
+ request_information = {
+ message: message,
+ env: :invisible_captcha_signup_bot_detected,
+ ip: request.ip,
+ request_method: request.request_method,
+ fullpath: request.fullpath
+ }
+
+ Gitlab::AuthLogger.error(request_information)
+ end
+end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index df9e55fda2a..5a1f93dc609 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:export_metrics_to_csv_enabled)
end
def index
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 638934694e0..db10515c0b4 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController
include Recaptcha::Verify
include AcceptsPendingInvitations
include RecaptchaExperimentHelper
+ include InvisibleCaptcha
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb
new file mode 100644
index 00000000000..af98a611b8b
--- /dev/null
+++ b/app/helpers/sessions_helper.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module SessionsHelper
+ def unconfirmed_email?
+ flash[:alert] == t(:unconfirmed, scope: [:devise, :failure])
+ end
+end
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 2f10f08c839..0b1d3d1ddb3 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -1,20 +1,23 @@
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f|
.form-group
- = f.label "Username or email", for: "user_login", class: 'label-bold'
- = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' }
+ = f.label _('Username or email'), for: 'user_login', class: 'label-bold'
+ = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' }
.form-group
= f.label :password, class: 'label-bold'
- = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' }
+ = f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
- if devise_mapping.rememberable?
.remember-me
- %label{ for: "user_remember_me" }
+ %label{ for: 'user_remember_me' }
= f.check_box :remember_me, class: 'remember-me-checkbox'
%span Remember me
- .float-right.forgot-password
- = link_to "Forgot your password?", new_password_path(:user)
+ .float-right
+ - if unconfirmed_email?
+ = link_to _('Resend confirmation email'), new_user_confirmation_path
+ - else
+ = link_to _('Forgot your password?'), new_password_path(:user)
%div
- if captcha_enabled?
= recaptcha_tags
.submit-container.move-submit-down
- = f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' }
+ = f.submit _('Sign in'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 074edf645ba..2cd77af6877 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -5,6 +5,8 @@
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors
= render "devise/shared/error_messages", resource: resource
+ - if Feature.enabled?(:invisible_captcha)
+ = invisible_captcha
.name.form-group
= f.label :name, _('Full name'), class: 'label-bold'
= f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.")
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index d16e2dddbe0..d99063e344f 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -45,20 +45,20 @@
.form-group
= f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
- = f.select :layout, layout_choices, {}, class: 'form-control'
+ = f.select :layout, layout_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.')
.form-group
= f.label :dashboard, class: 'label-bold' do
= s_('Preferences|Default dashboard')
- = f.select :dashboard, dashboard_choices, {}, class: 'form-control'
+ = f.select :dashboard, dashboard_choices, {}, class: 'select2'
= render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific
.form-group
= f.label :project_view, class: 'label-bold' do
= s_('Preferences|Project overview content')
- = f.select :project_view, project_view_choices, {}, class: 'form-control'
+ = f.select :project_view, project_view_choices, {}, class: 'select2'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on a project’s overview page.')
@@ -82,7 +82,7 @@
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
- = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control'
+ = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2'
- if Feature.enabled?(:user_time_settings)
.col-sm-12
%hr
diff --git a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
new file mode 100644
index 00000000000..10f2b7eaed5
--- /dev/null
+++ b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml
@@ -0,0 +1,5 @@
+---
+title: Harmonize selections in user settings
+merge_request: 31110
+author: Marc Schwede
+type: other
diff --git a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
new file mode 100644
index 00000000000..21771c76873
--- /dev/null
+++ b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml
@@ -0,0 +1,5 @@
+---
+title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics'
+merge_request: 31679
+author:
+type: changed
diff --git a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
new file mode 100644
index 00000000000..a5f62dbcd56
--- /dev/null
+++ b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users to resend a confirmation link when the grace period has expired
+merge_request: 31476
+author:
+type: changed
diff --git a/config/initializers/invisible_captcha.rb b/config/initializers/invisible_captcha.rb
new file mode 100644
index 00000000000..5177c730596
--- /dev/null
+++ b/config/initializers/invisible_captcha.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+InvisibleCaptcha.setup do |config|
+ config.honeypots = %w(firstname lastname)
+ config.timestamp_enabled = true
+ config.timestamp_threshold = 4
+end
diff --git a/config/locales/invisible_captcha.en.yml b/config/locales/invisible_captcha.en.yml
new file mode 100644
index 00000000000..5978549c0c3
--- /dev/null
+++ b/config/locales/invisible_captcha.en.yml
@@ -0,0 +1,4 @@
+en:
+ invisible_captcha:
+ sentence_for_humans: If you are human, please ignore this field.
+ timestamp_error_message: That was a bit too quick! Please resubmit.
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 59c8bfe2964..680f2cd13c2 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -877,10 +877,10 @@ Other text includes deprecation notices and version-specific how-to information.
When a feature is available in EE-only tiers, add the corresponding tier according to the
feature availability:
+- For GitLab Core and GitLab.com Free: `**(CORE)**`.
- For GitLab Starter and GitLab.com Bronze: `**(STARTER)**`.
- For GitLab Premium and GitLab.com Silver: `**(PREMIUM)**`.
- For GitLab Ultimate and GitLab.com Gold: `**(ULTIMATE)**`.
-- For GitLab Core and GitLab.com Free: `**(CORE)**`.
To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the
keyword "only":
@@ -892,6 +892,7 @@ keyword "only":
For GitLab.com only tiers (when the feature is not available for self-hosted instances):
+- For GitLab Free and higher tiers: `**(FREE ONLY)**`.
- For GitLab Bronze and higher tiers: `**(BRONZE ONLY)**`.
- For GitLab Silver and higher tiers: `**(SILVER ONLY)**`.
- For GitLab Gold: `**(GOLD ONLY)**`.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index df6c485b1cb..295d9804497 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -202,8 +202,8 @@ Then select 'Internet Site' and press enter to confirm the hostname.
The Ruby interpreter is required to run GitLab.
-**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6
- dropped support for Ruby 2.4.x.
+**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2
+ dropped support for Ruby 2.5.x.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 83a9e7fe294..234e5acb394 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -40,7 +40,7 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions
-GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6.
+GitLab requires Ruby (MRI) 2.6. Support for Ruby versions below 2.6 (2.4, 2.5) will stop with GitLab 12.2.
You will have to use the standard MRI implementation of Ruby.
We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 0aef40262c9..df35638cba2 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -47,8 +47,8 @@ sudo service gitlab stop
### 3. Update Ruby
-NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped
-support for Ruby 2.4. Be sure to upgrade if necessary.
+NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped
+support for Ruby 2.5. Be sure to upgrade if necessary.
You can check which version you are running with `ruby -v`.
diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md
new file mode 100644
index 00000000000..db3e37bd0fb
--- /dev/null
+++ b/doc/user/analytics/productivity_analytics.md
@@ -0,0 +1,69 @@
+# Productivity Analytics **(PREMIUM)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flags `analytics` and `productivity_analytics`).
+
+Track development velocity with Productivity Analytics.
+
+For many companies, the development cycle is a blackbox and getting an estimate of how
+long, on average, it takes to deliver features is an enormous endeavor.
+
+While [Cycle Analytics](../project/cycle_analytics.md) focuses on the entire
+SDLC process, Productivity Analytics provides a way for Engineering Management to
+drill down in a systematic way to uncover patterns and causes for success or failure at
+an individual, project or group level.
+
+Productivity can slow down for many reasons ranging from degrading code base to quickly
+growing teams. In order to investigate, department or team leaders can start by visualizing the time
+it takes for merge requests to be merged.
+
+## Supported features
+
+Productivity Analytics allows GitLab users to:
+
+- Visualize typical Merge Request lifetime and statistics. Use a histogram
+ that shows the distribution of the time elapsed between creating and merging
+ Merge Requests.
+- Drill down into the most time consuming Merge Requests, select a number of outliers,
+ and filter down all subsequent charts to investigate potential causes.
+- Filter by group, project, author, label, milestone, or a specific date range.
+ Filter down, for example, to the Merge Requests of a specific author in a group
+ or project during a milestone or specific date range.
+
+## Accessing metrics and visualizations
+
+To access the **Productivity Analytics** page, go to **Analytics > Productivity Analytics**.
+
+The following metrics and visualizations are available on a project or group level:
+
+- Histogram showing the number of Merge Request that took a specified number of days to
+ merge after creation. Select a specific column to filter down subsequent charts.
+- Histogram showing a breakdown of the time taken (in hours) to merge a Merge Request.
+ The following intervals are available:
+ - Time from first commit to first comment.
+ - Time from first comment until last commit.
+ - Time from last commit to merge.
+- Histogram showing the size or complexity of a Merge Request, using the following:
+ - Number of commits per Merge Request.
+ - Number of lines of code per commit.
+ - Number of files touched.
+- Table showing list of Merge Requests with their respective times and size metrics.
+ Can be sorted by the above metrics.
+ - Users can sort by any of the above metrics
+
+## Retrieving data
+
+Users can retrieve three months of data when they deploy Productivity Analytics for the first time.
+
+To retrieve data for a different time span, run the following in the GitLab directory:
+
+```sh
+MERGED_AT_AFTER = <your_date> rake gitlab:productivity_analytics:recalc
+```
+
+## Permissions
+
+The **Productivity Analytics** dashboard can be accessed only:
+
+- On GitLab instances and namespaces on
+ [Premium or Silver tier](https://about.gitlab.com/pricing/) and above.
+- By users with [Reporter access](../permissions.md) and above.
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 6707b88c317..424bee6e9f1 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -1,5 +1,7 @@
# Cycle Analytics
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) at a group level in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flag `analytics`).
+
Cycle Analytics measures the time spent to go from an [idea to production] - also known
as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to
reach production, along with the time typically spent in each DevOps stage along the way.
@@ -13,10 +15,16 @@ calculates a separate median for each stage.
## Overview
-You can find the Cycle Analytics page under your project's **Project ➔ Cycle
-Analytics** tab.
+Cycle Analytics are available at a:
+
+- Group level from the top navigation bar **Analytics > Cycle Analytics**. **(PREMIUM)**
+
+ In the future, multiple groups will be selectable which will effectively make this an
+ instance-level feature.
+
+- Project level from a project's **Project > Cycle Analytics**.
-![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
+ ![Cycle Analytics landing page](img/cycle_analytics_landing_page.png)
There are seven stages that are tracked as part of the Cycle Analytics calculations.
@@ -134,7 +142,7 @@ A few notes:
## Permissions
-The current permissions on the Cycle Analytics dashboard are:
+The current permissions on the Project Cycle Analytics dashboard are:
- Public projects - anyone can access
- Internal projects - any authenticated user can access
@@ -142,6 +150,18 @@ The current permissions on the Cycle Analytics dashboard are:
You can [read more about permissions][permissions] in general.
+NOTE: **Note:**
+As of GitLab 12.2, the project-level page is deprecated. You should access
+project-level Cycle Analytics from **Analytics > Cycle Analytics** in the top
+navigation bar. We will ensure that the same project-level functionality is available
+to CE users in the new analytics space.
+
+For Cycle Analytics functionality introduced in GitLab 12.2 and later:
+
+- Users must have Reporter access or above.
+- Features are available only on
+ [Premium or Silver tiers](https://about.gitlab.com/pricing/) and above.
+
## More resources
Learn more about Cycle Analytics in the following resources:
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index ea58a08e127..6e0f39956d3 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -14,7 +14,7 @@ To enable Mattermost integration you must create an incoming webhook integration
1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
-it on `https://mattermost.example/admin_console/integrations/custom`.
+it on **Mattermost System Console > Integrations > Integration Management**, or on **Mattermost System Console > Integrations > Custom Integrations** in Mattermost versions 5.11 and earlier.
Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md
index 7b031f83cb1..d7d168710ef 100644
--- a/doc/user/project/issues/issue_data_and_actions.md
+++ b/doc/user/project/issues/issue_data_and_actions.md
@@ -50,7 +50,12 @@ The button to do this has a different label depending on whether the issue is al
#### 3. Assignee
-An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER).
+An issue can be assigned to:
+
+- Yourself.
+- Another person.
+- [Many people](#31-multiple-assignees-STARTER). **(STARTER)**
+
The assignee(s) can be changed as often as needed. The idea is that the assignees are
responsible for that issue until it's reassigned to someone else to take it from there.
When assigned to someone, it will appear in their assigned issues list.
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
index 56b81106c5f..4ec4fdd281f 100644
--- a/lib/tasks/services.rake
+++ b/lib/tasks/services.rake
@@ -86,7 +86,7 @@ namespace :services do
doc_start = Time.now
doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
- result = ERB.new(services_template, 0, '>')
+ result = ERB.new(services_template, trim_mode: '>')
.result(OpenStruct.new(services: services).instance_eval { binding })
File.open(doc_path, 'w') do |f|
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index c91c220f696..dd69fa1f8f6 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5077,6 +5077,9 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr ""
+msgid "Forgot your password?"
+msgstr ""
+
msgid "Fork"
msgstr ""
@@ -12527,6 +12530,9 @@ msgstr ""
msgid "Username is available."
msgstr ""
+msgid "Username or email"
+msgstr ""
+
msgid "Users"
msgstr ""
diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb
index faf3c990cb2..d05482f095e 100644
--- a/spec/controllers/registrations_controller_spec.rb
+++ b/spec/controllers/registrations_controller_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe RegistrationsController do
include TermsHelper
+ before do
+ stub_feature_flags(invisible_captcha: false)
+ end
+
describe '#create' do
let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } }
let(:user_params) { { user: base_user_params } }
@@ -88,6 +92,88 @@ describe RegistrationsController do
end
end
+ context 'when invisible captcha is enabled' do
+ before do
+ stub_feature_flags(invisible_captcha: true)
+ InvisibleCaptcha.timestamp_threshold = treshold
+ end
+
+ let(:treshold) { 4 }
+ let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } }
+ let(:form_rendered_time) { Time.current }
+ let(:submit_time) { form_rendered_time + treshold }
+ let(:auth_log_attributes) do
+ {
+ message: auth_log_message,
+ env: :invisible_captcha_signup_bot_detected,
+ ip: '0.0.0.0',
+ request_method: 'POST',
+ fullpath: '/users'
+ }
+ end
+
+ describe 'the honeypot has not been filled and the signup form has not been submitted too quickly' do
+ it 'creates an account' do
+ travel_to(submit_time) do
+ expect { post(:create, params: user_params, session: session_params) }.to change(User, :count).by(1)
+ end
+ end
+ end
+
+ describe 'honeypot spam detection' do
+ let(:user_params) { super().merge(firstname: 'Roy', lastname: 'Batty') }
+ let(:auth_log_message) { 'Invisible_Captcha_Honeypot_Request' }
+
+ it 'logs the request, refuses to create an account and renders an empty body' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_honeypot, 'Counter of blocked sign up attempts with filled honeypot')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to have_gitlab_http_status(200)
+ expect(response.body).to be_empty
+ end
+ end
+ end
+
+ describe 'timestamp spam detection' do
+ let(:auth_log_message) { 'Invisible_Captcha_Timestamp_Request' }
+
+ context 'the sign up form has been submitted without the invisible_captcha_timestamp parameter' do
+ let(:session_params) { nil }
+
+ it 'logs the request, refuses to create an account and displays a flash alert' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.'
+ end
+ end
+ end
+
+ context 'the sign up form has been submitted too quickly' do
+ let(:submit_time) { form_rendered_time }
+
+ it 'logs the request, refuses to create an account and displays a flash alert' do
+ travel_to(submit_time) do
+ expect(Gitlab::Metrics).to receive(:counter)
+ .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp')
+ .and_call_original
+ expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once
+ expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count)
+ expect(response).to redirect_to(new_user_session_path)
+ expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.'
+ end
+ end
+ end
+ end
+ end
+
context 'when terms are enforced' do
before do
enforce_terms
diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb
index 855cf22642e..832c4a57aa3 100644
--- a/spec/features/invites_spec.rb
+++ b/spec/features/invites_spec.rb
@@ -10,6 +10,7 @@ describe 'Invites' do
let(:group_invite) { group.group_members.invite.last }
before do
+ stub_feature_flags(invisible_captcha: false)
project.add_maintainer(owner)
group.add_user(owner, Gitlab::Access::OWNER)
group.add_developer('user@example.com', owner)
diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
index 5e52c82a234..4dbdea02e27 100644
--- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
+++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb
@@ -38,7 +38,7 @@ describe 'User visits the profile preferences page' do
describe 'User changes their default dashboard', :js do
it 'creates a flash message' do
- select 'Starred Projects', from: 'user_dashboard'
+ select2('stars', from: '#user_dashboard')
click_button 'Save'
wait_for_requests
@@ -47,7 +47,7 @@ describe 'User visits the profile preferences page' do
end
it 'updates their preference' do
- select 'Starred Projects', from: 'user_dashboard'
+ select2('stars', from: '#user_dashboard')
click_button 'Save'
wait_for_requests
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index dac8c8e7a29..1d8c9e7e426 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -95,6 +95,42 @@ describe 'Login' do
end
end
+ describe 'with an unconfirmed email address' do
+ let!(:user) { create(:user, confirmed_at: nil) }
+ let(:grace_period) { 2.days }
+
+ before do
+ stub_application_setting(send_user_confirmation_email: true)
+ allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period
+ end
+
+ context 'within the grace period' do
+ it 'allows to login' do
+ expect(authentication_metrics).to increment(:user_authenticated_counter)
+
+ gitlab_sign_in(user)
+
+ expect(page).not_to have_content('You have to confirm your email address before continuing.')
+ expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path)
+ end
+ end
+
+ context 'when the confirmation grace period is expired' do
+ it 'prevents the user from logging in and renders a resend confirmation email link' do
+ travel_to((grace_period + 1.day).from_now) do
+ expect(authentication_metrics)
+ .to increment(:user_unauthenticated_counter)
+ .and increment(:user_session_destroyed_counter).twice
+
+ gitlab_sign_in(user)
+
+ expect(page).to have_content('You have to confirm your email address before continuing.')
+ expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path)
+ end
+ end
+ end
+ end
+
describe 'with the ghost user' do
it 'disallows login' do
expect(authentication_metrics)
diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb
index f5897bffaf0..cf57fafc4f5 100644
--- a/spec/features/users/signup_spec.rb
+++ b/spec/features/users/signup_spec.rb
@@ -5,6 +5,10 @@ require 'spec_helper'
describe 'Signup' do
include TermsHelper
+ before do
+ stub_feature_flags(invisible_captcha: false)
+ end
+
let(:new_user) { build_stubbed(:user) }
describe 'username validation', :js do
diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb
new file mode 100644
index 00000000000..647771ace92
--- /dev/null
+++ b/spec/helpers/sessions_helper_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe SessionsHelper do
+ describe '#unconfirmed_email?' do
+ it 'returns true when the flash alert contains a devise failure unconfirmed message' do
+ flash[:alert] = t(:unconfirmed, scope: [:devise, :failure])
+ expect(helper.unconfirmed_email?).to be_truthy
+ end
+
+ it 'returns false when the flash alert does not contain a devise failure unconfirmed message' do
+ flash[:alert] = 'something else'
+ expect(helper.unconfirmed_email?).to be_falsey
+ end
+ end
+end
diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js
index 4541119dd2e..57f99a09002 100644
--- a/spec/javascripts/monitoring/charts/area_spec.js
+++ b/spec/javascripts/monitoring/charts/area_spec.js
@@ -24,7 +24,6 @@ describe('Area component', () => {
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
- store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true });
[mockGraphData] = store.state.monitoringDashboard.groups[0].metrics;
areaChart = shallowMount(Area, {
@@ -109,16 +108,6 @@ describe('Area component', () => {
});
});
- describe('when exportMetricsToCsvEnabled is disabled', () => {
- beforeEach(() => {
- store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false });
- });
-
- it('does not render the Download CSV button', () => {
- expect(areaChart.contains('glbutton-stub')).toBe(false);
- });
- });
-
describe('methods', () => {
describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at;
@@ -264,23 +253,5 @@ describe('Area component', () => {
expect(areaChart.vm.yAxisLabel).toBe('CPU');
});
});
-
- describe('csvText', () => {
- it('converts data from json to csv', () => {
- const header = `timestamp,${mockGraphData.y_label}`;
- const data = mockGraphData.queries[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
-
- expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`);
- });
- });
-
- describe('downloadLink', () => {
- it('produces a link to download metrics as csv', () => {
- const link = areaChart.vm.downloadLink;
-
- expect(link).toContain('blob:');
- });
- });
});
});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 36f650d5933..b78896c45fc 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
-import {
+import MonitoringMock, {
metricsGroupsAPIResponse,
mockApiEndpoint,
environmentData,
@@ -40,6 +40,7 @@ describe('Dashboard', () => {
let mock;
let store;
let component;
+ let mockGraphData;
beforeEach(() => {
setFixtures(`
@@ -482,4 +483,36 @@ describe('Dashboard', () => {
});
});
});
+
+ describe('when downloading metrics data as CSV', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ propsData: {
+ ...propsData,
+ },
+ store,
+ });
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
+ MonitoringMock.data,
+ );
+ [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics;
+ });
+
+ describe('csvText', () => {
+ it('converts metrics data from json to csv', () => {
+ const header = `timestamp,${mockGraphData.y_label}`;
+ const data = mockGraphData.queries[0].result[0].values;
+ const firstRow = `${data[0][0]},${data[0][1]}`;
+
+ expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`);
+ });
+ });
+
+ describe('downloadCsv', () => {
+ it('produces a link with a Blob', () => {
+ expect(component.downloadCsv(mockGraphData)).toContain(`blob:`);
+ });
+ });
+ });
});