Commit ad3678e3 authored by Matthias Käppler's avatar Matthias Käppler

Merge branch 'nicolasdular/learn-gitlab-page' into 'master'

Boilerplate for learn gitlab page experiment [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!53089
parents 26df3582 58b88f6b
<script>
import { GlLink } from '@gitlab/ui';
import { ACTION_TEXT } from '../constants';
export default {
components: { GlLink },
i18n: {
ACTION_TEXT,
},
props: {
actions: {
required: true,
type: Object,
},
},
};
</script>
<template>
<ul>
<li v-for="(value, action) in actions" :key="action">
<span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
<span v-else>
<gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
</span>
</li>
</ul>
</template>
<script>
import { GlLink } from '@gitlab/ui';
import { ACTION_TEXT } from '../constants';
export default {
components: { GlLink },
i18n: {
ACTION_TEXT,
},
props: {
actions: {
required: true,
type: Object,
},
},
};
</script>
<template>
<ul>
<li v-for="(value, action) in actions" :key="action">
<span v-if="value.completed">{{ $options.i18n.ACTION_TEXT[action] }}</span>
<span v-else>
<gl-link :href="value.url">{{ $options.i18n.ACTION_TEXT[action] }}</gl-link>
</span>
</li>
</ul>
</template>
import { s__ } from '~/locale';
export const ACTION_TEXT = {
gitWrite: s__('LearnGitLab|Create a repository'),
userAdded: s__('LearnGitLab|Invite your colleagues'),
pipelineCreated: s__('LearnGitLab|Set-up CI/CD'),
trialStarted: s__('LearnGitLab|Start a free trial of GitLab Gold'),
codeOwnersEnabled: s__('LearnGitLab|Add code owners'),
requiredMrApprovalsEnabled: s__('LearnGitLab|Enable require merge approvals'),
mergeRequestCreated: s__('LearnGitLab|Submit a merge request (MR)'),
securityScanEnabled: s__('LearnGitLab|Run a Security scan using CI/CD'),
};
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlabA from '../components/learn_gitlab_a.vue';
import LearnGitlabB from '../components/learn_gitlab_b.vue';
function initLearnGitlab() {
const el = document.getElementById('js-learn-gitlab-app');
if (!el) {
return false;
}
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const { learnGitlabA } = gon.experiments;
return new Vue({
el,
render(createElement) {
return createElement(learnGitlabA ? LearnGitlabA : LearnGitlabB, { props: { actions } });
},
});
}
initLearnGitlab();
# frozen_string_literal: true
class Projects::LearnGitlabController < Projects::ApplicationController
before_action :authenticate_user!
before_action :check_experiment_enabled?
feature_category :users
def index
push_frontend_experiment(:learn_gitlab_a, subject: current_user)
push_frontend_experiment(:learn_gitlab_b, subject: current_user)
end
private
def check_experiment_enabled?
return access_denied! unless helpers.learn_gitlab_experiment_enabled?(project)
end
end
# frozen_string_literal: true
module LearnGitlabHelper
def learn_gitlab_experiment_enabled?(project)
return false unless current_user
return false unless experiment_enabled_for_user?
learn_gitlab_onboarding_available?(project)
end
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
action_urls.map do |action, url|
[
action,
url: url,
completed: attributes[OnboardingProgress.column_name(action)].present?
]
end.to_h
end
private
ACTION_ISSUE_IDS = {
git_write: 2,
pipeline_created: 4,
merge_request_created: 6,
user_added: 7,
trial_started: 13,
required_mr_approvals_enabled: 15,
code_owners_enabled: 16
}.freeze
ACTION_DOC_URLS = {
security_scan_enabled: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/#gitlab-security-dashboard-security-center-and-vulnerability-reports'
}.freeze
def action_urls
ACTION_ISSUE_IDS.transform_values { |id| project_issue_url(learn_gitlab_project, id) }.merge(ACTION_DOC_URLS)
end
def learn_gitlab_project
@learn_gitlab_project ||= LearnGitlab.new(current_user).project
end
def onboarding_progress(project)
OnboardingProgress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end
def experiment_enabled_for_user?
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_a, subject: current_user) ||
Gitlab::Experimentation.in_experiment_group?(:learn_gitlab_b, subject: current_user)
end
def learn_gitlab_onboarding_available?(project)
OnboardingProgress.onboarding?(project.namespace) &&
LearnGitlab.new(current_user).available?
end
end
...@@ -433,6 +433,8 @@ module ProjectsHelper ...@@ -433,6 +433,8 @@ module ProjectsHelper
nav_tabs += package_nav_tabs(project, current_user) nav_tabs += package_nav_tabs(project, current_user)
nav_tabs << :learn_gitlab if learn_gitlab_experiment_enabled?(project)
nav_tabs nav_tabs
end end
# rubocop:enable Metrics/CyclomaticComplexity # rubocop:enable Metrics/CyclomaticComplexity
......
...@@ -47,6 +47,10 @@ class OnboardingProgress < ApplicationRecord ...@@ -47,6 +47,10 @@ class OnboardingProgress < ApplicationRecord
safe_find_or_create_by(namespace: namespace) safe_find_or_create_by(namespace: namespace)
end end
def onboarding?(namespace)
where(namespace: namespace).any?
end
def register(namespace, action) def register(namespace, action)
return unless root_namespace?(namespace) && ACTIONS.include?(action) return unless root_namespace?(namespace) && ACTIONS.include?(action)
......
...@@ -33,6 +33,13 @@ ...@@ -33,6 +33,13 @@
= link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do = link_to project_releases_path(@project), title: _('Releases'), class: 'shortcuts-project-releases' do
%span= _('Releases') %span= _('Releases')
- if project_nav_tab? :learn_gitlab
= nav_link(controller: :learn_gitlab, html_options: { class: 'home' }) do
= link_to project_learn_gitlab_path(@project) do
.nav-icon-container
= sprite_icon('home')
%span.nav-item-name
= _('Learn GitLab')
- if project_nav_tab? :files - if project_nav_tab? :files
= nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do = nav_link(controller: sidebar_repository_paths, unless: -> { current_path?('projects/graphs#charts') }) do
......
- breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab")
#js-learn-gitlab-app{ data: { actions: onboarding_actions_data(@project).to_json } }
---
name: learn_gitlab_a_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/281022
milestone: '13.9'
type: experiment
group: group::conversion
default_enabled: false
---
name: learn_gitlab_b_experiment_percentage
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53089
rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/306
milestone: '13.9'
type: experiment
group: group::conversion
default_enabled: false
...@@ -87,6 +87,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -87,6 +87,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end end
end end
get :learn_gitlab, action: :index, controller: 'learn_gitlab'
namespace :ci do namespace :ci do
resource :lint, only: [:show, :create] resource :lint, only: [:show, :create]
resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor' resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor'
......
...@@ -95,6 +95,12 @@ module Gitlab ...@@ -95,6 +95,12 @@ module Gitlab
trial_onboarding_issues: { trial_onboarding_issues: {
tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues' tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues'
}, },
learn_gitlab_a: {
tracking_category: 'Growth::Conversion::Experiment::LearnGitLabA'
},
learn_gitlab_b: {
tracking_category: 'Growth::Activation::Experiment::LearnGitLabB'
},
in_product_marketing_emails: { in_product_marketing_emails: {
tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails' tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails'
} }
......
...@@ -17442,6 +17442,30 @@ msgstr "" ...@@ -17442,6 +17442,30 @@ msgstr ""
msgid "Learn more." msgid "Learn more."
msgstr "" msgstr ""
msgid "LearnGitLab|Add code owners"
msgstr ""
msgid "LearnGitLab|Create a repository"
msgstr ""
msgid "LearnGitLab|Enable require merge approvals"
msgstr ""
msgid "LearnGitLab|Invite your colleagues"
msgstr ""
msgid "LearnGitLab|Run a Security scan using CI/CD"
msgstr ""
msgid "LearnGitLab|Set-up CI/CD"
msgstr ""
msgid "LearnGitLab|Start a free trial of GitLab Gold"
msgstr ""
msgid "LearnGitLab|Submit a merge request (MR)"
msgstr ""
msgid "Leave" msgid "Leave"
msgstr "" msgstr ""
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::LearnGitlabController do
describe 'GET #index' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:learn_gitlab_experiment_enabled) { true }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { get :index, params: params }
before do
allow(controller.helpers).to receive(:learn_gitlab_experiment_enabled?).and_return(learn_gitlab_experiment_enabled)
end
context 'unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
end
context 'authenticated user' do
before do
sign_in(user)
end
it { is_expected.to render_template(:index) }
it 'pushes experiment to frontend' do
expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_a, subject: user)
expect(controller).to receive(:push_frontend_experiment).with(:learn_gitlab_b, subject: user)
subject
end
context 'learn_gitlab experiment not enabled' do
let(:learn_gitlab_experiment_enabled) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab Design A should render the loading state 1`] = `
<ul>
<li>
<span>
Create a repository
</span>
</li>
<li>
<span>
Invite your colleagues
</span>
</li>
<li>
<span>
Set-up CI/CD
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Start a free trial of GitLab Gold
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Add code owners
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Enable require merge approvals
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Submit a merge request (MR)
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Run a Security scan using CI/CD
</gl-link-stub>
</span>
</li>
</ul>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab Design B should render the loading state 1`] = `
<ul>
<li>
<span>
Create a repository
</span>
</li>
<li>
<span>
Invite your colleagues
</span>
</li>
<li>
<span>
Set-up CI/CD
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Start a free trial of GitLab Gold
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Add code owners
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Enable require merge approvals
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Submit a merge request (MR)
</gl-link-stub>
</span>
</li>
<li>
<span>
<gl-link-stub
href="http://example.com/"
>
Run a Security scan using CI/CD
</gl-link-stub>
</span>
</li>
</ul>
`;
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
const TEST_ACTIONS = {
gitWrite: {
url: 'http://example.com/',
completed: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
},
securityScanEnabled: {
url: 'http://example.com/',
completed: false,
},
};
describe('Learn GitLab Design A', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = () => {
wrapper = extendedWrapper(
shallowMount(LearnGitlabA, {
propsData: {
actions: TEST_ACTIONS,
},
}),
);
};
it('should render the loading state', () => {
createWrapper();
expect(wrapper.element).toMatchSnapshot();
});
});
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import LearnGitlabA from '~/pages/projects/learn_gitlab/components/learn_gitlab_a.vue';
const TEST_ACTIONS = {
gitWrite: {
url: 'http://example.com/',
completed: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
},
securityScanEnabled: {
url: 'http://example.com/',
completed: false,
},
};
describe('Learn GitLab Design B', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = () => {
wrapper = extendedWrapper(
shallowMount(LearnGitlabA, {
propsData: {
actions: TEST_ACTIONS,
},
}),
);
};
it('should render the loading state', () => {
createWrapper();
expect(wrapper.element).toMatchSnapshot();
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LearnGitlabHelper do
include AfterNextHelpers
include Devise::Test::ControllerHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
let_it_be(:namespace) { project.namespace }
before do
project.add_developer(user)
allow(helper).to receive(:user).and_return(user)
allow_next_instance_of(LearnGitlab) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
OnboardingProgress.onboard(namespace)
OnboardingProgress.register(namespace, :git_write)
end
describe '.onboarding_actions_data' do
subject(:onboarding_actions_data) { helper.onboarding_actions_data(project) }
it 'has all actions' do
expect(onboarding_actions_data.keys).to contain_exactly(
:git_write,
:pipeline_created,
:merge_request_created,
:user_added,
:trial_started,
:required_mr_approvals_enabled,
:code_owners_enabled,
:security_scan_enabled
)
end
it 'sets correct path and completion status' do
expect(onboarding_actions_data[:git_write]).to eq({
url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:git_write]),
completed: true
})
expect(onboarding_actions_data[:pipeline_created]).to eq({
url: project_issue_url(project, LearnGitlabHelper::ACTION_ISSUE_IDS[:pipeline_created]),
completed: false
})
end
end
describe '.learn_gitlab_experiment_enabled?' do
using RSpec::Parameterized::TableSyntax
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject { helper.learn_gitlab_experiment_enabled?(project) }
where(:experiment_a, :experiment_b, :onboarding, :learn_gitlab_available, :result) do
true | false | true | true | true
false | true | true | true | true
false | false | true | true | false
true | true | true | false | false
true | true | false | true | false
end
with_them do
before do
stub_experiment_for_subject(learn_gitlab_a: experiment_a, learn_gitlab_b: experiment_b)
allow(OnboardingProgress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(LearnGitlab, user).to receive(:available?).and_return(learn_gitlab_available)
end
context 'when signed in' do
before do
sign_in(user)
end
it { is_expected.to eq(result) }
end
context 'when not signed in' do
it { is_expected.to eq(false) }
end
end
end
end
...@@ -4,6 +4,7 @@ require 'spec_helper' ...@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ProjectsHelper do RSpec.describe ProjectsHelper do
include ProjectForksHelper include ProjectForksHelper
include AfterNextHelpers
let_it_be_with_reload(:project) { create(:project) } let_it_be_with_reload(:project) { create(:project) }
let_it_be_with_refind(:project_with_repo) { create(:project, :repository) } let_it_be_with_refind(:project_with_repo) { create(:project, :repository) }
...@@ -498,6 +499,20 @@ RSpec.describe ProjectsHelper do ...@@ -498,6 +499,20 @@ RSpec.describe ProjectsHelper do
it { is_expected.not_to include(:confluence) } it { is_expected.not_to include(:confluence) }
it { is_expected.to include(:wiki) } it { is_expected.to include(:wiki) }
end end
context 'learn gitlab experiment' do
context 'when it is enabled' do
before do
expect(helper).to receive(:learn_gitlab_experiment_enabled?).with(project).and_return(true)
end
it { is_expected.to include(:learn_gitlab) }
end
context 'when it is not enabled' do
it { is_expected.not_to include(:learn_gitlab) }
end
end
end end
describe '#can_view_operations_tab?' do describe '#can_view_operations_tab?' do
......
...@@ -114,6 +114,22 @@ RSpec.describe OnboardingProgress do ...@@ -114,6 +114,22 @@ RSpec.describe OnboardingProgress do
end end
end end
describe '.onboarding?' do
subject(:onboarding?) { described_class.onboarding?(namespace) }
context 'when onboarded' do
before do
described_class.onboard(namespace)
end
it { is_expected.to eq true }
end
context 'when not onboarding' do
it { is_expected.to eq false }
end
end
describe '.register' do describe '.register' do
subject(:register_action) { described_class.register(namespace, action) } subject(:register_action) { described_class.register(namespace, action) }
......
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