Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-01-30 15:10:44 +00:00
parent 7cf8c080ed
commit 412fe7ab55
59 changed files with 211 additions and 2256 deletions

View File

@ -602,6 +602,8 @@
when: never
- <<: *if-merge-request-targeting-stable-branch
when: never
- <<: *if-merge-request-labels-pipeline-expedite
when: never
.rails:rules:predictive-default-rules:
rules:

View File

@ -7,7 +7,7 @@ Gitlab/DocUrl:
- 'app/graphql/types/merge_request_type.rb'
- 'app/graphql/types/notes/diff_position_input_type.rb'
- 'app/graphql/types/query_complexity_type.rb'
- 'app/helpers/learn_gitlab_helper.rb'
- 'ee/app/helpers/projects/learn_gitlab_helper.rb'
- 'app/models/integrations/apple_app_store.rb'
- 'app/models/integrations/microsoft_teams.rb'
- 'app/presenters/dev_ops_report/metric_presenter.rb'

View File

@ -726,6 +726,5 @@ Gitlab/StrongMemoizeAttr:
- 'lib/sidebars/groups/menus/merge_requests_menu.rb'
- 'lib/sidebars/projects/menus/analytics_menu.rb'
- 'lib/sidebars/projects/menus/issues_menu.rb'
- 'lib/sidebars/projects/menus/learn_gitlab_menu.rb'
- 'lib/unnested_in_filters/rewriter.rb'
- 'tooling/graphql/docs/helper.rb'

View File

@ -194,7 +194,6 @@ Layout/FirstHashElementIndentation:
- 'spec/frontend/fixtures/autocomplete_sources.rb'
- 'spec/graphql/types/ci/detailed_status_type_spec.rb'
- 'spec/helpers/groups/observability_helper_spec.rb'
- 'spec/helpers/learn_gitlab_helper_spec.rb'
- 'spec/helpers/projects/pages_helper_spec.rb'
- 'spec/helpers/routing/pseudonymization_helper_spec.rb'
- 'spec/initializers/rack_multipart_patch_spec.rb'

View File

@ -4041,7 +4041,6 @@ Layout/LineLength:
- 'spec/helpers/groups/group_members_helper_spec.rb'
- 'spec/helpers/groups_helper_spec.rb'
- 'spec/helpers/icons_helper_spec.rb'
- 'spec/helpers/invite_members_helper_spec.rb'
- 'spec/helpers/issuables_helper_spec.rb'
- 'spec/helpers/issues_helper_spec.rb'
- 'spec/helpers/labels_helper_spec.rb'

View File

@ -1525,7 +1525,6 @@ RSpec/ContextWording:
- 'spec/helpers/groups_helper_spec.rb'
- 'spec/helpers/ide_helper_spec.rb'
- 'spec/helpers/integrations_helper_spec.rb'
- 'spec/helpers/invite_members_helper_spec.rb'
- 'spec/helpers/jira_connect_helper_spec.rb'
- 'spec/helpers/labels_helper_spec.rb'
- 'spec/helpers/listbox_helper_spec.rb'

View File

@ -163,7 +163,6 @@ RSpec/ExpectInHook:
- 'spec/graphql/mutations/design_management/move_spec.rb'
- 'spec/helpers/commits_helper_spec.rb'
- 'spec/helpers/groups_helper_spec.rb'
- 'spec/helpers/invite_members_helper_spec.rb'
- 'spec/helpers/projects_helper_spec.rb'
- 'spec/helpers/search_helper_spec.rb'
- 'spec/helpers/users_helper_spec.rb'

View File

@ -36,7 +36,7 @@ RSpec/FactoryBot/AvoidCreate:
- 'ee/spec/helpers/ee/issuables_helper_spec.rb'
- 'ee/spec/helpers/ee/issues_helper_spec.rb'
- 'ee/spec/helpers/ee/labels_helper_spec.rb'
- 'ee/spec/helpers/ee/learn_gitlab_helper_spec.rb'
- 'ee/spec/helpers/projects/learn_gitlab_helper_spec.rb'
- 'ee/spec/helpers/ee/lock_helper_spec.rb'
- 'ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb'
- 'ee/spec/helpers/ee/namespaces_helper_spec.rb'
@ -305,7 +305,6 @@ RSpec/FactoryBot/AvoidCreate:
- 'spec/helpers/keyset_helper_spec.rb'
- 'spec/helpers/labels_helper_spec.rb'
- 'spec/helpers/lazy_image_tag_helper_spec.rb'
- 'spec/helpers/learn_gitlab_helper_spec.rb'
- 'spec/helpers/markup_helper_spec.rb'
- 'spec/helpers/members_helper_spec.rb'
- 'spec/helpers/merge_requests_helper_spec.rb'

View File

@ -584,7 +584,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/helpers/ee/invite_members_helper_spec.rb'
- 'ee/spec/helpers/ee/issues_helper_spec.rb'
- 'ee/spec/helpers/ee/labels_helper_spec.rb'
- 'ee/spec/helpers/ee/learn_gitlab_helper_spec.rb'
- 'ee/spec/helpers/ee/lock_helper_spec.rb'
- 'ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb'
- 'ee/spec/helpers/ee/namespaces_helper_spec.rb'
@ -2583,7 +2582,6 @@ RSpec/MissingFeatureCategory:
- 'ee/spec/workers/merge_trains/refresh_worker_spec.rb'
- 'ee/spec/workers/namespaces/sync_namespace_name_worker_spec.rb'
- 'ee/spec/workers/new_epic_worker_spec.rb'
- 'ee/spec/workers/onboarding/create_learn_gitlab_worker_spec.rb'
- 'ee/spec/workers/personal_access_tokens/groups/policy_worker_spec.rb'
- 'ee/spec/workers/personal_access_tokens/instance/policy_worker_spec.rb'
- 'ee/spec/workers/post_receive_spec.rb'
@ -6051,7 +6049,6 @@ RSpec/MissingFeatureCategory:
- 'spec/lib/sidebars/projects/menus/infrastructure_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/invite_team_members_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/issues_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/learn_gitlab_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/merge_requests_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/monitor_menu_spec.rb'
- 'spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb'
@ -6555,7 +6552,6 @@ RSpec/MissingFeatureCategory:
- 'spec/models/oauth_access_grant_spec.rb'
- 'spec/models/oauth_access_token_spec.rb'
- 'spec/models/onboarding/completion_spec.rb'
- 'spec/models/onboarding/learn_gitlab_spec.rb'
- 'spec/models/onboarding/progress_spec.rb'
- 'spec/models/operations/feature_flag_spec.rb'
- 'spec/models/operations/feature_flags/strategy_spec.rb'

View File

@ -1065,7 +1065,6 @@ Style/IfUnlessModifier:
- 'spec/features/projects/tree/create_file_spec.rb'
- 'spec/graphql/mutations/releases/update_spec.rb'
- 'spec/helpers/application_settings_helper_spec.rb'
- 'spec/helpers/invite_members_helper_spec.rb'
- 'spec/lib/container_registry/gitlab_api_client_spec.rb'
- 'spec/lib/gitlab/background_migration/disable_expiration_policies_linked_to_no_container_images_spec.rb'
- 'spec/lib/gitlab/config/entry/validators/nested_array_helpers_spec.rb'

View File

@ -504,7 +504,7 @@ gem 'ssh_data', '~> 1.3'
gem 'spamcheck', '~> 1.0.0'
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 15.5.2'
gem 'gitaly', '~> 15.8.0-rc1'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2'

View File

@ -199,7 +199,7 @@
{"name":"gettext_i18n_rails","version":"1.8.0","platform":"ruby","checksum":"95e5cf8440b1e08705b27f2bccb56143272c5a7a0dabcf54ea1bd701140a496f"},
{"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"},
{"name":"git","version":"1.11.0","platform":"ruby","checksum":"7e95ba4da8298a0373ef1a6862aa22007d761f3c8274b675aa787966fecea0f1"},
{"name":"gitaly","version":"15.5.2","platform":"ruby","checksum":"62babe0596a4505bf95051ea50f17160055e6cf6cacf209273691542120d7881"},
{"name":"gitaly","version":"15.8.0.pre.rc1","platform":"ruby","checksum":"9244245b602c6c903eb0e3b3629b51e888af179cbbe339269095a1ab9113dbb5"},
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
{"name":"gitlab-dangerfiles","version":"3.6.6","platform":"ruby","checksum":"cabfe23490120188a653c827a32121bdd4abf4e9e91d1754bf170dd7e93781f1"},

View File

@ -566,7 +566,7 @@ GEM
rails (>= 3.2.0)
git (1.11.0)
rchardet (~> 1.8)
gitaly (15.5.2)
gitaly (15.8.0.pre.rc1)
grpc (~> 1.0)
gitlab (4.19.0)
httparty (~> 0.20)
@ -1667,7 +1667,7 @@ DEPENDENCIES
gettext (~> 3.3)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 15.5.2)
gitaly (~> 15.8.0.pre.rc1)
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 3.6.6)
gitlab-experiment (~> 0.7.1)

View File

@ -1,15 +0,0 @@
<script>
import { s__ } from '~/locale';
export default {
name: 'IncludedInTrialIndicator',
i18n: {
trialOnly: s__('LearnGitlab|- Included in trial'),
},
};
</script>
<template>
<span class="gl-font-style-italic gl-text-gray-500" data-testid="trial-only">
{{ $options.i18n.trialOnly }}
</span>
</template>

