Commit 7aefdbc4 authored by Alex Buijs's avatar Alex Buijs

Add CI/CD templates picker for empty pipelines page

This adds sample CI/CD templates for easily creating a
.gitlab-ci.yml file for your project.

Changelog: added
parent 98b1b656
......@@ -16,7 +16,6 @@ export default {
consuming tasks, so you can spend more time creating.`),
aboutRunnersBtnText: s__('Pipelines|Learn about Runners'),
installRunnersBtnText: s__('Pipelines|Install GitLab Runners'),
getStartedBtnText: s__('Pipelines|Get started with CI/CD'),
codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
readable, and accessible to contributors, use GitLab CI/CD
......@@ -55,9 +54,6 @@ export default {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
isPipelineEmptyStateTemplatesExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
},
isCodeQualityExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('code_quality_walkthrough'));
},
......@@ -81,37 +77,8 @@ export default {
</script>
<template>
<div>
<gitlab-experiment
v-if="isPipelineEmptyStateTemplatesExperimentActive"
name="pipeline_empty_state_templates"
>
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.getStartedBtnText"
:primary-button-link="ciHelpPagePath"
/>
</template>
<template #candidate>
<pipelines-ci-templates />
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
>
<template #actions>
<gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.getStartedBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
<gitlab-experiment v-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
<template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.codeQualityTitle"
......@@ -127,23 +94,7 @@ export default {
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCiRunnerTemplatesExperimentActive" name="ci_runner_templates">
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
>
<template #actions>
<gl-button
:href="ciHelpPagePath"
variant="confirm"
@click="trackCiRunnerTemplatesClick('get_started_button_clicked')"
>
{{ $options.i18n.getStartedBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
<template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.title"
......@@ -169,14 +120,7 @@ export default {
</gl-empty-state>
</template>
</gitlab-experiment>
<gl-empty-state
v-else-if="canSetCi"
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.getStartedBtnText"
:primary-button-link="ciHelpPagePath"
/>
<pipelines-ci-templates v-else-if="canSetCi" />
<gl-empty-state
v-else
title=""
......
<script>
import { GlAvatar, GlButton, GlCard, GlSprintf } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import { HELLO_WORLD_TEMPLATE_KEY } from '../../constants';
export default {
......@@ -12,6 +12,7 @@ export default {
GlCard,
GlSprintf,
},
mixins: [Tracking.mixin()],
HELLO_WORLD_TEMPLATE_KEY,
i18n: {
cta: s__('Pipelines|Use template'),
......@@ -56,10 +57,9 @@ export default {
},
methods: {
trackEvent(template) {
const tracking = new ExperimentTracking('pipeline_empty_state_templates', {
this.track('template_clicked', {
label: template,
});
tracking.event('template_clicked');
},
},
};
......
......@@ -49,7 +49,6 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format|
format.html do
enable_pipeline_empty_state_templates_experiment
enable_code_quality_walkthrough_experiment
enable_ci_runner_templates_experiment
end
......@@ -301,18 +300,6 @@ class Projects::PipelinesController < Projects::ApplicationController
params.permit(:scope, :username, :ref, :status)
end
def enable_pipeline_empty_state_templates_experiment
experiment(:pipeline_empty_state_templates, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.control {}
e.candidate {}
e.record!
end
end
def enable_code_quality_walkthrough_experiment
experiment(:code_quality_walkthrough, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
......
......@@ -30,9 +30,7 @@ module Ci
project.has_ci? && project.builds_enabled?
end
# This list of templates is for the pipeline_empty_state_templates experiment
# and will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/326299
def experiment_suggested_ci_templates
def suggested_ci_templates
[
{ name: 'Android', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/android.svg') },
{ name: 'Bash', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/bash.svg') },
......
......@@ -23,6 +23,6 @@
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project),
"has-gitlab-ci" => has_gitlab_ci?(@project).to_s,
"add-ci-yml-path" => can?(current_user, :create_pipeline, @project) && @project.present(current_user: current_user).add_ci_yml_path,
"suggested-ci-templates" => experiment_suggested_ci_templates.to_json,
"suggested-ci-templates" => suggested_ci_templates.to_json,
"code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path,
"ci-runner-settings-path" => project_settings_ci_cd_path(@project, ci_runner_templates: true, anchor: 'js-runners-settings') } }
---
name: pipeline_empty_state_templates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57286
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326299
milestone: '13.11'
type: experiment
group: group::activation
default_enabled: false
......@@ -23927,9 +23927,6 @@ msgstr ""
msgid "Pipelines|Get familiar with GitLab CI/CD syntax by starting with a simple pipeline that runs a “Hello world” script."
msgstr ""
msgid "Pipelines|Get started with CI/CD"
msgstr ""
msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating."
msgstr ""
......
......@@ -284,10 +284,6 @@ RSpec.describe Projects::PipelinesController do
subject { project.namespace }
context 'pipeline_empty_state_templates experiment' do
it_behaves_like 'tracks assignment and records the subject', :pipeline_empty_state_templates, :namespace
end
context 'code_quality_walkthrough experiment' do
it_behaves_like 'tracks assignment and records the subject', :code_quality_walkthrough, :namespace
end
......
......@@ -783,7 +783,7 @@ RSpec.describe 'Pipelines', :js do
end
it 'renders empty state' do
expect(page).to have_content 'Build with confidence'
expect(page).to have_content 'Use a sample CI/CD template'
end
end
end
......
import '~/commons';
import { mount } from '@vue/test-utils';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
const createWrapper = (props = {}) => {
wrapper = mount(EmptyState, {
provide: {
addCiYmlPath: '',
suggestedCiTemplates: [],
},
propsData: {
emptyStateSvgPath: 'foo.svg',
canSetCi: true,
......@@ -27,27 +34,8 @@ describe('Pipelines Empty State', () => {
wrapper = null;
});
it('should render empty state SVG', () => {
expect(findIllustration().attributes('src')).toBe('foo.svg');
});
it('should render empty state header', () => {
expect(wrapper.text()).toContain('Build with confidence');
});
it('should render empty state information', () => {
expect(wrapper.text()).toContain(
'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
'consuming tasks, so you can spend more time creating',
);
});
it('should render button with help path', () => {
expect(findButton().attributes('href')).toBe('/help/ci/quick_start/index.md');
});
it('should render button text', () => {
expect(findButton().text()).toBe('Get started with CI/CD');
it('should render the CI/CD templates', () => {
expect(pipelinesCiTemplates()).toExist();
});
});
......
import '~/commons';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { mockTracking } from 'helpers/tracking_helper';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
const addCiYmlPath = "/-/new/main?commit_message='Add%20.gitlab-ci.yml'";
......@@ -9,12 +10,9 @@ const suggestedCiTemplates = [
{ name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
];
jest.mock('~/experimentation/experiment_tracking');
describe('Pipelines CI Templates', () => {
let wrapper;
const GlEmoji = { template: '<img/>' };
let trackingSpy;
const createWrapper = () => {
return shallowMount(PipelinesCiTemplate, {
......@@ -22,9 +20,6 @@ describe('Pipelines CI Templates', () => {
addCiYmlPath,
suggestedCiTemplates,
},
stubs: {
GlEmoji,
},
});
};
......@@ -88,24 +83,25 @@ describe('Pipelines CI Templates', () => {
describe('tracking', () => {
beforeEach(() => {
wrapper = createWrapper();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('sends an event when template is clicked', () => {
findTemplateLinks().at(0).vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: 'Android',
});
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
it('sends an event when Hello-World template is clicked', () => {
findTestTemplateLinks().at(0).vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: 'Hello-World',
});
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
});
});
......@@ -12,6 +12,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
......@@ -82,6 +83,10 @@ describe('Pipelines', () => {
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
mount(PipelinesComponent, {
provide: {
addCiYmlPath: '',
suggestedCiTemplates: [],
},
propsData: {
store: new Store(),
projectId: mockProjectId,
......@@ -551,52 +556,74 @@ describe('Pipelines', () => {
await waitForPromises();
});
it('renders empty state', () => {
expect(findEmptyState().text()).toContain('Build with confidence');
expect(findEmptyState().text()).toContain(
'GitLab CI/CD can automatically build, test, and deploy your code.',
);
expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
expect(findEmptyState().find(GlButton).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
describe('when the code_quality_walkthrough experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'code_quality_walkthrough');
getExperimentVariant.mockReturnValue('candidate');
});
it('renders another CTA button', () => {
expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
paths.codeQualityPagePath,
);
describe('the control state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('control');
});
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
});
describe('the candidate state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders another CTA button', () => {
expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
paths.codeQualityPagePath,
);
});
});
});
describe('when the ci_runner_templates experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'ci_runner_templates');
getExperimentVariant.mockReturnValue('candidate');
});
it('renders two buttons', () => {
expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
'Install GitLab Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
paths.ciRunnerSettingsPath,
);
expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
'Learn about Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
describe('the control state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('control');
});
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
});
describe('the candidate state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders two buttons', () => {
expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
'Install GitLab Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
paths.ciRunnerSettingsPath,
);
expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
'Learn about Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
});
});
});
......
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