Commit 8486ee1e authored by Alex Buijs's avatar Alex Buijs

Add runners availability section to the pipeline zeostate page

As an experiment to drive free to paid conversion.
parent dcfdc833
import { s__ } from '~/locale';
// Values for CI_CONFIG_STATUS_* comes from lint graphQL
export const CI_CONFIG_STATUS_INVALID = 'INVALID';
export const CI_CONFIG_STATUS_VALID = 'VALID';
......@@ -62,3 +64,45 @@ export const TEMPLATE_REPOSITORY_URL =
'https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates';
export const COMMIT_SHA_POLL_INTERVAL = 1000;
export const RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME = 'runners_availability_section';
export const RUNNERS_SETTINGS_LINK_CLICKED_EVENT = 'runners_settings_link_clicked';
export const RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT = 'runners_documentation_link_clicked';
export const RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT = 'runners_settings_button_clicked';
export const I18N = {
title: s__('Pipelines|Get started with GitLab CI/CD'),
runners: {
title: s__('Pipelines|Runners are available to run your jobs now'),
subtitle: s__(
'Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners.',
),
},
noRunners: {
title: s__('Pipelines|No runners detected'),
subtitle: s__(
'Pipelines|A GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. Install GitLab Runner and register your own runners to get started with CI/CD.',
),
cta: s__('Pipelines|Install GitLab Runner'),
},
learnBasics: {
title: s__('Pipelines|Learn the basics of pipelines and .yml files'),
subtitle: s__(
'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.',
),
gettingStarted: {
title: s__('Pipelines|"Hello world" with GitLab CI'),
description: s__(
'Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a "Hello world" script to see how it runs, explore how CI/CD works.',
),
cta: s__('Pipelines|Try test template'),
},
},
templates: {
title: s__('Pipelines|Ready to set up CI/CD for your project?'),
subtitle: s__(
"Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD.",
),
description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'),
cta: s__('Pipelines|Use template'),
},
};
......@@ -49,6 +49,11 @@ export default {
required: false,
default: null,
},
anyRunnersAvailable: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
ciHelpPagePath() {
......@@ -120,7 +125,11 @@ export default {
</gl-empty-state>
</template>
</gitlab-experiment>
<pipelines-ci-templates v-else-if="canSetCi" />
<pipelines-ci-templates
v-else-if="canSetCi"
:ci-runner-settings-path="ciRunnerSettingsPath"
:any-runners-available="anyRunnersAvailable"
/>
<gl-empty-state
v-else
title=""
......
......@@ -112,6 +112,11 @@ export default {
required: false,
default: null,
},
anyRunnersAvailable: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -382,6 +387,7 @@ export default {
:can-set-ci="canCreatePipeline"
:code-quality-page-path="codeQualityPagePath"
:ci-runner-settings-path="ciRunnerSettingsPath"
:any-runners-available="anyRunnersAvailable"
/>
<gl-empty-state
......
......@@ -39,6 +39,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params,
codeQualityPagePath,
ciRunnerSettingsPath,
anyRunnersAvailable,
} = el.dataset;
return new Vue({
......@@ -78,6 +79,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params: JSON.parse(params),
codeQualityPagePath,
ciRunnerSettingsPath,
anyRunnersAvailable: parseBoolean(anyRunnersAvailable),
},
});
},
......
......@@ -56,6 +56,7 @@ class Projects::PipelinesController < Projects::ApplicationController
format.html do
enable_code_quality_walkthrough_experiment
enable_ci_runner_templates_experiment
enable_runners_availability_section_experiment
end
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
......@@ -335,6 +336,18 @@ class Projects::PipelinesController < Projects::ApplicationController
end
end
def enable_runners_availability_section_experiment
return unless current_user
return unless can?(current_user, :create_pipeline, project)
return if @pipelines_count.to_i > 0
return if helpers.has_gitlab_ci?(project)
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
e.candidate {}
e.publish_to_database
end
end
def should_track_ci_cd_pipelines?
params[:chart].blank? || params[:chart] == 'pipelines'
end
......
......@@ -78,6 +78,37 @@ module Ci
pipeline.stuck?
end
def pipelines_list_data(project, list_url)
artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
data = {
endpoint: list_url,
project_id: project.id,
params: params.to_json,
artifacts_endpoint: downloadable_artifacts_project_pipeline_path(project, artifacts_endpoint_placeholder, format: :json),
artifacts_endpoint_placeholder: artifacts_endpoint_placeholder,
pipeline_schedule_url: pipeline_schedules_path(project),
empty_state_svg_path: image_path('illustrations/pipelines_empty.svg'),
error_state_svg_path: image_path('illustrations/pipelines_failed.svg'),
no_pipelines_svg_path: image_path('illustrations/pipelines_pending.svg'),
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,
pipeline_editor_path: can?(current_user, :create_pipeline, project) && project_ci_pipeline_editor_path(project),
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')
}
experiment(:runners_availability_section, namespace: project.root_ancestor) do |e|
e.candidate { data[:any_runners_available] = project.active_runners.exists?.to_s }
end
data
end
private
def warning_markdown(pipeline)
......
- page_title _('Pipelines')
- add_page_specific_style 'page_bundles/pipelines'
- add_page_specific_style 'page_bundles/ci_status'
- artifacts_endpoint_placeholder = ':pipeline_artifacts_id'
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
- list_url = project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough])
- add_page_startup_api_call list_url
#pipelines-list-vue{ data: { endpoint: list_url,
project_id: @project.id,
params: params.to_json,
"artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
"artifacts-endpoint-placeholder" => artifacts_endpoint_placeholder,
"pipeline-schedule-url" => pipeline_schedules_path(@project),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"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,
"pipeline-editor-path" => can?(current_user, :create_pipeline, @project) && project_ci_pipeline_editor_path(@project),
"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') } }
#pipelines-list-vue{ data: pipelines_list_data(@project, list_url) }
---
name: runners_availability_section
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80717
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/352850
milestone: '14.9'
type: experiment
group: group::activation
default_enabled: false
......@@ -26906,6 +26906,12 @@ msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
msgid "Pipelines|\"Hello world\" with GitLab CI"
msgstr ""
msgid "Pipelines|A GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. Install GitLab Runner and register your own runners to get started with CI/CD."
msgstr ""
msgid "Pipelines|API"
msgstr ""
......@@ -26957,7 +26963,7 @@ msgstr ""
msgid "Pipelines|Editor"
msgstr ""
msgid "Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline."
msgid "Pipelines|Get familiar with GitLab CI syntax by setting up a simple pipeline running a \"Hello world\" script to see how it runs, explore how CI/CD works."
msgstr ""
msgid "Pipelines|Get started with GitLab CI/CD"
......@@ -26966,12 +26972,18 @@ 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 ""
msgid "Pipelines|GitLab Runner is an application that works with GitLab CI/CD to run jobs in a pipeline. There are active runners available to run your jobs right now. If you prefer, you can %{settingsLinkStart}configure your runners%{settingsLinkEnd} or %{docsLinkStart}learn more%{docsLinkEnd} about runners."
msgstr ""
msgid "Pipelines|If you are unsure, please ask a project maintainer to review it for you."
msgstr ""
msgid "Pipelines|Improve code quality with GitLab CI/CD"
msgstr ""
msgid "Pipelines|Install GitLab Runner"
msgstr ""
msgid "Pipelines|Install GitLab Runners"
msgstr ""
......@@ -26984,6 +26996,9 @@ msgstr ""
msgid "Pipelines|Learn about Runners"
msgstr ""
msgid "Pipelines|Learn the basics of pipelines and .yml files"
msgstr ""
msgid "Pipelines|Lint"
msgstr ""
......@@ -26999,6 +27014,9 @@ msgstr ""
msgid "Pipelines|More Information"
msgstr ""
msgid "Pipelines|No runners detected"
msgstr ""
msgid "Pipelines|No triggers have been created yet. Add one using the form above."
msgstr ""
......@@ -27011,9 +27029,15 @@ msgstr ""
msgid "Pipelines|Project cache successfully reset."
msgstr ""
msgid "Pipelines|Ready to set up CI/CD for your project?"
msgstr ""
msgid "Pipelines|Revoke trigger"
msgstr ""
msgid "Pipelines|Runners are available to run your jobs now"
msgstr ""
msgid "Pipelines|Something went wrong while cleaning runners cache."
msgstr ""
......@@ -27077,15 +27101,12 @@ msgstr ""
msgid "Pipelines|Trigger user has insufficient permissions to project"
msgstr ""
msgid "Pipelines|Use a CI/CD template"
msgid "Pipelines|Try test template"
msgstr ""
msgid "Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works."
msgstr ""
msgid "Pipelines|Use a sample CI/CD template"
msgstr ""
msgid "Pipelines|Use a template based on your project's language or framework to get started with GitLab CI/CD."
msgstr ""
......
......@@ -299,6 +299,10 @@ RSpec.describe Projects::PipelinesController do
context 'ci_runner_templates experiment' do
it_behaves_like 'tracks assignment and records the subject', :ci_runner_templates, :namespace
end
context 'runners_availability_section experiment' do
it_behaves_like 'tracks assignment and records the subject', :runners_availability_section, :namespace
end
end
describe 'GET #show' do
......
......@@ -913,7 +913,7 @@ RSpec.describe 'Pipelines', :js do
end
it 'renders empty state' do
expect(page).to have_content 'Use a sample CI/CD template'
expect(page).to have_content 'Try test template'
end
end
end
......
import '~/commons';
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlSprintf } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { stubExperiments } from 'helpers/experimentation_helper';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
import {
RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
} from '~/pipeline_editor/constants';
const pipelineEditorPath = '/-/ci/editor';
const suggestedCiTemplates = [
......@@ -10,16 +22,20 @@ const suggestedCiTemplates = [
{ name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
];
jest.mock('~/experimentation/experiment_tracking');
describe('Pipelines CI Templates', () => {
let wrapper;
let trackingSpy;
const createWrapper = () => {
return shallowMount(PipelinesCiTemplate, {
const createWrapper = (propsData = {}, stubs = {}) => {
return shallowMountExtended(PipelinesCiTemplate, {
provide: {
pipelineEditorPath,
suggestedCiTemplates,
},
propsData,
stubs,
});
};
......@@ -28,6 +44,9 @@ describe('Pipelines CI Templates', () => {
const findTemplateLinks = () => wrapper.findAll('[data-testid="template-link"]');
const findTemplateNames = () => wrapper.findAll('[data-testid="template-name"]');
const findTemplateLogos = () => wrapper.findAll('[data-testid="template-logo"]');
const findSettingsLink = () => wrapper.findByTestId('settings-link');
const findDocumentationLink = () => wrapper.findByTestId('documentation-link');
const findSettingsButton = () => wrapper.findByTestId('settings-button');
afterEach(() => {
wrapper.destroy();
......@@ -69,7 +88,7 @@ describe('Pipelines CI Templates', () => {
it('has the description of the template', () => {
expect(findTemplateDescriptions().at(0).text()).toBe(
'CI/CD template to test and deploy your Android project.',
sprintf(I18N.templates.description, { name: 'Android' }),
);
});
......@@ -104,4 +123,73 @@ describe('Pipelines CI Templates', () => {
});
});
});
describe('when the runners_availability_section experiment is active', () => {
beforeEach(() => {
stubExperiments({ runners_availability_section: 'candidate' });
});
describe('when runners are available', () => {
beforeEach(() => {
wrapper = createWrapper({ anyRunnersAvailable: true }, { GitlabExperiment, GlSprintf });
});
it('renders the templates', () => {
expect(findTestTemplateLinks().exists()).toBe(true);
expect(findTemplateLinks().exists()).toBe(true);
});
it('show the runners available section', () => {
expect(wrapper.text()).toContain(I18N.runners.title);
});
it('tracks an event when clicking the settings link', () => {
findSettingsLink().vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith(
RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
RUNNERS_SETTINGS_LINK_CLICKED_EVENT,
);
});
it('tracks an event when clicking the documentation link', () => {
findDocumentationLink().vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith(
RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
);
});
});
describe('when runners are not available', () => {
beforeEach(() => {
wrapper = createWrapper({ anyRunnersAvailable: false }, { GitlabExperiment, GlButton });
});
it('does not render the templates', () => {
expect(findTestTemplateLinks().exists()).toBe(false);
expect(findTemplateLinks().exists()).toBe(false);
});
it('show the no runners available section', () => {
expect(wrapper.text()).toContain(I18N.noRunners.title);
});
it('tracks an event when clicking the settings button', () => {
findSettingsButton().trigger('click');
expect(ExperimentTracking).toHaveBeenCalledWith(
RUNNERS_AVAILABILITY_SECTION_EXPERIMENT_NAME,
);
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith(
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
);
});
});
});
});
......@@ -93,4 +93,63 @@ RSpec.describe Ci::PipelinesHelper do
end
end
end
describe '#pipelines_list_data' do
let_it_be(:project) { create(:project) }
subject(:data) { helper.pipelines_list_data(project, 'list_url') }
before do
allow(helper).to receive(:can?).and_return(true)
end
it 'has the expected keys' do
expect(subject.keys).to match_array([:endpoint,
:project_id,
:params,
:artifacts_endpoint,
:artifacts_endpoint_placeholder,
:pipeline_schedule_url,
:empty_state_svg_path,
:error_state_svg_path,
:no_pipelines_svg_path,
:can_create_pipeline,
:new_pipeline_path,
:ci_lint_path,
:reset_cache_path,
:has_gitlab_ci,
:pipeline_editor_path,
:suggested_ci_templates,
:code_quality_page_path,
:ci_runner_settings_path])
end
describe 'the `any_runners_available` attribute' do
subject { data[:any_runners_available] }
context 'when the `runners_availability_section` experiment variant is control' do
before do
stub_experiments(runners_availability_section: :control)
end
it { is_expected.to be_nil }
end
context 'when the `runners_availability_section` experiment variant is candidate' do
before do
stub_experiments(runners_availability_section: :candidate)
end
context 'when there are no runners' do
it { is_expected.to eq('false') }
end
context 'when there are runners' do
let!(:runner) { create(:ci_runner, :project, projects: [project]) }
it { is_expected.to eq('true') }
end
end
end
end
end
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