View File

@ -1,146 +0,0 @@
<script>
import { GlProgressBar, GlSprintf, GlAlert } from '@gitlab/ui';
import eventHub from '~/invite_members/event_hub';
import { s__ } from '~/locale';
import { getCookie, removeCookie, parseBoolean } from '~/lib/utils/common_utils';
import { ACTION_LABELS, ACTION_SECTIONS, INVITE_MODAL_OPEN_COOKIE } from '../constants';
import LearnGitlabSectionCard from './learn_gitlab_section_card.vue';
export default {
components: { GlProgressBar, GlSprintf, GlAlert, LearnGitlabSectionCard },
i18n: {
title: s__('LearnGitLab|Learn GitLab'),
description: s__(
'LearnGitLab|Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.',
),
percentageCompleted: s__(`LearnGitLab|%{percentage}%{percentSymbol} completed`),
successfulInvitations: s__(
"LearnGitLab|Your team is growing! You've successfully invited new team members to the %{projectName} project.",
),
},
props: {
actions: {
required: true,
type: Object,
},
sections: {
required: true,
type: Object,
},
project: {
required: true,
type: Object,
},
},
data() {
return {
showSuccessfulInvitationsAlert: false,
actionsData: this.actions,
};
},
actionSections: Object.keys(ACTION_SECTIONS),
computed: {
maxValue() {
return Object.keys(this.actionsData).length;
},
progressValue() {
return Object.values(this.actionsData).filter((a) => a.completed).length;
},
progressPercentage() {
return Math.round((this.progressValue / this.maxValue) * 100);
},
},
mounted() {
if (this.getCookieForInviteMembers()) {
this.openInviteMembersModal('celebrate');
}
eventHub.$on('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
beforeDestroy() {
eventHub.$off('showSuccessfulInvitationsAlert', this.handleShowSuccessfulInvitationsAlert);
},
methods: {
getCookieForInviteMembers() {
const value = parseBoolean(getCookie(INVITE_MODAL_OPEN_COOKIE));
removeCookie(INVITE_MODAL_OPEN_COOKIE);
return value;
},
openInviteMembersModal(mode) {
eventHub.$emit('openModal', { mode, source: 'learn-gitlab' });
},
handleShowSuccessfulInvitationsAlert() {
this.showSuccessfulInvitationsAlert = true;
this.markActionAsCompleted('userAdded');
},
actionsFor(section) {
const actions = Object.fromEntries(
Object.entries(this.actionsData).filter(
([action]) => ACTION_LABELS[action].section === section,
),
);
return actions;
},
svgFor(section) {
return this.sections[section].svg;
},
markActionAsCompleted(completedAction) {
Object.keys(this.actionsData).forEach((action) => {
if (action === completedAction) {
this.actionsData[action].completed = true;
this.modifySidebarPercentage();
}
});
},
modifySidebarPercentage() {
const el = document.querySelector('.sidebar-top-level-items .active .count');
el.textContent = `${this.progressPercentage}%`;
},
},
};
</script>
<template>
<div>
<gl-alert
v-if="showSuccessfulInvitationsAlert"
class="gl-mt-5"
@dismiss="showSuccessfulInvitationsAlert = false"
>
<gl-sprintf :message="$options.i18n.successfulInvitations">
<template #projectName>
<strong>{{ project.name }}</strong>
</template>
</gl-sprintf>
</gl-alert>
<div class="row">
<div class="gl-mb-7 gl-ml-5">
<h1 class="gl-font-size-h1">{{ $options.i18n.title }}</h1>
<p class="gl-text-gray-700 gl-mb-0">{{ $options.i18n.description }}</p>
</div>
</div>
<div class="gl-mb-3">
<p class="gl-text-gray-500 gl-mb-2" data-testid="completion-percentage">
<gl-sprintf :message="$options.i18n.percentageCompleted">
<template #percentage>{{ progressPercentage }}</template>
<template #percentSymbol>%</template>
</gl-sprintf>
</p>
<gl-progress-bar :value="progressValue" :max="maxValue" />
</div>
<div class="row">
<div
v-for="section in $options.actionSections"
:key="section"
class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<learn-gitlab-section-card
:section="section"
:svg="svgFor(section)"
:actions="actionsFor(section)"
/>
</div>
</div>
</div>
</template>

View File

@ -1,56 +0,0 @@
<script>
import { GlCard } from '@gitlab/ui';
import { ACTION_LABELS, ACTION_SECTIONS } from '../constants';
import LearnGitlabSectionLink from './learn_gitlab_section_link.vue';
export default {
name: 'LearnGitlabSectionCard',
components: { GlCard, LearnGitlabSectionLink },
i18n: {
...ACTION_SECTIONS,
},
props: {
section: {
required: true,
type: String,
},
svg: {
required: true,
type: String,
},
actions: {
required: true,
type: Object,
},
},
computed: {
sortedActions() {
return Object.entries(this.actions).sort(
(a1, a2) => ACTION_LABELS[a1[0]].position - ACTION_LABELS[a2[0]].position,
);
},
},
};
</script>
<template>
<gl-card
class="gl-pt-0 h-100"
header-class="gl-bg-white gl-border-0 gl-pb-0"
body-class="gl-pt-0"
>
<template #header>
<img :src="svg" />
<h2 class="gl-font-lg gl-mb-3">{{ $options.i18n[section].title }}</h2>
<p class="gl-text-gray-700 gl-mb-6">{{ $options.i18n[section].description }}</p>
</template>
<template #default>
<learn-gitlab-section-link
v-for="[action, value] in sortedActions"
:key="action"
:action="action"
:value="value"
/>
</template>
</gl-card>
</template>

View File

@ -1,151 +0,0 @@
<script>
import { uniqueId } from 'lodash';
import { GlLink, GlIcon, GlButton, GlPopover, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
import GitlabExperiment from '~/experimentation/components/gitlab_experiment.vue';
import { isExperimentVariant } from '~/experimentation/utils';
import eventHub from '~/invite_members/event_hub';
import { s__, __ } from '~/locale';
import { ACTION_LABELS } from '../constants';
import IncludedInTrialIndicator from './included_in_trial_indicator.vue';
export default {
name: 'LearnGitlabSectionLink',
components: {
GlLink,
GlIcon,
GlButton,
GlPopover,
GitlabExperiment,
IncludedInTrialIndicator,
},
directives: {
GlTooltip,
},
i18n: {
contactAdmin: s__('LearnGitlab|Contact your administrator to enable this action.'),
viewAdminList: s__('LearnGitlab|View administrator list'),
watchHow: __('Watch how'),
},
props: {
action: {
required: true,
type: String,
},
value: {
required: true,
type: Object,
},
},
data() {
return {
popoverId: uniqueId('contact-admin-'),
};
},
computed: {
showInviteModalLink() {
return (
this.action === 'userAdded' && isExperimentVariant('invite_for_help_continuous_onboarding')
);
},
openInNewTab() {
return ACTION_LABELS[this.action]?.openInNewTab === true || this.value.openInNewTab === true;
},
popoverText() {
return this.value.message || this.$options.i18n.contactAdmin;
},
},
methods: {
openModal() {
eventHub.$emit('openModal', { source: 'learn_gitlab' });
},
actionLabelValue(value) {
return ACTION_LABELS[this.action][value];
},
},
};
</script>
<template>
<div class="gl-mb-4">
<div class="flex align-items-center">
<span v-if="value.completed" class="gl-text-green-500">
<gl-icon name="check-circle-filled" :size="16" data-testid="completed-icon" />
{{ actionLabelValue('title') }}
<included-in-trial-indicator v-if="actionLabelValue('trialRequired')" />
</span>
<div v-else-if="showInviteModalLink">
<gl-link
data-track-action="click_link"
:data-track-label="actionLabelValue('trackLabel')"
data-track-property="Growth::Activation::Experiment::InviteForHelpContinuousOnboarding"
data-testid="invite-for-help-continuous-onboarding-experiment-link"
@click="openModal"
>{{ actionLabelValue('title') }}</gl-link
>
<included-in-trial-indicator v-if="actionLabelValue('trialRequired')" />
</div>
<div v-else-if="value.enabled">
<gl-link
:target="openInNewTab ? '_blank' : '_self'"
:href="value.url"
data-testid="uncompleted-learn-gitlab-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-track-action="click_link"
:data-track-label="actionLabelValue('trackLabel')"
>{{ actionLabelValue('title') }}</gl-link
>
<included-in-trial-indicator v-if="actionLabelValue('trialRequired')" />
</div>
<template v-else>
<div data-testid="disabled-learn-gitlab-link">{{ actionLabelValue('title') }}</div>
<gl-button
:id="popoverId"
category="tertiary"
icon="question-o"
class="ml-auto"
:aria-label="popoverText"
size="small"
data-testid="contact-admin-popover-trigger"
/>
<gl-popover
:target="popoverId"
placement="top"
triggers="hover focus"
data-testid="contact-admin-popover"
>
<p>{{ popoverText }}</p>
<gl-link
:href="value.url"
class="font-size-inherit"
data-testid="view-administrator-link-text"
>
{{ $options.i18n.viewAdminList }}
</gl-link>
</gl-popover>
</template>
<gitlab-experiment name="video_tutorials_continuous_onboarding">
<template #control></template>
<template #candidate>
<gl-button
v-if="actionLabelValue('videoTutorial')"
v-gl-tooltip
category="tertiary"
icon="live-preview"
:title="$options.i18n.watchHow"
:aria-label="$options.i18n.watchHow"
:href="actionLabelValue('videoTutorial')"
target="_blank"
class="ml-auto"
size="small"
data-testid="video-tutorial-link"
data-track-action="click_video_link"
:data-track-label="actionLabelValue('trackLabel')"
data-track-property="Growth::Conversion::Experiment::LearnGitLab"
data-track-experiment="video_tutorials_continuous_onboarding"
/>
</template>
</gitlab-experiment>
</div>
</div>
</template>

View File

@ -1,133 +0,0 @@
import { s__ } from '~/locale';
export const ACTION_LABELS = {
gitWrite: {
title: s__('LearnGitLab|Create a repository'),
actionLabel: s__('LearnGitLab|Create a repository'),
description: s__('LearnGitLab|Create or import your first repository into your new project.'),
trackLabel: 'create_a_repository',
section: 'workspace',
position: 1,
},
userAdded: {
title: s__('LearnGitLab|Invite your colleagues'),
actionLabel: s__('LearnGitLab|Invite your colleagues'),
description: s__(
'LearnGitLab|GitLab works best as a team. Invite your colleague to enjoy all features.',
),
trackLabel: 'invite_your_colleagues',
section: 'workspace',
position: 0,
},
pipelineCreated: {
title: s__("LearnGitLab|Set up your first project's CI/CD"),
actionLabel: s__('LearnGitLab|Set up CI/CD'),
description: s__('LearnGitLab|Save time by automating your integration and deployment tasks.'),
trackLabel: 'set_up_your_first_project_s_ci_cd',
section: 'workspace',
position: 2,
},
trialStarted: {
title: s__('LearnGitLab|Start a free trial of GitLab Ultimate'),
actionLabel: s__('LearnGitLab|Try GitLab Ultimate for free'),
description: s__('LearnGitLab|Try all GitLab features for 30 days, no credit card required.'),
trackLabel: 'start_a_free_trial_of_gitlab_ultimate',
section: 'workspace',
position: 3,
openInNewTab: true,
},
codeOwnersEnabled: {
title: s__('LearnGitLab|Add code owners'),
actionLabel: s__('LearnGitLab|Add code owners'),
description: s__(
'LearnGitLab|Prevent unexpected changes to important assets by assigning ownership of files and paths.',
),
trackLabel: 'add_code_owners',
trialRequired: true,
section: 'workspace',
position: 4,
openInNewTab: true,
videoTutorial: 'https://vimeo.com/670896787',
},
requiredMrApprovalsEnabled: {
title: s__('LearnGitLab|Enable require merge approvals'),
actionLabel: s__('LearnGitLab|Enable require merge approvals'),
description: s__('LearnGitLab|Route code reviews to the right reviewers, every time.'),
trackLabel: 'enable_require_merge_approvals',
trialRequired: true,
section: 'workspace',
position: 5,
openInNewTab: true,
videoTutorial: 'https://vimeo.com/670904904',
},
mergeRequestCreated: {
title: s__('LearnGitLab|Submit a merge request (MR)'),
actionLabel: s__('LearnGitLab|Submit a merge request (MR)'),
description: s__('LearnGitLab|Review and edit proposed changes to source code.'),
trackLabel: 'submit_a_merge_request_mr',
section: 'plan',
position: 1,
},
issueCreated: {
title: s__('LearnGitLab|Create an issue'),
actionLabel: s__('LearnGitLab|Create an issue'),
description: s__(
'LearnGitLab|Create/import issues (tickets) to collaborate on ideas and plan work.',
),
trackLabel: 'create_an_issue',
section: 'plan',
position: 0,
},
securityScanEnabled: {
title: s__('LearnGitLab|Run a Security scan using CI/CD'),
actionLabel: s__('LearnGitLab|Run a Security scan using CI/CD'),
description: s__('LearnGitLab|Scan your code to uncover vulnerabilities before deploying.'),
trackLabel: 'run_a_security_scan_using_ci_cd',
section: 'deploy',
position: 1,
},
licenseScanningRun: {
title: s__('LearnGitLab|Scan dependencies for licenses'),
trackLabel: 'scan_dependencies_for_licenses',
trialRequired: true,
section: 'deploy',
position: 2,
},
secureDependencyScanningRun: {
title: s__('LearnGitLab|Scan dependencies for vulnerabilities'),
trackLabel: 'scan_dependencies_for_vulnerabilities',
trialRequired: true,
section: 'deploy',
position: 3,
},
secureDastRun: {
title: s__('LearnGitLab|Analyze your application for vulnerabilities with DAST'),
trackLabel: 'analyze_your_application_for_vulnerabilities_with_dast',
trialRequired: true,
section: 'deploy',
position: 4,
},
};
export const ACTION_SECTIONS = {
workspace: {
title: s__('LearnGitLab|Set up your workspace'),
description: s__(
"LearnGitLab|Complete these tasks first so you can enjoy GitLab's features to their fullest:",
),
},
plan: {
title: s__('LearnGitLab|Plan and execute'),
description: s__(
'LearnGitLab|Create a workflow for your new workspace, and learn how GitLab features work together:',
),
},
deploy: {
title: s__('LearnGitLab|Deploy'),
description: s__(
'LearnGitLab|Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:',
),
},
};
export const INVITE_MODAL_OPEN_COOKIE = 'confetti_post_signup';

View File

@ -1,31 +0,0 @@
import Vue from 'vue';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import LearnGitlab from '../components/learn_gitlab.vue';
function initLearnGitlab() {
const el = document.getElementById('js-learn-gitlab-app');
if (!el) {
return false;
}
const actions = convertObjectPropsToCamelCase(JSON.parse(el.dataset.actions));
const sections = convertObjectPropsToCamelCase(JSON.parse(el.dataset.sections));
const project = convertObjectPropsToCamelCase(JSON.parse(el.dataset.project));
return new Vue({
el,
render(createElement) {
return createElement(LearnGitlab, {
props: { actions, sections, project },
});
},
});
}
initInviteMembersModal();
initInviteMembersTrigger();
initLearnGitlab();

View File

@ -203,7 +203,7 @@ export default {
:is="linkComponent"
ref="link"
v-gl-hover-load="handlePreload"
v-gl-tooltip:tooltip-container
v-gl-tooltip="{ placement: 'left', boundary: 'viewport' }"
:title="fullPath"
:to="routerLinkTo"
:href="url"

View File

@ -1,33 +0,0 @@
# frozen_string_literal: true
module Projects
class LearnGitlabController < Projects::ApplicationController
before_action :authenticate_user!
before_action :verify_learn_gitlab_available!
before_action :enable_invite_for_help_continuous_onboarding_experiment
before_action :enable_video_tutorials_continuous_onboarding_experiment
feature_category :user_profile
urgency :low, [:index]
def index; end
private
def verify_learn_gitlab_available!
access_denied! unless helpers.learn_gitlab_enabled?(project)
end
def enable_invite_for_help_continuous_onboarding_experiment
return unless current_user.can?(:admin_group_member, project.namespace)
experiment(:invite_for_help_continuous_onboarding, namespace: project.namespace) do |e|
e.candidate {}
end
end
def enable_video_tutorials_continuous_onboarding_experiment
experiment(:video_tutorials_continuous_onboarding, namespace: project&.namespace).publish
end
end
end

View File

@ -222,7 +222,13 @@ class ProjectsController < Projects::ApplicationController
end
def housekeeping
::Repositories::HousekeepingService.new(@project, :gc).execute
task = if ::Feature.enabled?(:eager_housekeeping_on_manual_jobs, @project)
:eager
else
:gc
end
::Repositories::HousekeepingService.new(@project, task).execute
redirect_to(
project_path(@project),

View File

@ -1,6 +0,0 @@
# frozen_string_literal: true
class VideoTutorialsContinuousOnboardingExperiment < ApplicationExperiment
control {}
candidate {}
end

View File

@ -45,7 +45,7 @@ module InviteMembersHelper
full_path: source.full_path
}
if show_invite_members_for_task?(source)
if current_user && show_invite_members_for_task?(source)
dataset.merge!(
tasks_to_be_done_options: tasks_to_be_done_options.to_json,
projects: projects_for_source(source).to_json,
@ -71,11 +71,9 @@ module InviteMembersHelper
{}
end
def show_invite_members_for_task?(source)
return unless current_user
invite_for_help_continuous_onboarding = source.is_a?(Project) && experiment(:invite_for_help_continuous_onboarding, namespace: source.namespace).assigned.name == 'candidate'
params[:open_modal] == 'invite_members_for_task' || invite_for_help_continuous_onboarding
# Overridden in EE
def show_invite_members_for_task?(_source)
params[:open_modal] == 'invite_members_for_task'
end
def tasks_to_be_done_options

View File

@ -1,106 +0,0 @@
# frozen_string_literal: true
module LearnGitlabHelper
IMAGE_PATH_PLAN = "learn_gitlab/section_plan.svg"
IMAGE_PATH_DEPLOY = "learn_gitlab/section_deploy.svg"
IMAGE_PATH_WORKSPACE = "learn_gitlab/section_workspace.svg"
LICENSE_SCANNING_RUN_URL = 'https://docs.gitlab.com/ee/user/compliance/license_compliance/index.html'
def learn_gitlab_enabled?(project)
return false unless current_user
learn_gitlab_onboarding_available?(project)
end
def learn_gitlab_data(project)
{
actions: onboarding_actions_data(project).to_json,
sections: onboarding_sections_data.to_json,
project: onboarding_project_data(project).to_json
}
end
def learn_gitlab_onboarding_available?(project)
Onboarding::Progress.onboarding?(project.namespace) &&
Onboarding::LearnGitlab.new(current_user).available?
end
private
def onboarding_actions_data(project)
attributes = onboarding_progress(project).attributes.symbolize_keys
action_urls(project).to_h do |action, url|
[
action,
{
url: url,
completed: attributes[Onboarding::Progress.column_name(action)].present?,
enabled: true
}
]
end
end
def onboarding_sections_data
{
workspace: {
svg: image_path(IMAGE_PATH_WORKSPACE)
},
plan: {
svg: image_path(IMAGE_PATH_PLAN)
},
deploy: {
svg: image_path(IMAGE_PATH_DEPLOY)
}
}
end
def onboarding_project_data(project)
{ name: project.name }
end
def action_urls(project)
action_issue_urls.merge(
issue_created: project_issues_path(project),
git_write: project_path(project),
merge_request_created: project_merge_requests_path(project),
user_added: project_members_url(project),
**deploy_section_action_urls(project)
)
end
def action_issue_urls
Onboarding::Completion::ACTION_ISSUE_IDS.transform_values do |id|
project_issue_url(learn_gitlab_project, id)
end
end
def deploy_section_action_urls(project)
experiment(
:security_actions_continuous_onboarding,
namespace: project.namespace,
user: current_user,
sticky_to: current_user
) do |e|
e.control { { security_scan_enabled: project_security_configuration_path(project) } }
e.candidate do
{
license_scanning_run: LICENSE_SCANNING_RUN_URL,
secure_dependency_scanning_run: project_security_configuration_path(project, anchor: 'dependency-scanning'),
secure_dast_run: project_security_configuration_path(project, anchor: 'dast')
}
end
end.run
end
def learn_gitlab_project
@learn_gitlab_project ||= Onboarding::LearnGitlab.new(current_user).project
end
def onboarding_progress(project)
Onboarding::Progress.find_by(namespace: project.namespace) # rubocop: disable CodeReuse/ActiveRecord
end
end
LearnGitlabHelper.prepend_mod_with('LearnGitlabHelper')

View File

@ -117,7 +117,6 @@ module SidebarsHelper
{
current_user: user,
container: project,
learn_gitlab_enabled: learn_gitlab_enabled?(project),
current_ref: current_ref,
ref_type: ref_type,
jira_issues_integration: project_jira_issues_integration?,

View File

@ -1,38 +0,0 @@
# frozen_string_literal: true
module Onboarding
class LearnGitlab
PROJECT_NAME = 'Learn GitLab'
PROJECT_NAME_ULTIMATE_TRIAL = 'Learn GitLab - Ultimate trial'
BOARD_NAME = 'GitLab onboarding'
LABEL_NAME = 'Novice'
def initialize(current_user)
@current_user = current_user
end
def available?
project && board && label
end
def project
@project ||= current_user.projects.find_by_name([PROJECT_NAME, PROJECT_NAME_ULTIMATE_TRIAL])
end
def board
return unless project
@board ||= project.boards.find_by_name(BOARD_NAME)
end
def label
return unless project
@label ||= project.labels.find_by_name(LABEL_NAME)
end
private
attr_reader :current_user
end
end

View File

@ -1,8 +0,0 @@
- breadcrumb_title _("Learn GitLab")
- page_title _("Learn GitLab")
- add_page_specific_style 'page_bundles/learn_gitlab'
- data = learn_gitlab_data(@project)
= render 'projects/invite_members_modal', project: @project
#js-learn-gitlab-app{ data: data }

View File

@ -57,7 +57,7 @@ module GitGarbageCollectMethods
end
def gc?(task)
task == :gc || task == :prune
%i[gc eager prune].include?(task)
end
def try_obtain_lease(key)
@ -84,8 +84,11 @@ module GitGarbageCollectMethods
repository = resource.repository.raw_repository
client = repository.gitaly_repository_client
if task == :prune
case task
when :prune
client.prune_unreachable_objects
when :eager
client.optimize_repository(eager: true)
else
client.optimize_repository
end

View File

@ -0,0 +1,8 @@
---
name: eager_housekeeping_on_manual_jobs
introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/108970'
rollout_issue_url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/388097'
milestone: '15.9'
type: development
group: group::gitaly
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: invite_for_help_continuous_onboarding
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73846
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/345708
milestone: '14.5'
type: experiment
group: group::activation
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: video_tutorials_continuous_onboarding
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82274
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351916
milestone: '14.9'
type: experiment
group: group::acquisition
default_enabled: false

View File

@ -92,8 +92,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
get :learn_gitlab, action: :index, controller: 'learn_gitlab'
namespace :ci do
resource :lint, only: [:show, :create]
resource :pipeline_editor, only: [:show], controller: :pipeline_editor, path: 'editor'

View File

@ -24,7 +24,7 @@ works with a set of [ETL](#etl) Pipelines leveraging from the current [GitLab AP
![Simplified Component Overview](img/bulk_imports_overview_v13_7.png)
### [ETL](https://www.ibm.com/cloud/learn/etl)
### [ETL](https://www.ibm.com/topics/etl)
<!-- Direct quote from the IBM URL link -->

View File

@ -100,7 +100,7 @@ If you have any questions or need help, visit [Getting Help](https://about.gitla
communicate with the GitLab community. GitLab prefers [asynchronous communication](https://about.gitlab.com/handbook/communication/#internal-communication) over real-time communication.
We do encourage you to connect and hang out with us. GitLab has a Gitter room dedicated for [contributors](https://gitter.im/gitlab/contributors), which is bridged with our
internal Slack. We actively monitor this channel. There is also a community-run [Discord server](https://discord.gg/gitlab) where you can
internal Slack. We actively monitor this channel. There is also a community-run [Discord server](https://discord.com/invite/gitlab) where you can
find other contributors in the `#contributors` channel.
Thanks for your contribution!

View File

@ -14,7 +14,7 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
**General resources**:
- [📚 Official Introduction to GraphQL](https://graphql.org/learn/)
- [📚 Official Introduction to Apollo](https://www.apollographql.com/tutorials/fullstack-quickstart/introduction)
- [📚 Official Introduction to Apollo](https://www.apollographql.com/tutorials/fullstack-quickstart/01-introduction)
**GraphQL at GitLab**:

View File

@ -872,7 +872,13 @@ module API
authorize_admin_project
begin
::Repositories::HousekeepingService.new(user_project, :gc).execute
task = if ::Feature.enabled?(:eager_housekeeping_on_manual_jobs, user_project)
:eager
else
:gc
end
::Repositories::HousekeepingService.new(user_project, task).execute
rescue ::Repositories::HousekeepingService::LeaseTaken => error
conflict!(error.message)
end

View File

@ -24,8 +24,20 @@ module Gitlab
response.exists
end
def optimize_repository
request = Gitaly::OptimizeRepositoryRequest.new(repository: @gitaly_repo)
# Optimize the repository. By default, this will perform heuristical housekeeping in the repository, which
# is the recommended approach and will only optimize what needs to be optimized. If `eager = true`, then
# Gitaly will instead be asked to perform eager housekeeping. As a consequence the housekeeping run will take a
# _lot_ longer. It is not recommended to use eager housekeeping in general, but only in situations where it is
# explicitly required.
def optimize_repository(eager: false)
strategy = if eager
Gitaly::OptimizeRepositoryRequest::Strategy::STRATEGY_EAGER
else
Gitaly::OptimizeRepositoryRequest::Strategy::STRATEGY_HEURISTICAL
end
request = Gitaly::OptimizeRepositoryRequest.new(repository: @gitaly_repo,
strategy: strategy)
gitaly_client_call(@storage, :repository_service, :optimize_repository, request, timeout: GitalyClient.long_timeout)
end

View File

@ -1,63 +0,0 @@
# frozen_string_literal: true
module Sidebars
module Projects
module Menus
class LearnGitlabMenu < ::Sidebars::Menu
include Gitlab::Utils::StrongMemoize
override :link
def link
project_learn_gitlab_path(context.project)
end
override :active_routes
def active_routes
{ controller: :learn_gitlab }
end
override :title
def title
_('Learn GitLab')
end
override :has_pill?
def has_pill?
context.learn_gitlab_enabled
end
override :pill_count
def pill_count
strong_memoize(:pill_count) do
percentage = Onboarding::Completion.new(
context.project.namespace,
context.current_user
).percentage
"#{percentage}%"
end
end
override :extra_nav_link_html_options
def extra_nav_link_html_options
{
class: 'home',
data: {
track_label: 'learn_gitlab'
}
}
end
override :sprite_icon
def sprite_icon
'bulb'
end
override :render?
def render?
context.learn_gitlab_enabled
end
end
end
end
end

View File

@ -19,7 +19,6 @@ module Sidebars
def add_menus
add_menu(Sidebars::Projects::Menus::ProjectInformationMenu.new(context))
add_menu(Sidebars::Projects::Menus::LearnGitlabMenu.new(context))
add_menu(Sidebars::Projects::Menus::RepositoryMenu.new(context))
add_menu(Sidebars::Projects::Menus::IssuesMenu.new(context))
add_menu(Sidebars::Projects::Menus::ExternalIssueTrackerMenu.new(context))

View File

@ -1,49 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::LearnGitlabController, feature_category: :onboarding do
describe 'GET #index' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: create(:group)) }
let(:learn_gitlab_enabled) { true }
let(:params) { { namespace_id: project.namespace.to_param, project_id: project } }
subject(:action) { get :index, params: params }
before do
project.namespace.add_owner(user)
allow(controller.helpers).to receive(:learn_gitlab_enabled?).and_return(learn_gitlab_enabled)
end
context 'for unauthenticated user' do
it { is_expected.to have_gitlab_http_status(:redirect) }
end
context 'for authenticated user' do
before do
sign_in(user)
end
it { is_expected.to render_template(:index) }
context 'when learn_gitlab is not available' do
let(:learn_gitlab_enabled) { false }
it { is_expected.to have_gitlab_http_status(:not_found) }
end
context 'with invite_for_help_continuous_onboarding experiment' do
it 'tracks the assignment', :experiment do
stub_experiments(invite_for_help_continuous_onboarding: true)
expect(experiment(:invite_for_help_continuous_onboarding))
.to track(:assignment).with_context(namespace: project.namespace).on_next_instance
action
end
end
end
end
end

View File

@ -633,45 +633,67 @@ RSpec.describe ProjectsController do
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
context 'when authenticated as owner' do
before do
group.add_owner(user)
sign_in(user)
shared_examples 'a housekeeping job' do
context 'when authenticated as owner' do
before do
group.add_owner(user)
sign_in(user)
allow(Repositories::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
allow(Repositories::HousekeepingService).to receive(:new).with(project, expected_task).and_return(housekeeping)
end
it 'forces a full garbage collection' do
expect(housekeeping).to receive(:execute).once
post :housekeeping,
params: {
namespace_id: project.namespace.path,
id: project.path
}
expect(response).to have_gitlab_http_status(:found)
end
end
it 'forces a full garbage collection' do
expect(housekeeping).to receive(:execute).once
context 'when authenticated as developer' do
let(:developer) { create(:user) }
post :housekeeping,
params: {
namespace_id: project.namespace.path,
id: project.path
}
before do
group.add_developer(developer)
end
expect(response).to have_gitlab_http_status(:found)
it 'does not execute housekeeping' do
expect(housekeeping).not_to receive(:execute)
post :housekeeping,
params: {
namespace_id: project.namespace.path,
id: project.path
}
expect(response).to have_gitlab_http_status(:found)
end
end
end
context 'when authenticated as developer' do
let(:developer) { create(:user) }
context 'with :eager_housekeeping_on_manual_jobs disabled' do
let(:expected_task) { :gc }
before do
group.add_developer(developer)
stub_feature_flags(eager_housekeeping_on_manual_jobs: false)
end
it 'does not execute housekeeping' do
expect(housekeeping).not_to receive(:execute)
it_behaves_like 'a housekeeping job'
end
post :housekeeping,
params: {
namespace_id: project.namespace.path,
id: project.path
}
context 'with :eager_housekeeping_on_manual_jobs enabled' do
let(:expected_task) { :eager }
expect(response).to have_gitlab_http_status(:found)
before do
stub_feature_flags(eager_housekeeping_on_manual_jobs: true)
end
it_behaves_like 'a housekeeping job'
end
end

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe VideoTutorialsContinuousOnboardingExperiment do
it "defines a control and candidate" do
expect(subject.behaviors.keys).to match_array(%w[control candidate])
end
end

View File

@ -1,62 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab Section Card renders correctly 1`] = `
<gl-card-stub
bodyclass="gl-pt-0"
class="gl-pt-0 h-100"
footerclass=""
headerclass="gl-bg-white gl-border-0 gl-pb-0"
>
<img
src="workspace.svg"
/>
<h2
class="gl-font-lg gl-mb-3"
>
Set up your workspace
</h2>
<p
class="gl-text-gray-700 gl-mb-6"
>
Complete these tasks first so you can enjoy GitLab's features to their fullest:
</p>
<learn-gitlab-section-link-stub
action="userAdded"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="issueCreated"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="gitWrite"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="mergeRequestCreated"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="securityScanEnabled"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="pipelineCreated"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="trialStarted"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="codeOwnersEnabled"
value="[object Object]"
/>
<learn-gitlab-section-link-stub
action="requiredMrApprovalsEnabled"
value="[object Object]"
/>
</gl-card-stub>
`;

View File

@ -1,409 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Learn GitLab renders correctly 1`] = `
<div>
<!---->
<div
class="row"
>
<div
class="gl-mb-7 gl-ml-5"
>
<h1
class="gl-font-size-h1"
>
Learn GitLab
</h1>
<p
class="gl-text-gray-700 gl-mb-0"
>
Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.
</p>
</div>
</div>
<div
class="gl-mb-3"
>
<p
class="gl-text-gray-500 gl-mb-2"
data-testid="completion-percentage"
>
22% completed
</p>
<div
class="progress"
max="9"
value="2"
>
<div
aria-valuemax="9"
aria-valuemin="0"
aria-valuenow="2"
class="progress-bar"
role="progressbar"
style="width: 22.22222222222222%;"
/>
</div>
</div>
<div
class="row"
>
<div
class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
class="gl-card gl-pt-0 h-100"
>
<div
class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
<img
src="workspace.svg"
/>
<h2
class="gl-font-lg gl-mb-3"
>
Set up your workspace
</h2>
<p
class="gl-text-gray-700 gl-mb-6"
>
Complete these tasks first so you can enjoy GitLab's features to their fullest:
</p>
</div>
<div
class="gl-card-body gl-pt-0"
>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<span
class="gl-text-green-500"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
role="img"
>
<use
href="#check-circle-filled"
/>
</svg>
Invite your colleagues
<!---->
</span>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<span
class="gl-text-green-500"
>
<svg
aria-hidden="true"
class="gl-icon s16"
data-testid="completed-icon"
role="img"
>
<use
href="#check-circle-filled"
/>
</svg>
Create a repository
<!---->
</span>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="set_up_your_first_project_s_ci_cd"
href="http://example.com/"
target="_self"
>
Set up your first project's CI/CD
</a>
<!---->
</div>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="start_a_free_trial_of_gitlab_ultimate"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Start a free trial of GitLab Ultimate
</a>
<!---->
</div>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="add_code_owners"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Add code owners
</a>
<span
class="gl-font-style-italic gl-text-gray-500"
data-testid="trial-only"
>
- Included in trial
</span>
</div>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="enable_require_merge_approvals"
href="http://example.com/"
rel="noopener noreferrer"
target="_blank"
>
Enable require merge approvals
</a>
<span
class="gl-font-style-italic gl-text-gray-500"
data-testid="trial-only"
>
- Included in trial
</span>
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
<div
class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
class="gl-card gl-pt-0 h-100"
>
<div
class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
<img
src="plan.svg"
/>
<h2
class="gl-font-lg gl-mb-3"
>
Plan and execute
</h2>
<p
class="gl-text-gray-700 gl-mb-6"
>
Create a workflow for your new workspace, and learn how GitLab features work together:
</p>
</div>
<div
class="gl-card-body gl-pt-0"
>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="create_an_issue"
href="http://example.com/"
target="_self"
>
Create an issue
</a>
<!---->
</div>
<!---->
</div>
</div>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="submit_a_merge_request_mr"
href="http://example.com/"
target="_self"
>
Submit a merge request (MR)
</a>
<!---->
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
<div
class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
>
<div
class="gl-card gl-pt-0 h-100"
>
<div
class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
>
<img
src="deploy.svg"
/>
<h2
class="gl-font-lg gl-mb-3"
>
Deploy
</h2>
<p
class="gl-text-gray-700 gl-mb-6"
>
Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
</p>
</div>
<div
class="gl-card-body gl-pt-0"
>
<div
class="gl-mb-4"
>
<div
class="flex align-items-center"
>
<div>
<a
class="gl-link"
data-qa-selector="uncompleted_learn_gitlab_link"
data-testid="uncompleted-learn-gitlab-link"
data-track-action="click_link"
data-track-label="run_a_security_scan_using_ci_cd"
href="https://docs.gitlab.com/ee/foobar/"
rel="noopener noreferrer"
target="_blank"
>
Run a Security scan using CI/CD
</a>
<!---->
</div>
<!---->
</div>
</div>
</div>
<!---->
</div>
</div>
</div>
</div>
`;

View File

@ -1,27 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue';
import { testActions } from './mock_data';
const defaultSection = 'workspace';
const testImage = 'workspace.svg';
describe('Learn GitLab Section Card', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = () => {
wrapper = shallowMount(LearnGitlabSectionCard, {
propsData: { section: defaultSection, actions: testActions, svg: testImage },
});
};
it('renders correctly', () => {
createWrapper({ completed: false });
expect(wrapper.element).toMatchSnapshot();
});
});

View File

@ -1,233 +0,0 @@
import { GlPopover, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubExperiments } from 'helpers/experimentation_helper';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
import eventHub from '~/invite_members/event_hub';
import LearnGitlabSectionLink from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue';
import { ACTION_LABELS } from '~/pages/projects/learn_gitlab/constants';
const defaultAction = 'gitWrite';
const defaultProps = {
title: 'Create Repository',
description: 'Some description',
url: 'https://example.com',
completed: false,
enabled: true,
};
const openInNewTabProps = {
url: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/',
openInNewTab: true,
};
describe('Learn GitLab Section Link', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const createWrapper = (action = defaultAction, props = {}) => {
wrapper = extendedWrapper(
mount(LearnGitlabSectionLink, {
propsData: { action, value: { ...defaultProps, ...props } },
}),
);
};
const openInviteMembesrModalLink = () =>
wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
const findDisabledLink = () => wrapper.findByTestId('disabled-learn-gitlab-link');
const findPopoverTrigger = () => wrapper.findByTestId('contact-admin-popover-trigger');
const findPopover = () => wrapper.findComponent(GlPopover);
const findPopoverLink = () => findPopover().findComponent(GlLink);
const videoTutorialLink = () => wrapper.find('[data-testid="video-tutorial-link"]');
it('renders no icon when not completed', () => {
createWrapper(undefined, { completed: false });
expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(false);
});
it('renders the completion icon when completed', () => {
createWrapper(undefined, { completed: true });
expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(true);
});
it('renders no trial only when it is not required', () => {
createWrapper();
expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(false);
});
it('renders trial only when trial is required', () => {
createWrapper('codeOwnersEnabled');
expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
});
describe('disabled links', () => {
beforeEach(() => {
createWrapper('trialStarted', { enabled: false });
});
it('renders text without a link', () => {
expect(findDisabledLink().exists()).toBe(true);
expect(findDisabledLink().text()).toBe(ACTION_LABELS.trialStarted.title);
expect(findDisabledLink().attributes('href')).toBeUndefined();
});
it('renders a popover trigger with question icon', () => {
expect(findPopoverTrigger().exists()).toBe(true);
expect(findPopoverTrigger().props('icon')).toBe('question-o');
expect(findPopoverTrigger().attributes('aria-label')).toBe(
LearnGitlabSectionLink.i18n.contactAdmin,
);
});
it('renders a popover', () => {
expect(findPopoverTrigger().attributes('id')).toBe(findPopover().props('target'));
expect(findPopover().props()).toMatchObject({
placement: 'top',
triggers: 'hover focus',
});
});
it('renders default disabled message', () => {
expect(findPopover().text()).toContain(LearnGitlabSectionLink.i18n.contactAdmin);
});
it('renders custom disabled message if provided', () => {
createWrapper('trialStarted', { enabled: false, message: 'Custom message' });
expect(findPopover().text()).toContain('Custom message');
});
it('renders a link inside the popover', () => {
expect(findPopoverLink().exists()).toBe(true);
expect(findPopoverLink().attributes('href')).toBe(defaultProps.url);
});
});
describe('links marked with openInNewTab', () => {
beforeEach(() => {
createWrapper('securityScanEnabled', openInNewTabProps);
});
it('renders links with blank target', () => {
const linkElement = findUncompletedLink();
expect(linkElement.exists()).toBe(true);
expect(linkElement.attributes('target')).toEqual('_blank');
});
it('tracks the click', () => {
const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
findUncompletedLink().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
label: 'run_a_security_scan_using_ci_cd',
});
unmockTracking();
});
});
describe('rendering a link to open the invite_members modal instead of a regular link', () => {
it.each`
action | experimentVariant | showModal
${'userAdded'} | ${'candidate'} | ${true}
${'userAdded'} | ${'control'} | ${false}
${defaultAction} | ${'candidate'} | ${false}
${defaultAction} | ${'control'} | ${false}
`(
'when the invite_for_help_continuous_onboarding experiment has variant: $experimentVariant and action is $action, the modal link is shown: $showModal',
({ action, experimentVariant, showModal }) => {
stubExperiments({ invite_for_help_continuous_onboarding: experimentVariant });
createWrapper(action);
expect(openInviteMembesrModalLink().exists()).toBe(showModal);
},
);
});
describe('clicking the link to open the invite_members modal', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit').mockImplementation();
stubExperiments({ invite_for_help_continuous_onboarding: 'candidate' });
createWrapper('userAdded');
});
it('calls the eventHub', () => {
openInviteMembesrModalLink().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('openModal', { source: 'learn_gitlab' });
});
it('tracks the click', async () => {
const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(openInviteMembesrModalLink().element);
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
label: 'invite_your_colleagues',
property: 'Growth::Activation::Experiment::InviteForHelpContinuousOnboarding',
});
unmockTracking();
});
});
describe('video_tutorials_continuous_onboarding experiment', () => {
describe('when control', () => {
beforeEach(() => {
stubExperiments({ video_tutorials_continuous_onboarding: 'control' });
createWrapper('codeOwnersEnabled');
});
it('renders no video link', () => {
expect(videoTutorialLink().exists()).toBe(false);
});
});
describe('when candidate', () => {
beforeEach(() => {
stubExperiments({ video_tutorials_continuous_onboarding: 'candidate' });
createWrapper('codeOwnersEnabled');
});
it('renders video link with blank target', () => {
const videoLinkElement = videoTutorialLink();
expect(videoLinkElement.exists()).toBe(true);
expect(videoLinkElement.attributes('target')).toEqual('_blank');
});
it('tracks the click', () => {
const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
videoTutorialLink().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_video_link', {
label: 'add_code_owners',
property: 'Growth::Conversion::Experiment::LearnGitLab',
context: {
data: {
experiment: 'video_tutorials_continuous_onboarding',
variant: 'candidate',
},
schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
},
});
unmockTracking();
});
});
});
});

View File

@ -1,113 +0,0 @@
import { GlProgressBar, GlAlert } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Cookies from '~/lib/utils/cookies';
import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
import eventHub from '~/invite_members/event_hub';
import { INVITE_MODAL_OPEN_COOKIE } from '~/pages/projects/learn_gitlab/constants';
import { testActions, testSections, testProject } from './mock_data';
describe('Learn GitLab', () => {
let wrapper;
let sidebar;
const createWrapper = () => {
wrapper = mount(LearnGitlab, {
propsData: {
actions: testActions,
sections: testSections,
project: testProject,
},
});
};
beforeEach(() => {
sidebar = document.createElement('div');
sidebar.innerHTML = `
<div class="sidebar-top-level-items">
<div class="active">
<div class="count"></div>
</div>
</div>
`;
document.body.appendChild(sidebar);
createWrapper();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
sidebar.remove();
});
it('renders correctly', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders the progress percentage', () => {
const text = wrapper.find('[data-testid="completion-percentage"]').text();
expect(text).toBe('22% completed');
});
it('renders the progress bar with correct values', () => {
const progressBar = wrapper.findComponent(GlProgressBar);
expect(progressBar.attributes('value')).toBe('2');
expect(progressBar.attributes('max')).toBe('9');
});
describe('Invite Members Modal', () => {
let spy;
let cookieSpy;
beforeEach(() => {
spy = jest.spyOn(eventHub, '$emit');
cookieSpy = jest.spyOn(Cookies, 'remove');
});
afterEach(() => {
Cookies.remove(INVITE_MODAL_OPEN_COOKIE);
});
it('emits openModal', () => {
Cookies.set(INVITE_MODAL_OPEN_COOKIE, true);
createWrapper();
expect(spy).toHaveBeenCalledWith('openModal', {
mode: 'celebrate',
source: 'learn-gitlab',
});
expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
});
it('does not emit openModal when cookie is not set', () => {
createWrapper();
expect(spy).not.toHaveBeenCalled();
expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
});
});
describe('when the showSuccessfulInvitationsAlert event is fired', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => {
eventHub.$emit('showSuccessfulInvitationsAlert');
});
it('displays the successful invitations alert', () => {
expect(findAlert().exists()).toBe(true);
});
it('displays a message with the project name', () => {
expect(findAlert().text()).toBe(
"Your team is growing! You've successfully invited new team members to the test-project project.",
);
});
it('modifies the sidebar percentage', () => {
expect(sidebar.textContent.trim()).toBe('22%');
});
});
});

View File

@ -1,12 +0,0 @@
import { shallowMount } from '@vue/test-utils';
import IncludedInTrialIndicator from '~/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue';
describe('Learn GitLab Trial Card', () => {
it('renders correctly', () => {
const wrapper = shallowMount(IncludedInTrialIndicator);
expect(wrapper.text()).toEqual('- Included in trial');
wrapper.destroy();
});
});

View File

@ -1,73 +0,0 @@
export const testActions = {
gitWrite: {
url: 'http://example.com/',
completed: true,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
userAdded: {
url: 'http://example.com/',
completed: true,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
pipelineCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
trialStarted: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
codeOwnersEnabled: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
requiredMrApprovalsEnabled: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
mergeRequestCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
securityScanEnabled: {
url: 'https://docs.gitlab.com/ee/foobar/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
openInNewTab: true,
},
issueCreated: {
url: 'http://example.com/',
completed: false,
svg: 'http://example.com/images/illustration.svg',
enabled: true,
},
};
export const testSections = {
workspace: {
svg: 'workspace.svg',
},
deploy: {
svg: 'deploy.svg',
},
plan: {
svg: 'plan.svg',
},
};
export const testProject = {
name: 'test-project',
};

View File

@ -36,7 +36,8 @@ RSpec.describe InviteMembersHelper do
end
it 'provides the correct attributes' do
expect(helper.common_invite_group_modal_data(group, GroupMember, 'false')).to include({ groups_filter: 'descendant_groups', parent_id: group.id })
expect(helper.common_invite_group_modal_data(group, GroupMember, 'false'))
.to include({ groups_filter: 'descendant_groups', parent_id: group.id })
end
end
@ -46,7 +47,8 @@ RSpec.describe InviteMembersHelper do
end
it 'does not return filter attributes' do
expect(helper.common_invite_group_modal_data(project.group, ProjectMember, 'true').keys).not_to include(:groups_filter, :parent_id)
expect(helper.common_invite_group_modal_data(project.group, ProjectMember, 'true').keys)
.not_to include(:groups_filter, :parent_id)
end
end
end
@ -64,7 +66,7 @@ RSpec.describe InviteMembersHelper do
expect(helper.common_invite_modal_dataset(project)).to include(attributes)
end
context 'tasks_to_be_done' do
context 'with tasks_to_be_done' do
using RSpec::Parameterized::TableSyntax
subject(:output) { helper.common_invite_modal_dataset(source) }
@ -79,9 +81,7 @@ RSpec.describe InviteMembersHelper do
{ value: :issues, text: 'Create/import issues (tickets) to collaborate on ideas and plan work' }
].to_json
)
expect(output[:projects]).to eq(
[{ id: project.id, title: project.title }].to_json
)
expect(output[:projects]).to eq([{ id: project.id, title: project.title }].to_json)
expect(output[:new_project_path]).to eq(
source.is_a?(Project) ? '' : new_project_path(namespace_id: group.id)
)
@ -93,8 +93,8 @@ RSpec.describe InviteMembersHelper do
end
end
context 'inviting members for tasks' do
where(:open_modal_param_present?, :logged_in?, :expected?) do
context 'when inviting members for tasks' do
where(:open_modal_param?, :logged_in?, :expected?) do
true | true | true
true | false | false
false | true | false
@ -104,7 +104,7 @@ RSpec.describe InviteMembersHelper do
with_them do
before do
allow(helper).to receive(:current_user).and_return(developer) if logged_in?
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' }) if open_modal_param_present?
allow(helper).to receive(:params).and_return({ open_modal: 'invite_members_for_task' }) if open_modal_param?
end
context 'when the source is a project' do
@ -120,36 +120,6 @@ RSpec.describe InviteMembersHelper do
end
end
end
context 'the invite_for_help_continuous_onboarding experiment' do
where(:invite_for_help_continuous_onboarding?, :logged_in?, :expected?) do
true | true | true
true | false | false
false | true | false
false | false | false
end
with_them do
before do
allow(helper).to receive(:current_user).and_return(developer) if logged_in?
stub_experiments(invite_for_help_continuous_onboarding: :candidate) if invite_for_help_continuous_onboarding?
end
context 'when the source is a project' do
let_it_be(:source) { project }
it_behaves_like 'including the tasks to be done attributes'
end
context 'when the source is a group' do
let_it_be(:source) { group }
let(:expected?) { false }
it_behaves_like 'including the tasks to be done attributes'
end
end
end
end
end
@ -172,11 +142,9 @@ RSpec.describe InviteMembersHelper do
end
context 'when the user can not manage project members' do
before do
expect(helper).to receive(:can?).with(owner, :admin_project_member, project).and_return(false)
end
it 'returns false' do
expect(helper).to receive(:can?).with(owner, :admin_project_member, project).and_return(false)
expect(helper.can_invite_members_for_project?(project)).to eq false
end
end

