Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1572e2a376
commit
96135034f4
|
@ -0,0 +1,154 @@
|
|||
<script>
|
||||
import { pickBy } from 'lodash';
|
||||
import { mapActions } from 'vuex';
|
||||
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
|
||||
export default {
|
||||
i18n: {
|
||||
search: __('Search'),
|
||||
label: __('Label'),
|
||||
author: __('Author'),
|
||||
},
|
||||
components: { FilteredSearch },
|
||||
inject: ['initialFilterParams'],
|
||||
props: {
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterParams: this.initialFilterParams,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
urlParams() {
|
||||
const { authorUsername, labelName, search } = this.filterParams;
|
||||
let notParams = {};
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
|
||||
notParams = pickBy(
|
||||
{
|
||||
'not[label_name][]': this.filterParams.not.labelName,
|
||||
'not[author_username]': this.filterParams.not.authorUsername,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...notParams,
|
||||
author_username: authorUsername,
|
||||
'label_name[]': labelName,
|
||||
search,
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['performSearch']),
|
||||
handleFilter(filters) {
|
||||
this.filterParams = this.getFilterParams(filters);
|
||||
|
||||
updateHistory({
|
||||
url: setUrlParams(this.urlParams, window.location.href, true, false, true),
|
||||
title: document.title,
|
||||
replace: true,
|
||||
});
|
||||
|
||||
this.performSearch();
|
||||
},
|
||||
getFilteredSearchValue() {
|
||||
const { authorUsername, labelName, search } = this.filterParams;
|
||||
const filteredSearchValue = [];
|
||||
|
||||
if (authorUsername) {
|
||||
filteredSearchValue.push({
|
||||
type: 'author_username',
|
||||
value: { data: authorUsername, operator: '=' },
|
||||
});
|
||||
}
|
||||
|
||||
if (labelName?.length) {
|
||||
filteredSearchValue.push(
|
||||
...labelName.map((label) => ({
|
||||
type: 'label_name',
|
||||
value: { data: label, operator: '=' },
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (this.filterParams['not[authorUsername]']) {
|
||||
filteredSearchValue.push({
|
||||
type: 'author_username',
|
||||
value: { data: this.filterParams['not[authorUsername]'], operator: '!=' },
|
||||
});
|
||||
}
|
||||
|
||||
if (this.filterParams['not[labelName]']) {
|
||||
filteredSearchValue.push(
|
||||
...this.filterParams['not[labelName]'].map((label) => ({
|
||||
type: 'label_name',
|
||||
value: { data: label, operator: '!=' },
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
if (search) {
|
||||
filteredSearchValue.push(search);
|
||||
}
|
||||
|
||||
return filteredSearchValue;
|
||||
},
|
||||
getFilterParams(filters = []) {
|
||||
const notFilters = filters.filter((item) => item.value.operator === '!=');
|
||||
const equalsFilters = filters.filter((item) => item.value.operator === '=');
|
||||
|
||||
return { ...this.generateParams(equalsFilters), not: { ...this.generateParams(notFilters) } };
|
||||
},
|
||||
generateParams(filters = []) {
|
||||
const filterParams = {};
|
||||
const labels = [];
|
||||
const plainText = [];
|
||||
|
||||
filters.forEach((filter) => {
|
||||
switch (filter.type) {
|
||||
case 'author_username':
|
||||
filterParams.authorUsername = filter.value.data;
|
||||
break;
|
||||
case 'label_name':
|
||||
labels.push(filter.value.data);
|
||||
break;
|
||||
case 'filtered-search-term':
|
||||
if (filter.value.data) plainText.push(filter.value.data);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
if (labels.length) {
|
||||
filterParams.labelName = labels;
|
||||
}
|
||||
|
||||
if (plainText.length) {
|
||||
filterParams.search = plainText.join(' ');
|
||||
}
|
||||
return filterParams;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<filtered-search
|
||||
class="gl-w-full"
|
||||
namespace=""
|
||||
:tokens="tokens"
|
||||
:search-input-placeholder="$options.i18n.search"
|
||||
:initial-filter-value="getFilteredSearchValue()"
|
||||
@onFilter="handleFilter"
|
||||
/>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
mutation dismissUserCallout($input: UserCalloutCreateInput!) {
|
||||
userCalloutCreate(input: $input) {
|
||||
errors
|
||||
userCallout {
|
||||
dismissedAt
|
||||
featureName
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<script>
|
||||
import { GlAlert } from '@gitlab/ui';
|
||||
import { getAlert } from '../lib/alerts';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlAlert,
|
||||
},
|
||||
props: {
|
||||
alertKey: {
|
||||
type: Symbol,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
alert() {
|
||||
return getAlert(this.alertKey);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-alert v-bind="alert.props" @dismiss="alert.dismiss($store)">
|
||||
<component :is="alert.message" />
|
||||
</gl-alert>
|
||||
</template>
|
|
@ -1,4 +1,5 @@
|
|||
<script>
|
||||
import { debounce } from 'lodash';
|
||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import {
|
||||
EDITOR_TYPE_DIFF,
|
||||
|
@ -34,11 +35,13 @@ import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
|
|||
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
|
||||
import { extractMarkdownImagesFromEntries } from '../stores/utils';
|
||||
import { getPathParent, readFileAsDataURL, registerSchema, isTextFile } from '../utils';
|
||||
import FileAlert from './file_alert.vue';
|
||||
import FileTemplatesBar from './file_templates/bar.vue';
|
||||
|
||||
export default {
|
||||
name: 'RepoEditor',
|
||||
components: {
|
||||
FileAlert,
|
||||
ContentViewer,
|
||||
DiffViewer,
|
||||
FileTemplatesBar,
|
||||
|
@ -57,6 +60,7 @@ export default {
|
|||
globalEditor: null,
|
||||
modelManager: new ModelManager(),
|
||||
isEditorLoading: true,
|
||||
unwatchCiYaml: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -74,6 +78,7 @@ export default {
|
|||
'currentProjectId',
|
||||
]),
|
||||
...mapGetters([
|
||||
'getAlert',
|
||||
'currentMergeRequest',
|
||||
'getStagedFile',
|
||||
'isEditModeActive',
|
||||
|
@ -82,6 +87,9 @@ export default {
|
|||
'getJsonSchemaForPath',
|
||||
]),
|
||||
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
|
||||
alertKey() {
|
||||
return this.getAlert(this.file);
|
||||
},
|
||||
fileEditor() {
|
||||
return getFileEditorOrDefault(this.fileEditors, this.file.path);
|
||||
},
|
||||
|
@ -136,6 +144,16 @@ export default {
|
|||
},
|
||||
},
|
||||
watch: {
|
||||
'file.name': {
|
||||
handler() {
|
||||
this.stopWatchingCiYaml();
|
||||
|
||||
if (this.file.name === '.gitlab-ci.yml') {
|
||||
this.startWatchingCiYaml();
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
file(newVal, oldVal) {
|
||||
if (oldVal.pending) {
|
||||
this.removePendingTab(oldVal);
|
||||
|
@ -216,6 +234,7 @@ export default {
|
|||
'removePendingTab',
|
||||
'triggerFilesChange',
|
||||
'addTempImage',
|
||||
'detectGitlabCiFileAlerts',
|
||||
]),
|
||||
...mapActions('editor', ['updateFileEditor']),
|
||||
initEditor() {
|
||||
|
@ -422,6 +441,18 @@ export default {
|
|||
|
||||
this.updateFileEditor({ path: this.file.path, data });
|
||||
},
|
||||
startWatchingCiYaml() {
|
||||
this.unwatchCiYaml = this.$watch(
|
||||
'file.content',
|
||||
debounce(this.detectGitlabCiFileAlerts, 500),
|
||||
);
|
||||
},
|
||||
stopWatchingCiYaml() {
|
||||
if (this.unwatchCiYaml) {
|
||||
this.unwatchCiYaml();
|
||||
this.unwatchCiYaml = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
viewerTypes,
|
||||
FILE_VIEW_MODE_EDITOR,
|
||||
|
@ -439,9 +470,8 @@ export default {
|
|||
role="button"
|
||||
data-testid="edit-tab"
|
||||
@click.prevent="updateEditor({ viewMode: $options.FILE_VIEW_MODE_EDITOR })"
|
||||
>{{ __('Edit') }}</a
|
||||
>
|
||||
{{ __('Edit') }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="previewMode" :class="previewTabCSS">
|
||||
<a
|
||||
|
@ -454,7 +484,8 @@ export default {
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<file-templates-bar v-if="showFileTemplatesBar(file.name)" />
|
||||
<file-alert v-if="alertKey" :alert-key="alertKey" />
|
||||
<file-templates-bar v-else-if="showFileTemplatesBar(file.name)" />
|
||||
<div
|
||||
v-show="showEditor"
|
||||
ref="editor"
|
||||
|
|
|
@ -56,11 +56,12 @@ export function initIde(el, options = {}) {
|
|||
webIDEHelpPagePath: el.dataset.webIdeHelpPagePath,
|
||||
forkInfo: el.dataset.forkInfo ? JSON.parse(el.dataset.forkInfo) : null,
|
||||
});
|
||||
this.setInitialData({
|
||||
this.init({
|
||||
clientsidePreviewEnabled: parseBoolean(el.dataset.clientsidePreviewEnabled),
|
||||
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
|
||||
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
|
||||
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
|
||||
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
|
||||
});
|
||||
},
|
||||
beforeDestroy() {
|
||||
|
@ -68,7 +69,7 @@ export function initIde(el, options = {}) {
|
|||
this.$emit('destroy');
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setEmptyStateSvgs', 'setLinks', 'setInitialData']),
|
||||
...mapActions(['setEmptyStateSvgs', 'setLinks', 'init']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement(rootComponent);
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
import { GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { __ } from '~/locale';
|
||||
|
||||
export default {
|
||||
components: { GlSprintf, GlLink },
|
||||
message: __(
|
||||
"No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}",
|
||||
),
|
||||
computed: {
|
||||
helpLink() {
|
||||
return helpPagePath('ci/environments/index.md');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span>
|
||||
<gl-sprintf :message="$options.message">
|
||||
<template #link="{ content }">
|
||||
<gl-link
|
||||
:href="helpLink"
|
||||
target="_blank"
|
||||
data-track-action="click_link"
|
||||
data-track-experiment="in_product_guidance_environments_webide"
|
||||
>{{ content }}</gl-link
|
||||
>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</span>
|
||||
</template>
|
|
@ -0,0 +1,20 @@
|
|||
import { leftSidebarViews } from '../../constants';
|
||||
import EnvironmentsMessage from './environments.vue';
|
||||
|
||||
const alerts = [
|
||||
{
|
||||
key: Symbol('ALERT_ENVIRONMENT'),
|
||||
show: (state, file) =>
|
||||
state.currentActivityView === leftSidebarViews.commit.name &&
|
||||
file.path === '.gitlab-ci.yml' &&
|
||||
state.environmentsGuidanceAlertDetected &&
|
||||
!state.environmentsGuidanceAlertDismissed,
|
||||
props: { variant: 'tip' },
|
||||
dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'),
|
||||
message: EnvironmentsMessage,
|
||||
},
|
||||
];
|
||||
|
||||
export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key;
|
||||
|
||||
export const getAlert = (key) => alerts.find((x) => x.key === key);
|
|
@ -18,3 +18,4 @@ const getClient = memoize(() =>
|
|||
);
|
||||
|
||||
export const query = (...args) => getClient().query(...args);
|
||||
export const mutate = (...args) => getClient().mutate(...args);
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
|
||||
import Api from '~/api';
|
||||
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import { query } from './gql';
|
||||
import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql';
|
||||
import { query, mutate } from './gql';
|
||||
|
||||
const fetchApiProjectData = (projectPath) => Api.project(projectPath).then(({ data }) => data);
|
||||
|
||||
|
@ -101,4 +103,16 @@ export default {
|
|||
const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`;
|
||||
return axios.post(url);
|
||||
},
|
||||
getCiConfig(projectPath, content) {
|
||||
return query({
|
||||
query: ciConfig,
|
||||
variables: { projectPath, content },
|
||||
}).then(({ data }) => data.ciConfig);
|
||||
},
|
||||
dismissUserCallout(name) {
|
||||
return mutate({
|
||||
mutation: dismissUserCallout,
|
||||
variables: { input: { featureName: name } },
|
||||
}).then(({ data }) => data);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import * as types from './mutation_types';
|
|||
|
||||
export const redirectToUrl = (self, url) => visitUrl(url);
|
||||
|
||||
export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
|
||||
export const init = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
|
||||
|
||||
export const discardAllChanges = ({ state, commit, dispatch }) => {
|
||||
state.changedFiles.forEach((file) => dispatch('restoreOriginalFile', file.path));
|
||||
|
@ -316,3 +316,4 @@ export * from './actions/tree';
|
|||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
export * from './actions/merge_request';
|
||||
export * from './actions/alert';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import service from '../../services';
|
||||
import {
|
||||
DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
|
||||
DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
|
||||
} from '../mutation_types';
|
||||
|
||||
export const detectGitlabCiFileAlerts = ({ dispatch }, content) =>
|
||||
dispatch('detectEnvironmentsGuidance', content);
|
||||
|
||||
export const detectEnvironmentsGuidance = ({ commit, state }, content) =>
|
||||
service.getCiConfig(state.currentProjectId, content).then((data) => {
|
||||
commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages);
|
||||
});
|
||||
|
||||
export const dismissEnvironmentsGuidance = ({ commit }) =>
|
||||
service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => {
|
||||
commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT);
|
||||
});
|
|
@ -262,3 +262,5 @@ export const getJsonSchemaForPath = (state, getters) => (path) => {
|
|||
fileMatch: [`*${path}`],
|
||||
};
|
||||
};
|
||||
|
||||
export * from './getters/alert';
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { findAlertKeyToShow } from '../../lib/alerts';
|
||||
|
||||
export const getAlert = (state) => (file) => findAlertKeyToShow(state, file);
|
|
@ -70,3 +70,8 @@ export const RENAME_ENTRY = 'RENAME_ENTRY';
|
|||
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
|
||||
|
||||
export const RESTORE_TREE = 'RESTORE_TREE';
|
||||
|
||||
// Alert mutation types
|
||||
|
||||
export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT';
|
||||
export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import * as types from './mutation_types';
|
||||
import alertMutations from './mutations/alert';
|
||||
import branchMutations from './mutations/branch';
|
||||
import fileMutations from './mutations/file';
|
||||
import mergeRequestMutation from './mutations/merge_request';
|
||||
|
@ -244,4 +245,5 @@ export default {
|
|||
...fileMutations,
|
||||
...treeMutations,
|
||||
...branchMutations,
|
||||
...alertMutations,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import {
|
||||
DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
|
||||
DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
|
||||
} from '../mutation_types';
|
||||
|
||||
export default {
|
||||
[DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) {
|
||||
if (!stages) {
|
||||
return;
|
||||
}
|
||||
const hasEnvironments = stages?.nodes?.some((stage) =>
|
||||
stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)),
|
||||
);
|
||||
const hasParsedCi = Array.isArray(stages.nodes);
|
||||
|
||||
state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi;
|
||||
},
|
||||
[DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) {
|
||||
state.environmentsGuidanceAlertDismissed = true;
|
||||
},
|
||||
};
|
|
@ -30,4 +30,6 @@ export default () => ({
|
|||
renderWhitespaceInCode: false,
|
||||
editorTheme: DEFAULT_THEME,
|
||||
codesandboxBundlerUrl: null,
|
||||
environmentsGuidanceAlertDismissed: false,
|
||||
environmentsGuidanceAlertDetected: false,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class InProductGuidanceEnvironmentsWebideExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
|
||||
exclude :has_environments?
|
||||
|
||||
def control_behavior
|
||||
false
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def has_environments?
|
||||
!context.project.environments.empty?
|
||||
end
|
||||
end
|
|
@ -17,7 +17,8 @@ module IdeHelper
|
|||
'file-path' => @path,
|
||||
'merge-request' => @merge_request,
|
||||
'fork-info' => @fork_info&.to_json,
|
||||
'project' => convert_to_project_entity_json(@project)
|
||||
'project' => convert_to_project_entity_json(@project),
|
||||
'enable-environments-guidance' => enable_environments_guidance?.to_s
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -28,6 +29,18 @@ module IdeHelper
|
|||
|
||||
API::Entities::Project.represent(project).to_json
|
||||
end
|
||||
|
||||
def enable_environments_guidance?
|
||||
experiment(:in_product_guidance_environments_webide, project: @project) do |e|
|
||||
e.try { !has_dismissed_ide_environments_callout? }
|
||||
|
||||
e.run
|
||||
end
|
||||
end
|
||||
|
||||
def has_dismissed_ide_environments_callout?
|
||||
current_user.dismissed_callout?(feature_name: 'web_ide_ci_environments_guidance')
|
||||
end
|
||||
end
|
||||
|
||||
::IdeHelper.prepend_if_ee('::EE::IdeHelper')
|
||||
|
|
|
@ -107,6 +107,8 @@ class Group < Namespace
|
|||
|
||||
scope :with_users, -> { includes(:users) }
|
||||
|
||||
scope :with_onboarding_progress, -> { joins(:onboarding_progress) }
|
||||
|
||||
scope :by_id, ->(groups) { where(id: groups) }
|
||||
|
||||
scope :for_authorized_group_members, -> (user_ids) do
|
||||
|
|
|
@ -31,7 +31,8 @@ class UserCallout < ApplicationRecord
|
|||
unfinished_tag_cleanup_callout: 27,
|
||||
eoa_bronze_plan_banner: 28, # EE-only
|
||||
pipeline_needs_banner: 29,
|
||||
pipeline_needs_hover_tip: 30
|
||||
pipeline_needs_hover_tip: 30,
|
||||
web_ide_ci_environments_guidance: 31
|
||||
}
|
||||
|
||||
validates :user, presence: true
|
||||
|
|
|
@ -66,7 +66,6 @@ module Namespaces
|
|||
Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group)
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def groups_for_track
|
||||
onboarding_progress_scope = OnboardingProgress
|
||||
.completed_actions_with_latest_in_range(completed_actions, range)
|
||||
|
@ -75,9 +74,18 @@ module Namespaces
|
|||
# Filtering out sub-groups is a temporary fix to prevent calling
|
||||
# `.root_ancestor` on groups that are not root groups.
|
||||
# See https://gitlab.com/groups/gitlab-org/-/epics/5594 for more information.
|
||||
Group.where(parent_id: nil).joins(:onboarding_progress).merge(onboarding_progress_scope)
|
||||
Group
|
||||
.top_most
|
||||
.with_onboarding_progress
|
||||
.merge(onboarding_progress_scope)
|
||||
.merge(subscription_scope)
|
||||
end
|
||||
|
||||
def subscription_scope
|
||||
{}
|
||||
end
|
||||
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def users_for_group(group)
|
||||
group.users
|
||||
.where(email_opted_in: true)
|
||||
|
@ -136,3 +144,5 @@ module Namespaces
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
Namespaces::InProductMarketingEmailsService.prepend_ee_mod
|
||||
|
|
|
@ -15,11 +15,17 @@ module Spam
|
|||
|
||||
def execute
|
||||
spamcheck_result = nil
|
||||
spamcheck_attribs = {}
|
||||
|
||||
external_spam_check_round_trip_time = Benchmark.realtime do
|
||||
spamcheck_result = spamcheck_verdict
|
||||
spamcheck_result, spamcheck_attribs = spamcheck_verdict
|
||||
end
|
||||
|
||||
# assign result to a var and log it before reassigning to nil when monitorMode is true
|
||||
original_spamcheck_result = spamcheck_result
|
||||
|
||||
spamcheck_result = nil if spamcheck_attribs&.fetch("monitorMode", "false") == "true"
|
||||
|
||||
akismet_result = akismet_verdict
|
||||
|
||||
# filter out anything we don't recognise, including nils.
|
||||
|
@ -33,7 +39,8 @@ module Spam
|
|||
|
||||
logger.info(class: self.class.name,
|
||||
akismet_verdict: akismet_verdict,
|
||||
spam_check_verdict: spamcheck_result,
|
||||
spam_check_verdict: original_spamcheck_result,
|
||||
extra_attributes: spamcheck_attribs,
|
||||
spam_check_rtt: external_spam_check_round_trip_time.real,
|
||||
final_verdict: final_verdict,
|
||||
username: user.username,
|
||||
|
@ -61,21 +68,23 @@ module Spam
|
|||
return unless Gitlab::CurrentSettings.spam_check_endpoint_enabled
|
||||
|
||||
begin
|
||||
result, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
|
||||
return unless result
|
||||
result, attribs, _error = spamcheck_client.issue_spam?(spam_issue: target, user: user, context: context)
|
||||
return [nil, attribs] unless result
|
||||
|
||||
# @TODO log if error is not nil https://gitlab.com/gitlab-org/gitlab/-/issues/329545
|
||||
|
||||
return [result, attribs] if result == NOOP || attribs["monitorMode"] == "true"
|
||||
|
||||
# Duplicate logic with Akismet logic in #akismet_verdict
|
||||
if Gitlab::Recaptcha.enabled? && result != ALLOW
|
||||
CONDITIONAL_ALLOW
|
||||
[CONDITIONAL_ALLOW, attribs]
|
||||
else
|
||||
result
|
||||
[result, attribs]
|
||||
end
|
||||
rescue StandardError => e
|
||||
Gitlab::ErrorTracking.log_exception(e)
|
||||
# Default to ALLOW if any errors occur
|
||||
ALLOW
|
||||
[ALLOW, attribs]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
|
||||
|
||||
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
|
||||
= form_for @project, html: { class: 'new_project' } do |f|
|
||||
= form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f|
|
||||
%hr
|
||||
= render "shared/import_form", f: f
|
||||
= render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
|
||||
|
|
|
@ -6,8 +6,14 @@
|
|||
= f.label :import_url, class: 'label-bold' do
|
||||
%span
|
||||
= _('Git repository URL')
|
||||
= f.text_field :import_url, value: import_url.sanitized_url,
|
||||
autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true
|
||||
= f.text_field :import_url,
|
||||
value: import_url.sanitized_url,
|
||||
autocomplete: 'off',
|
||||
class: 'form-control gl-form-input',
|
||||
placeholder: 'https://gitlab.company.com/group/project.git',
|
||||
required: true,
|
||||
pattern: '(?:git|https?):\/\/.*/.*\.git$',
|
||||
title: _('Please provide a valid URL ending with .git')
|
||||
|
||||
.row
|
||||
.form-group.col-md-6
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
.check-all-holder.d-none.d-sm-block.hidden
|
||||
= check_box_tag "check-all-issues", nil, false, class: "check-all-issues left"
|
||||
- if Feature.enabled?(:boards_filtered_search, @group) && is_epic_board
|
||||
#js-board-filtered-search
|
||||
#js-board-filtered-search{ data: { full_path: @group&.full_path } }
|
||||
- else
|
||||
.issues-other-filters.filtered-search-wrapper.d-flex.flex-column.flex-md-row
|
||||
.filtered-search-box
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Enforce .git suffix when importing git repo
|
||||
merge_request: 61115
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: in_product_guidance_environments_webide
|
||||
introduced_by_url:
|
||||
rollout_issue_url:
|
||||
milestone: '13.12'
|
||||
type: experiment
|
||||
group: group::release
|
||||
default_enabled: false
|
|
@ -14418,6 +14418,7 @@ Name of the feature that the callout is for.
|
|||
| <a id="usercalloutfeaturenameenumunfinished_tag_cleanup_callout"></a>`UNFINISHED_TAG_CLEANUP_CALLOUT` | Callout feature name for unfinished_tag_cleanup_callout. |
|
||||
| <a id="usercalloutfeaturenameenumwebhooks_moved"></a>`WEBHOOKS_MOVED` | Callout feature name for webhooks_moved. |
|
||||
| <a id="usercalloutfeaturenameenumweb_ide_alert_dismissed"></a>`WEB_IDE_ALERT_DISMISSED` | Callout feature name for web_ide_alert_dismissed. |
|
||||
| <a id="usercalloutfeaturenameenumweb_ide_ci_environments_guidance"></a>`WEB_IDE_CI_ENVIRONMENTS_GUIDANCE` | Callout feature name for web_ide_ci_environments_guidance. |
|
||||
|
||||
### `UserState`
|
||||
|
||||
|
|
|
@ -45,7 +45,7 @@ module Gitlab
|
|||
metadata: { 'authorization' =>
|
||||
Gitlab::CurrentSettings.spam_check_api_key })
|
||||
verdict = convert_verdict_to_gitlab_constant(response.verdict)
|
||||
[verdict, response.error]
|
||||
[verdict, response.extra_attributes.to_h, response.error]
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -22044,6 +22044,9 @@ msgstr ""
|
|||
msgid "No data to display"
|
||||
msgstr ""
|
||||
|
||||
msgid "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "No deployments found"
|
||||
msgstr ""
|
||||
|
||||
|
@ -24322,6 +24325,9 @@ msgstr ""
|
|||
msgid "Please provide a valid URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid URL ending with .git"
|
||||
msgstr ""
|
||||
|
||||
msgid "Please provide a valid YouTube URL or ID"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe InProductGuidanceEnvironmentsWebideExperiment, :experiment do
|
||||
subject { described_class.new(project: project) }
|
||||
|
||||
let(:project) { create(:project, :repository) }
|
||||
|
||||
before do
|
||||
stub_experiments(in_product_guidance_environments_webide: :candidate)
|
||||
end
|
||||
|
||||
it 'excludes projects with environments' do
|
||||
create(:environment, project: project)
|
||||
expect(subject).to exclude(project: project)
|
||||
end
|
||||
|
||||
it 'does not exlude projects without environments' do
|
||||
expect(subject).not_to exclude(project: project)
|
||||
end
|
||||
end
|
|
@ -355,6 +355,16 @@ RSpec.describe 'New project', :js do
|
|||
expect(git_import_instructions).to have_content 'Git repository URL'
|
||||
end
|
||||
|
||||
it 'reports error if repo URL does not end with .git' do
|
||||
fill_in 'project_import_url', with: 'http://foo/bar'
|
||||
fill_in 'project_name', with: 'import-project-without-git-suffix'
|
||||
fill_in 'project_path', with: 'import-project-without-git-suffix'
|
||||
|
||||
click_button 'Create project'
|
||||
|
||||
expect(page).to have_text('Please provide a valid URL ending with .git')
|
||||
end
|
||||
|
||||
it 'keeps "Import project" tab open after form validation error' do
|
||||
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
|
||||
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
|
||||
import { createStore } from '~/boards/stores';
|
||||
import * as urlUtility from '~/lib/utils/url_utility';
|
||||
import { __ } from '~/locale';
|
||||
import FilteredSearchBarRoot from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
|
||||
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
|
||||
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('BoardFilteredSearch', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
const tokens = [
|
||||
{
|
||||
icon: 'labels',
|
||||
title: __('Label'),
|
||||
type: 'label_name',
|
||||
operators: [
|
||||
{ value: '=', description: 'is' },
|
||||
{ value: '!=', description: 'is not' },
|
||||
],
|
||||
token: LabelToken,
|
||||
unique: false,
|
||||
symbol: '~',
|
||||
fetchLabels: () => new Promise(() => {}),
|
||||
},
|
||||
{
|
||||
icon: 'pencil',
|
||||
title: __('Author'),
|
||||
type: 'author_username',
|
||||
operators: [
|
||||
{ value: '=', description: 'is' },
|
||||
{ value: '!=', description: 'is not' },
|
||||
],
|
||||
symbol: '@',
|
||||
token: AuthorToken,
|
||||
unique: true,
|
||||
fetchAuthors: () => new Promise(() => {}),
|
||||
},
|
||||
];
|
||||
|
||||
const createComponent = ({ initialFilterParams = {} } = {}) => {
|
||||
wrapper = shallowMount(BoardFilteredSearch, {
|
||||
provide: { initialFilterParams, fullPath: '' },
|
||||
store,
|
||||
propsData: {
|
||||
tokens,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findFilteredSearch = () => wrapper.findComponent(FilteredSearchBarRoot);
|
||||
|
||||
beforeEach(() => {
|
||||
// this needed for actions call for performSearch
|
||||
window.gon = { features: {} };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
describe('default', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders FilteredSearch', () => {
|
||||
expect(findFilteredSearch().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('passes the correct tokens to FilteredSearch', () => {
|
||||
expect(findFilteredSearch().props('tokens')).toEqual(tokens);
|
||||
});
|
||||
|
||||
describe('when onFilter is emitted', () => {
|
||||
it('calls performSearch', () => {
|
||||
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: '' } }]);
|
||||
|
||||
expect(store.dispatch).toHaveBeenCalledWith('performSearch');
|
||||
});
|
||||
|
||||
it('calls historyPushState', () => {
|
||||
jest.spyOn(urlUtility, 'updateHistory');
|
||||
findFilteredSearch().vm.$emit('onFilter', [{ value: { data: 'searchQuery' } }]);
|
||||
|
||||
expect(urlUtility.updateHistory).toHaveBeenCalledWith({
|
||||
replace: true,
|
||||
title: '',
|
||||
url: 'http://test.host/',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('sets the url params to the correct results', async () => {
|
||||
const mockFilters = [
|
||||
{ type: 'author_username', value: { data: 'root', operator: '=' } },
|
||||
{ type: 'label_name', value: { data: 'label', operator: '=' } },
|
||||
{ type: 'label_name', value: { data: 'label2', operator: '=' } },
|
||||
];
|
||||
jest.spyOn(urlUtility, 'updateHistory');
|
||||
findFilteredSearch().vm.$emit('onFilter', mockFilters);
|
||||
|
||||
expect(urlUtility.updateHistory).toHaveBeenCalledWith({
|
||||
title: '',
|
||||
replace: true,
|
||||
url: 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label2',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when url params are already set', () => {
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
|
||||
jest.spyOn(store, 'dispatch');
|
||||
|
||||
createComponent({ initialFilterParams: { authorUsername: 'root', labelName: ['label'] } });
|
||||
});
|
||||
|
||||
it('passes the correct props to FilterSearchBar', () => {
|
||||
expect(findFilteredSearch().props('initialFilterValue')).toEqual([
|
||||
{ type: 'author_username', value: { data: 'root', operator: '=' } },
|
||||
{ type: 'label_name', value: { data: 'label', operator: '=' } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -510,6 +510,7 @@ describe('RepoEditor', () => {
|
|||
},
|
||||
});
|
||||
await vm.$nextTick();
|
||||
await vm.$nextTick();
|
||||
|
||||
expect(vm.initEditor).toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { GlLink } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import Environments from '~/ide/lib/alerts/environments.vue';
|
||||
|
||||
describe('~/ide/lib/alerts/environment.vue', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(Environments);
|
||||
});
|
||||
|
||||
it('shows a message regarding environments', () => {
|
||||
expect(wrapper.text()).toBe(
|
||||
"No deployments detected. Use environments to control your software's continuous deployment. Learn more about deployment jobs.",
|
||||
);
|
||||
});
|
||||
|
||||
it('links to the help page on environments', () => {
|
||||
expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/help/ci/environments/index.md');
|
||||
});
|
||||
});
|
|
@ -2,9 +2,11 @@ import axios from 'axios';
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
|
||||
import Api from '~/api';
|
||||
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
|
||||
import services from '~/ide/services';
|
||||
import { query } from '~/ide/services/gql';
|
||||
import { query, mutate } from '~/ide/services/gql';
|
||||
import { escapeFileUrl } from '~/lib/utils/url_utility';
|
||||
import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.graphql';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
jest.mock('~/api');
|
||||
|
@ -299,4 +301,33 @@ describe('IDE services', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe('getCiConfig', () => {
|
||||
const TEST_PROJECT_PATH = 'foo/bar';
|
||||
const TEST_CI_CONFIG = 'test config';
|
||||
|
||||
it('queries with the given CI config and project', () => {
|
||||
const result = { data: { ciConfig: { test: 'data' } } };
|
||||
query.mockResolvedValue(result);
|
||||
return services.getCiConfig(TEST_PROJECT_PATH, TEST_CI_CONFIG).then((data) => {
|
||||
expect(data).toEqual(result.data.ciConfig);
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
query: ciConfig,
|
||||
variables: { projectPath: TEST_PROJECT_PATH, content: TEST_CI_CONFIG },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('dismissUserCallout', () => {
|
||||
it('mutates the callout to dismiss', () => {
|
||||
const result = { data: { callouts: { test: 'data' } } };
|
||||
mutate.mockResolvedValue(result);
|
||||
return services.dismissUserCallout('test').then((data) => {
|
||||
expect(data).toEqual(result.data);
|
||||
expect(mutate).toHaveBeenCalledWith({
|
||||
mutation: dismissUserCallout,
|
||||
variables: { input: { featureName: 'test' } },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import testAction from 'helpers/vuex_action_helper';
|
||||
import service from '~/ide/services';
|
||||
import {
|
||||
detectEnvironmentsGuidance,
|
||||
dismissEnvironmentsGuidance,
|
||||
} from '~/ide/stores/actions/alert';
|
||||
import * as types from '~/ide/stores/mutation_types';
|
||||
|
||||
jest.mock('~/ide/services');
|
||||
|
||||
describe('~/ide/stores/actions/alert', () => {
|
||||
describe('detectEnvironmentsGuidance', () => {
|
||||
it('should try to fetch CI info', () => {
|
||||
const stages = ['a', 'b', 'c'];
|
||||
service.getCiConfig.mockResolvedValue({ stages });
|
||||
|
||||
return testAction(
|
||||
detectEnvironmentsGuidance,
|
||||
'the content',
|
||||
{ currentProjectId: 'gitlab/test' },
|
||||
[{ type: types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, payload: stages }],
|
||||
[],
|
||||
() => expect(service.getCiConfig).toHaveBeenCalledWith('gitlab/test', 'the content'),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('dismissCallout', () => {
|
||||
it('should try to dismiss the given callout', () => {
|
||||
const callout = { featureName: 'test', dismissedAt: 'now' };
|
||||
|
||||
service.dismissUserCallout.mockResolvedValue({ userCalloutCreate: { userCallout: callout } });
|
||||
|
||||
return testAction(
|
||||
dismissEnvironmentsGuidance,
|
||||
undefined,
|
||||
{},
|
||||
[{ type: types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT }],
|
||||
[],
|
||||
() =>
|
||||
expect(service.dismissUserCallout).toHaveBeenCalledWith(
|
||||
'web_ide_ci_environments_guidance',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@ import eventHub from '~/ide/eventhub';
|
|||
import { createRouter } from '~/ide/ide_router';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import {
|
||||
init,
|
||||
stageAllChanges,
|
||||
unstageAllChanges,
|
||||
toggleFileFinder,
|
||||
|
@ -54,15 +55,15 @@ describe('Multi-file store actions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('setInitialData', () => {
|
||||
it('commits initial data', (done) => {
|
||||
store
|
||||
.dispatch('setInitialData', { canCommit: true })
|
||||
.then(() => {
|
||||
expect(store.state.canCommit).toBeTruthy();
|
||||
done();
|
||||
})
|
||||
.catch(done.fail);
|
||||
describe('init', () => {
|
||||
it('commits initial data and requests user callouts', () => {
|
||||
return testAction(
|
||||
init,
|
||||
{ canCommit: true },
|
||||
store.state,
|
||||
[{ type: 'SET_INITIAL_DATA', payload: { canCommit: true } }],
|
||||
[],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import { getAlert } from '~/ide/lib/alerts';
|
||||
import EnvironmentsMessage from '~/ide/lib/alerts/environments.vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import * as getters from '~/ide/stores/getters/alert';
|
||||
import { file } from '../../helpers';
|
||||
|
||||
describe('IDE store alert getters', () => {
|
||||
let localState;
|
||||
let localStore;
|
||||
|
||||
beforeEach(() => {
|
||||
localStore = createStore();
|
||||
localState = localStore.state;
|
||||
});
|
||||
|
||||
describe('alerts', () => {
|
||||
describe('shows an alert about environments', () => {
|
||||
let alert;
|
||||
|
||||
beforeEach(() => {
|
||||
const f = file('.gitlab-ci.yml');
|
||||
localState.openFiles.push(f);
|
||||
localState.currentActivityView = 'repo-commit-section';
|
||||
localState.environmentsGuidanceAlertDetected = true;
|
||||
localState.environmentsGuidanceAlertDismissed = false;
|
||||
|
||||
const alertKey = getters.getAlert(localState)(f);
|
||||
alert = getAlert(alertKey);
|
||||
});
|
||||
|
||||
it('has a message suggesting to use environments', () => {
|
||||
expect(alert.message).toEqual(EnvironmentsMessage);
|
||||
});
|
||||
|
||||
it('dispatches to dismiss the callout on dismiss', () => {
|
||||
jest.spyOn(localStore, 'dispatch').mockImplementation();
|
||||
alert.dismiss(localStore);
|
||||
expect(localStore.dispatch).toHaveBeenCalledWith('dismissEnvironmentsGuidance');
|
||||
});
|
||||
|
||||
it('should be a tip alert', () => {
|
||||
expect(alert.props).toEqual({ variant: 'tip' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import * as types from '~/ide/stores/mutation_types';
|
||||
import mutations from '~/ide/stores/mutations/alert';
|
||||
|
||||
describe('~/ide/stores/mutations/alert', () => {
|
||||
const state = {};
|
||||
|
||||
describe(types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT, () => {
|
||||
it('checks the stages for any that configure environments', () => {
|
||||
mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, {
|
||||
nodes: [{ groups: { nodes: [{ jobs: { nodes: [{}] } }] } }],
|
||||
});
|
||||
expect(state.environmentsGuidanceAlertDetected).toBe(true);
|
||||
mutations[types.DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, {
|
||||
nodes: [{ groups: { nodes: [{ jobs: { nodes: [{ environment: {} }] } }] } }],
|
||||
});
|
||||
expect(state.environmentsGuidanceAlertDetected).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT, () => {
|
||||
it('stops environments guidance', () => {
|
||||
mutations[types.DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state);
|
||||
expect(state.environmentsGuidanceAlertDismissed).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -45,5 +45,35 @@ RSpec.describe IdeHelper do
|
|||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'environments guidance experiment', :experiment do
|
||||
before do
|
||||
stub_experiments(in_product_guidance_environments_webide: :candidate)
|
||||
self.instance_variable_set(:@project, project)
|
||||
end
|
||||
|
||||
context 'when project has no enviornments' do
|
||||
it 'enables environment guidance' do
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'true')
|
||||
end
|
||||
|
||||
context 'and the callout has been dismissed' do
|
||||
it 'disables environment guidance' do
|
||||
callout = create(:user_callout, feature_name: :web_ide_ci_environments_guidance, user: project.creator)
|
||||
callout.update!(dismissed_at: Time.now - 1.week)
|
||||
allow(helper).to receive(:current_user).and_return(User.find(project.creator.id))
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the project has environments' do
|
||||
it 'disables environment guidance' do
|
||||
create(:environment, project: project)
|
||||
|
||||
expect(helper.ide_data).to include('enable-environments-guidance' => 'false')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,12 +9,20 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
let_it_be(:user) { create(:user, organization: 'GitLab') }
|
||||
let(:verdict_value) { nil }
|
||||
let(:error_value) { "" }
|
||||
|
||||
let(:attribs_value) do
|
||||
extra_attributes = Google::Protobuf::Map.new(:string, :string)
|
||||
extra_attributes["monitorMode"] = "false"
|
||||
extra_attributes
|
||||
end
|
||||
|
||||
let_it_be(:issue) { create(:issue, description: 'Test issue description') }
|
||||
|
||||
let(:response) do
|
||||
verdict = ::Spamcheck::SpamVerdict.new
|
||||
verdict.verdict = verdict_value
|
||||
verdict.error = error_value
|
||||
verdict.extra_attributes = attribs_value
|
||||
verdict
|
||||
end
|
||||
|
||||
|
@ -45,7 +53,7 @@ RSpec.describe Gitlab::Spamcheck::Client do
|
|||
let(:verdict_value) { verdict }
|
||||
|
||||
it "returns expected spam constant" do
|
||||
expect(subject).to eq([expected, ""])
|
||||
expect(subject).to eq([expected, { "monitorMode" => "false" }, ""])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -632,6 +632,16 @@ RSpec.describe Group do
|
|||
it { is_expected.to match_array([private_group, internal_group]) }
|
||||
end
|
||||
|
||||
describe 'with_onboarding_progress' do
|
||||
subject { described_class.with_onboarding_progress }
|
||||
|
||||
it 'joins onboarding_progress' do
|
||||
create(:onboarding_progress, namespace: group)
|
||||
|
||||
expect(subject).to eq([group])
|
||||
end
|
||||
end
|
||||
|
||||
describe 'for_authorized_group_members' do
|
||||
let_it_be(:group_member1) { create(:group_member, source: private_group, user_id: user1.id, access_level: Gitlab::Access::OWNER) }
|
||||
|
||||
|
|
|
@ -23,12 +23,17 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
described_class.new(user: user, target: issue, request: request, options: {})
|
||||
end
|
||||
|
||||
let(:attribs) do
|
||||
extra_attributes = { "monitorMode" => "false" }
|
||||
extra_attributes
|
||||
end
|
||||
|
||||
describe '#execute' do
|
||||
subject { service.execute }
|
||||
|
||||
before do
|
||||
allow(service).to receive(:akismet_verdict).and_return(nil)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return(nil)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return([nil, attribs])
|
||||
end
|
||||
|
||||
context 'if all services return nil' do
|
||||
|
@ -63,7 +68,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
context 'and they are supported' do
|
||||
before do
|
||||
allow(service).to receive(:akismet_verdict).and_return(DISALLOW)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs])
|
||||
end
|
||||
|
||||
it 'renders the more restrictive verdict' do
|
||||
|
@ -74,7 +79,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
context 'and one is supported' do
|
||||
before do
|
||||
allow(service).to receive(:akismet_verdict).and_return('nonsense')
|
||||
allow(service).to receive(:spamcheck_verdict).and_return(BLOCK_USER)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs])
|
||||
end
|
||||
|
||||
it 'renders the more restrictive verdict' do
|
||||
|
@ -85,13 +90,29 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
context 'and none are supported' do
|
||||
before do
|
||||
allow(service).to receive(:akismet_verdict).and_return('nonsense')
|
||||
allow(service).to receive(:spamcheck_verdict).and_return('rubbish')
|
||||
allow(service).to receive(:spamcheck_verdict).and_return(['rubbish', attribs])
|
||||
end
|
||||
|
||||
it 'renders the more restrictive verdict' do
|
||||
expect(subject).to eq ALLOW
|
||||
end
|
||||
end
|
||||
|
||||
context 'and attribs - monitorMode is true' do
|
||||
let(:attribs) do
|
||||
extra_attributes = { "monitorMode" => "true" }
|
||||
extra_attributes
|
||||
end
|
||||
|
||||
before do
|
||||
allow(service).to receive(:akismet_verdict).and_return(DISALLOW)
|
||||
allow(service).to receive(:spamcheck_verdict).and_return([BLOCK_USER, attribs])
|
||||
end
|
||||
|
||||
it 'renders the more restrictive verdict' do
|
||||
expect(subject).to eq(DISALLOW)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -170,16 +191,42 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:error) { '' }
|
||||
let(:verdict) { nil }
|
||||
|
||||
let(:attribs) do
|
||||
extra_attributes = { "monitorMode" => "false" }
|
||||
extra_attributes
|
||||
end
|
||||
|
||||
before do
|
||||
allow(service).to receive(:spamcheck_client).and_return(spam_client)
|
||||
allow(spam_client).to receive(:issue_spam?).and_return([verdict, error])
|
||||
allow(spam_client).to receive(:issue_spam?).and_return([verdict, attribs, error])
|
||||
end
|
||||
|
||||
context 'if the result is a NOOP verdict' do
|
||||
let(:verdict) { NOOP }
|
||||
|
||||
it 'returns the verdict' do
|
||||
expect(subject).to eq([NOOP, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
context 'if attribs - monitorMode is true' do
|
||||
let(:attribs) do
|
||||
extra_attributes = { "monitorMode" => "true" }
|
||||
extra_attributes
|
||||
end
|
||||
|
||||
let(:verdict) { ALLOW }
|
||||
|
||||
it 'returns the verdict' do
|
||||
expect(subject).to eq([ALLOW, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
context 'the result is a valid verdict' do
|
||||
let(:verdict) { ALLOW }
|
||||
|
||||
it 'returns the verdict' do
|
||||
expect(subject).to eq ALLOW
|
||||
expect(subject).to eq([ALLOW, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -203,7 +250,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:verdict) { verdict_value }
|
||||
|
||||
it "returns expected spam constant" do
|
||||
expect(subject).to eq(expected)
|
||||
expect(subject).to eq([expected, attribs])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -218,7 +265,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
::Spam::SpamConstants::DISALLOW,
|
||||
::Spam::SpamConstants::BLOCK_USER].each do |verdict_value|
|
||||
let(:verdict) { verdict_value }
|
||||
let(:expected) { verdict_value }
|
||||
let(:expected) { [verdict_value, attribs] }
|
||||
|
||||
it "returns expected spam constant" do
|
||||
expect(subject).to eq(expected)
|
||||
|
@ -230,7 +277,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:verdict) { :this_is_fine }
|
||||
|
||||
it 'returns the string' do
|
||||
expect(subject).to eq verdict
|
||||
expect(subject).to eq([verdict, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -238,7 +285,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:verdict) { '' }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to eq verdict
|
||||
expect(subject).to eq([verdict, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -246,7 +293,7 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:verdict) { nil }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
expect(subject).to eq([nil, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -254,17 +301,19 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:error) { "Sorry Dave, I can't do that" }
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be_nil
|
||||
expect(subject).to eq([nil, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
context 'the requested is aborted' do
|
||||
let(:attribs) { nil }
|
||||
|
||||
before do
|
||||
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::Aborted)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(ALLOW)
|
||||
expect(subject).to eq([ALLOW, attribs])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -273,18 +322,20 @@ RSpec.describe Spam::SpamVerdictService do
|
|||
let(:error) { 'oh noes!' }
|
||||
|
||||
it 'renders the verdict' do
|
||||
expect(subject).to eq DISALLOW
|
||||
expect(subject).to eq [DISALLOW, attribs]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'if the endpoint times out' do
|
||||
let(:attribs) { nil }
|
||||
|
||||
before do
|
||||
allow(spam_client).to receive(:issue_spam?).and_raise(GRPC::DeadlineExceeded)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject).to be(ALLOW)
|
||||
expect(subject).to eq([ALLOW, attribs])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue