Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-29 12:08:48 +00:00
parent ad2789aeba
commit e5f2a04e9d
104 changed files with 722 additions and 451 deletions

View File

@ -83,8 +83,8 @@ const Api = {
tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
freezePeriodPath: '/api/:version/projects/:id/freeze_periods/:freeze_period_id',
usageDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
serviceDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
serviceDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
@ -875,7 +875,7 @@ const Api = {
return null;
}
const url = Api.buildUrl(this.usageDataIncrementCounterPath);
const url = Api.buildUrl(this.serviceDataIncrementCounterPath);
const headers = {
'Content-Type': 'application/json',
};
@ -888,7 +888,7 @@ const Api = {
return null;
}
const url = Api.buildUrl(this.usageDataIncrementUniqueUsersPath);
const url = Api.buildUrl(this.serviceDataIncrementUniqueUsersPath);
const headers = {
'Content-Type': 'application/json',
};

View File

@ -41,7 +41,7 @@ import {
TOGGLE_TODO_ERROR,
designDeletionError,
} from '../../utils/error_messages';
import { trackDesignDetailView, usagePingDesignDetailView } from '../../utils/tracking';
import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
const DEFAULT_SCALE = 1;
@ -292,7 +292,7 @@ export default {
);
if (this.glFeatures.usageDataDesignAction) {
usagePingDesignDetailView();
servicePingDesignDetailView();
}
},
updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {

View File

@ -14,7 +14,7 @@ export const DESIGN_SNOWPLOW_EVENT_TYPES = {
UPDATE_DESIGN: 'update_design',
};
export const DESIGN_USAGE_PING_EVENT_TYPES = {
export const DESIGN_SERVICE_PING_EVENT_TYPES = {
DESIGN_ACTION: 'design_action',
};
@ -52,8 +52,8 @@ export function trackDesignUpdate() {
}
/**
* Track "design detail" view via usage ping
* Track "design detail" view via service ping
*/
export function usagePingDesignDetailView() {
Api.trackRedisHllUserEvent(DESIGN_USAGE_PING_EVENT_TYPES.DESIGN_ACTION);
export function servicePingDesignDetailView() {
Api.trackRedisHllUserEvent(DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION);
}

View File

@ -11,6 +11,7 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
@ -70,6 +71,10 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
import eventHub from '../eventhub';
import searchIterationsQuery from '../queries/search_iterations.query.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
import searchUsersQuery from '../queries/search_users.query.graphql';
import IssueCardTimeInfo from './issue_card_time_info.vue';
export default {
@ -94,9 +99,6 @@ export default {
autocompleteAwardEmojisPath: {
default: '',
},
autocompleteUsersPath: {
default: '',
},
calendarPath: {
default: '',
},
@ -118,6 +120,9 @@ export default {
hasIssueWeightsFeature: {
default: false,
},
hasIterationsFeature: {
default: false,
},
hasMultipleIssueAssigneesFeature: {
default: false,
},
@ -139,15 +144,6 @@ export default {
newIssuePath: {
default: '',
},
projectIterationsPath: {
default: '',
},
projectLabelsPath: {
default: '',
},
projectMilestonesPath: {
default: '',
},
projectPath: {
default: '',
},
@ -233,7 +229,7 @@ export default {
if (gon.current_user_id) {
preloadedAuthors.push({
id: gon.current_user_id,
id: convertToGraphQLId('User', gon.current_user_id), // eslint-disable-line @gitlab/require-i18n-strings
name: gon.current_user_fullname,
username: gon.current_username,
avatar_url: gon.current_user_avatar_url,
@ -308,7 +304,7 @@ export default {
});
}
if (this.projectIterationsPath) {
if (this.hasIterationsFeature) {
tokens.push({
type: TOKEN_TYPE_ITERATION,
title: TOKEN_TITLE_ITERATION,
@ -407,19 +403,42 @@ export default {
: epics.filter((epic) => epic.id === number);
},
fetchLabels(search) {
return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search);
return this.$apollo
.query({
query: searchLabelsQuery,
variables: { projectPath: this.projectPath, search },
})
.then(({ data }) => data.project.labels.nodes);
},
fetchMilestones(search) {
return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
return this.$apollo
.query({
query: searchMilestonesQuery,
variables: { projectPath: this.projectPath, search },
})
.then(({ data }) => data.project.milestones.nodes);
},
fetchIterations(search) {
const id = Number(search);
return !search || Number.isNaN(id)
? axios.get(this.projectIterationsPath, { params: { search } })
: axios.get(this.projectIterationsPath, { params: { id } });
const variables =
!search || Number.isNaN(id)
? { projectPath: this.projectPath, search }
: { projectPath: this.projectPath, id };
return this.$apollo
.query({
query: searchIterationsQuery,
variables,
})
.then(({ data }) => data.project.iterations.nodes);
},
fetchUsers(search) {
return axios.get(this.autocompleteUsersPath, { params: { search } });
return this.$apollo
.query({
query: searchUsersQuery,
variables: { projectPath: this.projectPath, search },
})
.then(({ data }) => data.project.projectMembers.nodes.map((member) => member.user));
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;

View File

@ -82,7 +82,6 @@ export function mountIssuesListApp() {
const {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate,
canEdit,
@ -95,6 +94,7 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
hasIterationsFeature,
hasMultipleIssueAssigneesFeature,
hasProjectIssues,
importCsvIssuesPath,
@ -106,9 +106,6 @@ export function mountIssuesListApp() {
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
projectIterationsPath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
quickActionsHelpPath,
resetPath,
@ -122,7 +119,6 @@ export function mountIssuesListApp() {
apolloProvider,
provide: {
autocompleteAwardEmojisPath,
autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
@ -130,15 +126,13 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
hasProjectIssues: parseBoolean(hasProjectIssues),
isSignedIn: parseBoolean(isSignedIn),
issuesPath,
jiraIntegrationPath,
newIssuePath,
projectIterationsPath,
projectLabelsPath,
projectMilestonesPath,
projectPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),

View File

@ -0,0 +1,10 @@
query searchIterations($projectPath: ID!, $search: String, $id: ID) {
project(fullPath: $projectPath) {
iterations(title: $search, id: $id) {
nodes {
id
title
}
}
}
}

View File

@ -0,0 +1,12 @@
query searchLabels($projectPath: ID!, $search: String) {
project(fullPath: $projectPath) {
labels(searchTerm: $search, includeAncestorGroups: true) {
nodes {
id
color
textColor
title
}
}
}
}

View File

@ -0,0 +1,10 @@
query searchMilestones($projectPath: ID!, $search: String) {
project(fullPath: $projectPath) {
milestones(searchTitle: $search, includeAncestors: true) {
nodes {
id
title
}
}
}
}

View File

@ -0,0 +1,14 @@
query searchUsers($projectPath: ID!, $search: String) {
project(fullPath: $projectPath) {
projectMembers(search: $search) {
nodes {
user {
id
avatarUrl
name
username
}
}
}
}
}

View File

@ -31,7 +31,7 @@ import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
import initUsagePingConsent from './usage_ping_consent';
import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@ -86,7 +86,7 @@ function deferredInitialisation() {
initBreadcrumbs();
initTodoToggle();
initLogoAnimation();
initUsagePingConsent();
initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
initFrequentItemDropdowns();

View File

@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
@ -41,6 +42,7 @@ export default {
},
data() {
return {
starterTemplateName: STARTER_TEMPLATE_NAME,
ciConfigData: {},
failureType: null,
failureReasons: [],
@ -160,7 +162,7 @@ export default {
variables() {
return {
projectPath: this.projectFullPath,
templateName: STARTER_TEMPLATE_NAME,
templateName: this.starterTemplateName,
};
},
skip({ isNewCiConfigFile }) {
@ -203,6 +205,9 @@ export default {
}
},
},
mounted() {
this.loadTemplateFromURL();
},
methods: {
hideFailure() {
this.showFailure = false;
@ -258,6 +263,14 @@ export default {
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
loadTemplateFromURL() {
const templateName = queryToObject(window.location.search)?.template;
if (templateName) {
this.starterTemplateName = templateName;
this.setNewEmptyCiConfigFile();
}
},
},
};
</script>

View File

@ -16,7 +16,6 @@ export default {
consuming tasks, so you can spend more time creating.`),
aboutRunnersBtnText: s__('Pipelines|Learn about Runners'),
installRunnersBtnText: s__('Pipelines|Install GitLab Runners'),
getStartedBtnText: s__('Pipelines|Get started with CI/CD'),
codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
readable, and accessible to contributors, use GitLab CI/CD
@ -55,9 +54,6 @@ export default {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
isPipelineEmptyStateTemplatesExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
},
isCodeQualityExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('code_quality_walkthrough'));
},
@ -81,37 +77,8 @@ export default {
</script>
<template>
<div>
<gitlab-experiment
v-if="isPipelineEmptyStateTemplatesExperimentActive"
name="pipeline_empty_state_templates"
>
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.getStartedBtnText"
:primary-button-link="ciHelpPagePath"
/>
</template>
<template #candidate>
<pipelines-ci-templates />
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
>
<template #actions>
<gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
{{ $options.i18n.getStartedBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
<gitlab-experiment v-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
<template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.codeQualityTitle"
@ -127,23 +94,7 @@ export default {
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCiRunnerTemplatesExperimentActive" name="ci_runner_templates">
<template #control>
<gl-empty-state
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
>
<template #actions>
<gl-button
:href="ciHelpPagePath"
variant="confirm"
@click="trackCiRunnerTemplatesClick('get_started_button_clicked')"
>
{{ $options.i18n.getStartedBtnText }}
</gl-button>
</template>
</gl-empty-state>
</template>
<template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.title"
@ -169,14 +120,7 @@ export default {
</gl-empty-state>
</template>
</gitlab-experiment>
<gl-empty-state
v-else-if="canSetCi"
:title="$options.i18n.title"
:svg-path="emptyStateSvgPath"
:description="$options.i18n.description"
:primary-button-text="$options.i18n.getStartedBtnText"
:primary-button-link="ciHelpPagePath"
/>
<pipelines-ci-templates v-else-if="canSetCi" />
<gl-empty-state
v-else
title=""

View File

@ -1,9 +1,9 @@
<script>
import { GlAvatar, GlButton, GlCard, GlSprintf } from '@gitlab/ui';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { HELLO_WORLD_TEMPLATE_KEY } from '../../constants';
import { STARTER_TEMPLATE_NAME } from '~/pipeline_editor/constants';
import Tracking from '~/tracking';
export default {
components: {
@ -12,7 +12,8 @@ export default {
GlCard,
GlSprintf,
},
HELLO_WORLD_TEMPLATE_KEY,
mixins: [Tracking.mixin()],
STARTER_TEMPLATE_NAME,
i18n: {
cta: s__('Pipelines|Use template'),
testTemplates: {
@ -20,10 +21,10 @@ export default {
subtitle: s__(
'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.',
),
helloWorld: {
title: s__('Pipelines|“Hello world” with GitLab CI/CD'),
gettingStarted: {
title: s__('Pipelines|Get started with GitLab CI/CD'),
description: s__(
'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a simple pipeline that runs a “Hello world” script.',
'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline.',
),
},
},
@ -35,31 +36,30 @@ export default {
description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'),
},
},
inject: ['addCiYmlPath', 'suggestedCiTemplates'],
inject: ['pipelineEditorPath', 'suggestedCiTemplates'],
data() {
const templates = this.suggestedCiTemplates.map(({ name, logo }) => {
return {
name,
logo,
link: mergeUrlParams({ template: name }, this.addCiYmlPath),
link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
description: sprintf(this.$options.i18n.templates.description, { name }),
};
});
return {
templates,
helloWorldTemplateUrl: mergeUrlParams(
{ template: HELLO_WORLD_TEMPLATE_KEY },
this.addCiYmlPath,
gettingStartedTemplateUrl: mergeUrlParams(
{ template: STARTER_TEMPLATE_NAME },
this.pipelineEditorPath,
),
};
},
methods: {
trackEvent(template) {
const tracking = new ExperimentTracking('pipeline_empty_state_templates', {
this.track('template_clicked', {
label: template,
});
tracking.event('template_clicked');
},
},
};
@ -82,18 +82,18 @@ export default {
<div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
<div class="gl-mb-3">
<strong class="gl-text-gray-800 gl-mb-2">{{
$options.i18n.testTemplates.helloWorld.title
$options.i18n.testTemplates.gettingStarted.title
}}</strong>
</div>
<p class="gl-font-sm">{{ $options.i18n.testTemplates.helloWorld.description }}</p>
<p class="gl-font-sm">{{ $options.i18n.testTemplates.gettingStarted.description }}</p>
</div>
<gl-button
category="primary"
variant="confirm"
:href="helloWorldTemplateUrl"
:href="gettingStartedTemplateUrl"
data-testid="test-template-link"
@click="trackEvent($options.HELLO_WORLD_TEMPLATE_KEY)"
@click="trackEvent($options.STARTER_TEMPLATE_NAME)"
>
{{ $options.i18n.cta }}
</gl-button>

View File

@ -35,6 +35,3 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
// The key of the template is the same as the filename
export const HELLO_WORLD_TEMPLATE_KEY = 'Hello-World';

View File

@ -29,7 +29,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
errorStateSvgPath,
noPipelinesSvgPath,
newPipelinePath,
addCiYmlPath,
pipelineEditorPath,
suggestedCiTemplates,
canCreatePipeline,
hasGitlabCi,
@ -44,7 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
return new Vue({
el,
provide: {
addCiYmlPath,
pipelineEditorPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),

View File

@ -5,19 +5,20 @@ import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
export default () => {
$('body').on('click', '.js-usage-consent-action', (e) => {
$('body').on('click', '.js-service-ping-consent-action', (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // overwrite rails listener
const { url, checkEnabled, pingEnabled } = e.target.dataset;
const { url, checkEnabled, servicePingEnabled } = e.target.dataset;
const data = {
application_setting: {
version_check_enabled: parseBoolean(checkEnabled),
usage_ping_enabled: parseBoolean(pingEnabled),
service_ping_enabled: parseBoolean(servicePingEnabled),
},
};
const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message'));
const hideConsentMessage = () =>
hideFlash(document.querySelector('.service-ping-consent-message'));
axios
.put(url, data)

View File

@ -28,7 +28,8 @@ export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
export const USAGE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
export const USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST = 'static_site_editor_merge_requests';
export const SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
export const SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST =
'static_site_editor_merge_requests';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';

View File

@ -9,8 +9,8 @@ import {
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
TRACKING_ACTION_CREATE_COMMIT,
TRACKING_ACTION_CREATE_MERGE_REQUEST,
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '../constants';
@ -58,7 +58,7 @@ const createUpdateSourceFileAction = (sourcePath, content) => [
const commit = (projectId, message, branch, actions) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_COMMIT);
Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT);
return Api.commitMultiple(
projectId,
@ -74,7 +74,7 @@ const commit = (projectId, message, branch, actions) => {
const createMergeRequest = (projectId, title, description, sourceBranch, targetBranch) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST);
Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
return Api.createProjectMergeRequest(
projectId,

View File

@ -6,6 +6,7 @@ import {
GlDropdownSectionHeader,
GlLoadingIcon,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { DEBOUNCE_DELAY } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
@ -128,12 +129,12 @@ export default {
},
},
methods: {
handleInput({ data }) {
handleInput: debounce(function debouncedSearch({ data }) {
this.searchKey = data;
setTimeout(() => {
if (!this.suggestionsLoading) this.$emit('fetch-suggestions', data);
}, DEBOUNCE_DELAY);
},
if (!this.suggestionsLoading) {
this.$emit('fetch-suggestions', data);
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(activeTokenValue) {
// Make sure that;
// 1. Recently used values feature is enabled

View File

@ -7,6 +7,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
@ -30,7 +31,7 @@ export default {
data() {
return {
iterations: this.config.initialIterations || [],
loading: true,
loading: false,
};
},
computed: {
@ -38,7 +39,9 @@ export default {
return this.value.data;
},
activeIteration() {
return this.iterations.find((iteration) => iteration.id === Number(this.currentValue));
return this.iterations.find(
(iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue),
);
},
defaultIterations() {
return this.config.defaultIterations || DEFAULT_ITERATIONS;
@ -55,6 +58,9 @@ export default {
},
},
methods: {
getValue(iteration) {
return String(getIdFromGraphQLId(iteration.id));
},
fetchIterationBySearchTerm(searchTerm) {
const fetchPromise = this.config.fetchPath
? this.config.fetchIterations(this.config.fetchPath, searchTerm)
@ -102,7 +108,7 @@ export default {
<gl-filtered-search-suggestion
v-for="iteration in iterations"
:key="iteration.id"
:value="String(iteration.id)"
:value="getValue(iteration)"
>
{{ iteration.title }}
</gl-filtered-search-suggestion>

View File

@ -35,7 +35,7 @@ export default {
return {
milestones: this.config.initialMilestones || [],
defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: true,
loading: false,
};
},
computed: {
@ -60,11 +60,16 @@ export default {
},
methods: {
fetchMilestoneBySearchTerm(searchTerm = '') {
if (this.loading) {
return;
}
this.loading = true;
this.config
.fetchMilestones(searchTerm)
.then(({ data }) => {
this.milestones = data.sort(sortMilestonesByDueDate);
.then((response) => {
const data = Array.isArray(response) ? response : response.data;
this.milestones = data.slice().sort(sortMilestonesByDueDate);
})
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
.finally(() => {

View File

@ -1,3 +1,3 @@
.usage-data {
.service-data-payload-container {
max-height: 400px;
}

View File

@ -151,14 +151,14 @@ module AuthenticatesWithTwoFactor
def handle_two_factor_failure(user, method, message)
user.increment_failed_attempts!
log_failed_two_factor(user, method, request.remote_ip)
log_failed_two_factor(user, method)
Gitlab::AppLogger.info("Failed Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
flash.now[:alert] = message
prompt_for_two_factor(user)
end
def log_failed_two_factor(user, method, ip_address)
def log_failed_two_factor(user, method)
# overridden in EE
end

View File

@ -98,7 +98,7 @@ module AuthenticatesWithTwoFactorForAdminMode
def admin_handle_two_factor_failure(user, method, message)
user.increment_failed_attempts!
log_failed_two_factor(user, method, request.remote_ip)
log_failed_two_factor(user, method)
Gitlab::AppLogger.info("Failed Admin Mode Login: user=#{user.username} ip=#{request.remote_ip} method=#{method}")
flash.now[:alert] = message

View File

@ -15,7 +15,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
MAX_PER_PAGE = 20
feature_category :continuous_integration
feature_category :build_artifacts
def index
# Loading artifacts is very expensive in projects with a lot of artifacts.

View File

@ -8,7 +8,7 @@ class Projects::BuildArtifactsController < Projects::ApplicationController
before_action :extract_ref_name_and_path
before_action :validate_artifacts!, except: [:download]
feature_category :continuous_integration
feature_category :build_artifacts
def download
redirect_to download_project_job_artifacts_path(project, job, params: request.query_parameters)

View File

@ -49,7 +49,6 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format|
format.html do
enable_pipeline_empty_state_templates_experiment
enable_code_quality_walkthrough_experiment
enable_ci_runner_templates_experiment
end
@ -301,18 +300,6 @@ class Projects::PipelinesController < Projects::ApplicationController
params.permit(:scope, :username, :ref, :status)
end
def enable_pipeline_empty_state_templates_experiment
experiment(:pipeline_empty_state_templates, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user
e.exclude! if @pipelines_count.to_i > 0
e.exclude! if helpers.has_gitlab_ci?(project)
e.control {}
e.candidate {}
e.record!
end
end
def enable_code_quality_walkthrough_experiment
experiment(:code_quality_walkthrough, namespace: project.root_ancestor) do |e|
e.exclude! unless current_user

View File

@ -50,7 +50,7 @@ module Projects
end
def create_params
params.require(:project_access_token).permit(:name, :expires_at, scopes: [])
params.require(:project_access_token).permit(:name, :expires_at, :access_level, scopes: [])
end
def set_index_vars

View File

@ -127,6 +127,9 @@ module Types
field :timelogs, Types::TimelogType.connection_type, null: false,
description: 'Timelogs on the issue.'
field :project_id, GraphQL::INT_TYPE, null: false, method: :project_id,
description: 'ID of the issue project.'
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end

View File

@ -30,9 +30,7 @@ module Ci
project.has_ci? && project.builds_enabled?
end
# This list of templates is for the pipeline_empty_state_templates experiment
# and will be cleaned up with https://gitlab.com/gitlab-org/gitlab/-/issues/326299
def experiment_suggested_ci_templates
def suggested_ci_templates
[
{ name: 'Android', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/android.svg') },
{ name: 'Bash', logo: image_path('illustrations/third-party-logos/ci_cd-template-logos/bash.svg') },

View File

@ -181,7 +181,6 @@ module IssuesHelper
def issues_list_data(project, current_user, finder)
{
autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json),
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
calendar_path: url_for(safe_params.merge(calendar_url_options)),
can_bulk_update: can?(current_user, :admin_issue, project).to_s,
@ -201,8 +200,6 @@ module IssuesHelper
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project, issue: { milestone_id: finder.milestones.first.try(:id) }),
project_import_jira_path: project_import_jira_path(project),
project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
project_milestones_path: project_milestones_path(project, format: :json),
project_path: project.full_path,
quick_actions_help_path: help_page_path('user/project/quick_actions'),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),

View File

@ -43,7 +43,7 @@ module UserCalloutsHelper
end
def show_customize_homepage_banner?
!user_dismissed?(CUSTOMIZE_HOMEPAGE)
current_user.default_dashboard? && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end
def show_feature_flags_new_version?

View File

@ -808,6 +808,10 @@ class User < ApplicationRecord
# Instance methods
#
def default_dashboard?
dashboard == self.class.column_defaults['dashboard']
end
def full_path
username
end

View File

@ -135,10 +135,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
ide_edit_path(project, default_branch_or_main, 'README.md')
end
def add_ci_yml_path
add_special_file_path(file_name: ci_config_path_or_default)
end
def add_code_quality_ci_yml_path
add_special_file_path(
file_name: ci_config_path_or_default,

View File

@ -16,7 +16,7 @@ class AuditEventService
@author = build_author(author)
@entity = entity
@details = details
@ip_address = resolve_ip_address(@details, @author)
@ip_address = resolve_ip_address(@author)
end
# Builds the @details attribute for authentication
@ -64,9 +64,8 @@ class AuditEventService
end
end
def resolve_ip_address(details, author)
details[:ip_address].presence ||
Gitlab::RequestContext.instance.client_ip ||
def resolve_ip_address(author)
Gitlab::RequestContext.instance.client_ip ||
author.current_sign_in_ip
end

View File

@ -16,11 +16,12 @@ module ResourceAccessTokens
return error(user.errors.full_messages.to_sentence) unless user.persisted?
member = create_membership(resource, user)
access_level = params[:access_level] || Gitlab::Access::MAINTAINER
member = create_membership(resource, user, access_level)
unless member.persisted?
delete_failed_user(user)
return error("Could not provision maintainer access to project access token")
return error("Could not provision #{Gitlab::Access.human_access(access_level).downcase} access to project access token")
end
token_response = create_personal_access_token(user)
@ -102,8 +103,8 @@ module ResourceAccessTokens
Gitlab::Auth.resource_bot_scopes
end
def create_membership(resource, user)
resource.add_user(user, :maintainer, expires_at: params[:expires_at])
def create_membership(resource, user, access_level)
resource.add_user(user, access_level, expires_at: params[:expires_at])
end
def log_event(token)

View File

@ -1,4 +1,4 @@
- payload_class = 'js-usage-ping-payload'
- payload_class = 'js-service-ping-payload'
= form_for @application_setting, url: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
@ -17,23 +17,23 @@
.form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do
= _('Enable usage ping')
= _('Enable service ping')
.form-text.text-muted
- if can_be_configured
%p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
- usage_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: usage_ping_path }
%p.mb-2= s_('%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { usage_ping_link_start: usage_ping_link_start, usage_ping_link_end: '</a>'.html_safe }
- service_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'usage-ping')
- service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: service_ping_path }
%p.mb-2= s_('%{service_ping_link_start}Learn more%{service_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { service_ping_link_start: service_ping_link_start, service_ping_link_end: '</a>'.html_safe }
%button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
.gl-spinner.js-spinner.gl-display-none.gl-mr-2
.js-text.d-inline= _('Preview payload')
%pre.usage-data.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
%pre.service-data-payload-container.js-syntax-highlight.code.highlight.mt-2.d-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
- else
= _('The usage ping is disabled, and cannot be configured through this form.')
- deactivating_usage_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping')
- deactivating_usage_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_usage_ping_path }
= s_('For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}.').html_safe % { deactivating_usage_ping_link_start: deactivating_usage_ping_link_start, deactivating_usage_ping_link_end: '</a>'.html_safe }
= _('Service ping is disabled, and cannot be configured through this form.')
- deactivating_service_ping_path = help_page_path('development/usage_ping/index.md', anchor: 'disable-usage-ping')
- deactivating_service_ping_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: deactivating_service_ping_path }
= s_('For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}.').html_safe % { deactivating_service_ping_link_start: deactivating_service_ping_link_start, deactivating_service_ping_link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"

View File

@ -47,7 +47,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable or disable version check and usage ping.')
= _('Enable or disable version check and service ping.')
.settings-content
= render 'usage'

View File

@ -3,15 +3,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, dashboard_projects_url(rss_url_options), title: "All activity")
- if show_customize_homepage_banner?
= content_for :customize_homepage_banner do
.gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
.js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
callouts_path: user_callouts_path,
callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE,
track_label: 'home_page' } }
= render_dashboard_ultimate_trial(current_user)
- page_title _("Projects")

View File

@ -14,7 +14,7 @@
= render "layouts/header/service_templates_deprecation_callout"
= render "layouts/nav/classification_level_banner"
= yield :flash_message
= render "shared/ping_consent"
= render "shared/service_ping_consent"
= render_account_recovery_regular_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"

View File

@ -1,5 +1,6 @@
- breadcrumb_title _('Two-Factor Authentication')
- page_title _('Two-Factor Authentication'), _('Account')
- add_to_breadcrumbs(_('Two-Factor Authentication'), profile_account_path)
- add_to_breadcrumbs _('Account'), profile_account_path
- @content_class = "limit-container-width" unless fluid_layout
- webauthn_enabled = Feature.enabled?(:webauthn)

View File

@ -22,7 +22,7 @@
"ci-lint-path" => can?(current_user, :create_pipeline, @project) && project_ci_lint_path(@project),
"reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project),
"has-gitlab-ci" => has_gitlab_ci?(@project).to_s,
"add-ci-yml-path" => can?(current_user, :create_pipeline, @project) && @project.present(current_user: current_user).add_ci_yml_path,
"suggested-ci-templates" => experiment_suggested_ci_templates.to_json,
"pipeline-editor-path" => can?(current_user, :create_pipeline, @project) && project_ci_pipeline_editor_path(@project),
"suggested-ci-templates" => suggested_ci_templates.to_json,
"code-quality-page-path" => @project.present(current_user: current_user).add_code_quality_ci_yml_path,
"ci-runner-settings-path" => project_settings_ci_cd_path(@project, ci_runner_templates: true, anchor: 'js-runners-settings') } }

View File

@ -33,8 +33,11 @@
= render 'shared/access_tokens/form',
type: type,
path: project_settings_access_tokens_path(@project),
project: @project,
token: @project_access_token,
scopes: @scopes,
access_levels: ProjectMember.access_level_roles,
default_access_level: Gitlab::Access::MAINTAINER,
prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'limiting-scopes-of-a-project-access-token')

View File

@ -0,0 +1,10 @@
- if show_customize_homepage_banner?
= content_for :customize_homepage_banner do
.gl-display-none.gl-md-display-block{ class: "gl-pt-6! gl-pb-2! #{(container_class unless @no_container)} #{@content_class}" }
.js-customize-homepage-banner{ data: { svg_path: image_path('illustrations/monitoring/getting_started.svg'),
preferences_behavior_path: profile_preferences_path(anchor: 'behavior'),
callouts_path: user_callouts_path,
callouts_feature_id: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE,
track_label: 'home_page' } }
= render template: 'dashboard/projects/index'

View File

@ -1,5 +1,5 @@
- if session[:ask_for_usage_stats_consent]
.ping-consent-message.gl-alert.gl-alert-info
.service-ping-consent-message.gl-alert.gl-alert-info
= sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
%button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss') }
= sprite_icon('close', css_class: 'gl-icon')
@ -8,7 +8,7 @@
- settings_link = link_to _('your settings'), metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), class: 'gl-link'
= s_('To help improve GitLab, we would like to periodically %{docs_link}. This can be changed at any time in %{settings_link}.').html_safe % { docs_link: docs_link, settings_link: settings_link }
.gl-alert-actions.gl-mt-3
- send_usage_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- send_service_data_path = admin_application_settings_path(application_setting: { version_check_enabled: 1, usage_ping_enabled: 1 })
- not_now_path = admin_application_settings_path(application_setting: { version_check_enabled: 0, usage_ping_enabled: 0 })
= link_to _("Send usage data"), send_usage_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-ping-enabled': true, class: 'js-usage-consent-action alert-link btn gl-button btn-info'
= link_to _("Don't send usage data"), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-ping-enabled': false, class: 'js-usage-consent-action alert-link btn gl-button btn-default gl-ml-2'
= link_to _("Send service data"), send_service_data_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': true, 'data-service-ping-enabled': true, class: 'js-service-ping-consent-action alert-link btn gl-button btn-info'
= link_to _("Don't send service data"), not_now_path, 'data-url' => admin_application_settings_path, method: :put, 'data-check-enabled': false, 'data-service-ping-enabled': false, class: 'js-service-ping-consent-action alert-link btn gl-button btn-default gl-ml-2'

View File

@ -1,6 +1,9 @@
- title = local_assigns.fetch(:title, _('Add a %{type}') % { type: type })
- prefix = local_assigns.fetch(:prefix, :personal_access_token)
- help_path = local_assigns.fetch(:help_path)
- project = local_assigns.fetch(:project, false)
- access_levels = local_assigns.fetch(:access_levels, false)
- default_access_level = local_assigns.fetch(:default_access_level, false)
%h5.gl-mt-0
= title
@ -29,6 +32,14 @@
.js-access-tokens-expires-at
= f.text_field :expires_at, class: 'datepicker gl-datepicker-input form-control gl-form-input', placeholder: 'YYYY-MM-DD', autocomplete: 'off', data: { js_name: 'expiresAt' }
- if project
.row
.form-group.col-md-6
= label_tag :access_level, _("Select a role"), class: "label-bold"
.select-wrapper
= select_tag :"#{prefix}[access_level]", options_for_select(access_levels, default_access_level), class: "form-control project-access-select select-control", data: { qa_selector: 'access_token_access_level' }
= sprite_icon('chevron-down', css_class: "gl-icon gl-absolute gl-top-3 gl-right-3 gl-text-gray-200")
.form-group
%b{ :'aria-describedby' => 'select_scope_help_text' }
= s_('Tokens|Select scopes')

View File

@ -47,7 +47,7 @@
= link_to _('New issue'), button_path, class: 'gl-button btn btn-confirm', id: 'new_issue_link'
- if show_import_button
.js-csv-import-export-buttons{ data: { show_import_button: show_import_button.to_s, issuable_type: issuable_type, import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), container_class: 'gl-display-inline-flex gl-vertical-align-middle', show_label: 'true' } }
.js-csv-import-export-buttons{ data: { show_import_button: 'true', issuable_type: issuable_type, import_csv_issues_path: import_csv_namespace_project_issues_path, can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes), container_class: 'gl-display-inline-flex gl-vertical-align-middle', show_label: 'true' } }
%hr
%p.gl-text-center.gl-mb-0
%strong

View File

@ -17,6 +17,7 @@
- auto_devops
- backup_restore
- boards
- build_artifacts
- chatops
- cloud_native_installation
- cluster_cost_management
@ -83,12 +84,14 @@
- mlops
- mobile_signing_deployment
- navigation
- not_owned
- omnibus_package
- on_call_schedule_management
- onboarding
- package_registry
- pages
- performance_testing
- pipeline_abuse_prevention
- pipeline_authoring
- planning_analytics
- privacy_control_center
@ -110,6 +113,7 @@
- self_monitoring
- serverless
- service_desk
- service_ping
- sharding
- snippets
- source_code_management
@ -127,7 +131,6 @@
- value_stream_management
- vulnerability_database
- vulnerability_management
- web_firewall
- web_ide
- wiki
- workflow_automation

View File

@ -1,8 +0,0 @@
---
name: pipeline_empty_state_templates
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57286
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326299
milestone: '13.11'
type: experiment
group: group::activation
default_enabled: false

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddLastSyncedAtToLicenses < ActiveRecord::Migration[6.1]
def change
add_column :licenses, :last_synced_at, :datetime_with_timezone
end
end

View File

@ -38,6 +38,12 @@ class FinalizePushEventPayloadsBigintConversion < ActiveRecord::Migration[6.1]
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:event_id_convert_to_bigint)} TO #{quote_column_name(:event_id)}"
execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:event_id_convert_to_bigint)}"
# We need to update the trigger function in order to make PostgreSQL to
# regenerate the execution plan for it. This is to avoid type mismatch errors like
# "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name(:event_id, :event_id_convert_to_bigint)
execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
# Swap defaults
change_column_default TABLE_NAME, :event_id, nil
change_column_default TABLE_NAME, :event_id_convert_to_bigint, 0

View File

@ -0,0 +1 @@
705c4cf981f1929f8e8e4d8a8a3c12613516d65e59c71ac79048224cd97c47cc

View File

@ -14461,7 +14461,8 @@ CREATE TABLE licenses (
data text NOT NULL,
created_at timestamp without time zone,
updated_at timestamp without time zone,
cloud boolean DEFAULT false
cloud boolean DEFAULT false,
last_synced_at timestamp with time zone
);
CREATE SEQUENCE licenses_id_seq

View File

@ -279,6 +279,23 @@ p.each do |project|
end
```
### Bulk update push rules for _all_ projects
For example, enable **Check whether the commit author is a GitLab user** and **Do not allow users to remove Git tags with `git push`** checkboxes, and create a filter for allowing commits from a specific e-mail domain only:
``` ruby
Project.find_each do |p|
pr = p.push_rule || PushRule.new(project: p)
# Check whether the commit author is a GitLab user
pr.member_check = true
# Do not allow users to remove Git tags with `git push`
pr.deny_delete_tag = true
# Commit author's email
pr.author_email_regex = '@domain\.com$'
pr.save!
end
```
## Bulk update to change all the Jira integrations to Jira instance-level values
To change all Jira project to use the instance-level integration settings:

View File

@ -164,7 +164,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
### Add note to existing issue thread
Adds a new note to the thread. This can also [create a thread from a single comment](../user/discussions/#start-a-thread-by-replying-to-a-standard-comment).
Adds a new note to the thread. This can also [create a thread from a single comment](../user/discussions/#create-a-thread-by-replying-to-a-standard-comment).
**WARNING**
Notes can be added to other items than comments, such as system notes, making them threads.
@ -581,7 +581,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitla
### Add note to existing epic thread
Adds a new note to the thread. This can also
[create a thread from a single comment](../user/discussions/#start-a-thread-by-replying-to-a-standard-comment).
[create a thread from a single comment](../user/discussions/#create-a-thread-by-replying-to-a-standard-comment).
```plaintext
POST /groups/:id/epics/:epic_id/discussions/:discussion_id/notes
@ -966,7 +966,7 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
### Add note to existing merge request thread
Adds a new note to the thread. This can also
[create a thread from a single comment](../user/discussions/#start-a-thread-by-replying-to-a-standard-comment).
[create a thread from a single comment](../user/discussions/#create-a-thread-by-replying-to-a-standard-comment).
```plaintext
POST /projects/:id/merge_requests/:merge_request_iid/discussions/:discussion_id/notes

View File

@ -8805,6 +8805,7 @@ Relationship between an epic and an issue.
| <a id="epicissuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="epicissuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="epicissueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="epicissueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="epicissuerelationpath"></a>`relationPath` | [`String`](#string) | URI path of the epic-issue relation. |
| <a id="epicissuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
| <a id="epicissueseverity"></a>`severity` | [`IssuableSeverity`](#issuableseverity) | Severity level of the incident. |
@ -9853,6 +9854,7 @@ Returns [`VulnerabilitySeveritiesCount`](#vulnerabilityseveritiescount).
| <a id="issuemovedto"></a>`movedTo` | [`Issue`](#issue) | Updated Issue after it got moved to another project. |
| <a id="issuenotes"></a>`notes` | [`NoteConnection!`](#noteconnection) | All notes on this noteable. (see [Connections](#connections)) |
| <a id="issueparticipants"></a>`participants` | [`UserCoreConnection`](#usercoreconnection) | List of participants in the issue. (see [Connections](#connections)) |
| <a id="issueprojectid"></a>`projectId` | [`Int!`](#int) | ID of the issue project. |
| <a id="issuerelativeposition"></a>`relativePosition` | [`Int`](#int) | Relative position of the issue (used for positioning in epic tree and issue boards). |
| <a id="issueseverity"></a>`severity` | [`IssuableSeverity`](#issuableseverity) | Severity level of the incident. |
| <a id="issuesladueat"></a>`slaDueAt` | [`Time`](#time) | Timestamp of when the issue SLA expires. |

View File

@ -59,12 +59,13 @@ POST projects/:id/access_tokens
| `id` | integer or string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
| `name` | String | yes | The name of the project access token |
| `scopes` | `Array[String]` | yes | [List of scopes](../user/project/settings/project_access_tokens.md#limiting-scopes-of-a-project-access-token) |
| `access_level` | Integer | no | A valid access level. Default value is 40 (Maintainer). Other allowed values are 10 (Guest), 20 (Reporter), and 30 (Developer). |
| `expires_at` | Date | no | The token expires at midnight UTC on that date |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type:application/json" \
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31" }' \
--data '{ "name":"test_token", "scopes":["api", "read_repository"], "expires_at":"2021-01-31", "access_level": 30 }' \
"https://gitlab.example.com/api/v4/projects/<project_id>/access_tokens"
```
@ -82,7 +83,7 @@ curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" \
"id" : 58,
"expires_at" : "2021-01-31",
"token" : "D4y...Wzr",
"access_level": 40
"access_level": 30
}
```

View File

@ -115,7 +115,7 @@ pre-push:
skip: false
```
For more information, check out [this Lefthook documentation section](https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md#skipping-commands).
For more information, check out [Lefthook documentation Skipping commands section](https://github.com/evilmartians/lefthook/blob/master/docs/full_guide.md#skipping-commands).
## Ruby, Rails, RSpec

View File

@ -19,7 +19,7 @@ feature is still under development, and is not ready for production use.
By default, GitLab is configured to use only one main database. To
opt-in to use a main database, and CI database, modify the
`config/database.yml` file to have a `primary` and a `ci` database
`config/database.yml` file to have a `main` and a `ci` database
configurations. For example, given a `config/database.yml` like below:
```yaml
@ -48,7 +48,7 @@ Edit the `config/database.yml` to look like this:
```yaml
development:
primary:
main:
adapter: postgresql
encoding: unicode
database: gitlabhq_development
@ -69,7 +69,7 @@ development:
statement_timeout: 120s
test: &test
primary:
main:
adapter: postgresql
encoding: unicode
database: gitlabhq_test
@ -90,9 +90,6 @@ test: &test
statement_timeout: 120s
```
Note that we use `primary` in the `config/database.yml` to refer to the main
database. This is to match the default name Rails has.
### Migrations
Any migrations that affect `Ci::BaseModel` models

View File

@ -392,8 +392,12 @@ end
If a large number of background jobs get scheduled at once, queueing of jobs may
occur while jobs wait for a worker node to be become available. This is normal
and gives the system resilience by allowing it to gracefully handle spikes in
traffic. Some jobs, however, are more sensitive to latency than others. Examples
of these jobs include:
traffic. Some jobs, however, are more sensitive to latency than others.
In general, latency-sensitive jobs perform operations that a user could
reasonably expect to happen synchronously, rather than asynchronously in a
background worker. A common example is a write following an action. Examples of
these jobs include:
1. A job which updates a merge request following a push to a branch.
1. A job which invalidates a cache of known branches for a project after a push

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 496 KiB

View File

@ -34,6 +34,50 @@ You can create comments in places like:
Each object can have as many as 5,000 comments.
## Create a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9.
When you reply to a standard comment, you create a thread.
Prerequisites:
- You must have at least the [Guest role](../permissions.md#project-members-permissions).
- You must be in an issue, merge request, or epic. Commits and snippets threads are not supported.
To create a thread by replying to a comment:
1. On the top right of the comment, select **{comment}** (**Reply to comment**).
![Reply to comment button](img/reply_to_comment_button.png)
The reply area is displayed.
1. Type your reply.
1. Select **Comment** or **Add comment now** (depending on where in the UI you are replying).
The top comment is converted to a thread.
## Create a thread without replying to a comment
You can create a thread without replying to a standard comment.
Prerequisites:
- You must have at least the [Guest role](../permissions.md#project-members-permissions).
- You must be in an issue, commit, snippet, or merge request.
To create a thread:
1. Type a comment.
1. Below the comment, to the right of the **Comment** button, select the down arrow (**{chevron-down}**).
1. From the list, select **Start thread**.
1. Select **Start thread** again.
A threaded comment is created.
![Thread comment](img/discussion_comment.png)
## Reply to a comment by sending email
If you have ["reply by email"](../../administration/reply_by_email.md) configured,
@ -61,7 +105,7 @@ Thread resolution helps keep track of progress during planning or code review.
Every thread in merge requests, commits, commit diffs, and
snippets is initially displayed as unresolved. They can then be individually resolved by anyone
with at least Developer access to the project or by the author of the change being reviewed.
with at least the Developer role to the project or by the author of the change being reviewed.
If the thread has been resolved and a non-member un-resolves their own response,
this also unresolves the discussion thread.
If the non-member then resolves this same response, this resolves the discussion thread.
@ -188,33 +232,19 @@ From now on, any threads on a diff are resolved by default if a push
makes that diff section outdated. Threads on lines that don't change and
top-level resolvable threads are not automatically resolved.
## Commit threads
## Add a comment to a commit
You can add comments and threads to a particular commit under your
project's **Repository > Commits**.
You can add comments and threads to a particular commit.
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Repository > Commits**.
1. Below the commits, in the **Comment** field, enter a comment.
1. Select **Comment** or select the down arrow (**{chevron-down}**) to select **Start thread**.
WARNING:
Threads created this way are lost if the commit ID changes after a
force push.
## Threaded discussions
While resolvable threads are only available to merge request diffs,
threads can also be added without a diff. You can start a specific
thread which looks like a thread, on issues, commits, snippets, and
merge requests.
To start a threaded discussion, select the **Comment** button toggle dropdown,
select **Start thread**, and then select **Start thread** when you're ready to
post the comment.
![Comment type toggle](img/comment_type_toggle.gif)
This posts a comment with a single thread to allow you to discuss specific
comments in greater detail.
![Thread comment](img/discussion_comment.png)
## Image threads
Sometimes a thread is revolved around an image. With image threads,
@ -320,27 +350,6 @@ After you select one of the filters in a given issue or merge request, GitLab sa
your preference, so that it persists when you visit the same page again
from any device you're logged into.
## Start a thread by replying to a standard comment
> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9
To reply to a standard (non-thread) comment, you can use the **Reply to comment** button.
![Reply to comment button](img/reply_to_comment_button.png)
The **Reply to comment** button is only displayed if you have permissions to reply to an existing thread, or start a thread from a standard comment.
Selecting the **Reply to comment** button brings the reply area into focus and you can type your reply.
![Reply to comment feature](img/reply_to_comment.gif)
Replying to a non-thread comment converts the non-thread comment to a
thread after the reply is submitted. This conversion is considered an edit
to the original comment, so a note about when it was last edited appears underneath it.
This feature exists only for issues, merge requests, and epics. Commits, snippets, and merge request diff threads are
not supported yet.
## Assign an issue to the commenting user
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/191455) in GitLab 13.1.

View File

@ -288,7 +288,7 @@ supports [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown).
After you write a comment, you can:
- Click **Comment** to publish your comment.
- Choose **Start thread** from the dropdown list and start a new [thread](../../discussions/index.md#threaded-discussions)
- Choose **Start thread** from the dropdown list and start a new [thread](../../discussions/index.md#create-a-thread-without-replying-to-a-comment)
in that issue's main thread to discuss specific points. This invites other participants
to reply directly to your thread, keeping related comments grouped together.

View File

@ -29,6 +29,7 @@ For examples of how you can use a project access token to authenticate with the
1. Navigate to the project you would like to create an access token for.
1. In the **Settings** menu choose **Access Tokens**.
1. Choose a name and optional expiry date for the token.
1. Choose the access level the token should have in the project.
1. Choose the [desired scopes](#limiting-scopes-of-a-project-access-token).
1. Click the **Create project access token** button.
1. Save the project access token somewhere safe. Once you leave or refresh
@ -42,7 +43,7 @@ For examples of how you can use a project access token to authenticate with the
Project bot users are [GitLab-created service accounts](../../../subscriptions/self_managed/index.md#billable-users) and do not count as licensed seats.
For each project access token created, a bot user is created and added to the project with
[Maintainer level permissions](../../permissions.md#project-members-permissions).
the [specified level permissions](../../permissions.md#project-members-permissions).
For the bot:

View File

@ -4,7 +4,7 @@ module API
class JobArtifacts < ::API::Base
before { authenticate_non_get! }
feature_category :continuous_integration
feature_category :build_artifacts
# EE::API::JobArtifacts would override the following helpers
helpers do

View File

@ -58,6 +58,7 @@ module API
requires :id, type: String, desc: "The #{source_type} ID"
requires :name, type: String, desc: "Resource access token name"
requires :scopes, type: Array[String], desc: "The permissions of the token"
optional :access_level, type: Integer, desc: "The access level of the token in the project"
optional :expires_at, type: Date, desc: "The expiration date of the token"
end
post ':id/access_tokens' do

View File

@ -3,8 +3,9 @@
module Backup
# Backup and restores repositories using gitaly-backup
class GitalyBackup
def initialize(progress)
def initialize(progress, parallel: nil)
@progress = progress
@parallel = parallel
end
def start(type)
@ -19,8 +20,11 @@ module Backup
raise Error, "unknown backup type: #{type}"
end
args = []
args += ['-parallel', @parallel.to_s] if type == :create && @parallel
@read_io, @write_io = IO.pipe
@pid = Process.spawn(bin_path, command, '-path', backup_repos_path, in: @read_io, out: progress)
@pid = Process.spawn(bin_path, command, '-path', backup_repos_path, *args, in: @read_io, out: @progress)
end
def wait
@ -48,9 +52,11 @@ module Backup
}.merge(Gitlab::GitalyClient.connection_data(repository.storage)).to_json)
end
private
def parallel_enqueue?
false
end
attr_reader :progress
private
def started?
@pid.present?

View File

@ -44,6 +44,10 @@ module Backup
end
end
def parallel_enqueue?
true
end
private
attr_reader :progress

View File

@ -12,7 +12,10 @@ module Backup
def dump(max_concurrency:, max_storage_concurrency:)
strategy.start(:create)
if max_concurrency <= 1 && max_storage_concurrency <= 1
# gitaly-backup is designed to handle concurrency on its own. So we want
# to avoid entering the buggy concurrency code here when gitaly-backup
# is enabled.
if (max_concurrency <= 1 && max_storage_concurrency <= 1) || !strategy.parallel_enqueue?
return enqueue_consecutive
end

View File

@ -1,14 +0,0 @@
# To contribute improvements to CI/CD templates, please follow the Development guide at:
# https://docs.gitlab.com/ee/development/cicd/templates.html
# This specific template is located at:
# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Grails.gitlab-ci.yml
#
# This file is a template demonstrating the `script` keyword.
# Learn more about this keyword here: https://docs.gitlab.com/ee/ci/yaml/README.html#script
# After committing this template, visit CI/CD > Jobs to see the script output.
job:
script:
# provide a shell script as argument for this keyword.
- echo "Hello World"

View File

@ -298,7 +298,8 @@ namespace :gitlab do
def repository_backup_strategy
if Feature.enabled?(:gitaly_backup)
Backup::GitalyBackup.new(progress)
max_concurrency = ENV['GITLAB_BACKUP_MAX_CONCURRENCY'].presence
Backup::GitalyBackup.new(progress, parallel: max_concurrency)
else
Backup::GitalyRpcBackup.new(progress)
end

View File

@ -839,6 +839,9 @@ msgid_plural "%{securityScanner} results are not available because a pipeline ha
msgstr[0] ""
msgstr[1] ""
msgid "%{service_ping_link_start}Learn more%{service_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
msgid "%{size} %{unit}"
msgstr ""
@ -972,9 +975,6 @@ msgstr ""
msgid "%{total} warnings found: showing first %{warningsDisplayed}"
msgstr ""
msgid "%{usage_ping_link_start}Learn more%{usage_ping_link_end} about what information is shared with GitLab Inc."
msgstr ""
msgid "%{userName} (cannot merge)"
msgstr ""
@ -11482,7 +11482,7 @@ msgstr ""
msgid "Don't paste the private part of the GPG key. Paste the public part which begins with '-----BEGIN PGP PUBLIC KEY BLOCK-----'."
msgstr ""
msgid "Don't send usage data"
msgid "Don't send service data"
msgstr ""
msgid "Don't show again"
@ -12034,7 +12034,7 @@ msgstr ""
msgid "Enable or disable the Pseudonymizer data collection."
msgstr ""
msgid "Enable or disable version check and usage ping."
msgid "Enable or disable version check and service ping."
msgstr ""
msgid "Enable protected paths rate limit"
@ -12052,6 +12052,9 @@ msgstr ""
msgid "Enable reCAPTCHA, Invisible Captcha, Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}"
msgstr ""
msgid "Enable service ping"
msgstr ""
msgid "Enable shared runners for all projects and subgroups in this group."
msgstr ""
@ -12076,9 +12079,6 @@ msgstr ""
msgid "Enable unauthenticated request rate limit"
msgstr ""
msgid "Enable usage ping"
msgstr ""
msgid "Enable version check"
msgstr ""
@ -14065,7 +14065,7 @@ msgstr ""
msgid "For more information, see the File Hooks documentation."
msgstr ""
msgid "For more information, see the documentation on %{deactivating_usage_ping_link_start}deactivating the usage ping%{deactivating_usage_ping_link_end}."
msgid "For more information, see the documentation on %{deactivating_service_ping_link_start}deactivating service ping%{deactivating_service_ping_link_end}."
msgstr ""
msgid "Forgot your password?"
@ -23924,10 +23924,10 @@ msgstr ""
msgid "Pipelines|Editor"
msgstr ""
msgid "Pipelines|Get familiar with GitLab CI/CD syntax by starting with a simple pipeline that runs a “Hello world” script."
msgid "Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline."
msgstr ""
msgid "Pipelines|Get started with CI/CD"
msgid "Pipelines|Get started with GitLab CI/CD"
msgstr ""
msgid "Pipelines|GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time consuming tasks, so you can spend more time creating."
@ -24068,9 +24068,6 @@ msgstr ""
msgid "Pipelines|parent"
msgstr ""
msgid "Pipelines|“Hello world” with GitLab CI/CD"
msgstr ""
msgid "Pipeline|Actions"
msgstr ""
@ -29336,7 +29333,7 @@ msgstr ""
msgid "Send report"
msgstr ""
msgid "Send usage data"
msgid "Send service data"
msgstr ""
msgid "Sentry API URL"
@ -29462,6 +29459,9 @@ msgstr ""
msgid "Service URL"
msgstr ""
msgid "Service ping is disabled, and cannot be configured through this form."
msgstr ""
msgid "ServiceDesk|Enable Service Desk"
msgstr ""
@ -32113,7 +32113,7 @@ msgstr ""
msgid "The Advanced Search in GitLab is a powerful search service that saves you time. Instead of creating duplicate code and wasting time, you can now search for code within other teams that can help your own project."
msgstr ""
msgid "The Compliance Dashboard gives you the ability to see a group's merge request activity by providing a high-level view for all projects in the group."
msgid "The Compliance Report captures merged changes that violate compliance best practices."
msgstr ""
msgid "The GitLab subscription service (customers.gitlab.com) is currently experiencing an outage. You can monitor the status and get updates at %{linkStart}status.gitlab.com%{linkEnd}."
@ -32541,9 +32541,6 @@ msgstr ""
msgid "The uploaded file was invalid. Supported file extensions are %{extensions}."
msgstr ""
msgid "The usage ping is disabled, and cannot be configured through this form."
msgstr ""
msgid "The user is being deleted."
msgstr ""

View File

@ -59,7 +59,7 @@
"@gitlab/favicon-overlay": "2.0.0",
"@gitlab/svgs": "1.201.0",
"@gitlab/tributejs": "1.0.0",
"@gitlab/ui": "30.0.0",
"@gitlab/ui": "30.0.1",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "6.1.3-2",
"@rails/ujs": "6.1.3-2",

View File

@ -284,10 +284,6 @@ RSpec.describe Projects::PipelinesController do
subject { project.namespace }
context 'pipeline_empty_state_templates experiment' do
it_behaves_like 'tracks assignment and records the subject', :pipeline_empty_state_templates, :namespace
end
context 'code_quality_walkthrough experiment' do
it_behaves_like 'tracks assignment and records the subject', :code_quality_walkthrough, :namespace
end

View File

@ -61,6 +61,14 @@ RSpec.describe Projects::Settings::AccessTokensController do
expect { subject }.not_to change { User.count }
end
end
context 'with custom access level' do
let(:access_token_params) { { name: 'Nerd bot', scopes: ["api"], expires_at: Date.today + 1.month, access_level: 20 } }
subject { post :create, params: { namespace_id: project.namespace, project_id: project }.merge(project_access_token: access_token_params) }
it_behaves_like 'project access tokens available #create'
end
end
describe '#revoke', :sidekiq_inline do

View File

@ -128,11 +128,31 @@ RSpec.describe RootController do
end
end
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
get :index
context 'who uses the default dashboard setting', :aggregate_failures do
render_views
expect(response).to render_template 'dashboard/projects/index'
context 'with customize homepage banner' do
it 'renders the default dashboard' do
get :index
expect(response).to render_template 'root/index'
expect(response.body).to have_css('.js-customize-homepage-banner')
end
end
context 'without customize homepage banner' do
before do
Users::DismissUserCalloutService.new(
container: nil, current_user: user, params: { feature_name: UserCalloutsHelper::CUSTOMIZE_HOMEPAGE }
).execute
end
it 'renders the default dashboard' do
get :index
expect(response).to render_template 'root/index'
expect(response.body).not_to have_css('.js-customize-homepage-banner')
end
end
end
end

View File

@ -567,7 +567,7 @@ RSpec.describe 'Admin updates settings' do
wait_for_requests
expect(page).to have_selector '.js-usage-ping-payload'
expect(page).to have_selector '.js-service-ping-payload'
expect(page).to have_button 'Hide payload'
expect(page).to have_content expected_payload_content
end

View File

@ -18,12 +18,6 @@ RSpec.describe 'Dashboard Projects' do
end
end
it 'shows the customize banner', :js do
visit dashboard_projects_path
expect(page).to have_content('Do you want to customize this page?')
end
context 'when user has access to the project' do
it 'shows role badge' do
visit dashboard_projects_path

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Root path' do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
before do
project.add_developer(user)
sign_in(user)
end
it 'shows the customize banner', :js do
visit root_path
expect(page).to have_content('Do you want to customize this page?')
end
end

View File

@ -783,7 +783,7 @@ RSpec.describe 'Pipelines', :js do
end
it 'renders empty state' do
expect(page).to have_content 'Build with confidence'
expect(page).to have_content 'Use a sample CI/CD template'
end
end
end

View File

@ -27,7 +27,7 @@ RSpec.describe 'Usage stats consent' do
expect(page).to have_content(message)
click_link 'Send usage data'
click_link 'Send service data'
expect(page).not_to have_content(message)
expect(page).to have_content('Application settings saved successfully')

View File

@ -1481,7 +1481,7 @@ describe('Api', () => {
'Content-Type': 'application/json',
};
describe('when usage data increment counter is called with feature flag disabled', () => {
describe('when service data increment counter is called with feature flag disabled', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: false };
});
@ -1495,7 +1495,7 @@ describe('Api', () => {
});
});
describe('when usage data increment counter is called', () => {
describe('when service data increment counter is called', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: true };
});
@ -1526,7 +1526,7 @@ describe('Api', () => {
window.gon.current_user_id = 1;
});
describe('when usage data increment unique users is called with feature flag disabled', () => {
describe('when service data increment unique users is called with feature flag disabled', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: false };
});
@ -1541,7 +1541,7 @@ describe('Api', () => {
});
});
describe('when usage data increment unique users is called', () => {
describe('when service data increment unique users is called', () => {
beforeEach(() => {
gon.features = { ...gon.features, usageDataApi: true };
});

View File

@ -20,7 +20,7 @@ import {
import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_USAGE_PING_EVENT_TYPES,
DESIGN_SERVICE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
import createFlash from '~/flash';
import mockAllVersions from '../../mock_data/all_versions';
@ -391,7 +391,7 @@ describe('Design management design index page', () => {
});
describe('with usage_data_design_action enabled', () => {
it('tracks design view usage ping', () => {
it('tracks design view service ping', () => {
createComponent(
{ loading: true },
{
@ -402,13 +402,13 @@ describe('Design management design index page', () => {
);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledWith(
DESIGN_USAGE_PING_EVENT_TYPES.DESIGN_ACTION,
DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION,
);
});
});
describe('with usage_data_design_action disabled', () => {
it("doesn't track design view usage ping", () => {
it("doesn't track design view service ping", () => {
createComponent({ loading: true });
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(0);
});

View File

@ -15,6 +15,7 @@ import {
urlParams,
} from 'jest/issues_list/mock_data';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
@ -54,19 +55,18 @@ describe('IssuesListApp component', () => {
localVue.use(VueApollo);
const defaultProvide = {
autocompleteUsersPath: 'autocomplete/users/path',
calendarPath: 'calendar/path',
canBulkUpdate: false,
emptyStateSvgPath: 'empty-state.svg',
exportCsvPath: 'export/csv/path',
hasBlockedIssuesFeature: true,
hasIssueWeightsFeature: true,
hasIterationsFeature: true,
hasProjectIssues: true,
isSignedIn: false,
issuesPath: 'path/to/issues',
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
projectLabelsPath: 'project/labels/path',
projectPath: 'path/to/project',
rssPath: 'rss/path',
showNewIssueLink: true,
@ -545,9 +545,13 @@ describe('IssuesListApp component', () => {
});
it('renders all tokens', () => {
const preloadedAuthors = [
{ ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] },
{ type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] },
{ type: TOKEN_TYPE_AUTHOR, preloadedAuthors },
{ type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MY_REACTION },

View File

@ -12,6 +12,7 @@ import PipelineEditorMessages from '~/pipeline_editor/components/ui/pipeline_edi
import { COMMIT_SUCCESS, COMMIT_FAILURE } from '~/pipeline_editor/constants';
import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.graphql';
import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
import getTemplate from '~/pipeline_editor/graphql/queries/get_starter_template.query.graphql';
import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import {
@ -47,6 +48,7 @@ describe('Pipeline editor app component', () => {
let mockApollo;
let mockBlobContentData;
let mockCiConfigData;
let mockGetTemplate;
const createComponent = ({ blobLoading = false, options = {}, provide = {} } = {}) => {
wrapper = shallowMount(PipelineEditorApp, {
@ -81,6 +83,7 @@ describe('Pipeline editor app component', () => {
const handlers = [
[getBlobContent, mockBlobContentData],
[getCiConfigData, mockCiConfigData],
[getTemplate, mockGetTemplate],
];
mockApollo = createMockApollo(handlers);
@ -112,6 +115,7 @@ describe('Pipeline editor app component', () => {
beforeEach(() => {
mockBlobContentData = jest.fn();
mockCiConfigData = jest.fn();
mockGetTemplate = jest.fn();
});
afterEach(() => {
@ -318,4 +322,29 @@ describe('Pipeline editor app component', () => {
expect(findEditorHome().exists()).toBe(true);
});
});
describe('when a template parameter is present in the URL', () => {
const { location } = window;
beforeEach(() => {
delete window.location;
window.location = new URL('https://localhost?template=Android');
});
afterEach(() => {
window.location = location;
});
it('renders the given template', async () => {
await createComponentWithApollo();
expect(mockGetTemplate).toHaveBeenCalledWith({
projectPath: mockProjectFullPath,
templateName: 'Android',
});
expect(findEmptyState().exists()).toBe(false);
expect(findTextEditor().exists()).toBe(true);
});
});
});

View File

@ -1,14 +1,21 @@
import '~/commons';
import { mount } from '@vue/test-utils';
import EmptyState from '~/pipelines/components/pipelines_list/empty_state.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
describe('Pipelines Empty State', () => {
let wrapper;
const findIllustration = () => wrapper.find('img');
const findButton = () => wrapper.find('a');
const pipelinesCiTemplates = () => wrapper.findComponent(PipelinesCiTemplates);
const createWrapper = (props = {}) => {
wrapper = mount(EmptyState, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
},
propsData: {
emptyStateSvgPath: 'foo.svg',
canSetCi: true,
@ -27,27 +34,8 @@ describe('Pipelines Empty State', () => {
wrapper = null;
});
it('should render empty state SVG', () => {
expect(findIllustration().attributes('src')).toBe('foo.svg');
});
it('should render empty state header', () => {
expect(wrapper.text()).toContain('Build with confidence');
});
it('should render empty state information', () => {
expect(wrapper.text()).toContain(
'GitLab CI/CD can automatically build, test, and deploy your code. Let GitLab take care of time',
'consuming tasks, so you can spend more time creating',
);
});
it('should render button with help path', () => {
expect(findButton().attributes('href')).toBe('/help/ci/quick_start/index.md');
});
it('should render button text', () => {
expect(findButton().text()).toBe('Get started with CI/CD');
it('should render the CI/CD templates', () => {
expect(pipelinesCiTemplates()).toExist();
});
});

View File

@ -1,30 +1,25 @@
import '~/commons';
import { shallowMount } from '@vue/test-utils';
import ExperimentTracking from '~/experimentation/experiment_tracking';
import { mockTracking } from 'helpers/tracking_helper';
import PipelinesCiTemplate from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
const addCiYmlPath = "/-/new/main?commit_message='Add%20.gitlab-ci.yml'";
const pipelineEditorPath = '/-/ci/editor';
const suggestedCiTemplates = [
{ name: 'Android', logo: '/assets/illustrations/logos/android.svg' },
{ name: 'Bash', logo: '/assets/illustrations/logos/bash.svg' },
{ name: 'C++', logo: '/assets/illustrations/logos/c_plus_plus.svg' },
];
jest.mock('~/experimentation/experiment_tracking');
describe('Pipelines CI Templates', () => {
let wrapper;
const GlEmoji = { template: '<img/>' };
let trackingSpy;
const createWrapper = () => {
return shallowMount(PipelinesCiTemplate, {
provide: {
addCiYmlPath,
pipelineEditorPath,
suggestedCiTemplates,
},
stubs: {
GlEmoji,
},
});
};
@ -44,9 +39,9 @@ describe('Pipelines CI Templates', () => {
wrapper = createWrapper();
});
it('links to the hello world template', () => {
it('links to the getting started template', () => {
expect(findTestTemplateLinks().at(0).attributes('href')).toBe(
addCiYmlPath.concat('&template=Hello-World'),
pipelineEditorPath.concat('?template=Getting-Started'),
);
});
});
@ -68,7 +63,7 @@ describe('Pipelines CI Templates', () => {
it('links to the correct template', () => {
expect(findTemplateLinks().at(0).attributes('href')).toBe(
addCiYmlPath.concat('&template=Android'),
pipelineEditorPath.concat('?template=Android'),
);
});
@ -88,24 +83,25 @@ describe('Pipelines CI Templates', () => {
describe('tracking', () => {
beforeEach(() => {
wrapper = createWrapper();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('sends an event when template is clicked', () => {
findTemplateLinks().at(0).vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: 'Android',
});
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
it('sends an event when Hello-World template is clicked', () => {
it('sends an event when Getting-Started template is clicked', () => {
findTestTemplateLinks().at(0).vm.$emit('click');
expect(ExperimentTracking).toHaveBeenCalledWith('pipeline_empty_state_templates', {
label: 'Hello-World',
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
label: 'Getting-Started',
});
expect(ExperimentTracking.prototype.event).toHaveBeenCalledWith('template_clicked');
});
});
});

View File

@ -12,6 +12,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import { RAW_TEXT_WARNING } from '~/pipelines/constants';
import Store from '~/pipelines/stores/pipelines_store';
@ -82,6 +83,10 @@ describe('Pipelines', () => {
const createComponent = (props = defaultProps) => {
wrapper = extendedWrapper(
mount(PipelinesComponent, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
},
propsData: {
store: new Store(),
projectId: mockProjectId,
@ -551,52 +556,74 @@ describe('Pipelines', () => {
await waitForPromises();
});
it('renders empty state', () => {
expect(findEmptyState().text()).toContain('Build with confidence');
expect(findEmptyState().text()).toContain(
'GitLab CI/CD can automatically build, test, and deploy your code.',
);
expect(findEmptyState().find(GlButton).text()).toBe('Get started with CI/CD');
expect(findEmptyState().find(GlButton).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
describe('when the code_quality_walkthrough experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'code_quality_walkthrough');
getExperimentVariant.mockReturnValue('candidate');
});
it('renders another CTA button', () => {
expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
paths.codeQualityPagePath,
);
describe('the control state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('control');
});
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
});
describe('the candidate state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders another CTA button', () => {
expect(findEmptyState().findComponent(GlButton).text()).toBe('Add a code quality job');
expect(findEmptyState().findComponent(GlButton).attributes('href')).toBe(
paths.codeQualityPagePath,
);
});
});
});
describe('when the ci_runner_templates experiment is active', () => {
beforeAll(() => {
getExperimentData.mockImplementation((name) => name === 'ci_runner_templates');
getExperimentVariant.mockReturnValue('candidate');
});
it('renders two buttons', () => {
expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
'Install GitLab Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
paths.ciRunnerSettingsPath,
);
expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
'Learn about Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
describe('the control state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('control');
});
it('renders the CI/CD templates', () => {
expect(wrapper.find(PipelinesCiTemplates)).toExist();
});
});
describe('the candidate state', () => {
beforeAll(() => {
getExperimentVariant.mockReturnValue('candidate');
});
it('renders two buttons', () => {
expect(findEmptyState().findAllComponents(GlButton).length).toBe(2);
expect(findEmptyState().findAllComponents(GlButton).at(0).text()).toBe(
'Install GitLab Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(0).attributes('href')).toBe(
paths.ciRunnerSettingsPath,
);
expect(findEmptyState().findAllComponents(GlButton).at(1).text()).toBe(
'Learn about Runners',
);
expect(findEmptyState().findAllComponents(GlButton).at(1).attributes('href')).toBe(
'/help/ci/quick_start/index.md',
);
});
});
});

View File

@ -114,7 +114,7 @@ describe('Grouped test reports app', () => {
setReports(newFailedTestReports);
});
it('tracks usage ping metric when enabled', () => {
it('tracks service ping metric when enabled', () => {
mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: true } });
findExpandButton().trigger('click');
@ -132,7 +132,7 @@ describe('Grouped test reports app', () => {
expect(Api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
});
it('does not track usage ping metric when disabled', () => {
it('does not track service ping metric when disabled', () => {
mountComponent({ glFeatures: { usageDataITestingSummaryWidgetTotal: false } });
findExpandButton().trigger('click');

View File

@ -8,8 +8,8 @@ import {
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
TRACKING_ACTION_CREATE_COMMIT,
TRACKING_ACTION_CREATE_MERGE_REQUEST,
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '~/static_site_editor/constants';
@ -237,7 +237,7 @@ describe('submitContentChanges', () => {
});
});
describe('sends the correct Usage Ping tracking event', () => {
describe('sends the correct Service Ping tracking event', () => {
beforeEach(() => {
jest.spyOn(Api, 'trackRedisCounterEvent').mockResolvedValue({ data: '' });
});
@ -245,7 +245,7 @@ describe('submitContentChanges', () => {
it('for commiting changes', () => {
return submitContentChanges(buildPayload()).then(() => {
expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
);
});
});
@ -253,7 +253,7 @@ describe('submitContentChanges', () => {
it('for creating a merge request', () => {
return submitContentChanges(buildPayload()).then(() => {
expect(Api.trackRedisCounterEvent).toHaveBeenCalledWith(
USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
);
});
});

View File

@ -18,7 +18,7 @@ RSpec.describe GitlabSchema.types['Issue'] do
confidential discussion_locked upvotes downvotes user_notes_count user_discussions_count web_path web_url relative_position
emails_disabled subscribed time_estimate total_time_spent human_time_estimate human_total_time_spent closed_at created_at updated_at task_completion_status
design_collection alert_management_alert severity current_user_todos moved moved_to
create_note_email timelogs]
create_note_email timelogs project_id]
fields.each do |field_name|
expect(described_class).to have_graphql_field(field_name)

View File

@ -294,7 +294,6 @@ RSpec.describe IssuesHelper do
expected = {
autocomplete_award_emojis_path: autocomplete_award_emojis_path,
autocomplete_users_path: autocomplete_users_path(active: true, current_user: true, project_id: project.id, format: :json),
calendar_path: '#',
can_bulk_update: 'true',
can_edit: 'true',
@ -313,8 +312,6 @@ RSpec.describe IssuesHelper do
max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes),
new_issue_path: new_project_issue_path(project, issue: { milestone_id: finder.milestones.first.id }),
project_import_jira_path: project_import_jira_path(project),
project_labels_path: project_labels_path(project, include_ancestor_groups: true, format: :json),
project_milestones_path: project_milestones_path(project, format: :json),
project_path: project.full_path,
quick_actions_help_path: help_page_path('user/project/quick_actions'),
reset_path: new_issuable_address_project_path(project, issuable_type: 'issue'),

View File

@ -97,7 +97,17 @@ RSpec.describe UserCalloutsHelper do
allow(helper).to receive(:user_dismissed?).with(described_class::CUSTOMIZE_HOMEPAGE) { false }
end
it { is_expected.to be true }
context 'when user is on the default dashboard' do
it { is_expected.to be true }
end
context 'when user is not on the default dashboard' do
before do
user.dashboard = 'stars'
end
it { is_expected.to be false }
end
end
context 'when user dismissed' do

View File

@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Backup::GitalyBackup do
let(:parallel) { nil }
let(:progress) do
Tempfile.new('progress').tap do |progress|
progress.unlink
@ -13,7 +14,7 @@ RSpec.describe Backup::GitalyBackup do
progress.close
end
subject { described_class.new(progress) }
subject { described_class.new(progress, parallel: parallel) }
context 'unknown' do
it 'fails to start unknown' do
@ -30,6 +31,8 @@ RSpec.describe Backup::GitalyBackup do
project_snippet = create(:project_snippet, :repository, project: project)
personal_snippet = create(:personal_snippet, :repository, author: project.owner)
expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, { in: anything, out: progress }).and_call_original
subject.start(:create)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
subject.enqueue(project, Gitlab::GlRepository::WIKI)
@ -45,6 +48,17 @@ RSpec.describe Backup::GitalyBackup do
expect(File).to exist(File.join(Gitlab.config.backup.path, 'repositories', project_snippet.disk_path + '.bundle'))
end
context 'parallel option set' do
let(:parallel) { 3 }
it 'passes parallel option through' do
expect(Process).to receive(:spawn).with(anything, 'create', '-path', anything, '-parallel', '3', { in: anything, out: progress }).and_call_original
subject.start(:create)
subject.wait
end
end
it 'raises when the exit code not zero' do
expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))
@ -83,6 +97,8 @@ RSpec.describe Backup::GitalyBackup do
copy_bundle_to_backup_path('personal_snippet_repo.bundle', personal_snippet.disk_path + '.bundle')
copy_bundle_to_backup_path('project_snippet_repo.bundle', project_snippet.disk_path + '.bundle')
expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original
subject.start(:restore)
subject.enqueue(project, Gitlab::GlRepository::PROJECT)
subject.enqueue(project, Gitlab::GlRepository::WIKI)
@ -100,6 +116,17 @@ RSpec.describe Backup::GitalyBackup do
expect(collect_commit_shas.call(project_snippet.repository)).to eq(['6e44ba56a4748be361a841e759c20e421a1651a1'])
end
context 'parallel option set' do
let(:parallel) { 3 }
it 'does not pass parallel option through' do
expect(Process).to receive(:spawn).with(anything, 'restore', '-path', anything, { in: anything, out: progress }).and_call_original
subject.start(:restore)
subject.wait
end
end
it 'raises when the exit code not zero' do
expect(subject).to receive(:bin_path).and_return(Gitlab::Utils.which('false'))

View File

@ -4,7 +4,8 @@ require 'spec_helper'
RSpec.describe Backup::Repositories do
let(:progress) { spy(:stdout) }
let(:strategy) { spy(:strategy) }
let(:parallel_enqueue) { true }
let(:strategy) { spy(:strategy, parallel_enqueue?: parallel_enqueue) }
subject { described_class.new(progress, strategy: strategy) }
@ -80,6 +81,22 @@ RSpec.describe Backup::Repositories do
end
end
context 'concurrency with a strategy without parallel enqueueing support' do
let(:parallel_enqueue) { false }
it 'enqueues all projects sequentially' do
expect(Thread).not_to receive(:new)
expect(strategy).to receive(:start).with(:create)
projects.each do |project|
expect(strategy).to receive(:enqueue).with(project, Gitlab::GlRepository::PROJECT)
end
expect(strategy).to receive(:wait)
subject.dump(max_concurrency: 2, max_storage_concurrency: 2)
end
end
[4, 10].each do |max_storage_concurrency|
context "max_storage_concurrency #{max_storage_concurrency}", quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/241701' do
let(:storage_keys) { %w[default test_second_storage] }

View File

@ -5756,6 +5756,20 @@ RSpec.describe User do
end
end
describe '#default_dashboard?' do
it 'is the default dashboard' do
user = build(:user)
expect(user.default_dashboard?).to be true
end
it 'is not the default dashboard' do
user = build(:user, dashboard: 'stars')
expect(user.default_dashboard?).to be false
end
end
describe '.dormant' do
it 'returns dormant users' do
freeze_time do

View File

@ -212,8 +212,9 @@ RSpec.describe API::ResourceAccessTokens do
end
describe "POST projects/:id/access_tokens" do
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at } }
let(:params) { { name: "test", scopes: ["api"], expires_at: expires_at, access_level: access_level } }
let(:expires_at) { 1.month.from_now }
let(:access_level) { 20 }
subject(:create_token) { post api("/projects/#{project_id}/access_tokens", user), params: params }
@ -232,6 +233,7 @@ RSpec.describe API::ResourceAccessTokens do
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq("test")
expect(json_response["scopes"]).to eq(["api"])
expect(json_response["access_level"]).to eq(20)
expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
expect(json_response["token"]).to be_present
end
@ -249,6 +251,21 @@ RSpec.describe API::ResourceAccessTokens do
expect(json_response["expires_at"]).to eq(nil)
end
end
context "when 'access_level' is not set" do
let(:access_level) { nil }
it 'creates a project access token with the default access level', :aggregate_failures do
create_token
expect(response).to have_gitlab_http_status(:created)
expect(json_response["name"]).to eq("test")
expect(json_response["scopes"]).to eq(["api"])
expect(json_response["access_level"]).to eq(40)
expect(json_response["expires_at"]).to eq(expires_at.to_date.iso8601)
expect(json_response["token"]).to be_present
end
end
end
context "with invalid params" do

View File

@ -79,15 +79,14 @@ RSpec.describe AuditEventService do
context 'with IP address', :request_store do
using RSpec::Parameterized::TableSyntax
where(:from_caller, :from_context, :from_author_sign_in, :output) do
'192.168.0.1' | '192.168.0.2' | '192.168.0.3' | '192.168.0.1'
nil | '192.168.0.2' | '192.168.0.3' | '192.168.0.2'
nil | nil | '192.168.0.3' | '192.168.0.3'
where(:from_context, :from_author_sign_in, :output) do
'192.168.0.2' | '192.168.0.3' | '192.168.0.2'
nil | '192.168.0.3' | '192.168.0.3'
end
with_them do
let(:user) { create(:user, current_sign_in_ip: from_author_sign_in) }
let(:audit_service) { described_class.new(user, user, with: 'standard', ip_address: from_caller) }
let(:audit_service) { described_class.new(user, user, with: 'standard') }
before do
allow(Gitlab::RequestContext.instance).to receive(:client_ip).and_return(from_context)

View File

@ -88,12 +88,28 @@ RSpec.describe ResourceAccessTokens::CreateService do
end
end
it 'adds the bot user as a maintainer in the resource' do
response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
context 'access level' do
context 'when user does not specify an access level' do
it 'adds the bot user as a maintainer in the resource' do
response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
expect(resource.members.maintainers.map(&:user_id)).to include(bot_user.id)
end
end
context 'when user specifies an access level' do
let_it_be(:params) { { access_level: Gitlab::Access::DEVELOPER } }
it 'adds the bot user with the specified access level in the resource' do
response = subject
access_token = response.payload[:access_token]
bot_user = access_token.user
expect(resource.members.developers.map(&:user_id)).to include(bot_user.id)
end
end
end
context 'personal access token' do

Some files were not shown because too many files have changed in this diff Show More