View File

@ -1,162 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LearnGitlabHelper, feature_category: :onboarding do
include AfterNextHelpers
include Devise::Test::ControllerHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, name: Onboarding::LearnGitlab::PROJECT_NAME, namespace: user.namespace) }
let_it_be(:namespace) { project.namespace }
before do
allow_next_instance_of(Onboarding::LearnGitlab) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
end
Onboarding::Progress.onboard(namespace)
Onboarding::Progress.register(namespace, :git_write)
end
describe '#learn_gitlab_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_enabled?(project) }
where(:onboarding, :learn_gitlab_available, :result) do
true | true | true
true | false | false
false | true | false
end
with_them do
before do
allow(Onboarding::Progress).to receive(:onboarding?).with(project.namespace).and_return(onboarding)
allow_next(Onboarding::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
end
context 'when not signed in' do
it { is_expected.to eq(false) }
end
end
describe '#learn_gitlab_data' do
subject(:learn_gitlab_data) { helper.learn_gitlab_data(project) }
let(:onboarding_actions_data) { Gitlab::Json.parse(learn_gitlab_data[:actions]).deep_symbolize_keys }
let(:onboarding_sections_data) { Gitlab::Json.parse(learn_gitlab_data[:sections]).deep_symbolize_keys }
let(:onboarding_project_data) { Gitlab::Json.parse(learn_gitlab_data[:project]).deep_symbolize_keys }
shared_examples 'has all data' do
it 'has all actions' do
expected_keys = [
:issue_created,
:git_write,
:pipeline_created,
:merge_request_created,
:user_added,
:trial_started,
:required_mr_approvals_enabled,
:code_owners_enabled,
:security_scan_enabled
]
expect(onboarding_actions_data.keys).to contain_exactly(*expected_keys)
end
it 'has all section data', :aggregate_failures do
expect(onboarding_sections_data.keys).to contain_exactly(:deploy, :plan, :workspace)
expect(onboarding_sections_data.values.map(&:keys)).to match_array([[:svg]] * 3)
end
it 'has all project data', :aggregate_failures do
expect(onboarding_project_data.keys).to contain_exactly(:name)
expect(onboarding_project_data.values).to match_array([project.name])
end
end
it_behaves_like 'has all data'
it 'sets correct completion statuses' do
expect(onboarding_actions_data).to match({
issue_created: a_hash_including(completed: false),
git_write: a_hash_including(completed: true),
pipeline_created: a_hash_including(completed: false),
merge_request_created: a_hash_including(completed: false),
user_added: a_hash_including(completed: false),
trial_started: a_hash_including(completed: false),
required_mr_approvals_enabled: a_hash_including(completed: false),
code_owners_enabled: a_hash_including(completed: false),
security_scan_enabled: a_hash_including(completed: false)
})
end
describe 'security_actions_continuous_onboarding experiment' do
let(:base_paths) do
{
trial_started: a_hash_including(url: %r{/learn_gitlab/-/issues/2\z}),
pipeline_created: a_hash_including(url: %r{/learn_gitlab/-/issues/7\z}),
code_owners_enabled: a_hash_including(url: %r{/learn_gitlab/-/issues/10\z}),
required_mr_approvals_enabled: a_hash_including(url: %r{/learn_gitlab/-/issues/11\z}),
issue_created: a_hash_including(url: %r{/learn_gitlab/-/issues\z}),
git_write: a_hash_including(url: %r{/learn_gitlab\z}),
user_added: a_hash_including(url: %r{/learn_gitlab/-/project_members\z}),
merge_request_created: a_hash_including(url: %r{/learn_gitlab/-/merge_requests\z})
}
end
context 'when control' do
before do
stub_experiments(security_actions_continuous_onboarding: :control)
end
it 'sets correct paths' do
expect(onboarding_actions_data).to match(
base_paths.merge(
security_scan_enabled: a_hash_including(
url: %r{/learn_gitlab/-/security/configuration\z}
)
)
)
end
end
context 'when candidate' do
before do
stub_experiments(security_actions_continuous_onboarding: :candidate)
end
it 'sets correct paths' do
expect(onboarding_actions_data).to match(
base_paths.merge(
license_scanning_run: a_hash_including(
url: described_class::LICENSE_SCANNING_RUN_URL
),
secure_dependency_scanning_run: a_hash_including(
url: project_security_configuration_path(project, anchor: 'dependency-scanning')
),
secure_dast_run: a_hash_including(
url: project_security_configuration_path(project, anchor: 'dast')
)
)
)
end
end
end
end
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::GitalyClient::RepositoryService do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project) }
let_it_be(:project) { create(:project, :repository) }
let(:storage_name) { project.repository_storage }
let(:relative_path) { project.disk_path + '.git' }
let(:client) { described_class.new(project.repository) }
@ -22,13 +22,38 @@ RSpec.describe Gitlab::GitalyClient::RepositoryService do
end
describe '#optimize_repository' do
it 'sends a optimize_repository message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:optimize_repository)
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return(double(:optimize_repository))
shared_examples 'a repository optimization' do
it 'sends a optimize_repository message' do
expect_any_instance_of(Gitaly::RepositoryService::Stub)
.to receive(:optimize_repository)
.with(gitaly_request_with_params(
strategy: expected_strategy
), kind_of(Hash))
.and_call_original
client.optimize_repository
client.optimize_repository(**params)
end
end
context 'with default parameter' do
let(:params) { {} }
let(:expected_strategy) { :STRATEGY_HEURISTICAL }
it_behaves_like 'a repository optimization'
end
context 'with heuristical housekeeping strategy' do
let(:params) { { eager: false } }
let(:expected_strategy) { :STRATEGY_HEURISTICAL }
it_behaves_like 'a repository optimization'
end
context 'with eager housekeeping strategy' do
let(:params) { { eager: true } }
let(:expected_strategy) { :STRATEGY_EAGER }
it_behaves_like 'a repository optimization'
end
end

