Commit 12c2e6a4 authored by Miguel Rincon's avatar Miguel Rincon

Merge branch 'nicolasdular/ci-templates-empty-state' into 'master'

Experiment: Show CI templates on Pipeline Empty State [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!57286
parents cba2df9e b6e21073
<script>
import { GlEmptyState } from '@gitlab/ui';
import Experiment from '~/experimentation/components/experiment.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
export default {
i18n: {
......@@ -15,6 +17,8 @@ export default {
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
Experiment,
PipelinesCiTemplates,
},
props: {
emptyStateSvgPath: {
......@@ -35,19 +39,26 @@ export default {
</script>
<template>
<div>
<gl-empty-state
v-if="canSetCi"
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.btnText"
:primary-button-link="ciHelpPagePath"
/>
<gl-empty-state
v-else
title=""
:svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription"
/>
<experiment name="pipeline_empty_state_templates">
<template #control>
<gl-empty-state
v-if="canSetCi"
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.btnText"
:primary-button-link="ciHelpPagePath"
/>
<gl-empty-state
v-else
title=""
:svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription"
/>
</template>
<template #candidate>
<pipelines-ci-templates />
</template>
</experiment>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { SUGGESTED_CI_TEMPLATES } from '../../constants';
export default {
components: {
GlButton,
},
i18n: {
title: s__('Pipelines|Try a sample CI/CD file'),
subtitle: s__(
'Pipelines|Use a sample file to implement GitLab CI/CD based on your project’s language/framework.',
),
cta: s__('Pipelines|Use template'),
description: s__(
'Pipelines|Continuous deployment template to test and deploy your %{name} project.',
),
errorMessage: s__('Pipelines|An error occurred. Please try again.'),
},
inject: ['addCiYmlPath'],
data() {
const templates = Object.keys(SUGGESTED_CI_TEMPLATES).map((key) => {
return {
name: key,
logoPath: SUGGESTED_CI_TEMPLATES[key].logoPath,
link: mergeUrlParams({ template: key }, this.addCiYmlPath),
description: sprintf(this.$options.i18n.description, { name: key }),
};
});
return {
templates,
};
},
};
</script>
<template>
<div>
<h2>{{ $options.i18n.title }}</h2>
<p>{{ $options.i18n.subtitle }}</p>
<ul class="gl-list-style-none gl-pl-0">
<li v-for="template in templates" :key="template.key">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-5 gl-pt-5"
>
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
<img
width="64"
height="64"
:src="template.logoPath"
class="gl-mr-6"
data-testid="template-logo"
/>
<div class="gl-flex-direction-row">
<strong class="gl-text-gray-800">{{ template.name }}</strong>
<p class="gl-mb-0" data-testid="template-description">{{ template.description }}</p>
</div>
</div>
<gl-button
category="primary"
variant="confirm"
:href="template.link"
data-testid="template-link"
>{{ $options.i18n.cta }}</gl-button
>
</div>
</li>
</ul>
</div>
</template>
......@@ -35,3 +35,40 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
// The keys of the Object are the same as the `key` value in the template list we get from the API.
export const SUGGESTED_CI_TEMPLATES = {
Android: { logoPath: '/assets/illustrations/logos/android.svg' },
Bash: { logoPath: '/assets/illustrations/logos/bash.svg' },
'C++': { logoPath: '/assets/illustrations/logos/c_plus_plus.svg' },
Clojure: { logoPath: '/assets/illustrations/logos/clojure.svg' },
Composer: { logoPath: '/assets/illustrations/logos/composer.svg' },
Crystal: { logoPath: '/assets/illustrations/logos/crystal.svg' },
Dart: { logoPath: '/assets/illustrations/logos/dart.svg' },
Django: { logoPath: '/assets/illustrations/logos/django.svg' },
Docker: { logoPath: '/assets/illustrations/logos/docker.svg' },
Elixir: { logoPath: '/assets/illustrations/logos/elixir.svg' },
'iOS-Fastlane': { logoPath: '/assets/illustrations/logos/fastlane.svg' },
Flutter: { logoPath: '/assets/illustrations/logos/flutter.svg' },
Go: { logoPath: '/assets/illustrations/logos/go_logo.svg' },
Gradle: { logoPath: '/assets/illustrations/logos/gradle.svg' },
Grails: { logoPath: '/assets/illustrations/logos/grails.svg' },
dotNET: { logoPath: '/assets/illustrations/logos/dotnet.svg' },
Rails: { logoPath: '/assets/illustrations/logos/rails.svg' },
Julia: { logoPath: '/assets/illustrations/logos/julia.svg' },
Laravel: { logoPath: '/assets/illustrations/logos/laravel.svg' },
Latex: { logoPath: '/assets/illustrations/logos/latex.svg' },
Maven: { logoPath: '/assets/illustrations/logos/maven.svg' },
Mono: { logoPath: '/assets/illustrations/logos/mono.svg' },
Nodejs: { logoPath: '/assets/illustrations/logos/node_js.svg' },
npm: { logoPath: '/assets/illustrations/logos/npm.svg' },
OpenShift: { logoPath: '/assets/illustrations/logos/openshift.svg' },
Packer: { logoPath: '/assets/illustrations/logos/packer.svg' },
PHP: { logoPath: '/assets/illustrations/logos/php.svg' },
Python: { logoPath: '/assets/illustrations/logos/python.svg' },
Ruby: { logoPath: '/assets/illustrations/logos/ruby.svg' },
Rust: { logoPath: '/assets/illustrations/logos/rust.svg' },
Scala: { logoPath: '/assets/illustrations/logos/scala.svg' },
Swift: { logoPath: '/assets/illustrations/logos/swift.svg' },
Terraform: { logoPath: '/assets/illustrations/logos/terraform.svg' },
};
......@@ -27,6 +27,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
errorStateSvgPath,
noPipelinesSvgPath,
newPipelinePath,
addCiYmlPath,
canCreatePipeline,
hasGitlabCi,
ciLintPath,
......@@ -37,6 +38,9 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
return new Vue({
el,
provide: {
addCiYmlPath,
},
data() {
return {
store: new PipelinesStore(),
......
......@@ -46,7 +46,17 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = limited_pipelines_count(project)
respond_to do |format|
format.html
format.html do
experiment(:pipeline_empty_state_templates, actor: current_user) do |e|
e.exclude! unless current_user
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.use {}
e.try {}
e.track(:view, value: project.namespace_id)
end
end
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
......
......@@ -135,6 +135,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
ide_edit_path(project, default_branch_or_master, 'README.md')
end
def add_ci_yml_path
add_special_file_path(file_name: ci_config_path_or_default)
end
def license_short_name
license = repository.license
license&.nickname || license&.name || 'LICENSE'
......
......@@ -14,5 +14,6 @@
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
"ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project),
"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 } }
"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 } }
---
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
......@@ -22787,6 +22787,9 @@ msgstr ""
msgid "Pipelines|API"
msgstr ""
msgid "Pipelines|An error occurred. Please try again."
msgstr ""
msgid "Pipelines|Are you sure you want to run this pipeline?"
msgstr ""
......@@ -22805,6 +22808,9 @@ msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Continuous deployment template to test and deploy your %{name} project."
msgstr ""
msgid "Pipelines|Copy trigger token"
msgstr ""
......@@ -22916,6 +22922,15 @@ msgstr ""
msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr ""
msgid "Pipelines|Try a sample CI/CD file"
msgstr ""
msgid "Pipelines|Use a sample file to implement GitLab CI/CD based on your project’s language/framework."
msgstr ""
msgid "Pipelines|Use template"
msgstr ""
msgid "Pipelines|Validating GitLab CI configuration…"
msgstr ""
......
......@@ -273,6 +273,23 @@ RSpec.describe Projects::PipelinesController do
end
end
describe 'GET #index' do
context 'pipeline_empty_state_templates experiment' do
before do
stub_application_setting(auto_devops_enabled: false)
end
it 'tracks the view', :experiment do
expect(experiment(:pipeline_empty_state_templates))
.to track(:view, value: project.namespace_id)
.with_context(actor: user)
.on_next_instance
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end
describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline, project: project) }
......
import { shallowMount } from '@vue/test-utils';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
import { SUGGESTED_CI_TEMPLATES } from '~/pipelines/constants';
const addCiYmlPath = "/-/new/master?commit_message='Add%20.gitlab-ci.yml'";
describe('Pipelines CI Templates', () => {
let wrapper;
const createWrapper = () => {
return shallowMount(PipelinesCiTemplate, {
provide: {
addCiYmlPath,
},
});
};
const findTemplateDescriptions = () => wrapper.findAll('[data-testid="template-description"]');
const findTemplateLinks = () => wrapper.findAll('[data-testid="template-link"]');
const findTemplateLogos = () => wrapper.findAll('[data-testid="template-logo"]');
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('renders templates', () => {
beforeEach(() => {
wrapper = createWrapper();
});
it('renders all suggested templates', () => {
const content = wrapper.text();
const keys = Object.keys(SUGGESTED_CI_TEMPLATES);
expect(content).toContain(...keys);
});
it('links to the correct template', () => {
expect(findTemplateLinks().at(0).attributes('href')).toEqual(
addCiYmlPath.concat('&template=Android'),
);
});
it('has the description of the template', () => {
expect(findTemplateDescriptions().at(0).text()).toEqual(
'Continuous deployment template to test and deploy your Android project.',
);
});
it('has the right logo of the template', () => {
expect(findTemplateLogos().at(0).attributes('src')).toEqual(
'/assets/illustrations/logos/android.svg',
);
});
});
});
......@@ -897,10 +897,10 @@
stylelint-declaration-strict-value "1.7.7"
stylelint-scss "3.18.0"
"@gitlab/svgs@1.185.0":
version "1.185.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.185.0.tgz#15b5c6d680b5fcfc2deb2a5decef427939e34ed7"
integrity sha512-1XIyOm8MyTZi8H0v9WVnqVDziTLH8Q5H/fKfBj+nzprHNYvJj2fvz+EV9N5luF90KTzlQf1QYCbHWe2zKLZuUw==
"@gitlab/svgs@1.188.0":
version "1.188.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.188.0.tgz#b98e279663776cf2c7bebaacc19eaab78362ccd8"
integrity sha512-7skRsKn3jzUpXwz0wOvQgVXZ2n1f7iZ5KURyUSWHe3gLMVWAPJmGBHHtdNSIq9hhsdVFPcwYBaKm26KvnkZr5A==
"@gitlab/tributejs@1.0.0":
version "1.0.0"
......
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