Commit 96dc583b authored by Rémy Coutable's avatar Rémy Coutable

Merge branch 'code-quality-walkthrough' into 'master'

Experiment: Code quality walkthrough [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58900
parents fc930922 d136dd27
......@@ -2,6 +2,7 @@
import $ from 'jquery';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import initCodeQualityWalkthrough from '~/code_quality_walkthrough';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
......@@ -38,6 +39,13 @@ const initPopovers = () => {
}
};
const initCodeQualityWalkthroughStep = () => {
const codeQualityWalkthroughEl = document.querySelector('.js-code-quality-walkthrough');
if (codeQualityWalkthroughEl) {
initCodeQualityWalkthrough(codeQualityWalkthroughEl);
}
};
export const initUploadForm = () => {
const uploadBlobForm = $('.js-upload-blob-form');
if (uploadBlobForm.length) {
......@@ -74,6 +82,7 @@ export default () => {
isMarkdown,
});
initPopovers();
initCodeQualityWalkthroughStep();
})
.catch((e) => createFlash(e));
......
<script>
import { GlPopover, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
import { STEPS, STEPSTATES } from '../constants';
import {
isWalkthroughEnabled,
getExperimentSettings,
setExperimentSettings,
track,
} from '../utils';
export default {
target: '#js-code-quality-walkthrough',
components: {
GlPopover,
GlSprintf,
GlButton,
GlAlert,
},
props: {
step: {
type: String,
required: true,
},
link: {
type: String,
required: false,
default: null,
},
},
data() {
return {
dismissedSettings: getExperimentSettings(),
currentStep: STEPSTATES[this.step],
};
},
computed: {
isPopoverVisible() {
return (
[
STEPS.commitCiFile,
STEPS.runningPipeline,
STEPS.successPipeline,
STEPS.failedPipeline,
].includes(this.step) &&
isWalkthroughEnabled() &&
!this.isDismissed
);
},
isAlertVisible() {
return this.step === STEPS.troubleshootJob && isWalkthroughEnabled() && !this.isDismissed;
},
isDismissed() {
return this.dismissedSettings[this.step];
},
title() {
return this.currentStep?.title || '';
},
body() {
return this.currentStep?.body || '';
},
buttonText() {
return this.currentStep?.buttonText || '';
},
buttonLink() {
return [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step) ? this.link : '';
},
placement() {
return this.currentStep?.placement || 'bottom';
},
offset() {
return this.currentStep?.offset || 0;
},
},
created() {
this.trackDisplayed();
},
updated() {
this.trackDisplayed();
},
methods: {
onDismiss() {
this.$set(this.dismissedSettings, this.step, true);
setExperimentSettings(this.dismissedSettings);
const action = [STEPS.successPipeline, STEPS.failedPipeline].includes(this.step)
? 'view_logs'
: 'dismissed';
this.trackAction(action);
},
trackDisplayed() {
if (this.isPopoverVisible || this.isAlertVisible) {
this.trackAction('displayed');
}
},
trackAction(action) {
track(`${this.step}_${action}`);
},
},
};
</script>
<template>
<div>
<gl-popover
v-if="isPopoverVisible"
:key="step"
:target="$options.target"
:placement="placement"
:offset="offset"
show
triggers="manual"
container="viewport"
>
<template #title>
<gl-sprintf :message="title">
<template #emoji="{ content }">
<gl-emoji class="gl-mr-2" :data-name="content"
/></template>
</gl-sprintf>
</template>
<gl-sprintf :message="body">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
<template #lineBreak>
<div class="gl-mt-5"></div>
</template>
<template #emoji="{ content }">
<gl-emoji :data-name="content" />
</template>
</gl-sprintf>
<div class="gl-mt-2 gl-text-right">
<gl-button category="tertiary" variant="link" :href="buttonLink" @click="onDismiss">
{{ buttonText }}
</gl-button>
</div>
</gl-popover>
<gl-alert
v-if="isAlertVisible"
variant="tip"
:title="title"
:primary-button-text="buttonText"
:primary-button-link="link"
class="gl-my-5"
@primaryAction="trackAction('clicked')"
@dismiss="onDismiss"
>
{{ body }}
</gl-alert>
</div>
</template>
import { s__ } from '~/locale';
export const EXPERIMENT_NAME = 'code_quality_walkthrough';
export const STEPS = {
commitCiFile: 'commit_ci_file',
runningPipeline: 'running_pipeline',
successPipeline: 'success_pipeline',
failedPipeline: 'failed_pipeline',
troubleshootJob: 'troubleshoot_job',
};
export const STEPSTATES = {
[STEPS.commitCiFile]: {
title: s__("codeQualityWalkthrough|Let's start by creating a new CI file."),
body: s__(
'codeQualityWalkthrough|To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page.',
),
buttonText: s__('codeQualityWalkthrough|Got it'),
placement: 'right',
offset: 90,
},
[STEPS.runningPipeline]: {
title: s__(
'codeQualityWalkthrough|Congrats! Your first pipeline is running %{emojiStart}zap%{emojiEnd}',
),
body: s__(
"codeQualityWalkthrough|Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!",
),
buttonText: s__('codeQualityWalkthrough|Got it'),
offset: 97,
},
[STEPS.successPipeline]: {
title: s__(
"codeQualityWalkthrough|Well done! You've just automated your code quality review. %{emojiStart}raised_hands%{emojiEnd}",
),
body: s__(
'codeQualityWalkthrough|A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs.',
),
buttonText: s__('codeQualityWalkthrough|View the logs'),
offset: 98,
},
[STEPS.failedPipeline]: {
title: s__(
"codeQualityWalkthrough|Something went wrong. %{emojiStart}thinking%{emojiEnd} Let's fix it.",
),
body: s__(
"codeQualityWalkthrough|Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it.",
),
buttonText: s__('codeQualityWalkthrough|View the logs'),
offset: 98,
},
[STEPS.troubleshootJob]: {
title: s__('codeQualityWalkthrough|Troubleshoot your code quality job'),
body: s__(
'codeQualityWalkthrough|Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation.',
),
buttonText: s__('codeQualityWalkthrough|Read the documentation'),
},
};
export const PIPELINE_STATUSES = {
running: 'running',
successWithWarnings: 'success-with-warnings',
success: 'success',
failed: 'failed',
};
import Vue from 'vue';
import Step from './components/step.vue';
export default (el) =>
new Vue({
el,
render(createElement) {
return createElement(Step, {
props: {
step: el.dataset.step,
},
});
},
});
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
import { setCookie, getCookie, getParameterByName } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
import { EXPERIMENT_NAME } from './constants';
export function getExperimentSettings() {
return JSON.parse(getCookie(EXPERIMENT_NAME) || '{}');
}
export function setExperimentSettings(settings) {
setCookie(EXPERIMENT_NAME, settings);
}
export function isWalkthroughEnabled() {
return getParameterByName(EXPERIMENT_NAME);
}
export function track(action) {
const { data } = getExperimentSettings();
if (data) {
Tracking.event(EXPERIMENT_NAME, action, {
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data,
},
});
}
}
export function startCodeQualityWalkthrough() {
const data = getExperimentData(EXPERIMENT_NAME);
if (data) {
setExperimentSettings({ data });
}
}
......@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
import { sprintf } from '~/locale';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
......@@ -32,6 +33,7 @@ export default {
GlLoadingIcon,
SharedRunner: () => import('ee_component/jobs/components/shared_runner_limit_block.vue'),
GlAlert,
CodeQualityWalkthrough,
},
directives: {
SafeHtml,
......@@ -72,6 +74,11 @@ export default {
required: false,
default: null,
},
codeQualityHelpUrl: {
type: String,
required: false,
default: null,
},
},
computed: {
...mapState([
......@@ -120,6 +127,10 @@ export default {
shouldRenderHeaderCallout() {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
shouldRenderCodeQualityWalkthrough() {
return this.job.status.group === 'failed-with-warnings';
},
},
watch: {
// Once the job log is loaded,
......@@ -216,6 +227,11 @@ export default {
>
<div v-safe-html="job.callout_message"></div>
</gl-alert>
<code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough"
step="troubleshoot_job"
:link="codeQualityHelpUrl"
/>
</header>
<!-- EO Header Section -->
......
......@@ -13,6 +13,7 @@ export default () => {
const {
artifactHelpUrl,
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
......@@ -38,6 +39,7 @@ export default () => {
props: {
artifactHelpUrl,
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
......
<script>
import { GlEmptyState } from '@gitlab/ui';
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { startCodeQualityWalkthrough, track } from '~/code_quality_walkthrough/utils';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import { getExperimentData } from '~/experimentation/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import PipelinesCiTemplates from './pipelines_ci_templates.vue';
......@@ -12,11 +14,17 @@ export default {
test, and deploy your code. Let GitLab take care of time
consuming tasks, so you can spend more time creating.`),
btnText: 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
to analyze your code quality with every push to your project.`),
codeQualityBtnText: s__('Pipelines|Add a code quality job'),
noCiDescription: s__('Pipelines|This project is not currently set up to run pipelines.'),
},
name: 'PipelinesEmptyState',
components: {
GlEmptyState,
GlButton,
GitlabExperiment,
PipelinesCiTemplates,
},
......@@ -29,36 +37,82 @@ export default {
type: Boolean,
required: true,
},
codeQualityPagePath: {
type: String,
required: false,
default: null,
},
},
computed: {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
isPipelineEmptyStateTemplatesExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
},
},
mounted() {
startCodeQualityWalkthrough();
},
methods: {
trackClick() {
track('cta_clicked');
},
},
};
</script>
<template>
<div>
<gitlab-experiment name="pipeline_empty_state_templates">
<gitlab-experiment
v-if="isPipelineEmptyStateTemplatesExperimentActive"
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"
/>
</template>
<template #candidate>
<pipelines-ci-templates />
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="canSetCi" name="code_quality_walkthrough">
<template #control>
<gl-empty-state
v-else
title=""
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription"
/>
:description="$options.i18n.description"
>
<template #actions>
<gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.btnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
<template #candidate>
<pipelines-ci-templates />
<gl-empty-state
:title="$options.i18n.codeQualityTitle"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.codeQualityDescription"
>
<template #actions>
<gl-button :href="codeQualityPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.codeQualityBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
</gitlab-experiment>
<gl-empty-state
v-else
title=""
:svg-path="emptyStateSvgPath"
:description="$options.i18n.noCiDescription"
/>
</div>
</template>
......@@ -94,6 +94,11 @@ export default {
type: Object,
required: true,
},
codeQualityPagePath: {
type: String,
required: false,
default: null,
},
},
data() {
return {
......@@ -331,6 +336,7 @@ export default {
v-else-if="stateToRender === $options.stateMap.emptyState"
:empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
:code-quality-page-path="codeQualityPagePath"
/>
<gl-empty-state
......
<script>
import CodeQualityWalkthrough from '~/code_quality_walkthrough/components/step.vue';
import { PIPELINE_STATUSES } from '~/code_quality_walkthrough/constants';
import { CHILD_VIEW } from '~/pipelines/constants';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
export default {
components: {
CodeQualityWalkthrough,
CiBadge,
},
props: {
......@@ -23,15 +26,37 @@ export default {
isChildView() {
return this.viewType === CHILD_VIEW;
},
shouldRenderCodeQualityWalkthrough() {
return Object.values(PIPELINE_STATUSES).includes(this.pipelineStatus.group);
},
codeQualityStep() {
const prefix = [PIPELINE_STATUSES.successWithWarnings, PIPELINE_STATUSES.failed].includes(
this.pipelineStatus.group,
)
? 'failed'
: this.pipelineStatus.group;
return `${prefix}_pipeline`;
},
codeQualityBuildPath() {
return this.pipeline?.details?.code_quality_build_path;
},
},
};
</script>
<template>
<div>
<ci-badge
id="js-code-quality-walkthrough"
:status="pipelineStatus"
:show-text="!isChildView"
:icon-classes="'gl-vertical-align-middle!'"
data-qa-selector="pipeline_commit_status"
/>
<code-quality-walkthrough
v-if="shouldRenderCodeQualityWalkthrough"
:step="codeQualityStep"
:link="codeQualityBuildPath"
/>
</div>
</template>
......@@ -37,6 +37,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath,
projectId,
params,
codeQualityPagePath,
} = el.dataset;
return new Vue({
......@@ -74,6 +75,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
resetCachePath,
projectId,
params: JSON.parse(params),
codeQualityPagePath,
},
});
},
......
......@@ -31,6 +31,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action :editor_variables, except: [:show, :preview, :diff]
before_action :validate_diff_params, only: :diff
before_action :set_last_commit_sha, only: [:edit, :update]
before_action :track_experiment, only: :create
track_redis_hll_event :create, :update, name: 'g_edit_by_sfe'
......@@ -46,7 +47,7 @@ class Projects::BlobController < Projects::ApplicationController
def create
create_commit(Files::CreateService, success_notice: _("The file has been successfully created."),
success_path: -> { project_blob_path(@project, File.join(@branch_name, @file_path)) },
success_path: -> { create_success_path },
failure_view: :new,
failure_path: project_new_blob_path(@project, @ref))
end
......@@ -264,4 +265,18 @@ class Projects::BlobController < Projects::ApplicationController
def visitor_id
current_user&.id
end
def create_success_path
if params[:code_quality_walkthrough]
project_pipelines_path(@project, code_quality_walkthrough: true)
else
project_blob_path(@project, File.join(@branch_name, @file_path))
end
end
def track_experiment
return unless params[:code_quality_walkthrough]
experiment(:code_quality_walkthrough, namespace: @project.root_ancestor).track(:commit_created)
end
end
......@@ -59,6 +59,17 @@ class Projects::PipelinesController < Projects::ApplicationController
e.try {}
e.track(:view, value: project.namespace_id)
end
experiment(:code_quality_walkthrough, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
e.exclude! unless can?(current_user, :create_pipeline, project)
e.exclude! unless project.root_ancestor.recent?
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.use {}
e.try {}
e.track(:view, property: project.root_ancestor.id.to_s)
end
end
format.json do
Gitlab::PollingInterval.set_header(response, interval: POLLING_INTERVAL)
......@@ -223,7 +234,7 @@ class Projects::PipelinesController < Projects::ApplicationController
PipelineSerializer
.new(project: @project, current_user: @current_user)
.with_pagination(request, response)
.represent(@pipelines, disable_coverage: true, preload: true)
.represent(@pipelines, disable_coverage: true, preload: true, code_quality_walkthrough: params[:code_quality_walkthrough].present?)
end
def render_show
......
......@@ -15,7 +15,8 @@ module Ci
"build_stage" => @build.stage,
"log_state" => '',
"build_options" => javascript_build_options,
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs')
"retry_outdated_job_docs_url" => help_page_path('ci/pipelines/settings', anchor: 'retry-outdated-jobs'),
"code_quality_help_url" => help_page_path('user/project/merge_requests/code_quality', anchor: 'troubleshooting')
}
end
......
......@@ -139,6 +139,17 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
add_special_file_path(file_name: ci_config_path_or_default)
end
def add_code_quality_ci_yml_path
add_special_file_path(
file_name: ci_config_path_or_default,
commit_message: s_("CommitMessage|Add %{file_name} and create a code quality job") % { file_name: ci_config_path_or_default },
additional_params: {
template: 'Code-Quality',
code_quality_walkthrough: true
}
)
end
def license_short_name
license = repository.license
license&.nickname || license&.name || 'LICENSE'
......@@ -468,14 +479,15 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
end
def add_special_file_path(file_name:, commit_message: nil, branch_name: nil)
def add_special_file_path(file_name:, commit_message: nil, branch_name: nil, additional_params: {})
commit_message ||= s_("CommitMessage|Add %{file_name}") % { file_name: file_name }
project_new_blob_path(
project,
default_branch_or_main,
file_name: file_name,
commit_message: commit_message,
branch_name: branch_name
branch_name: branch_name,
**additional_params
)
end
end
......
......@@ -10,6 +10,11 @@ class PipelineDetailsEntity < Ci::PipelineEntity
expose :details do
expose :manual_actions, using: BuildActionEntity
expose :scheduled_actions, using: BuildActionEntity
expose :code_quality_build_path, if: -> (_, options) { options[:code_quality_walkthrough] } do |pipeline|
next unless code_quality_build = pipeline.builds.finished.find_by_name('code_quality')
project_job_path(pipeline.project, code_quality_build, code_quality_walkthrough: true)
end
end
expose :triggered_by_pipeline, as: :triggered_by, with: TriggeredPipelineEntity
......
- breadcrumb_title _("Repository")
- page_title _("New File"), @path.presence, @ref
%h3.page-title.blob-new-page-title
New file
%h3.page-title.blob-new-page-title#js-code-quality-walkthrough
= _('New file')
.js-code-quality-walkthrough{ data: { step: 'commit_ci_file' } }
.file-editor
= form_tag(project_create_blob_path(@project, @id), method: :post, class: 'js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths(@project)) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
- if params[:code_quality_walkthrough]
= hidden_field_tag 'code_quality_walkthrough', 'true'
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: project_tree_path(@project, @id)
......
......@@ -5,7 +5,7 @@
= render_if_exists "shared/shared_runners_minutes_limit_flash_message"
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json, code_quality_walkthrough: params[:code_quality_walkthrough]),
project_id: @project.id,
params: params.to_json,
"artifacts-endpoint" => downloadable_artifacts_project_pipeline_path(@project, artifacts_endpoint_placeholder, format: :json),
......@@ -20,4 +20,5 @@
"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" => experiment_suggested_ci_templates.to_json,
"code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path } }
---
name: code_quality_walkthrough
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58900
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/327229
milestone: "13.12"
type: experiment
group: group::activation
default_enabled: false
......@@ -8089,6 +8089,9 @@ msgstr ""
msgid "CommitMessage|Add %{file_name}"
msgstr ""
msgid "CommitMessage|Add %{file_name} and create a code quality job"
msgstr ""
msgid "CommitWidget|authored"
msgstr ""
......@@ -23980,6 +23983,9 @@ msgstr ""
msgid "Pipelines|API"
msgstr ""
msgid "Pipelines|Add a code quality job"
msgstr ""
msgid "Pipelines|Are you sure you want to run this pipeline?"
msgstr ""
......@@ -24037,6 +24043,9 @@ 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|It is recommended the code is reviewed thoroughly before running this pipeline with the parent project's CI resource."
msgstr ""
......@@ -24115,6 +24124,9 @@ msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
msgid "Pipelines|To keep your codebase simple, readable, and accessible to contributors, use GitLab CI/CD to analyze your code quality with every push to your project."
msgstr ""
msgid "Pipelines|Token"
msgstr ""
......@@ -38007,6 +38019,45 @@ msgstr ""
msgid "closed issue"
msgstr ""
msgid "codeQualityWalkthrough|A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs."
msgstr ""
msgid "codeQualityWalkthrough|Congrats! Your first pipeline is running %{emojiStart}zap%{emojiEnd}"
msgstr ""
msgid "codeQualityWalkthrough|Got it"
msgstr ""
msgid "codeQualityWalkthrough|Let's start by creating a new CI file."
msgstr ""
msgid "codeQualityWalkthrough|Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation."
msgstr ""
msgid "codeQualityWalkthrough|Read the documentation"
msgstr ""
msgid "codeQualityWalkthrough|Something went wrong. %{emojiStart}thinking%{emojiEnd} Let's fix it."
msgstr ""
msgid "codeQualityWalkthrough|To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page."
msgstr ""
msgid "codeQualityWalkthrough|Troubleshoot your code quality job"
msgstr ""
msgid "codeQualityWalkthrough|View the logs"
msgstr ""
msgid "codeQualityWalkthrough|Well done! You've just automated your code quality review. %{emojiStart}raised_hands%{emojiEnd}"
msgstr ""
msgid "codeQualityWalkthrough|Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it."
msgstr ""
msgid "codeQualityWalkthrough|Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!"
msgstr ""
msgid "collect usage information"
msgstr ""
......
......@@ -464,11 +464,36 @@ RSpec.describe Projects::BlobController do
sign_in(user)
end
it_behaves_like 'tracking unique hll events' do
subject(:request) { post :create, params: default_params }
it_behaves_like 'tracking unique hll events' do
let(:target_id) { 'g_edit_by_sfe' }
let(:expected_type) { instance_of(Integer) }
end
it 'redirects to blob' do
request
expect(response).to redirect_to(project_blob_path(project, 'master/docs/EXAMPLE_FILE'))
end
context 'when code_quality_walkthrough param is present' do
let(:default_params) { super().merge(code_quality_walkthrough: true) }
it 'redirects to the pipelines page' do
request
expect(response).to redirect_to(project_pipelines_path(project, code_quality_walkthrough: true))
end
it 'creates an "commit_created" experiment tracking event' do
experiment = double(track: true)
expect(controller).to receive(:experiment).with(:code_quality_walkthrough, namespace: project.root_ancestor).and_return(experiment)
request
expect(experiment).to have_received(:track).with(:commit_created)
end
end
end
end
......@@ -288,6 +288,17 @@ RSpec.describe Projects::PipelinesController do
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
context 'code_quality_walkthrough experiment' do
it 'tracks the view', :experiment do
expect(experiment(:code_quality_walkthrough))
.to track(:view, property: project.root_ancestor.id.to_s)
.with_context(namespace: project.root_ancestor)
.on_next_instance
get :index, params: { namespace_id: project.namespace, project_id: project }
end
end
end
describe 'GET #show' do
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component commit_ci_file step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="90"
placement="right"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="To begin with code quality, we first need to create a new CI file using our code editor. We added a code quality template in the code editor to help you get started %{emojiStart}wink%{emojiEnd} .%{lineBreak}Take some time to review the template, when you are ready, use the %{strongStart}commit changes%{strongEnd} button at the bottom of the page."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href=""
icon=""
size="medium"
variant="link"
>
Got it
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component failed_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="98"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="Your job failed. No worries - this happens. Let's view the logs, and see how we can fix it."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href="/group/project/-/jobs/:id?code_quality_walkthrough=true"
icon=""
size="medium"
variant="link"
>
View the logs
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component running_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="97"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="Your pipeline can take a few minutes to run. If you enabled email notifications, you'll receive an email with your pipeline status. In the meantime, why don't you get some coffee? You earned it!"
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href=""
icon=""
size="medium"
variant="link"
>
Got it
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component success_pipeline step renders a popover 1`] = `
<div>
<gl-popover-stub
container="viewport"
cssclasses=""
offset="98"
placement="bottom"
show=""
target="#js-code-quality-walkthrough"
triggers="manual"
>
<gl-sprintf-stub
message="A code quality job will now run every time you or your team members commit changes to your project. You can view the results of the code quality job in the job logs."
/>
<div
class="gl-mt-2 gl-text-right"
>
<gl-button-stub
buttontextclasses=""
category="tertiary"
href="/group/project/-/jobs/:id?code_quality_walkthrough=true"
icon=""
size="medium"
variant="link"
>
View the logs
</gl-button-stub>
</div>
</gl-popover-stub>
<!---->
</div>
`;
exports[`When the code_quality_walkthrough URL parameter is present Code Quality Walkthrough Step component troubleshoot_job step renders an alert 1`] = `
<div>
<!---->
<gl-alert-stub
class="gl-my-5"
dismissible="true"
dismisslabel="Dismiss"
primarybuttontext="Read the documentation"
secondarybuttonlink=""
secondarybuttontext=""
title="Troubleshoot your code quality job"
variant="tip"
>
Not sure how to fix your failed job? We have compiled some tips on how to troubleshoot code quality jobs in the documentation.
</gl-alert-stub>
</div>
`;
import { GlButton, GlPopover } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
import Step from '~/code_quality_walkthrough/components/step.vue';
import { EXPERIMENT_NAME, STEPS } from '~/code_quality_walkthrough/constants';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
getParameterByName: jest.fn(),
}));
let wrapper;
function factory({ step, link }) {
wrapper = shallowMount(Step, {
propsData: { step, link },
});
}
afterEach(() => {
wrapper.destroy();
});
const dummyLink = '/group/project/-/jobs/:id?code_quality_walkthrough=true';
const dummyContext = 'experiment_context';
const findButton = () => wrapper.findComponent(GlButton);
const findPopover = () => wrapper.findComponent(GlPopover);
describe('When the code_quality_walkthrough URL parameter is missing', () => {
beforeEach(() => {
getParameterByName.mockReturnValue(false);
});
it('does not render the component', () => {
factory({
step: STEPS.commitCiFile,
});
expect(findPopover().exists()).toBe(false);
});
});
describe('When the code_quality_walkthrough URL parameter is present', () => {
beforeEach(() => {
getParameterByName.mockReturnValue(true);
Cookies.set(EXPERIMENT_NAME, { data: dummyContext });
});
afterEach(() => {
Cookies.remove(EXPERIMENT_NAME);
});
describe('When mounting the component', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
factory({
step: STEPS.commitCiFile,
});
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.commitCiFile}_displayed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('When updating the component', () => {
beforeEach(() => {
factory({
step: STEPS.runningPipeline,
});
jest.spyOn(Tracking, 'event');
wrapper.setProps({ step: STEPS.successPipeline });
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.successPipeline}_displayed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('When dismissing a popover', () => {
beforeEach(() => {
factory({
step: STEPS.commitCiFile,
});
jest.spyOn(Cookies, 'set');
jest.spyOn(Tracking, 'event');
findButton().vm.$emit('click');
});
it('sets a cookie', () => {
expect(Cookies.set).toHaveBeenCalledWith(
EXPERIMENT_NAME,
{ commit_ci_file: true, data: dummyContext },
{ expires: 365 },
);
});
it('removes the popover', () => {
expect(findPopover().exists()).toBe(false);
});
it('tracks an event', () => {
expect(Tracking.event).toHaveBeenCalledWith(
EXPERIMENT_NAME,
`${STEPS.commitCiFile}_dismissed`,
{
context: {
schema: TRACKING_CONTEXT_SCHEMA,
data: dummyContext,
},
},
);
});
});
describe('Code Quality Walkthrough Step component', () => {
describe.each(Object.values(STEPS))('%s step', (step) => {
it(`renders ${step === STEPS.troubleshootJob ? 'an alert' : 'a popover'}`, () => {
const options = { step };
if ([STEPS.successPipeline, STEPS.failedPipeline].includes(step)) {
options.link = dummyLink;
}
factory(options);
expect(wrapper.element).toMatchSnapshot();
});
});
});
});
import '~/commons';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
......
......@@ -35,6 +35,7 @@ describe('Job App', () => {
const props = {
artifactHelpUrl: 'help/artifact',
deploymentHelpUrl: 'help/deployment',
codeQualityHelpPath: '/help/code_quality',
runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables',
terminalPath: 'jobs/123/terminal',
......
import '~/commons';
import { GlButton, GlEmptyState, GlFilteredSearch, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
......@@ -6,6 +7,7 @@ import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { getExperimentVariant } from '~/experimentation/utils';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
......@@ -19,6 +21,10 @@ import TablePagination from '~/vue_shared/components/pagination/table_pagination
import { stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash');
jest.mock('~/experimentation/utils', () => ({
...jest.requireActual('~/experimentation/utils'),
getExperimentVariant: jest.fn().mockReturnValue('control'),
}));
const mockProjectPath = 'twitter/flight';
const mockProjectId = '21';
......@@ -41,6 +47,7 @@ describe('Pipelines', () => {
ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
codeQualityPagePath: `${mockProjectPath}/-/new/master?commit_message=Add+.gitlab-ci.yml+and+create+a+code+quality+job&file_name=.gitlab-ci.yml&template=Code-Quality`,
};
const noPermissions = {
......@@ -551,6 +558,19 @@ describe('Pipelines', () => {
);
});
describe('when the code_quality_walkthrough experiment is active', () => {
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,
);
});
});
it('does not render filtered search', () => {
expect(findFilteredSearch().exists()).toBe(false);
});
......
import '~/commons';
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -5,11 +6,11 @@ import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mi
import PipelineOperations from '~/pipelines/components/pipelines_list/pipeline_operations.vue';
import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_triggerer.vue';
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesStatusBadge from '~/pipelines/components/pipelines_list/pipelines_status_badge.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
import eventHub from '~/pipelines/event_hub';
import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
jest.mock('~/pipelines/event_hub');
......@@ -42,7 +43,7 @@ describe('Pipelines Table', () => {
};
const findGlTable = () => wrapper.findComponent(GlTable);
const findStatusBadge = () => wrapper.findComponent(PipelinesStatusBadge);
const findStatusBadge = () => wrapper.findComponent(CiBadge);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findCommit = () => wrapper.findComponent(CommitComponent);
......
......@@ -794,6 +794,12 @@ RSpec.describe ProjectPresenter do
end
end
describe '#add_code_quality_ci_yml_path' do
subject { presenter.add_code_quality_ci_yml_path }
it { is_expected.to match(/code_quality_walkthrough=true.*template=Code-Quality/) }
end
describe 'empty_repo_upload_experiment?' do
subject { presenter.empty_repo_upload_experiment? }
......
......@@ -70,6 +70,20 @@ RSpec.describe PipelineDetailsEntity do
expect(subject[:flags][:retryable]).to eq false
end
end
it 'does not contain code_quality_build_path in details' do
expect(subject[:details]).not_to include :code_quality_build_path
end
context 'when option code_quality_walkthrough is set and pipeline is a success' do
let(:entity) do
described_class.represent(pipeline, request: request, code_quality_walkthrough: true)
end
it 'contains details.code_quality_build_path' do
expect(subject[:details]).to include :code_quality_build_path
end
end
end
context 'when pipeline is cancelable' do
......
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