View File

@ -1,79 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::LearnGitlabMenu do
let_it_be(:project) { build(:project) }
let_it_be(:learn_gitlab_enabled) { true }
let(:context) do
Sidebars::Projects::Context.new(
current_user: nil,
container: project,
learn_gitlab_enabled: learn_gitlab_enabled
)
end
subject { described_class.new(context) }
it 'does not contain any sub menu' do
expect(subject.has_items?).to be false
end
describe '#nav_link_html_options' do
let_it_be(:data_tracking) do
{
class: 'home',
data: {
track_label: 'learn_gitlab'
}
}
end
specify do
expect(subject.nav_link_html_options).to eq(data_tracking)
end
end
describe '#render?' do
context 'when learn gitlab experiment is enabled' do
it 'returns true' do
expect(subject.render?).to eq true
end
end
context 'when learn gitlab experiment is disabled' do
let(:learn_gitlab_enabled) { false }
it 'returns false' do
expect(subject.render?).to eq false
end
end
end
describe '#has_pill?' do
context 'when learn gitlab experiment is enabled' do
it 'returns true' do
expect(subject.has_pill?).to eq true
end
end
context 'when learn gitlab experiment is disabled' do
let(:learn_gitlab_enabled) { false }
it 'returns false' do
expect(subject.has_pill?).to eq false
end
end
end
describe '#pill_count' do
it 'returns pill count' do
expect_next_instance_of(Onboarding::Completion) do |onboarding|
expect(onboarding).to receive(:percentage).and_return(20)
end
expect(subject.pill_count).to eq '20%'
end
end
end

