Commit e0be1f2b authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch '6983-promote-starting-a-gitlab-com-trial' into 'master'

Promote the GitLab.com Gold trial on a users default dashboard

Closes #6983

See merge request gitlab-org/gitlab-ee!6947
parents 8e3d2001 0fb8a89b
import Visibility from 'visibilityjs';
import Vue from 'vue';
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '../persistent_user_callout';
import { s__, sprintf } from '../locale';
import Flash from '../flash';
import Poll from '../lib/utils/poll';
......@@ -65,7 +65,7 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
initDismissableCallout('.js-cluster-security-warning');
Clusters.initDismissableCallout();
initSettingsPanels();
setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
this.initApplications(clusterType);
......@@ -106,6 +106,12 @@ export default class Clusters {
});
}
static initDismissableCallout() {
const callout = document.querySelector('.js-cluster-security-warning');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
}
addListeners() {
if (this.showTokenButton) this.showTokenButton.addEventListener('click', this.showToken);
eventHub.$on('installApplication', this.installApplication);
......
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import Flash from '~/flash';
export default function initDismissableCallout(alertSelector) {
const alertEl = document.querySelector(alertSelector);
if (!alertEl) {
return;
}
const closeButtonEl = alertEl.getElementsByClassName('close')[0];
const { dismissEndpoint, featureId } = closeButtonEl.dataset;
closeButtonEl.addEventListener('click', () => {
axios
.post(dismissEndpoint, {
feature_name: featureId,
})
.then(() => {
$(alertEl).alert('close');
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
});
}
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
initDismissableCallout('.gcp-signup-offer');
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
});
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '~/persistent_user_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
function initGcpSignupCallout() {
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
}
document.addEventListener('DOMContentLoaded', () => {
const { page } = document.body.dataset;
const newClusterViews = [
......@@ -10,7 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
initDismissableCallout('.gcp-signup-offer');
initGcpSignupCallout();
initGkeDropdowns();
}
});
import initDismissableCallout from '~/dismissable_callout';
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
initDismissableCallout('.gcp-signup-offer');
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
});
import initDismissableCallout from '~/dismissable_callout';
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
import PersistentUserCallout from '../../persistent_user_callout';
import Project from './project';
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
......@@ -12,7 +12,9 @@ document.addEventListener('DOMContentLoaded', () => {
];
if (newClusterViews.indexOf(page) > -1) {
initDismissableCallout('.gcp-signup-offer');
const callout = document.querySelector('.gcp-signup-offer');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
initGkeDropdowns();
}
......
// if the "projects dashboard" is a user's default dashboard, when they visit the
// instance root index, the dashboard will be served by the root controller instead
// of a dashboard controller. The root index redirects for all other default dashboards.
import '../dashboard/projects/index';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import Flash from './flash';
export default class PersistentUserCallout {
constructor(container) {
const { dismissEndpoint, featureId } = container.dataset;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
this.init();
}
init() {
const closeButton = this.container.querySelector('.js-close');
closeButton.addEventListener('click', event => this.dismiss(event));
}
dismiss(event) {
event.preventDefault();
axios
.post(this.dismissEndpoint, {
feature_name: this.featureId,
})
.then(() => {
this.container.remove();
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
});
}
}
# frozen_string_literal: true
module DashboardHelper
prepend EE::DashboardHelper
def assigned_issues_dashboard_path
issues_dashboard_path(assignee_id: current_user.id)
end
......
# frozen_string_literal: true
module UserCalloutsHelper
prepend EE::UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
GCP_SIGNUP_OFFER = 'gcp_signup_offer'.freeze
......
# frozen_string_literal: true
module UsersHelper
prepend EE::UsersHelper
def user_link(user)
link_to(user.name, user_path(user),
title: user.email,
......
......@@ -6,7 +6,8 @@ class UserCallout < ActiveRecord::Base
enum feature_name: {
gke_cluster_integration: 1,
gcp_signup_offer: 2,
cluster_security_warning: 3
cluster_security_warning: 3,
gold_trial: 4
}
validates :user, presence: true
......
- link = link_to(s_('ClusterIntegration|sign up'), 'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert' }
%button.close{ type: "button", data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } } &times;
.bs-callout.gcp-signup-offer.alert.alert-block.alert-dismissable.prepend-top-default.append-bottom-default{ role: 'alert', data: { feature_id: UserCalloutsHelper::GCP_SIGNUP_OFFER, dismiss_endpoint: user_callouts_path } }
%button.close.js-close{ type: "button" } &times;
.gcp-signup-offer--content
.gcp-signup-offer--icon.append-right-8
= sprite_icon("information", size: 16)
......
......@@ -4,6 +4,9 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
= render_if_exists "shared/gold_trial_callout"
- page_title "Activity"
- header_title "Activity", activity_dashboard_path
......
- @hide_top_links = true
- page_title "Groups"
- header_title "Groups", dashboard_groups_path
= render_if_exists "shared/gold_trial_callout"
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
......
......@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
= render_if_exists "shared/gold_trial_callout"
.page-title-holder
%h1.page-title= _('Issues')
......
......@@ -2,6 +2,8 @@
- page_title _("Merge Requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_id: current_user.id)
= render_if_exists "shared/gold_trial_callout"
.page-title-holder
%h1.page-title= _('Merge Requests')
......
......@@ -4,6 +4,8 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
= render_if_exists "shared/gold_trial_callout"
- page_title "Projects"
- header_title "Projects", dashboard_projects_path
......
......@@ -4,6 +4,8 @@
- page_title "Starred Projects"
- header_title "Projects", dashboard_projects_path
= render_if_exists "shared/gold_trial_callout"
%div{ class: container_class }
= render "projects/last_push"
= render 'dashboard/projects_head'
......
......@@ -2,6 +2,8 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
= render_if_exists "shared/gold_trial_callout"
.page-title-holder
%h1.page-title= _('Todos')
......
......@@ -2,6 +2,8 @@
- page_title _("Groups")
- header_title _("Groups"), dashboard_groups_path
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/groups_head'
- else
......
......@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
......@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
......@@ -2,6 +2,8 @@
- page_title _("Projects")
- header_title _("Projects"), dashboard_projects_path
= render_if_exists "shared/gold_trial_callout"
- if current_user
= render 'dashboard/projects_head'
- else
......
......@@ -1907,6 +1907,7 @@ ActiveRecord::Schema.define(version: 20181107054254) do
add_index "namespaces", ["plan_id"], name: "index_namespaces_on_plan_id", using: :btree
add_index "namespaces", ["require_two_factor_authentication"], name: "index_namespaces_on_require_two_factor_authentication", using: :btree
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
add_index "namespaces", ["trial_ends_on"], name: "index_namespaces_on_trial_ends_on", where: "(trial_ends_on IS NOT NULL)", using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
create_table "note_diff_files", force: :cascade do |t|
......
import PersistentUserCallout from '~/persistent_user_callout';
document.addEventListener('DOMContentLoaded', () => {
const callout = document.querySelector('.js-gold-trial-callout');
if (callout) new PersistentUserCallout(callout); // eslint-disable-line no-new
});
// if the "projects dashboard" is a user's default dashboard, when they visit the
// instance root index, the dashboard will be served by the root controller instead
// of a dashboard controller. The root index redirects for all other default dashboards.
import '~/pages/root/index';
import '../dashboard/index';
......@@ -34,7 +34,7 @@
}
.svg-container {
margin-right: 15px;
margin-right: $gl-padding;
}
}
......@@ -77,6 +77,24 @@
margin: 0;
}
}
&.thin-callout {
.bordered-box {
padding: $gl-padding;
padding-left: 40px;
.close {
position: relative;
top: 0;
right: 0;
}
}
.svg-container .svg {
max-width: 39px;
max-height: 39px;
}
}
}
.promotion-modal {
......
module EE
module DashboardHelper
def controller_action_to_child_dashboards(controller = controller_name, action = action_name)
case "#{controller}##{action}"
when 'projects#index', 'root#index', 'projects#starred', 'projects#trending'
%w(projects)
when 'dashboard#activity'
%w(starred_project_activity project_activity)
when 'groups#index'
%w(groups)
when 'todos#index'
%w(todos)
when 'dashboard#issues'
%w(issues)
when 'dashboard#merge_requests'
%w(merge_requests)
else
[]
end
end
def user_default_dashboard?(user = current_user)
controller_action_to_child_dashboards.any? {|dashboard| dashboard == user.dashboard }
end
end
end
module EE
module UserCalloutsHelper
GOLD_TRIAL = 'gold_trial'.freeze
def show_gold_trial?(user = current_user)
!user_dismissed?(GOLD_TRIAL) &&
(::Gitlab.com? || Rails.env.development?) &&
!user.any_namespace_with_gold? &&
!user.any_namespace_with_trial?
end
end
end
......@@ -170,11 +170,36 @@ module EE
project_creation_level: project_creation_levels)
end
def any_namespace_with_trial?
::Namespace
.from("(#{namespace_union(:trial_ends_on)}) #{::Namespace.table_name}")
.where('trial_ends_on > ?', Time.now.utc)
.any?
end
def any_namespace_with_gold?
::Namespace
.includes(:plan)
.where("namespaces.id IN (#{namespace_union})") # rubocop:disable GitlabSecurity/SqlInjection
.where.not(plans: { id: nil })
.any?
end
override :has_current_license?
def has_current_license?
License.current.present?
end
def group_sso?(group)
return false unless group
if group_saml_identities.loaded?
group_saml_identities.any? { |identity| identity.saml_provider.group_id == group.id }
else
group_saml_identities.where(saml_provider: group.saml_provider).any?
end
end
override :ldap_sync_time
def ldap_sync_time
::Gitlab.config.ldap['sync_time']
......@@ -184,14 +209,13 @@ module EE
update_column :admin_email_unsubscribed_at, Time.now
end
def group_sso?(group)
return false unless group
private
if group_saml_identities.loaded?
group_saml_identities.any? { |identity| identity.saml_provider.group_id == group.id }
else
group_saml_identities.where(saml_provider: group.saml_provider).any?
end
def namespace_union(select = :id)
::Gitlab::SQL::Union.new([
::Namespace.select(select).where(type: nil, owner: self),
owned_groups.select(select).where(parent_id: nil)
]).to_sql
end
end
end
- if show_gold_trial? && user_default_dashboard?
.pt-1.d-none.d-md-block{ class: container_class }
.user-callout.promotion-callout.thin-callout.js-gold-trial-callout{ data: { uid: 'trial_callout_dismissed', feature_id: UserCalloutsHelper::GOLD_TRIAL, dismiss_endpoint: user_callouts_path } }
.bordered-box.justify-content-left.align-items-center
.svg-container
= image_tag 'illustrations/golden_tanuki.svg', class: 'svg'
.d-flex.flex-grow.align-items-center
.user-callout-copy.ml-0
%h5.mb-0.mt-0= _('Free Trial of GitLab.com Gold')
%p.mb-0
%span= _('Try all GitLab has to offer for 30 days.')
%span.d-none.d-sm-inline= _('No credit card required.')
= link_to _('Start your trial'), 'https://customers.gitlab.com/trials/new?gl_com=true', class: 'btn btn-primary mr-3 mt-2 mt-sm-0', target: '_blank'
%button.btn.btn-default.close.js-close{ type: 'button',
'aria-label' => _('Dismiss trial promotion') }
= sprite_icon('close', css_class: 'dismiss-icon')
---
title: Promote starting a GitLab.com Gold trial on the dashboard
merge_request: 6947
author:
type: other
# frozen_string_literal: true
class AddIndexToNamespaceTrialEndsOn < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :namespaces, :trial_ends_on, where: "trial_ends_on IS NOT NULL"
end
def down
remove_concurrent_index :namespaces, :trial_ends_on
end
end
require 'spec_helper'
describe 'Dashboard activity' do
let(:user) { create(:user) }
let(:page_path) { activity_dashboard_path }
it_behaves_like 'gold trial callout'
end
require 'spec_helper'
describe 'Dashboard groups' do
let(:user) { create(:user) }
let(:page_path) { dashboard_groups_path }
it_behaves_like 'gold trial callout'
end
require 'spec_helper'
describe 'Dashboard issues' do
let(:user) { create(:user) }
let(:page_path) { issues_dashboard_path }
it_behaves_like 'gold trial callout'
end
require 'spec_helper'
describe 'Dashboard merge requests' do
let(:user) { create(:user) }
let(:page_path) { merge_requests_dashboard_path }
it_behaves_like 'gold trial callout'
end
require 'spec_helper'
describe 'Dashboard projects' do
let(:user) { create(:user) }
let(:page_path) { dashboard_projects_path }
it_behaves_like 'gold trial callout'
end
require 'spec_helper'
describe 'Dashboard todos' do
let(:user) { create(:user) }
let(:page_path) { dashboard_todos_path }
it_behaves_like 'gold trial callout'
end
shared_examples 'gold trial callout' do
before do
sign_in(user)
end
it 'hides promotion callout if not .com' do
allow(Gitlab).to receive(:com?).and_return(false)
visit page_path
expect(page).not_to have_selector '.promotion-callout'
end
context '.com' do
before do
allow(Gitlab).to receive(:com?).and_return(true)
end
it 'shows dismissable promotion callout if default dashboard', :js do
allow_any_instance_of(EE::DashboardHelper).to receive(:user_default_dashboard?).and_return(true)
visit page_path
expect(page).to have_selector '.promotion-callout'
find('.js-close').click
expect(page).not_to have_selector '.promotion-callout'
end
it 'hides dismissable promotion callout if not default dashboard', :js do
allow_any_instance_of(EE::DashboardHelper).to receive(:user_default_dashboard?).and_return(false)
visit page_path
expect(page).not_to have_selector '.promotion-callout'
end
it 'hides promotion callout if a trial is active' do
group = create(:group, name: 'trial group', trial_ends_on: 1.year.from_now)
group.add_owner(user)
visit page_path
expect(page).not_to have_selector '.promotion-callout'
end
it 'hides promotion callout if a gold plan is active', :js do
group = create(:group, name: 'gold group', plan: :gold_plan)
group.add_owner(user)
visit page_path
expect(page).not_to have_selector '.promotion-callout'
end
end
end
......@@ -2869,6 +2869,9 @@ msgstr ""
msgid "Dismiss Merge Request promotion"
msgstr ""
msgid "Dismiss trial promotion"
msgstr ""
msgid "Do you want to customize how Google Code email addresses and usernames are imported into GitLab?"
msgstr ""
......@@ -3553,6 +3556,9 @@ msgstr ""
msgid "Found errors in your .gitlab-ci.yml:"
msgstr ""
msgid "Free Trial of GitLab.com Gold"
msgstr ""
msgid "From %{provider_title}"
msgstr ""
......@@ -5435,6 +5441,9 @@ msgstr ""
msgid "No contributions were found"
msgstr ""
msgid "No credit card required."
msgstr ""
msgid "No due date"
msgstr ""
......@@ -7705,6 +7714,9 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
msgid "Start your trial"
msgstr ""
msgid "Started"
msgstr ""
......@@ -8596,6 +8608,9 @@ msgstr ""
msgid "Try again"
msgstr ""
msgid "Try all GitLab has to offer for 30 days."
msgstr ""
msgid "Turn on Service Desk"
msgstr ""
......
......@@ -6,6 +6,7 @@ describe 'Dashboard Merge Requests' do
include ProjectForksHelper
let(:current_user) { create :user }
let(:user) { current_user }
let(:project) { create(:project) }
let(:public_project) { create(:project, :public, :repository) }
......
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