View File

@ -1,69 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Onboarding::LearnGitlab do
let_it_be(:current_user) { create(:user) }
let_it_be(:learn_gitlab_project) { create(:project, name: described_class::PROJECT_NAME) }
let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: described_class::BOARD_NAME) }
let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: described_class::LABEL_NAME) }
before do
learn_gitlab_project.add_developer(current_user)
end
describe '#available?' do
using RSpec::Parameterized::TableSyntax
where(:project, :board, :label, :expected_result) do
nil | nil | nil | nil
nil | nil | true | nil
nil | true | nil | nil
nil | true | true | nil
true | nil | nil | nil
true | nil | true | nil
true | true | nil | nil
true | true | true | true
end
with_them do
before do
allow_next_instance_of(described_class) do |learn_gitlab|
allow(learn_gitlab).to receive(:project).and_return(project)
allow(learn_gitlab).to receive(:board).and_return(board)
allow(learn_gitlab).to receive(:label).and_return(label)
end
end
subject { described_class.new(current_user).available? }
it { is_expected.to be expected_result }
end
end
describe '#project' do
subject { described_class.new(current_user).project }
it { is_expected.to eq learn_gitlab_project }
context 'when it is created during trial signup' do
let_it_be(:learn_gitlab_project) do
create(:project, name: described_class::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial')
end
it { is_expected.to eq learn_gitlab_project }
end
end
describe '#board' do
subject { described_class.new(current_user).board }
it { is_expected.to eq learn_gitlab_board }
end
describe '#label' do
subject { described_class.new(current_user).label }
it { is_expected.to eq learn_gitlab_label }
end
end

View File

@ -4716,51 +4716,73 @@ RSpec.describe API::Projects, feature_category: :projects do
end
describe 'POST /projects/:id/housekeeping' do
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
shared_examples 'a housekeeping job' do
let(:housekeeping) { Repositories::HousekeepingService.new(project) }
before do
allow(Repositories::HousekeepingService).to receive(:new).with(project, :gc).and_return(housekeeping)
end
context 'when authenticated as owner' do
it 'starts the housekeeping process' do
expect(housekeeping).to receive(:execute).once
post api("/projects/#{project.id}/housekeeping", user)
expect(response).to have_gitlab_http_status(:created)
before do
allow(Repositories::HousekeepingService).to receive(:new).with(project, expected_task).and_return(housekeeping)
end
context 'when housekeeping lease is taken' do
it 'returns conflict' do
expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken)
context 'when authenticated as owner' do
it 'starts the housekeeping process' do
expect(housekeeping).to receive(:execute).once
post api("/projects/#{project.id}/housekeeping", user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/)
expect(response).to have_gitlab_http_status(:created)
end
context 'when housekeeping lease is taken' do
it 'returns conflict' do
expect(housekeeping).to receive(:execute).once.and_raise(Repositories::HousekeepingService::LeaseTaken)
post api("/projects/#{project.id}/housekeeping", user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']).to match(/Somebody already triggered housekeeping for this resource/)
end
end
end
context 'when authenticated as developer' do
before do
project_member
end
it 'returns forbidden error' do
post api("/projects/#{project.id}/housekeeping", user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/projects/#{project.id}/housekeeping")
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
context 'when authenticated as developer' do
context 'with :eager_housekeeping_on_manual_jobs disabled' do
let(:expected_task) { :gc }
before do
project_member
stub_feature_flags(eager_housekeeping_on_manual_jobs: false)
end
it 'returns forbidden error' do
post api("/projects/#{project.id}/housekeeping", user3)
expect(response).to have_gitlab_http_status(:forbidden)
end
it_behaves_like 'a housekeeping job'
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/projects/#{project.id}/housekeeping")
context 'with :eager_housekeeping_on_manual_jobs enabled' do
let(:expected_task) { :eager }
expect(response).to have_gitlab_http_status(:unauthorized)
before do
stub_feature_flags(eager_housekeeping_on_manual_jobs: true)
end
it_behaves_like 'a housekeeping job'
end
end

View File

@ -966,11 +966,9 @@
- './ee/spec/helpers/ee/groups/settings_helper_spec.rb'
- './ee/spec/helpers/ee/hooks_helper_spec.rb'
- './ee/spec/helpers/ee/integrations_helper_spec.rb'
- './ee/spec/helpers/ee/invite_members_helper_spec.rb'
- './ee/spec/helpers/ee/issuables_helper_spec.rb'
- './ee/spec/helpers/ee/issues_helper_spec.rb'
- './ee/spec/helpers/ee/labels_helper_spec.rb'
- './ee/spec/helpers/ee/learn_gitlab_helper_spec.rb'
- './ee/spec/helpers/ee/lock_helper_spec.rb'
- './ee/spec/helpers/ee/namespaces_helper_spec.rb'
- './ee/spec/helpers/ee/namespace_user_cap_reached_alert_helper_spec.rb'
@ -3633,7 +3631,6 @@
- './spec/experiments/ios_specific_templates_experiment_spec.rb'
- './spec/experiments/require_verification_for_namespace_creation_experiment_spec.rb'
- './spec/experiments/security_reports_mr_widget_prompt_experiment_spec.rb'
- './spec/experiments/video_tutorials_continuous_onboarding_experiment_spec.rb'
- './spec/features/abuse_report_spec.rb'
- './spec/features/action_cable_logging_spec.rb'
- './spec/features/admin/admin_abuse_reports_spec.rb'
@ -5160,7 +5157,6 @@
- './spec/helpers/import_helper_spec.rb'
- './spec/helpers/instance_configuration_helper_spec.rb'
- './spec/helpers/integrations_helper_spec.rb'
- './spec/helpers/invite_members_helper_spec.rb'
- './spec/helpers/issuables_description_templates_helper_spec.rb'
- './spec/helpers/issuables_helper_spec.rb'
- './spec/helpers/issues_helper_spec.rb'

View File

@ -147,5 +147,19 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true|
subject.perform(resource.id, 'prune', lease_key, lease_uuid)
end
end
context 'eager' do
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
specify do
expect_next_instance_of(Gitlab::GitalyClient::RepositoryService, repository.raw_repository) do |instance|
expect(instance).to receive(:optimize_repository).with(eager: true).and_call_original
end
subject.perform(resource.id, 'eager', lease_key, lease_uuid)
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe 'layouts/nav/sidebar/_project' do
RSpec.describe 'layouts/nav/sidebar/_project', feature_category: :navigation do
let_it_be_with_reload(:project) { create(:project, :repository) }
let(:user) { project.first_owner }
@ -67,19 +67,6 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
describe 'Learn GitLab' do
it 'has a link to the learn GitLab' do
allow(view).to receive(:learn_gitlab_enabled?).and_return(true)
allow_next_instance_of(Onboarding::Completion) do |onboarding|
expect(onboarding).to receive(:percentage).and_return(20)
end
render
expect(rendered).to have_link('Learn GitLab', href: project_learn_gitlab_path(project))
end
end
describe 'Repository' do
it 'has a link to the project tree path' do
render