Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-10-18 18:15:29 +00:00
parent ff09936938
commit 9742ffd06b
114 changed files with 1296 additions and 915 deletions

View File

@ -74,6 +74,7 @@ For visibility, all `/chatops` commands that target production must be executed
and cross-posted (with the command results) to the responsible team's Slack channel.
- [ ] [Incrementally roll out](https://docs.gitlab.com/ee/development/feature_flags/controls.html#process) the feature on production.
- Example: `/chatops run feature set <feature-flag-name> <rollout-percentage> --actors`.
- Between every step wait for at least 15 minutes and monitor the appropriate graphs on https://dashboards.gitlab.net.
- [ ] After the feature has been 100% enabled, wait for [at least one day before releasing the feature](#release-the-feature).

View File

@ -70,7 +70,6 @@ SidekiqLoadBalancing/WorkerDataConsistency:
- 'app/workers/counters/cleanup_refresh_worker.rb'
- 'app/workers/create_commit_signature_worker.rb'
- 'app/workers/create_note_diff_file_worker.rb'
- 'app/workers/create_pipeline_worker.rb'
- 'app/workers/database/drop_detached_partitions_worker.rb'
- 'app/workers/database/partition_management_worker.rb'
- 'app/workers/delete_diff_files_worker.rb'

View File

@ -1 +1 @@
80b47e3c58c2e52495d579d2c4f2071b1a793e21
92eaec235ff4801ec059fa79fa891d67ae765c11

View File

@ -1 +1 @@
fc8928109f97c8239cde31aafc11c6b84bbd24c3
001ded581e320bcdae4294e1dda41b0c25cf00b2

View File

@ -8,7 +8,11 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
import { dispose, fixTitle } from '~/tooltips';
import { createAlert } from '~/alert';
import { FREQUENTLY_USED_EMOJIS_STORAGE_KEY } from '~/emoji/constants';
import {
EMOJI_THUMBS_UP,
EMOJI_THUMBS_DOWN,
FREQUENTLY_USED_EMOJIS_STORAGE_KEY,
} from '~/emoji/constants';
import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
@ -355,8 +359,8 @@ export class AwardsHandler {
checkMutuality(votesBlock, emoji) {
const awardUrl = this.getAwardUrl();
if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
const mutualVote = emoji === 'thumbsup' ? 'thumbsdown' : 'thumbsup';
if (emoji === EMOJI_THUMBS_UP || emoji === EMOJI_THUMBS_DOWN) {
const mutualVote = emoji === EMOJI_THUMBS_UP ? EMOJI_THUMBS_DOWN : EMOJI_THUMBS_UP;
const $emojiButton = votesBlock.find(`[data-name="${mutualVote}"]`).closest('button');
const isAlreadyVoted = $emojiButton.hasClass('active');
if (isAlreadyVoted) {
@ -375,7 +379,7 @@ export class AwardsHandler {
if (counterNumber > 1) {
counter.text(counterNumber - 1);
this.removeYouFromUserList($emojiButton);
} else if (emoji === 'thumbsup' || emoji === 'thumbsdown') {
} else if (emoji === EMOJI_THUMBS_UP || emoji === EMOJI_THUMBS_DOWN) {
dispose($emojiButton);
counter.text('0');
this.removeYouFromUserList($emojiButton);

View File

@ -63,7 +63,6 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
identityVerificationRequired: parseBoolean(identityVerificationRequired),
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
showJenkinsCiPrompt: parseBoolean(showJenkinsCiPrompt),
targetProjectFullPath: fullPath,
},
data() {
return {

View File

@ -14,9 +14,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
inject: {
targetProjectFullPath: {
default: '',
},
pipelineSchedulesPath: {
default: '',
},
@ -34,17 +31,8 @@ export default {
isTriggered() {
return this.pipeline.source === TRIGGER_ORIGIN;
},
isPipelineFromSource() {
if (!this.targetProjectFullPath) return true;
const fullPath = this.pipeline?.project?.full_path;
// We may or may not have a trailing slash in `targetProjectFullPath` value, so we account for both cases
return (
fullPath === `/${this.targetProjectFullPath}` || fullPath === this.targetProjectFullPath
);
},
isInFork() {
return !this.isPipelineFromSource;
isForked() {
return this.pipeline?.project?.forked;
},
showMergedResultsBadge() {
// A merge train pipeline is technically also a merged results pipeline,
@ -186,7 +174,7 @@ export default {
>{{ s__('Pipeline|merged results') }}</gl-badge
>
<gl-badge
v-if="isInFork"
v-if="isForked"
v-gl-tooltip
:title="__('Pipeline ran in fork of project')"
variant="info"

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import { mapActions, mapState } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
import createstore from './store';
export default (el) => {
@ -39,7 +40,7 @@ export default (el) => {
awards: this.awards,
canAwardEmoji: this.canAwardEmoji,
currentUserId: this.currentUserId,
defaultAwards: showDefaultAwardEmojis ? ['thumbsup', 'thumbsdown'] : [],
defaultAwards: showDefaultAwardEmojis ? [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN] : [],
selectedClass: 'selected',
},
on: {

View File

@ -1,6 +1,9 @@
export const FREQUENTLY_USED_KEY = 'frequently_used';
export const FREQUENTLY_USED_EMOJIS_STORAGE_KEY = 'frequently_used_emojis';
export const EMOJI_THUMBS_UP = 'thumbsup';
export const EMOJI_THUMBS_DOWN = 'thumbsdown';
export const CATEGORY_NAMES = [
FREQUENTLY_USED_KEY,
'custom',

View File

@ -4,6 +4,7 @@ import {
HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE,
} from '~/environments/constants';
import { subscribeToSocket } from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import { updateConnectionStatus } from '~/environments/graphql/resolvers/kubernetes/k8s_connection_status';
import { connectionStatus } from '~/environments/graphql/resolvers/kubernetes/constants';
import { buildKubernetesErrors } from '~/environments/helpers/k8s_integration_helper';
@ -51,8 +52,8 @@ const mapFluxItems = (fluxItem, resourceType) => {
return result;
};
const watchFluxResource = ({
watchPath,
const watchFluxResource = async ({
apiVersion,
resourceName,
namespace,
query,
@ -61,51 +62,73 @@ const watchFluxResource = ({
field,
client,
}) => {
const config = new Configuration(variables.configuration);
const watcherApi = new WatchApi(config);
const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType });
const fieldSelector = `metadata.name=${decodeURIComponent(resourceName)}`;
updateConnectionStatus(client, {
configuration: variables.configuration,
namespace,
resourceType,
status: connectionStatus.connecting,
});
watcherApi
.subscribeToStream(watchPath, { watch: true, fieldSelector })
.then((watcher) => {
let result = [];
watcher.on(EVENT_DATA, (data) => {
result = mapFluxItems(data[0], resourceType);
client.writeQuery({
query,
variables,
data: { [field]: result },
});
updateConnectionStatus(client, {
configuration: variables.configuration,
namespace,
resourceType,
status: connectionStatus.connected,
});
});
watcher.on(EVENT_TIMEOUT, () => {
updateConnectionStatus(client, {
configuration: variables.configuration,
namespace,
resourceType,
status: connectionStatus.disconnected,
});
});
})
.catch((err) => {
handleClusterError(err);
const updateFluxConnection = (status) => {
updateConnectionStatus(client, {
configuration: variables.configuration,
namespace,
resourceType,
status,
});
};
const updateQueryCache = (data) => {
const result = mapFluxItems(data[0], resourceType);
client.writeQuery({
query,
variables,
data: { [field]: result },
});
};
const watchFunction = async () => {
const config = new Configuration(variables.configuration);
const watcherApi = new WatchApi(config);
try {
const watcher = await watcherApi.subscribeToStream(watchPath, { watch: true, fieldSelector });
watcher.on(EVENT_DATA, (data) => {
updateQueryCache(data);
updateFluxConnection(connectionStatus.connected);
});
watcher.on(EVENT_TIMEOUT, () => updateFluxConnection(connectionStatus.disconnected));
} catch (err) {
await handleClusterError(err);
}
};
updateFluxConnection(connectionStatus.connecting);
if (gon?.features?.useWebsocketForK8sWatch) {
const watchId = `${resourceType}-${resourceName}`;
const [group, version] = apiVersion.split('/');
const watchParams = {
version,
group,
resource: resourceType,
fieldSelector,
namespace,
};
const cacheParams = {
updateQueryCache,
updateConnectionStatusFn: updateFluxConnection,
};
try {
await subscribeToSocket({
watchId,
watchParams,
configuration: variables.configuration,
cacheParams,
});
} catch {
await watchFunction();
}
} else {
await watchFunction();
}
};
const getFluxResource = ({ query, variables, field, resourceType, client }) => {
@ -122,10 +145,8 @@ const getFluxResource = ({ query, variables, field, resourceType, client }) => {
const apiVersion = fluxData?.apiVersion;
if (resourceName) {
const watchPath = buildFluxResourceWatchPath({ namespace, apiVersion, resourceType });
watchFluxResource({
watchPath,
apiVersion,
resourceName,
namespace,
query,

View File

@ -7,6 +7,7 @@ import {
buildWatchPath,
mapWorkloadItem,
mapEventItem,
subscribeToSocket,
} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import {
watchFluxKustomization,
@ -25,47 +26,110 @@ import { k8sLogs } from './k8s_logs';
const watchServices = ({ configuration, namespace, client }) => {
const query = k8sServicesQuery;
const watchPath = buildWatchPath({ resource: 'services', namespace });
const queryField = k8sResourceType.k8sServices;
watchWorkloadItems({ client, query, configuration, namespace, watchPath, queryField });
const watchPath = buildWatchPath({ resource: 'services', namespace });
const watchParams = {
version: 'v1',
resource: 'services',
namespace,
};
watchWorkloadItems({
client,
query,
configuration,
namespace,
watchPath,
queryField,
watchParams,
});
};
const watchPods = ({ configuration, namespace, client }) => {
const query = k8sPodsQuery;
const watchPath = buildWatchPath({ resource: 'pods', namespace });
const queryField = k8sResourceType.k8sPods;
watchWorkloadItems({ client, query, configuration, namespace, watchPath, queryField });
watchWorkloadItems({
client,
query,
configuration,
namespace,
watchPath,
queryField,
});
};
const watchDeployments = ({ configuration, namespace, client }) => {
const query = k8sDeploymentsQuery;
const watchPath = buildWatchPath({ resource: 'deployments', api: 'apis/apps/v1', namespace });
const queryField = k8sResourceType.k8sDeployments;
const watchParams = {
group: 'apps',
version: 'v1',
resource: 'deployments',
namespace,
};
watchWorkloadItems({ client, query, configuration, namespace, watchPath, queryField });
watchWorkloadItems({
client,
query,
configuration,
namespace,
watchPath,
queryField,
watchParams,
});
};
const watchEvents = ({ client, configuration, namespace, involvedObjectName, config }) => {
export const watchEvents = async ({
client,
configuration,
namespace,
involvedObjectName,
query,
}) => {
const fieldSelector = `involvedObject.name=${involvedObjectName}`;
const watchPath = buildWatchPath({ resource: 'events', namespace });
const watcherApi = new WatchApi(config);
const queryField = 'k8sEvents';
watcherApi
.subscribeToStream(watchPath, { fieldSelector, watch: true })
.then((watcher) => {
watcher.on(EVENT_DATA, (data) => {
const result = data.map(mapEventItem);
client.writeQuery({
query: k8sEventsQuery,
variables: { configuration, namespace, involvedObjectName },
data: { k8sEvents: result },
});
});
})
.catch((err) => {
handleClusterError(err);
const updateQueryCache = (data) => {
const result = data.map(mapEventItem);
client.writeQuery({
query,
variables: { configuration, namespace, involvedObjectName },
data: { [queryField]: result },
});
};
const watchFunction = async () => {
try {
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
const watchPath = buildWatchPath({ resource: 'events', namespace });
const watcher = await watcherApi.subscribeToStream(watchPath, { fieldSelector, watch: true });
watcher.on(EVENT_DATA, updateQueryCache);
} catch (err) {
await handleClusterError(err);
}
};
if (gon?.features?.useWebsocketForK8sWatch) {
const watchId = `events-io-${involvedObjectName}`;
const watchParams = { version: 'v1', resource: 'events', fieldSelector, namespace };
const cacheParams = {
updateQueryCache,
};
try {
await subscribeToSocket({ watchId, watchParams, configuration, cacheParams });
} catch {
await watchFunction();
}
} else {
await watchFunction();
}
};
const handleKubernetesMutationError = async (err) => {
@ -126,6 +190,7 @@ export const kubernetesMutations = {
export const kubernetesQueries = {
k8sPods(_, { configuration, namespace }, { client }) {
const query = k8sPodsQuery;
return getK8sPods({ client, query, configuration, namespace });
},
k8sServices(_, { configuration, namespace }, { client }) {
@ -200,7 +265,13 @@ export const kubernetesQueries = {
.then((res) => {
const data = res.items?.map(mapEventItem) ?? [];
watchEvents({ client, configuration, namespace, involvedObjectName, config });
watchEvents({
client,
configuration,
namespace,
involvedObjectName,
query: k8sEventsQuery,
});
return data;
})

View File

@ -29,14 +29,13 @@ import {
WORK_ITEM_TO_ISSUABLE_MAP,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_AWARD_EMOJI,
EMOJI_THUMBSUP,
EMOJI_THUMBSDOWN,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_LABELS,
WORK_ITEM_TYPE_ENUM_ISSUE,
WORK_ITEM_TYPE_ENUM_INCIDENT,
WORK_ITEM_TYPE_ENUM_TASK,
} from '~/work_items/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
import { BoardType } from '~/boards/constants';
import { STATUS_CLOSED, STATUS_OPEN, TYPE_EPIC } from '../constants';
import {
@ -521,9 +520,10 @@ export function updateUpvotesCount({ list, workItem, namespace = BoardType.proje
}
const upvotesCount =
currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBSUP)?.length ?? 0;
currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBS_UP)?.length ?? 0;
const downvotesCount =
currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBSDOWN)?.length ?? 0;
currentWidget[property].nodes.filter((emoji) => emoji.name === EMOJI_THUMBS_DOWN)?.length ??
0;
activeItem.upvotes = upvotesCount;
activeItem.downvotes = downvotesCount;
});

View File

@ -2,6 +2,7 @@ import {
CoreV1Api,
Configuration,
WatchApi,
webSocketWatchManager,
EVENT_DATA,
EVENT_TIMEOUT,
EVENT_ERROR,
@ -53,68 +54,101 @@ export const mapEventItem = ({
type,
}) => ({ lastTimestamp, eventTime, message, reason, source, type });
export const watchWorkloadItems = ({
export const subscribeToSocket = async ({ watchId, watchParams, configuration, cacheParams }) => {
const { updateQueryCache, updateConnectionStatusFn } = cacheParams;
try {
const watcher = await webSocketWatchManager.initConnection({
message: { watchId, watchParams },
configuration,
});
const handleConnectionStatus = (status) => {
if (updateConnectionStatusFn) {
updateConnectionStatusFn(status);
}
};
watcher.on(EVENT_DATA, watchId, (data) => {
updateQueryCache(data);
handleConnectionStatus(connectionStatus.connected);
});
watcher.on(EVENT_ERROR, watchId, () => {
handleConnectionStatus(connectionStatus.disconnected);
});
} catch (err) {
throw new Error(s__('KubernetesDashboard|Failed to establish WebSocket connection'));
}
};
export const watchWorkloadItems = async ({
client,
query,
configuration,
namespace,
watchPath,
queryField,
watchParams,
}) => {
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
updateConnectionStatus(client, {
configuration,
namespace,
resourceType: queryField,
status: connectionStatus.connecting,
});
const updateStatus = (status) =>
updateConnectionStatus(client, {
configuration,
namespace,
resourceType: queryField,
status,
});
watcherApi
.subscribeToStream(watchPath, { watch: true })
.then((watcher) => {
let result = [];
const updateQueryCache = (data) => {
const result = data.map(mapWorkloadItem);
client.writeQuery({
query,
variables: { configuration, namespace },
data: { [queryField]: result },
});
};
const watchFunction = async () => {
try {
const watcher = await watcherApi.subscribeToStream(watchPath, { watch: true });
watcher.on(EVENT_DATA, (data) => {
result = data.map(mapWorkloadItem);
client.writeQuery({
query,
variables: { configuration, namespace },
data: { [queryField]: result },
});
updateConnectionStatus(client, {
configuration,
namespace,
resourceType: queryField,
status: connectionStatus.connected,
});
updateQueryCache(data);
updateStatus(connectionStatus.connected);
});
watcher.on(EVENT_TIMEOUT, () => {
result = [];
updateConnectionStatus(client, {
configuration,
namespace,
resourceType: queryField,
status: connectionStatus.disconnected,
});
updateStatus(connectionStatus.disconnected);
});
watcher.on(EVENT_ERROR, () => {
result = [];
updateConnectionStatus(client, {
configuration,
namespace,
resourceType: queryField,
status: connectionStatus.disconnected,
});
updateStatus(connectionStatus.disconnected);
});
})
.catch((err) => {
handleClusterError(err);
});
} catch (err) {
await handleClusterError(err);
}
};
updateStatus(connectionStatus.connecting);
if (gon?.features?.useWebsocketForK8sWatch && watchParams) {
const watchId = namespace ? `${queryField}-n-${namespace}` : `${queryField}-all-namespaces`;
const cacheParams = {
updateQueryCache,
updateConnectionStatusFn: updateStatus,
};
try {
await subscribeToSocket({ watchId, watchParams, configuration, cacheParams });
} catch {
await watchFunction();
}
} else {
await watchFunction();
}
};
export const getK8sPods = ({
@ -135,6 +169,12 @@ export const getK8sPods = ({
return podsApi
.then((res) => {
const watchPath = buildWatchPath({ resource: 'pods', namespace });
const watchParams = {
version: 'v1',
resource: 'pods',
namespace,
};
watchWorkloadItems({
client,
query,
@ -142,6 +182,7 @@ export const getK8sPods = ({
namespace,
watchPath,
queryField,
watchParams,
});
const data = res?.items || [];

View File

@ -1,64 +0,0 @@
<script>
import { GlEmptyState } from '@gitlab/ui';
import emptySvgUrl from '@gitlab/svgs/dist/illustrations/status/status-new-md.svg';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import { MODEL_ENTITIES } from '../constants';
const emptyStateTranslations = {
[MODEL_ENTITIES.model]: {
title: s__('MlModelRegistry|Start tracking your machine learning models'),
description: s__('MlModelRegistry|Store and manage your machine learning models and versions'),
createNew: s__('MlModelRegistry|Add a model'),
},
[MODEL_ENTITIES.modelVersion]: {
title: s__('MlModelRegistry|Manage versions of your machine learning model'),
description: s__('MlModelRegistry|Use versions to track performance, parameters, and metadata'),
createNew: s__('MlModelRegistry|Create a model version'),
},
};
const helpLinks = {
[MODEL_ENTITIES.model]: helpPagePath('user/project/ml/model_registry/index', {
anchor: 'create-machine-learning-models-by-using-the-ui',
}),
[MODEL_ENTITIES.modelVersion]: helpPagePath('user/project/ml/model_registry/index', {
anchor: 'create-a-model-version-by-using-the-ui',
}),
};
export default {
components: {
GlEmptyState,
},
props: {
entityType: {
type: String,
required: true,
validator(value) {
return MODEL_ENTITIES[value] !== undefined;
},
},
},
computed: {
emptyStateValues() {
return {
...emptyStateTranslations[this.entityType],
helpPath: helpLinks[this.entityType],
emptySvgPath: emptySvgUrl,
};
},
},
};
</script>
<template>
<gl-empty-state
:title="emptyStateValues.title"
:primary-button-text="emptyStateValues.createNew"
:primary-button-link="emptyStateValues.helpPath"
:svg-path="emptyStateValues.emptySvgPath"
:description="emptyStateValues.description"
class="gl-py-8"
/>
</template>

View File

@ -88,7 +88,9 @@ export default {
this.$refs.modal.show();
},
submitForm() {
this.$refs.form.submit();
if (!this.deleteButtonDisabled) {
this.$refs.form.submit();
}
},
closeModal() {
this.$refs.modal.hide();
@ -117,7 +119,7 @@ export default {
</gl-sprintf>
</p>
<form ref="form" :action="path" method="post">
<form ref="form" :action="path" method="post" @submit.prevent="submitForm">
<div v-if="isProtected" class="gl-mt-4">
<p>
<gl-sprintf :message="$options.i18n.confirmationTextProtectedTag">

View File

@ -6,6 +6,7 @@ import EmojiPicker from '~/emoji/components/picker.vue';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { glEmojiTag } from '~/emoji';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
// Internal constant, specific to this component, used when no `currentUserId` is given
const NO_USER_ID = -1;
@ -65,8 +66,8 @@ export default {
};
return [
...(thumbsup ? [this.createAwardList('thumbsup', thumbsup)] : []),
...(thumbsdown ? [this.createAwardList('thumbsdown', thumbsdown)] : []),
...(thumbsup ? [this.createAwardList(EMOJI_THUMBS_UP, thumbsup)] : []),
...(thumbsdown ? [this.createAwardList(EMOJI_THUMBS_DOWN, thumbsdown)] : []),
...Object.entries(rest).map(([name, list]) => this.createAwardList(name, list)),
];
},

View File

@ -7,18 +7,17 @@ import AwardsList from '~/vue_shared/components/awards_list.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
import projectWorkItemAwardEmojiQuery from '../graphql/award_emoji.query.graphql';
import updateAwardEmojiMutation from '../graphql/update_award_emoji.mutation.graphql';
import {
EMOJI_THUMBSDOWN,
EMOJI_THUMBSUP,
WIDGET_TYPE_AWARD_EMOJI,
DEFAULT_PAGE_SIZE_EMOJIS,
I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR,
} from '../constants';
export default {
defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
defaultAwards: [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN],
isLoggedIn: isLoggedIn(),
components: {
AwardsList,

View File

@ -277,9 +277,6 @@ export const TODO_DONE_ICON = 'todo-done';
export const TODO_DONE_STATE = 'done';
export const TODO_PENDING_STATE = 'pending';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
export const WORK_ITEM_TO_ISSUABLE_MAP = {
[WIDGET_TYPE_ASSIGNEES]: 'assignees',
[WIDGET_TYPE_LABELS]: 'labels',

View File

@ -144,6 +144,14 @@ class ApplicationController < BaseActionController
end
end
def handle_unverified_request
Gitlab::Auth::Activity
.new(controller: self)
.user_csrf_token_mismatch!
super
end
def render(*args)
super.tap do
# Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse

View File

@ -16,6 +16,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:show] do
push_frontend_feature_flag(:k8s_tree_view, project)
push_frontend_feature_flag(:use_websocket_for_k8s_watch, project)
end
before_action :authorize_read_environment!

View File

@ -110,7 +110,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
def get_target_projects
MergeRequestTargetProjectFinder
.new(current_user: current_user, source_project: @project, project_feature: :repository)
.execute(include_routes: false, search: params[:search]).limit(20)
.execute(include_routes: false, include_fork_networks: true, search: params[:search]).limit(20)
end
def build_merge_request

View File

@ -11,9 +11,10 @@ class MergeRequestTargetProjectFinder
@project_feature = project_feature
end
def execute(search: nil, include_routes: false)
def execute(search: nil, include_routes: false, include_fork_networks: false)
if source_project.fork_network
items = include_routes ? projects.inc_routes : projects
items = include_fork_networks ? items.include_fork_networks : items
by_search(items, search).allow_cross_joins_across_databases(
url: "https://gitlab.com/gitlab-org/gitlab/-/issues/420046")
else

View File

@ -6,7 +6,7 @@ module VersionCheckHelper
def show_version_check?
return false unless Gitlab::CurrentSettings.version_check_enabled
current_user&.can_read_all_resources? && !User.single_user&.requires_usage_stats_consent?
current_user&.can_admin_all_resources? && !User.single_user&.requires_usage_stats_consent?
end
def gitlab_version_check

View File

@ -20,7 +20,8 @@ module Enums
deb: 11,
'cbl-mariner': 12,
wolfi: 13,
cargo: 14
cargo: 14,
swift: 15
}.with_indifferent_access.freeze
REACHABILITY_TYPES = {
@ -38,6 +39,7 @@ module Enums
nuget
pypi
cargo
swift
].freeze
CONTAINER_SCANNING_PURL_TYPES = %w[

View File

@ -22,7 +22,7 @@ class DeployKey < Key
scope :with_write_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_write_access) }
scope :with_readonly_access, -> { joins(:deploy_keys_projects).merge(DeployKeysProject.with_readonly_access) }
scope :are_public, -> { where(public: true) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, { namespace: :route }] }) }
scope :with_projects, -> { includes(deploy_keys_projects: { project: [:route, { namespace: :route }, :fork_network] }) }
scope :including_projects_with_write_access, -> { includes(:projects_with_write_access) }
scope :including_projects_with_readonly_access, -> { includes(:projects_with_readonly_access) }
scope :not_in, ->(keys) { where.not(id: keys.select(:id)) }

View File

@ -730,6 +730,7 @@ class Project < ApplicationRecord
scope :with_jira_dvcs_server, -> { joins(:feature_usage).merge(ProjectFeatureUsage.with_jira_dvcs_integration_enabled(cloud: false)) }
scope :by_name, ->(name) { where('projects.name LIKE ?', "#{sanitize_sql_like(name)}%") }
scope :inc_routes, -> { includes(:route, namespace: :route) }
scope :include_fork_networks, -> { includes(:fork_network) }
scope :with_statistics, -> { includes(:statistics) }
scope :with_namespace, -> { includes(:namespace) }
scope :joins_namespace, -> { joins(:namespace) }

View File

@ -17,4 +17,8 @@ class ProjectEntity < Grape::Entity
expose :refs_url do |project|
refs_project_path(project)
end
expose :forked, documentation: { type: 'boolean', example: true } do |project|
project.forked?
end
end

View File

@ -62,5 +62,5 @@
- elsif can?(current_user, :remove_group, @group)
= render 'groups/settings/remove', group: @group, remove_form_id: 'js-remove-group-form'
= render_if_exists 'groups/settings/restore', group: @group
= render_if_exists 'shared/groups_projects/settings/restore', context: @group
= render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: 'js-remove-group-form'

View File

@ -32,5 +32,5 @@
= render 'groups/settings/transfer', group: @group
= render 'groups/settings/remove', group: @group, remove_form_id: remove_form_id
= render_if_exists 'groups/settings/restore', group: @group
= render_if_exists 'shared/groups_projects/settings/restore', context: @group
= render_if_exists 'groups/settings/immediately_remove', group: @group, remove_form_id: remove_form_id

View File

@ -52,8 +52,6 @@
- c.with_description do
= s_('ProjectSettings|Housekeeping, export, archive, change path, transfer, and delete.')
- c.with_body do
= render_if_exists 'projects/settings/restore', project: @project
.gl-flex.gl-gap-5.gl-flex-col
= render Pajamas::CardComponent.new(header_options: { class: 'gl-px-5 gl-py-4 gl-border-b-1 gl-border-b-solid gl-border-gray-100' }, body_options: { class: 'gl-px-5 gl-py-4' }) do |c|
- c.with_header do
@ -105,6 +103,7 @@
= render 'remove_fork', project: @project
= render_if_exists 'shared/groups_projects/settings/restore', context: @project
= render 'remove', project: @project
- else
- if can?(current_user, :archive_project, @project)

View File

@ -3,7 +3,7 @@
class CreatePipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
data_consistency :always
data_consistency :sticky
sidekiq_options retry: 3
include PipelineQueue

View File

@ -1,9 +0,0 @@
---
name: consistent_ci_variable_masking
feature_issue_url:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/160097
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/483018
milestone: '17.5'
group: group::pipeline authoring
type: gitlab_com_derisk
default_enabled: false

View File

@ -0,0 +1,9 @@
---
name: use_websocket_for_k8s_watch
feature_issue_url: https://gitlab.com/groups/gitlab-org/-/epics/13380
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/168399
rollout_issue_url:
milestone: '17.5'
group: group::environments
type: wip
default_enabled: false

View File

@ -6,7 +6,7 @@
gitlab-com: true
available_in: [Premium, Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/gitlab_duo_chat/#in-the-editor-window
image_url: https://img.youtube.com/v1/5JbAM5g2VbQ/hqdefault.jpg
image_url: https://img.youtube.com/vi/5JbAM5g2VbQ/hqdefault.jpg
published_at: 2024-10-17
release: 17.5
@ -42,6 +42,6 @@
gitlab-com: true
available_in: [Ultimate]
documentation_link: https://docs.gitlab.com/ee/user/gitlab_duo_chat/examples#ask-about-a-specific-merge-request
image_url: https://img.youtube.com/v1/4muvSFuWWL4/hqdefault.jpg
image_url: https://img.youtube.com/vi/4muvSFuWWL4/hqdefault.jpg
published_at: 2024-10-17
release: 17.5

View File

@ -0,0 +1,9 @@
---
migration_job_name: RestoreOptInToGitlabCom
description: We deleted the email_opted_in field from the user table on GitLab.com. This was done by mistake. We would like to restore the values from the temporary table with a backup from Snowflake. We need to restore it to the onboarding_status column in the user_details table.
feature_category: activation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/165155
milestone: '17.6'
queued_migration_version: 20241004100956
finalize_after: '2024-11-16'
finalized_by: # version of the migration that finalized this BBM

View File

@ -8,4 +8,5 @@ description: TODO
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68171
milestone: '14.2'
gitlab_schema: gitlab_ci
sharding_key_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/458479
removed_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/169702
removed_in_milestone: '17.6'

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class RemoveWorkItemTypeCustomFieldsWorkItemTypeForeignKey < Gitlab::Database::Migration[2.2]
milestone '17.6'
disable_ddl_transaction!
def up
with_lock_retries do
remove_foreign_key :work_item_type_custom_fields, column: :work_item_type_id
end
end
def down
add_concurrent_foreign_key :work_item_type_custom_fields, :work_item_types,
column: :work_item_type_id, on_delete: :cascade
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddWorkItemTypeCustomFieldsCorrectWorkItemTypeForeignKey < Gitlab::Database::Migration[2.2]
milestone '17.6'
disable_ddl_transaction!
def up
add_concurrent_foreign_key :work_item_type_custom_fields, :work_item_types,
column: :work_item_type_id, target_column: :correct_id, on_delete: :cascade
end
def down
with_lock_retries do
remove_foreign_key :work_item_type_custom_fields, column: :work_item_type_id
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
class QueueRestoreOptInToGitlabCom < Gitlab::Database::Migration[2.2]
milestone '17.6'
restrict_gitlab_migration gitlab_schema: :gitlab_main
disable_ddl_transaction!
# https://gitlab.com/gitlab-com/gl-infra/production/-/issues/18367
TEMPORARY_TABLE_NAME = 'temp_user_details_issue18240'
MIGRATION = "RestoreOptInToGitlabCom"
DELAY_INTERVAL = 2.minutes
TABLE_NAME = :user_details
BATCH_COLUMN = :user_id
BATCH_SIZE = 3_000
SUB_BATCH_SIZE = 250
MAX_BATCH_SIZE = 10_000
def up
return unless should_run?
queue_batched_background_migration(
MIGRATION,
TABLE_NAME,
BATCH_COLUMN,
TEMPORARY_TABLE_NAME,
job_interval: DELAY_INTERVAL,
batch_size: BATCH_SIZE,
sub_batch_size: SUB_BATCH_SIZE,
max_batch_size: MAX_BATCH_SIZE
)
end
def down
return unless should_run?
delete_batched_background_migration(MIGRATION, TABLE_NAME, BATCH_COLUMN, [TEMPORARY_TABLE_NAME])
end
private
def should_run?
Gitlab.com_except_jh? && ApplicationRecord.connection.table_exists?(TEMPORARY_TABLE_NAME)
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
class DropCiBuildTraceMetadata < Gitlab::Database::Migration[2.2]
milestone '17.6'
def up
drop_table(:ci_build_trace_metadata, if_exists: true)
execute(<<~SQL)
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(100)}
PARTITION OF p_ci_build_trace_metadata FOR VALUES IN (100);
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(101)}
PARTITION OF p_ci_build_trace_metadata FOR VALUES IN (101);
CREATE TABLE IF NOT EXISTS #{fully_qualified_partition_name(102)}
PARTITION OF p_ci_build_trace_metadata FOR VALUES IN (102);
SQL
end
def down
drop_table(fully_qualified_partition_name(100), if_exists: true)
drop_table(fully_qualified_partition_name(101), if_exists: true)
drop_table(fully_qualified_partition_name(102), if_exists: true)
execute(<<~SQL)
CREATE TABLE IF NOT EXISTS ci_build_trace_metadata
PARTITION OF p_ci_build_trace_metadata FOR VALUES IN (100, 101, 102);
SQL
end
private
def fully_qualified_partition_name(value)
"#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.ci_build_trace_metadata_#{value}"
end
end

View File

@ -0,0 +1 @@
08147a6cb23f2c47c51540f86da313b5e5e7d2102bdc53797b67f9d697773a81

View File

@ -0,0 +1 @@
f8dc10977e72d248d218f95cffb772dc16dd73bd1ed5b153c16dd5a600642fcf

View File

@ -0,0 +1 @@
7e8133287cec74e687a8f4949e96505f6f7f90745407195d16fa8602aad5b467

View File

@ -0,0 +1 @@
852bea9dfd5a2074dc3eab8cbf865b572c373f6373831d66356edaa4edb5c7db

View File

@ -2958,6 +2958,18 @@ CREATE TABLE p_ci_build_tags (
)
PARTITION BY LIST (partition_id);
CREATE TABLE p_ci_build_trace_metadata (
build_id bigint NOT NULL,
partition_id bigint NOT NULL,
trace_artifact_id bigint,
last_archival_attempt_at timestamp with time zone,
archived_at timestamp with time zone,
archival_attempts smallint DEFAULT 0 NOT NULL,
checksum bytea,
remote_checksum bytea
)
PARTITION BY LIST (partition_id);
CREATE TABLE p_ci_builds (
status character varying,
finished_at timestamp without time zone,
@ -8336,29 +8348,6 @@ CREATE SEQUENCE ci_build_trace_chunks_id_seq
ALTER SEQUENCE ci_build_trace_chunks_id_seq OWNED BY ci_build_trace_chunks.id;
CREATE TABLE p_ci_build_trace_metadata (
build_id bigint NOT NULL,
partition_id bigint NOT NULL,
trace_artifact_id bigint,
last_archival_attempt_at timestamp with time zone,
archived_at timestamp with time zone,
archival_attempts smallint DEFAULT 0 NOT NULL,
checksum bytea,
remote_checksum bytea
)
PARTITION BY LIST (partition_id);
CREATE TABLE ci_build_trace_metadata (
build_id bigint NOT NULL,
partition_id bigint NOT NULL,
trace_artifact_id bigint,
last_archival_attempt_at timestamp with time zone,
archived_at timestamp with time zone,
archival_attempts smallint DEFAULT 0 NOT NULL,
checksum bytea,
remote_checksum bytea
);
CREATE TABLE ci_builds (
status character varying,
finished_at timestamp without time zone,
@ -22010,8 +21999,6 @@ ALTER TABLE ONLY namespace_descendants ATTACH PARTITION gitlab_partitions_static
ALTER TABLE ONLY namespace_descendants ATTACH PARTITION gitlab_partitions_static.namespace_descendants_31 FOR VALUES WITH (modulus 32, remainder 31);
ALTER TABLE ONLY p_ci_build_trace_metadata ATTACH PARTITION ci_build_trace_metadata FOR VALUES IN ('100', '101', '102');
ALTER TABLE ONLY p_ci_builds ATTACH PARTITION ci_builds FOR VALUES IN ('100');
ALTER TABLE ONLY p_ci_builds_metadata ATTACH PARTITION ci_builds_metadata FOR VALUES IN ('100');
@ -24227,12 +24214,6 @@ ALTER TABLE ONLY ci_build_report_results
ALTER TABLE ONLY ci_build_trace_chunks
ADD CONSTRAINT ci_build_trace_chunks_pkey PRIMARY KEY (id);
ALTER TABLE ONLY p_ci_build_trace_metadata
ADD CONSTRAINT p_ci_build_trace_metadata_pkey PRIMARY KEY (build_id, partition_id);
ALTER TABLE ONLY ci_build_trace_metadata
ADD CONSTRAINT ci_build_trace_metadata_pkey PRIMARY KEY (build_id, partition_id);
ALTER TABLE ONLY p_ci_builds_metadata
ADD CONSTRAINT p_ci_builds_metadata_pkey PRIMARY KEY (id, partition_id);
@ -25262,6 +25243,9 @@ ALTER TABLE ONLY p_ci_build_sources
ALTER TABLE ONLY p_ci_build_tags
ADD CONSTRAINT p_ci_build_tags_pkey PRIMARY KEY (id, partition_id);
ALTER TABLE ONLY p_ci_build_trace_metadata
ADD CONSTRAINT p_ci_build_trace_metadata_pkey PRIMARY KEY (build_id, partition_id);
ALTER TABLE ONLY p_ci_builds_execution_configs
ADD CONSTRAINT p_ci_builds_execution_configs_pkey PRIMARY KEY (id, partition_id);
@ -27325,10 +27309,6 @@ CREATE INDEX ca_aggregations_last_full_run_at ON analytics_cycle_analytics_aggre
CREATE INDEX ca_aggregations_last_incremental_run_at ON analytics_cycle_analytics_aggregations USING btree (last_incremental_run_at NULLS FIRST) WHERE (enabled IS TRUE);
CREATE INDEX index_p_ci_build_trace_metadata_on_trace_artifact_id ON ONLY p_ci_build_trace_metadata USING btree (trace_artifact_id);
CREATE INDEX ci_build_trace_metadata_trace_artifact_id_idx ON ci_build_trace_metadata USING btree (trace_artifact_id);
CREATE INDEX p_ci_builds_status_created_at_project_id_idx ON ONLY p_ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
CREATE INDEX ci_builds_gitlab_monitor_metrics ON ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text);
@ -30299,6 +30279,8 @@ CREATE INDEX index_p_ci_build_tags_on_project_id ON ONLY p_ci_build_tags USING b
CREATE UNIQUE INDEX index_p_ci_build_tags_on_tag_id_and_build_id_and_partition_id ON ONLY p_ci_build_tags USING btree (tag_id, build_id, partition_id);
CREATE INDEX index_p_ci_build_trace_metadata_on_trace_artifact_id ON ONLY p_ci_build_trace_metadata USING btree (trace_artifact_id);
CREATE INDEX index_p_ci_builds_execution_configs_on_pipeline_id ON ONLY p_ci_builds_execution_configs USING btree (pipeline_id);
CREATE INDEX index_p_ci_builds_execution_configs_on_project_id ON ONLY p_ci_builds_execution_configs USING btree (project_id);
@ -33619,10 +33601,6 @@ ALTER INDEX index_on_namespace_descendants_outdated ATTACH PARTITION gitlab_part
ALTER INDEX namespace_descendants_pkey ATTACH PARTITION gitlab_partitions_static.namespace_descendants_31_pkey;
ALTER INDEX p_ci_build_trace_metadata_pkey ATTACH PARTITION ci_build_trace_metadata_pkey;
ALTER INDEX index_p_ci_build_trace_metadata_on_trace_artifact_id ATTACH PARTITION ci_build_trace_metadata_trace_artifact_id_idx;
ALTER INDEX p_ci_builds_status_created_at_project_id_idx ATTACH PARTITION ci_builds_gitlab_monitor_metrics;
ALTER INDEX p_ci_builds_metadata_pkey ATTACH PARTITION ci_builds_metadata_pkey;
@ -35010,7 +34988,7 @@ ALTER TABLE ONLY packages_debian_group_architectures
ADD CONSTRAINT fk_92714bcab1 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY work_item_type_custom_fields
ADD CONSTRAINT fk_9447fad7b4 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(id) ON DELETE CASCADE;
ADD CONSTRAINT fk_9447fad7b4 FOREIGN KEY (work_item_type_id) REFERENCES work_item_types(correct_id) ON DELETE CASCADE;
ALTER TABLE ONLY workspaces_agent_configs
ADD CONSTRAINT fk_94660551c8 FOREIGN KEY (cluster_agent_id) REFERENCES cluster_agents(id) ON DELETE CASCADE;

View File

@ -126,6 +126,49 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
--data "mirror=false"
```
## Configure pull mirroring for a project v2
DETAILS:
**Status:** Experiment
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/494294) in GitLab 17.5. This feature is an [experiment](../policy/experiment-beta-support.md).
Configure pull mirroring settings.
Supported attributes:
| Attribute | Type | Required | Description |
|:---------------------------------|:--------|:---------|:------------|
| `enabled` | boolean | No | Enables pull mirroring on project when set to `true`. |
| `url` | string | No | URL of remote repository being mirrored. |
| `auth_user` | string | No | Username used for authentication of a project to pull mirror. |
| `auth_password` | string | No | Password used for authentication of a project to pull mirror. |
| `mirror_trigger_builds` | boolean | No | Trigger pipelines for mirror updates when set to `true`. |
| `only_mirror_protected_branches` | boolean | No | Limits mirroring to only protected branches when set to `true`. |
| `mirror_branch_regex` | String | No | Contains a regular expression. Only branches with names matching the regex are mirrored. Requires `only_mirror_protected_branches` to be disabled. |
Example request to add pull mirroring:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"enabled": true,
"url": "https://gitlab.example.com/group/project.git",
"auth_user": "user",
"auth_password": "password"
}' \
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull"
```
Example request to remove pull mirroring:
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/:id/mirror/pull" \
--data "enabled=false"
```
## Start the pull mirroring process for a project
Start the pull mirroring process for a project.

View File

@ -41,7 +41,7 @@ GET /search
| `order_by` | string | No | Allowed values are `created_at` only. If not set, results are sorted by `created_at` in descending order for basic search, or by the most relevant documents for advanced search.|
| `sort` | string | No | Allowed values are `asc` or `desc` only. If not set, results are sorted by `created_at` in descending order for basic search, or by the most relevant documents for advanced search.|
| `state` | string | No | Filter by state. Supports `issues` and `merge_requests` scopes; other scopes are ignored. |
| `fields` | array of strings | No | Array of fields you wish to search, allowed values are `title` only. Supports `merge_requests` scope; other scopes are ignored. Premium and Ultimate only. |
| `fields` | array of strings | No | Array of fields you wish to search, allowed values are `title` only. Supports `issues` and `merge_requests` scopes; other scopes are ignored. Premium and Ultimate only. |
### Scope: `projects`

View File

@ -1029,6 +1029,7 @@ the following are the names of GitLab Duo features:
- GitLab Duo AI Impact Dashboard
- GitLab Duo Chat
- GitLab Duo Code Explanation
- GitLab Duo Code Review
- GitLab Duo Code Review Summary
- GitLab Duo Code Suggestions
- GitLab Duo for the CLI

View File

@ -225,7 +225,7 @@ The following example uses GRIT to deploy the Google Kubernetes cluster and GitL
To have the cluster and GitLab Runner well configured, consider the following information:
- **How many job types do I need to cover?** This information comes from the access phase. The access phase aggregates metrics and identifies the number of resulting groups, considering organizational constraints. A **job type** is a collection of categorized jobs identified during the access phase. This categorization is based on the maximum resources needed by the job.
- **How many job types do I need to cover?** This information comes from the assess phase. The assess phase aggregates metrics and identifies the number of resulting groups, considering organizational constraints. A **job type** is a collection of categorized jobs identified during the access phase. This categorization is based on the maximum resources needed by the job.
- **How many GitLab Runner Managers do I need to run?** This information comes from the plan phase. If the organization manages projects separately, apply this framework to each project individually. This approach is relevant only when multiple job profiles are identified (for the entire organization or for a specific project), and they are all handled by an individual or a fleet of GitLab Runners. A basic configuration typically uses one GitLab Runner Manager per GKE cluster.
- **What is the estimated max concurrent CI/CD jobs?** This information represents an estimate of the maximum number of concurrent CI/CD jobs that are run at any point in time. This information is needed when configuring the GitLab Runner Manager by providing how long it waits during the `Prepare` stage: job pod scheduling on a node with limited available resources.

View File

@ -23,14 +23,14 @@ pipeline are ingested either after the job in the pipeline is complete or when t
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
For an overview, see [Vulnerability Management](https://www.youtube.com/watch?v=alMRIq5UPbw).
At all levels, the Vulnerability Report contains:
For projects and groups the Vulnerability Report contains:
- Totals of vulnerabilities per severity level.
- Filters for common vulnerability attributes.
- Details of each vulnerability, presented in tabular layout. For some vulnerabilities, the details
include a link to the relevant file and line number, in the default branch.
At the project level, the Vulnerability Report also contains:
For projects the Vulnerability Report also contains:
- A time stamp showing when it was updated, including a link to the latest pipeline.
- The number of failures that occurred in the most recent pipeline. Select the failure
@ -54,7 +54,7 @@ The issue icon (**{issues}**) indicates the issue's status. If [Jira issue suppo
issue link found in the **Activity** entry links out to the issue in Jira. Unlike GitLab issues, the
status of a Jira issue is not shown in the GitLab UI.
![Example project-level Vulnerability Report](img/vulnerability_report_v17_0.png)
![Example project Vulnerability Report](img/vulnerability_report_v17_0.png)
When vulnerabilities originate from a multi-project pipeline setup,
this page displays the vulnerabilities that originate from the selected project.
@ -135,11 +135,11 @@ If only GitLab analyzers are enabled, only those analyzers are listed.
### Project filter
The content of the Project filter depends on the current level:
The content of the Project filter varies:
- **Security Center**: Only projects you've [added to your personal Security Center](../security_dashboard/index.md#adding-projects-to-the-security-center).
- **Group level**: All projects in the group.
- **Project level**: Not applicable.
- **Group**: All projects in the group.
- **Project**: Not applicable.
### Activity filter
@ -182,13 +182,13 @@ The **GitLab Duo (AI)** filter is available when:
## Grouping vulnerabilities
> - Project-level grouping of vulnerabilities [introduced](https://gitlab.com/groups/gitlab-org/-/epics/10164) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `vulnerability_report_grouping`. Disabled by default.
> - Project-level grouping of vulnerabilities [enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134073) in GitLab 16.5.
> - Project-level grouping of vulnerabilities [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422509) in GitLab 16.6. Feature flag `vulnerability_report_grouping` removed.
> - Group-level grouping of vulnerabilities [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137778) in GitLab 16.7 with a flag named [`group_level_vulnerability_report_grouping`](https://gitlab.com/gitlab-org/gitlab/-/issues/432778). Disabled by default.
> - Group-level grouping of vulnerabilities [enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157949) in GitLab 17.2.
> - Group-level grouping of vulnerabilities [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/472669) in GitLab 17.3. Feature flag `group_level_vulnerability_report_grouping` removed.
> - Group-level OWASP top 10 grouping of vulnerabilities [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/437253) in GitLab 17.4. Feature flag `vulnerability_owasp_top_10_group` removed.
> - Grouping of vulnerabilities per project [introduced](https://gitlab.com/groups/gitlab-org/-/epics/10164) in GitLab 16.4 [with a flag](../../../administration/feature_flags.md) named `vulnerability_report_grouping`. Disabled by default.
> - Grouping of vulnerabilities per project [enabled on self-managed and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/134073) in GitLab 16.5.
> - Grouping of vulnerabilities per project [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/422509) in GitLab 16.6. Feature flag `vulnerability_report_grouping` removed.
> - Grouping of vulnerabilities per group [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/137778) in GitLab 16.7 with a flag named [`group_level_vulnerability_report_grouping`](https://gitlab.com/gitlab-org/gitlab/-/issues/432778). Disabled by default.
> - Grouping of vulnerabilities per group [enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/157949) in GitLab 17.2.
> - Grouping of vulnerabilities per group [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/472669) in GitLab 17.3. Feature flag `group_level_vulnerability_report_grouping` removed.
> - OWASP top 10 grouping of vulnerabilities per group [generally available](https://gitlab.com/gitlab-org/gitlab/-/issues/437253) in GitLab 17.4. Feature flag `vulnerability_owasp_top_10_group` removed.
> - Non-OWASP category in OWASP top 10 grouping is [default enabled on GitLab.com, self-managed, and GitLab Dedicated](https://gitlab.com/gitlab-org/gitlab/-/issues/463783) in GitLab 17.5 with a flag named `owasp_top_10_null_filtering`.
You can group vulnerabilities on the vulnerability report page to more efficiently triage them.

View File

@ -195,6 +195,17 @@ DETAILS:
- LLM: Anthropic [Claude 3 Haiku](https://docs.anthropic.com/en/docs/about-claude/models#claude-3-a-new-generation-of-ai)
- [View documentation](../project/issues/managing_issues.md#populate-an-issue-with-issue-description-generation).
### Code Review
DETAILS:
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
**Offering:** GitLab.com
**Status:** Experiment
- Automated code review of the proposed changes in your merge request.
- LLM: Anthropic [Claude 3.5 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet)
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#have-gitlab-duo-review-your-code).
### Code Review Summary
DETAILS:
@ -229,17 +240,6 @@ DETAILS:
- LLM: Anthropic [Claude 3.5 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet)
- [View documentation](../duo_workflow/index.md).
### GitLab Duo Code Review
DETAILS:
**Tier:** Ultimate
**Offering:** GitLab.com
**Status:** Experiment
- Automated code review of the proposed changes in your merge request.
- LLM: Anthropic [Claude 3.5 Sonnet](https://console.cloud.google.com/vertex-ai/publishers/anthropic/model-garden/claude-3-5-sonnet)
- [View documentation](../project/merge_requests/duo_in_merge_requests.md#gitlab-duo-code-review).
## Disable GitLab Duo features for specific groups or projects or an entire instance
Disable GitLab Duo features by [following these instructions](turn_on_off.md).

View File

@ -184,7 +184,7 @@ A personal access token can perform actions based on the assigned scopes.
| `read_registry` | Grants read-only (pull) access to [container registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the container registry is enabled. |
| `write_registry` | Grants read-write (push) access to [container registry](../packages/container_registry/index.md) images if a project is private and authorization is required. Available only when the container registry is enabled. |
| `sudo` | Grants permission to perform API actions as any user in the system, when authenticated as an administrator. |
| `admin_mode` | Grants permission to perform API actions as an administrator, when Admin Mode is enabled. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107875) in GitLab 15.8.) |
| `admin_mode` | Grants permission to perform API actions as an administrator, when Admin Mode is enabled. ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/107875) in GitLab 15.8. Available on GitLab self-managed only.) |
| `create_runner` | Grants permission to create runners. |
| `manage_runner` | Grants permission to manage runners. |
| `ai_features` | Grants permission to perform API actions for GitLab Duo. This scope is designed to work with the GitLab Duo Plugin for JetBrains. For all other extensions, see scope requirements. |

View File

@ -90,11 +90,12 @@ To generate a commit message with GitLab Duo:
- Contents of the file
- The filename
## GitLab Duo code review
## Have GitLab Duo review your code
DETAILS:
**Tier:** Ultimate with GitLab Duo Enterprise - [Start a trial](https://about.gitlab.com/solutions/gitlab-duo-pro/sales/?type=free-trial)
**Offering:** GitLab.com, Self-managed, GitLab Dedicated
**Offering:** GitLab.com
**Status:** Experiment
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/14825) in GitLab 17.5 as an [experiment](../../../policy/experiment-beta-support.md#experiment).
> - Feature flag `ai_review_merge_request` [disabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/456106).

View File

@ -17,7 +17,8 @@ module Gitlab
user_session_destroyed: 'Counter of user sessions being destroyed',
user_two_factor_authenticated: 'Counter of two factor authentications',
user_sessionless_authentication: 'Counter of sessionless authentications',
user_blocked: 'Counter of sign in attempts when user is blocked'
user_blocked: 'Counter of sign in attempts when user is blocked',
user_csrf_token_invalid: 'Counter of CSRF token validation failures'
}.freeze
def initialize(opts)
@ -61,6 +62,13 @@ module Gitlab
self.class.user_session_destroyed_counter_increment!
end
def user_csrf_token_mismatch!
label = @opts[:controller].class.name
label = 'other' unless label == 'GraphqlController'
self.class.user_csrf_token_invalid_counter.increment(controller: label)
end
def self.each_counter
COUNTERS.each_pair do |metric, description|
yield "#{metric}_counter", metric, description

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
class RestoreOptInToGitlabCom < BatchedMigrationJob
job_arguments :temporary_table_name
operation_name :update
feature_category :activation
def perform
each_sub_batch do |sub_batch|
sub_batch.connection.execute(
construct_query(
sub_batch
.where.not('onboarding_status::jsonb ? :key', key: 'email_opt_in')
.joins("INNER JOIN #{temporary_table_name} on user_id = #{temporary_table_name}.GITLAB_DOTCOM_ID")
)
)
end
end
private
def construct_query(sub_batch)
<<~SQL
UPDATE #{batch_table}
SET onboarding_status = jsonb_set(
#{batch_table}.onboarding_status, '{email_opt_in}', to_jsonb(#{temporary_table_name}.RESTORE_VALUE)
)
FROM #{temporary_table_name}
WHERE #{temporary_table_name}.GITLAB_DOTCOM_ID = #{batch_table}.user_id
AND #{temporary_table_name}.RESTORE_VALUE IS NOT NULL
AND #{batch_table}.user_id IN (#{sub_batch.select(:user_id).to_sql})
SQL
end
end
end
end

View File

@ -11,7 +11,7 @@ module Gitlab
token_size = token.bytesize
masked_string_size = MASKED_STRING.bytesize
mask = if Feature.enabled?(:consistent_ci_variable_masking, :instance) && token_size >= masked_string_size
mask = if token_size >= masked_string_size
MASKED_STRING + ('x' * (token_size - masked_string_size))
else
# While masked variables can't be less than 8 characters, this fallback case

View File

@ -12,11 +12,12 @@ module Gitlab
def initialize(
table:, sharding_key:, parent_table:, parent_sharding_key:,
foreign_key:, connection:, trigger_name: nil
foreign_key:, connection:, parent_table_primary_key: nil, trigger_name: nil
)
@table = table
@sharding_key = sharding_key
@parent_table = parent_table
@parent_table_primary_key = parent_table_primary_key
@parent_sharding_key = parent_sharding_key
@foreign_key = foreign_key
@name = trigger_name || generated_name
@ -28,7 +29,7 @@ module Gitlab
quoted_parent_table = quote_table_name(parent_table)
quoted_sharding_key = quote_column_name(sharding_key)
quoted_parent_sharding_key = quote_column_name(parent_sharding_key)
quoted_primary_key = quote_column_name('id')
quoted_primary_key = quote_column_name(parent_table_primary_key || 'id')
quoted_foreign_key = quote_column_name(foreign_key)
create_trigger_function(name) do
@ -59,7 +60,8 @@ module Gitlab
private
attr_reader :table, :sharding_key, :parent_table, :parent_sharding_key, :foreign_key, :connection
attr_reader :table, :sharding_key, :parent_table, :parent_table_primary_key, :parent_sharding_key,
:foreign_key, :connection
def generated_name
identifier = "#{table}_assign_#{sharding_key}"

View File

@ -31468,6 +31468,9 @@ msgstr ""
msgid "KubernetesDashboard|Failed"
msgstr ""
msgid "KubernetesDashboard|Failed to establish WebSocket connection"
msgstr ""
msgid "KubernetesDashboard|Job"
msgstr ""
@ -34736,9 +34739,6 @@ msgstr[1] ""
msgid "MlModelRegistry|(Optional)"
msgstr ""
msgid "MlModelRegistry|Add a model"
msgstr ""
msgid "MlModelRegistry|All artifact uploads failed or were canceled."
msgstr ""
@ -34775,9 +34775,6 @@ msgstr ""
msgid "MlModelRegistry|Create & import"
msgstr ""
msgid "MlModelRegistry|Create a model version"
msgstr ""
msgid "MlModelRegistry|Create model"
msgstr ""
@ -34994,15 +34991,9 @@ msgstr ""
msgid "MlModelRegistry|Setting up the client"
msgstr ""
msgid "MlModelRegistry|Start tracking your machine learning models"
msgstr ""
msgid "MlModelRegistry|Status"
msgstr ""
msgid "MlModelRegistry|Store and manage your machine learning models and versions"
msgstr ""
msgid "MlModelRegistry|Subfolder"
msgstr ""
@ -37987,9 +37978,6 @@ msgstr ""
msgid "Only accessible by %{membersPageLinkStart}project members%{membersPageLinkEnd}. Membership must be explicitly granted."
msgstr ""
msgid "Only active projects show up in the search and on the dashboard."
msgstr ""
msgid "Only effective when remote storage is enabled. Set to 0 for no size limit."
msgstr ""
@ -46351,24 +46339,15 @@ msgstr ""
msgid "Restore"
msgstr ""
msgid "Restore %{context}"
msgstr ""
msgid "Restore deployment"
msgstr ""
msgid "Restore group"
msgstr ""
msgid "Restore project"
msgstr ""
msgid "Restoring projects"
msgstr ""
msgid "Restoring the group will prevent the group, its subgroups and projects from being deleted on this date."
msgstr ""
msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it."
msgstr ""
msgid "Restrict access by IP address"
msgstr ""
@ -55149,9 +55128,6 @@ msgstr ""
msgid "The report has been successfully prepared."
msgstr ""
msgid "The repository can be committed to, and issues, comments and other entities can be created."
msgstr ""
msgid "The repository for this project does not exist."
msgstr ""
@ -55686,6 +55662,9 @@ msgstr ""
msgid "Third Party Advisory Link"
msgstr ""
msgid "This %{context} has been scheduled for deletion on %{strongStart}%{date}%{strongEnd}. To cancel the scheduled deletion, you can restore this %{context}, including all its resources."
msgstr ""
msgid "This %{issuableType} is confidential and should only be visible to team members with at least Reporter access."
msgstr ""
@ -55950,9 +55929,6 @@ msgstr ""
msgid "This group does not have any group runners yet."
msgstr ""
msgid "This group has been scheduled for permanent deletion on %{date}"
msgstr ""
msgid "This group has no active access tokens."
msgstr ""
@ -56300,9 +56276,6 @@ msgstr ""
msgid "This project was scheduled for deletion, but failed with the following message:"
msgstr ""
msgid "This project will be deleted on %{date}"
msgstr ""
msgid "This project's pipeline configuration is located outside this repository"
msgstr ""

View File

@ -1126,4 +1126,17 @@ RSpec.describe ApplicationController, feature_category: :shared do
expect(response).to have_gitlab_http_status(:service_unavailable)
end
end
describe 'cross-site request forgery protection handling' do
describe '#handle_unverified_request' do
it 'increments counter of invalid CSRF tokens detected' do
stub_authentication_activity_metrics do |metrics|
expect(metrics).to increment(:user_csrf_token_invalid_counter)
end
expect { described_class.new.handle_unverified_request }
.to raise_error(ActionController::InvalidAuthenticityToken)
end
end
end
end

View File

@ -323,11 +323,12 @@ RSpec.describe Projects::MergeRequests::CreationsController, feature_category: :
expect(json_response.size).to be(2)
forked_project = json_response.first
forked_project = json_response.detect { |project| project['id'] == fork_project.id }
expect(forked_project).to have_key('id')
expect(forked_project).to have_key('name')
expect(forked_project).to have_key('full_path')
expect(forked_project).to have_key('refs_url')
expect(forked_project).to have_key('forked')
end
end
end

View File

@ -34,7 +34,7 @@ RSpec.describe 'Help Pages', feature_category: :shared do
before do
stub_application_setting(version_check_enabled: true)
allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: false))
allow(user).to receive(:can_read_all_resources?).and_return(true)
allow(user).to receive(:can_admin_all_resources?).and_return(true)
sign_in(user)
visit help_path

View File

@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import loadAwardsHandler from '~/awards_handler';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
window.gl = window.gl || {};
@ -63,14 +64,14 @@ describe('AwardsHandler', () => {
u: '6.0',
},
{
n: 'thumbsup',
n: EMOJI_THUMBS_UP,
c: 'people',
e: '👍',
d: 'thumbs up sign',
u: '6.0',
},
{
n: 'thumbsdown',
n: EMOJI_THUMBS_DOWN,
c: 'people',
e: '👎',
d: 'thumbs down sign',
@ -200,13 +201,15 @@ describe('AwardsHandler', () => {
it('should handle :+1: and :-1: mutuality', () => {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').closest('button');
const $thumbsDownEmoji = $votesBlock.find('[data-name=thumbsdown]').closest('button');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
const $thumbsUpEmoji = $votesBlock.find(`[data-name=${EMOJI_THUMBS_UP}]`).closest('button');
const $thumbsDownEmoji = $votesBlock
.find(`[data-name=${EMOJI_THUMBS_DOWN}]`)
.closest('button');
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_UP, false);
expect($thumbsUpEmoji.hasClass('active')).toBe(true);
expect($thumbsDownEmoji.hasClass('active')).toBe(false);
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true);
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_DOWN, true);
expect($thumbsUpEmoji.hasClass('active')).toBe(false);
expect($thumbsDownEmoji.hasClass('active')).toBe(true);
@ -230,9 +233,9 @@ describe('AwardsHandler', () => {
it('should prepend "You" to the award tooltip', () => {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').closest('button');
const $thumbsUpEmoji = $votesBlock.find(`[data-name=${EMOJI_THUMBS_UP}]`).closest('button');
$thumbsUpEmoji.attr('data-title', 'sam, jerry, max, and andy');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_UP, false);
expect($thumbsUpEmoji.attr('title')).toBe('You, sam, jerry, max, and andy');
});
@ -240,9 +243,9 @@ describe('AwardsHandler', () => {
it('handles the special case where "You" is not cleanly comma separated', () => {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').closest('button');
const $thumbsUpEmoji = $votesBlock.find(`[data-name=${EMOJI_THUMBS_UP}]`).closest('button');
$thumbsUpEmoji.attr('data-title', 'sam');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_UP, false);
expect($thumbsUpEmoji.attr('title')).toBe('You and sam');
});
@ -252,10 +255,10 @@ describe('AwardsHandler', () => {
it('removes "You" from the front of the tooltip', () => {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').closest('button');
const $thumbsUpEmoji = $votesBlock.find(`[data-name=${EMOJI_THUMBS_UP}]`).closest('button');
$thumbsUpEmoji.attr('data-title', 'You, sam, jerry, max, and andy');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_UP, false);
expect($thumbsUpEmoji.attr('title')).toBe('sam, jerry, max, and andy');
});
@ -263,10 +266,10 @@ describe('AwardsHandler', () => {
it('handles the special case where "You" is not cleanly comma separated', () => {
const awardUrl = awardsHandler.getAwardUrl();
const $votesBlock = $('.js-awards-block').eq(0);
const $thumbsUpEmoji = $votesBlock.find('[data-name=thumbsup]').closest('button');
const $thumbsUpEmoji = $votesBlock.find(`[data-name=${EMOJI_THUMBS_UP}]`).closest('button');
$thumbsUpEmoji.attr('data-title', 'You and sam');
$thumbsUpEmoji.addClass('active');
awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false);
awardsHandler.addAward($votesBlock, awardUrl, EMOJI_THUMBS_UP, false);
expect($thumbsUpEmoji.attr('title')).toBe('sam');
});
@ -322,8 +325,8 @@ describe('AwardsHandler', () => {
awardsHandler.searchEmojis('thumb');
const $menu = $('.emoji-menu');
const $thumbsUpItem = $menu.find('[data-name=thumbsup]');
const $thumbsDownItem = $menu.find('[data-name=thumbsdown]');
const $thumbsUpItem = $menu.find(`[data-name=${EMOJI_THUMBS_UP}]`);
const $thumbsDownItem = $menu.find(`[data-name=${EMOJI_THUMBS_DOWN}]`);
expect($thumbsUpItem.is(':visible')).toBe(true);
expect($thumbsDownItem.is(':visible')).toBe(true);

View File

@ -122,7 +122,7 @@ describe('gl_emoji', () => {
await waitForPromises();
expect(glEmojiElement.outerHTML).toBe(
'<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/3/grey_question.png" align="absmiddle"></gl-emoji>',
`<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" align="absmiddle"></gl-emoji>`,
);
});

View File

@ -30,7 +30,6 @@ describe('Pipeline label component', () => {
propsData: { ...defaultProps, ...props },
provide: {
pipelineSchedulesPath: 'group/project/-/schedules',
targetProjectFullPath: projectPath,
...provide,
},
});
@ -151,67 +150,31 @@ describe('Pipeline label component', () => {
});
describe('fork badge', () => {
describe('when the pipeline path does not equal the project path', () => {
describe('with a trailing slash', () => {
beforeEach(() => {
const forkedPipeline = { ...defaultProps.pipeline };
forkedPipeline.project.full_path = '/test/forked';
describe('when project is not forked', () => {
it('does not render the badge', () => {
createComponent();
createComponent({
...forkedPipeline,
});
});
expect(findForkTag().exists()).toBe(false);
});
});
it('renders the badge', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
describe('when project is forked', () => {
beforeEach(() => {
const forkedPipeline = { ...defaultProps.pipeline };
forkedPipeline.project.forked = true;
createComponent({
...forkedPipeline,
});
});
describe('without a trailing slash', () => {
beforeEach(() => {
const forkedPipeline = { ...defaultProps.pipeline };
forkedPipeline.project.full_path = 'test/forked';
createComponent({
...forkedPipeline,
});
});
it('renders the badge', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
});
it('renders the badge', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
});
});
});
describe('when the project path equals the pipeline path', () => {
beforeEach(() => {
const sourcePipeline = { ...defaultProps.pipeline };
sourcePipeline.project.full_path = 'test/test';
createComponent({ ...sourcePipeline });
});
it('should not render', () => {
expect(findForkTag().exists()).toBe(false);
});
});
describe('when no targetBranchFullPath is provided', () => {
beforeEach(() => {
const forkedPipeline = { ...defaultProps.pipeline };
forkedPipeline.project.full_path = 'test/forked';
createComponent({ ...forkedPipeline }, { targetProjectFullPath: undefined });
});
it('does not render the badge', () => {
expect(findForkTag().exists()).toBe(false);
});
});
it('should render the merged results badge when the pipeline is a merged results pipeline', () => {
const mergedResultsPipeline = defaultProps.pipeline;
mergedResultsPipeline.flags.merged_result_pipeline = true;

View File

@ -33,9 +33,6 @@ describe('Pipeline Url Component', () => {
const createComponent = (props) => {
wrapper = shallowMountExtended(PipelineUrlComponent, {
propsData: { ...defaultProps, ...props },
provide: {
targetProjectFullPath: projectPath,
},
});
};

View File

@ -6,6 +6,7 @@ import AutocompleteHelper, {
createDataSource,
} from '~/content_editor/services/autocomplete_helper';
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
import {
MOCK_MEMBERS,
MOCK_COMMANDS,
@ -253,7 +254,7 @@ describe('AutocompleteHelper', () => {
const dataSource = autocompleteHelper.getDataSource('emoji');
const results = await dataSource.search('');
expect(results).toEqual([{ emoji: { name: 'thumbsup' }, fieldValue: 'thumbsup' }]);
expect(results).toEqual([{ emoji: { name: EMOJI_THUMBS_UP }, fieldValue: EMOJI_THUMBS_UP }]);
});
it('updates dataSourcesUrl correctly', () => {

View File

@ -13,6 +13,7 @@ import DesignReplyForm from '~/design_management/components/design_notes/design_
import ImportedBadge from '~/vue_shared/components/imported_badge.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import designNoteAwardEmojiToggleMutation from '~/design_management/graphql/mutations/design_note_award_emoji_toggle.mutation.graphql';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
import { mockAwardEmoji } from '../../mock_data/apollo_mock';
const scrollIntoViewMock = jest.fn();
@ -399,12 +400,12 @@ describe('Design note component', () => {
},
});
findEmojiPicker().vm.$emit('click', 'thumbsup');
findEmojiPicker().vm.$emit('click', EMOJI_THUMBS_UP);
expect(mutate).toHaveBeenCalledWith({
mutation: designNoteAwardEmojiToggleMutation,
variables: {
name: 'thumbsup',
name: EMOJI_THUMBS_UP,
awardableId: note.id,
},
optimisticResponse: {
@ -431,7 +432,7 @@ describe('Design note component', () => {
},
});
findEmojiPicker().vm.$emit('click', 'thumbsup');
findEmojiPicker().vm.$emit('click', EMOJI_THUMBS_UP);
expect(mutate).toHaveBeenCalled();

View File

@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
jest.mock('~/sentry/sentry_browser_wrapper');
jest.mock('~/vue_shared/plugins/global_toast');
@ -42,10 +43,10 @@ describe('Awards app actions', () => {
window.gon = { relative_url_root: relativeRootUrl };
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
.reply(HTTP_STATUS_OK, ['thumbsup'], { 'x-next-page': '2' });
.reply(HTTP_STATUS_OK, [EMOJI_THUMBS_UP], { 'x-next-page': '2' });
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
.reply(HTTP_STATUS_OK, ['thumbsdown']);
.reply(HTTP_STATUS_OK, [EMOJI_THUMBS_DOWN]);
});
it('commits FETCH_AWARDS_SUCCESS', async () => {
@ -53,7 +54,7 @@ describe('Awards app actions', () => {
actions.fetchAwards,
'1',
{ path: '/awards' },
[{ type: 'FETCH_AWARDS_SUCCESS', payload: ['thumbsup'] }],
[{ type: 'FETCH_AWARDS_SUCCESS', payload: [EMOJI_THUMBS_UP] }],
[{ type: 'fetchAwards', payload: '2' }],
);
});
@ -146,7 +147,7 @@ describe('Awards app actions', () => {
});
describe('removing an award', () => {
const mockData = { id: 1, name: 'thumbsup', user: { id: 1 } };
const mockData = { id: 1, name: EMOJI_THUMBS_UP, user: { id: 1 } };
describe('success', () => {
beforeEach(() => {
@ -156,7 +157,7 @@ describe('Awards app actions', () => {
it('commits REMOVE_AWARD', () => {
testAction(
actions.toggleAward,
'thumbsup',
EMOJI_THUMBS_UP,
{
path: '/awards',
currentUserId: 1,
@ -169,7 +170,7 @@ describe('Awards app actions', () => {
describe('error', () => {
const currentUserId = 1;
const name = 'thumbsup';
const name = EMOJI_THUMBS_UP;
beforeEach(() => {
mock

View File

@ -5,6 +5,7 @@ import {
REMOVE_AWARD,
} from '~/emoji/awards_app/store/mutation_types';
import mutations from '~/emoji/awards_app/store/mutations';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
describe('Awards app mutations', () => {
describe('SET_INITIAL_DATA', () => {
@ -29,27 +30,27 @@ describe('Awards app mutations', () => {
it('sets awards', () => {
const state = { awards: [] };
mutations[FETCH_AWARDS_SUCCESS](state, ['thumbsup']);
mutations[FETCH_AWARDS_SUCCESS](state, [EMOJI_THUMBS_UP]);
expect(state.awards).toEqual(['thumbsup']);
expect(state.awards).toEqual([EMOJI_THUMBS_UP]);
});
it('does not overwrite previously set awards', () => {
const state = { awards: ['thumbsup'] };
const state = { awards: [EMOJI_THUMBS_UP] };
mutations[FETCH_AWARDS_SUCCESS](state, ['thumbsdown']);
mutations[FETCH_AWARDS_SUCCESS](state, [EMOJI_THUMBS_DOWN]);
expect(state.awards).toEqual(['thumbsup', 'thumbsdown']);
expect(state.awards).toEqual([EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN]);
});
});
describe('ADD_NEW_AWARD', () => {
it('adds new award to array', () => {
const state = { awards: ['thumbsup'] };
const state = { awards: [EMOJI_THUMBS_UP] };
mutations[ADD_NEW_AWARD](state, 'thumbsdown');
mutations[ADD_NEW_AWARD](state, EMOJI_THUMBS_DOWN);
expect(state.awards).toEqual(['thumbsup', 'thumbsdown']);
expect(state.awards).toEqual([EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN]);
});
});

View File

@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Category from '~/emoji/components/category.vue';
import EmojiGroup from '~/emoji/components/emoji_group.vue';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
let wrapper;
function factory(propsData = {}) {
@ -18,7 +19,7 @@ describe('Emoji category component', () => {
beforeEach(() => {
factory({
category: 'Activity',
emojis: [['thumbsup'], ['thumbsdown']],
emojis: [[EMOJI_THUMBS_UP], [EMOJI_THUMBS_DOWN]],
});
});

View File

@ -3,6 +3,7 @@ import Vue from 'vue';
import { GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmojiGroup from '~/emoji/components/emoji_group.vue';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
Vue.config.ignoredElements = ['gl-emoji'];
@ -31,7 +32,7 @@ describe('Emoji group component', () => {
it('renders emojis', () => {
factory({
emojis: ['thumbsup', 'thumbsdown'],
emojis: [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN],
renderGroup: true,
});
@ -41,12 +42,12 @@ describe('Emoji group component', () => {
it('emits emoji-click', () => {
factory({
emojis: ['thumbsup', 'thumbsdown'],
emojis: [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN],
renderGroup: true,
});
wrapper.findComponent(GlButton).vm.$emit('click');
expect(wrapper.emitted('emoji-click')).toStrictEqual([['thumbsup']]);
expect(wrapper.emitted('emoji-click')).toStrictEqual([[EMOJI_THUMBS_UP]]);
});
});

View File

@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import EmojiList from '~/emoji/components/emoji_list.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
jest.mock('~/emoji', () => ({
initEmojiMap: jest.fn(() => Promise.resolve()),
@ -44,7 +45,7 @@ describe('Emoji list component', () => {
expect(JSON.parse(findDefaultSlot().text())).toEqual({
activity: {
emojis: [['thumbsup', 'thumbsdown']],
emojis: [[EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN]],
height: expect.any(Number),
top: expect.any(Number),
},

View File

@ -1,5 +1,6 @@
import { initEmojiMock } from 'helpers/emoji';
import { getFrequentlyUsedEmojis, addToFrequentlyUsed } from '~/emoji/components/utils';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
describe('getFrequentlyUsedEmojis', () => {
beforeAll(async () => {
@ -13,13 +14,13 @@ describe('getFrequentlyUsedEmojis', () => {
});
it('returns frequently used emojis object', async () => {
Storage.prototype.getItem = jest.fn(() => 'thumbsup,thumbsdown');
Storage.prototype.getItem = jest.fn(() => `${EMOJI_THUMBS_UP},${EMOJI_THUMBS_DOWN}`);
const frequentlyUsed = await getFrequentlyUsedEmojis();
expect(frequentlyUsed).toEqual({
frequently_used: {
emojis: [['thumbsup', 'thumbsdown']],
emojis: [[EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN]],
top: 0,
height: 71,
},
@ -27,13 +28,13 @@ describe('getFrequentlyUsedEmojis', () => {
});
it('only returns frequently used emojis that are in the possible emoji set', async () => {
Storage.prototype.getItem = jest.fn(() => 'thumbsup,thumbsdown,ack');
Storage.prototype.getItem = jest.fn(() => `${EMOJI_THUMBS_UP},${EMOJI_THUMBS_DOWN},ack`);
const frequentlyUsed = await getFrequentlyUsedEmojis();
expect(frequentlyUsed).toEqual({
frequently_used: {
emojis: [['thumbsup', 'thumbsdown']],
emojis: [[EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN]],
top: 0,
height: 71,
},
@ -45,27 +46,27 @@ describe('addToFrequentlyUsed', () => {
it('sets cookie value', () => {
Storage.prototype.getItem = jest.fn(() => null);
addToFrequentlyUsed('thumbsup');
addToFrequentlyUsed(EMOJI_THUMBS_UP);
expect(localStorage.setItem).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup');
expect(localStorage.setItem).toHaveBeenCalledWith('frequently_used_emojis', EMOJI_THUMBS_UP);
});
it('sets cookie value to include previously set cookie value', () => {
Storage.prototype.getItem = jest.fn(() => 'thumbsdown');
Storage.prototype.getItem = jest.fn(() => EMOJI_THUMBS_DOWN);
addToFrequentlyUsed('thumbsup');
addToFrequentlyUsed(EMOJI_THUMBS_UP);
expect(localStorage.setItem).toHaveBeenCalledWith(
'frequently_used_emojis',
'thumbsdown,thumbsup',
`${EMOJI_THUMBS_DOWN},${EMOJI_THUMBS_UP}`,
);
});
it('sets cookie value with uniq values', () => {
Storage.prototype.getItem = jest.fn(() => 'thumbsup');
Storage.prototype.getItem = jest.fn(() => EMOJI_THUMBS_UP);
addToFrequentlyUsed('thumbsup');
addToFrequentlyUsed(EMOJI_THUMBS_UP);
expect(localStorage.setItem).toHaveBeenCalledWith('frequently_used_emojis', 'thumbsup');
expect(localStorage.setItem).toHaveBeenCalledWith('frequently_used_emojis', EMOJI_THUMBS_UP);
});
});

View File

@ -29,7 +29,13 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
import { CACHE_KEY, CACHE_VERSION_KEY, NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
import {
CACHE_KEY,
CACHE_VERSION_KEY,
EMOJI_THUMBS_UP,
EMOJI_THUMBS_DOWN,
NEUTRAL_INTENT_MULTIPLIER,
} from '~/emoji/constants';
import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@ -668,12 +674,12 @@ describe('emoji', () => {
let score = NEUTRAL_INTENT_MULTIPLIER;
// Positive intent value retrieved from ~/emoji/intents.json
if (name === 'thumbsup') {
if (name === EMOJI_THUMBS_UP) {
score = 0.5;
}
// Negative intent value retrieved from ~/emoji/intents.json
if (name === 'thumbsdown') {
if (name === EMOJI_THUMBS_DOWN) {
score = 1.5;
}
@ -842,12 +848,12 @@ describe('emoji', () => {
'thumbs',
[
{
name: 'thumbsup',
name: EMOJI_THUMBS_UP,
field: 'd',
score: 0.5,
},
{
name: 'thumbsdown',
name: EMOJI_THUMBS_DOWN,
field: 'd',
score: 1.5,
},
@ -925,9 +931,9 @@ describe('emoji', () => {
});
it.each`
emoji | src
${'thumbsup'} | ${'/-/emojis/3/thumbsup.png'}
${'parrot'} | ${'parrot.gif'}
emoji | src
${EMOJI_THUMBS_UP} | ${`/-/emojis/${EMOJI_VERSION}/${EMOJI_THUMBS_UP}.png`}
${'parrot'} | ${'parrot.gif'}
`('returns $src for emoji with name $emoji', ({ emoji, src }) => {
expect(emojiFallbackImageSrc(emoji)).toBe(src);
});

View File

@ -1,13 +1,14 @@
import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
describe('Utils', () => {
describe('getEmojiScoreWithIntent', () => {
it.each`
emojiName | baseScore | finalScore
${'thumbsup'} | ${1} | ${1}
${'thumbsdown'} | ${1} | ${3}
${'neutralemoji'} | ${1} | ${2}
${'zerobaseemoji'} | ${0} | ${1}
emojiName | baseScore | finalScore
${EMOJI_THUMBS_UP} | ${1} | ${1}
${EMOJI_THUMBS_DOWN} | ${1} | ${3}
${'neutralemoji'} | ${1} | ${2}
${'zerobaseemoji'} | ${0} | ${1}
`('returns the correct score for $emojiName', ({ emojiName, baseScore, finalScore }) => {
expect(getEmojiScoreWithIntent(emojiName, baseScore)).toBe(finalScore);
});

View File

@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import { WatchApi } from '@gitlab/cluster-client';
import { WatchApi, WebSocketWatchManager } from '@gitlab/cluster-client';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
@ -23,14 +23,15 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const configuration = {
basePath: 'kas-proxy/',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
headers: { 'GitLab-Agent-Id': '1' },
};
beforeEach(() => {
mockResolvers = resolvers();
mock = new MockAdapter(axios);
gon.features = {
useWebsocketForK8sWatch: false,
};
});
afterEach(() => {
@ -64,7 +65,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('when the Kustomization data is present', () => {
beforeEach(() => {
mock
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.onGet(endpoint, { withCredentials: true, headers: configuration.headers })
.reply(HTTP_STATUS_OK, {
apiVersion,
...fluxKustomizationMock,
@ -107,11 +108,66 @@ describe('~/frontend/environments/graphql/resolvers', () => {
expect(kustomizationStatus).toEqual(fluxKustomizationMapped);
});
describe('when `useWebsocketForK8sWatch` feature is enabled', () => {
const mockWebsocketManager = WebSocketWatchManager.prototype;
const mockInitConnectionFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWebsocketManager);
});
beforeEach(() => {
gon.features = {
useWebsocketForK8sWatch: true,
};
jest
.spyOn(mockWebsocketManager, 'initConnection')
.mockImplementation(mockInitConnectionFn);
jest.spyOn(mockWebsocketManager, 'on').mockImplementation(jest.fn());
});
it('calls websocket API', async () => {
await mockResolvers.Query.fluxKustomization(
null,
{
configuration,
fluxResourcePath,
},
{ client },
);
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `kustomizations-${resourceName}`,
watchParams: {
fieldSelector: `metadata.name=${resourceName}`,
group: 'kustomize.toolkit.fluxcd.io',
namespace: resourceNamespace,
resource: 'kustomizations',
version: 'v1',
},
},
});
});
it("doesn't call watch API", async () => {
await mockResolvers.Query.fluxKustomization(
null,
{
configuration,
fluxResourcePath,
},
{ client },
);
expect(mockKustomizationStatusFn).not.toHaveBeenCalled();
});
});
});
it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
mock
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.onGet(endpoint, { withCredentials: true, headers: configuration.headers })
.reply(HTTP_STATUS_OK, {});
await mockResolvers.Query.fluxKustomization(
@ -172,7 +228,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('when the HelmRelease data is present', () => {
beforeEach(() => {
mock
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.onGet(endpoint, { withCredentials: true, headers: configuration.headers })
.reply(HTTP_STATUS_OK, {
apiVersion,
...fluxHelmReleaseMock,
@ -213,7 +269,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should not watch Kustomization by the metadata name from the cluster_client library when the data is not present', async () => {
mock
.onGet(endpoint, { withCredentials: true, headers: configuration.baseOptions.headers })
.onGet(endpoint, { withCredentials: true, headers: configuration.headers })
.reply(HTTP_STATUS_OK, {});
await mockResolvers.Query.fluxHelmRelease(

View File

@ -3,6 +3,7 @@ import {
CoreV1Api,
AppsV1Api,
WatchApi,
WebSocketWatchManager,
EVENT_DATA,
EVENT_TIMEOUT,
EVENT_ERROR,
@ -36,15 +37,16 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const configuration = {
basePath: 'kas-proxy/',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
headers: { 'GitLab-Agent-Id': '1', 'X-CSRF-Token': 'token' },
};
const namespace = 'default';
beforeEach(() => {
mockResolvers = resolvers();
mock = new MockAdapter(axios);
gon.features = {
useWebsocketForK8sWatch: false,
};
});
afterEach(() => {
@ -130,6 +132,47 @@ describe('~/frontend/environments/graphql/resolvers', () => {
data: { k8sPods: [] },
});
});
describe('when `useWebsocketForK8sWatch` feature is enabled', () => {
const mockWebsocketManager = WebSocketWatchManager.prototype;
const mockInitConnectionFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWebsocketManager);
});
beforeEach(() => {
gon.features = {
useWebsocketForK8sWatch: true,
};
jest
.spyOn(mockWebsocketManager, 'initConnection')
.mockImplementation(mockInitConnectionFn);
jest.spyOn(mockWebsocketManager, 'on').mockImplementation(jest.fn());
});
it('calls websocket API', async () => {
await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `k8sPods-n-${namespace}`,
watchParams: {
namespace,
resource: 'pods',
version: 'v1',
},
},
});
});
it("doesn't call watch API", async () => {
await mockResolvers.Query.k8sPods(null, { configuration, namespace }, { client });
expect(CoreV1Api.prototype.listCoreV1NamespacedPod).toHaveBeenCalled();
expect(mockPodsListWatcherFn).not.toHaveBeenCalled();
});
});
});
it('should not watch pods from the cluster_client library when the pods data is not present', async () => {
@ -216,6 +259,47 @@ describe('~/frontend/environments/graphql/resolvers', () => {
data: { k8sServices: [] },
});
});
describe('when `useWebsocketForK8sWatch` feature is enabled', () => {
const mockWebsocketManager = WebSocketWatchManager.prototype;
const mockInitConnectionFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWebsocketManager);
});
beforeEach(() => {
gon.features = {
useWebsocketForK8sWatch: true,
};
jest
.spyOn(mockWebsocketManager, 'initConnection')
.mockImplementation(mockInitConnectionFn);
jest.spyOn(mockWebsocketManager, 'on').mockImplementation(jest.fn());
});
it('calls websocket API', async () => {
await mockResolvers.Query.k8sServices(null, { configuration, namespace }, { client });
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `k8sServices-n-${namespace}`,
watchParams: {
namespace,
resource: 'services',
version: 'v1',
},
},
});
});
it("doesn't call watch API", async () => {
await mockResolvers.Query.k8sServices(null, { configuration, namespace }, { client });
expect(CoreV1Api.prototype.listCoreV1NamespacedService).toHaveBeenCalled();
expect(mockServicesListWatcherFn).not.toHaveBeenCalled();
});
});
});
it('should not watch services from the cluster_client library when the services data is not present', async () => {
@ -436,6 +520,56 @@ describe('~/frontend/environments/graphql/resolvers', () => {
data: { k8sEvents: [] },
});
});
describe('when `useWebsocketForK8sWatch` feature is enabled', () => {
const mockWebsocketManager = WebSocketWatchManager.prototype;
const mockInitConnectionFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWebsocketManager);
});
beforeEach(() => {
gon.features = {
useWebsocketForK8sWatch: true,
};
jest
.spyOn(mockWebsocketManager, 'initConnection')
.mockImplementation(mockInitConnectionFn);
jest.spyOn(mockWebsocketManager, 'on').mockImplementation(jest.fn());
});
it('calls websocket API', async () => {
await mockResolvers.Query.k8sEvents(
null,
{ configuration, namespace, involvedObjectName },
{ client },
);
expect(mockInitConnectionFn).toHaveBeenCalledWith({
configuration,
message: {
watchId: `events-io-${involvedObjectName}`,
watchParams: {
namespace,
resource: 'events',
fieldSelector: `involvedObject.name=${involvedObjectName}`,
version: 'v1',
},
},
});
});
it("doesn't call watch API", async () => {
await mockResolvers.Query.k8sEvents(
null,
{ configuration, namespace, involvedObjectName },
{ client },
);
expect(CoreV1Api.prototype.listCoreV1NamespacedEvent).toHaveBeenCalled();
expect(mockEventsListWatcherFn).not.toHaveBeenCalled();
});
});
});
it('should throw an error if the API call fails', async () => {

View File

@ -18,6 +18,7 @@ import {
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
export const getIssuesQueryResponse = {
data: {
@ -203,8 +204,8 @@ export const locationSearch = [
'type[]=feature',
'not[type][]=bug',
'not[type][]=incident',
'my_reaction_emoji=thumbsup',
'not[my_reaction_emoji]=thumbsdown',
`my_reaction_emoji=${EMOJI_THUMBS_UP}`,
`not[my_reaction_emoji]=${EMOJI_THUMBS_DOWN}`,
'confidential=yes',
'iteration_id=4',
'iteration_id=12',
@ -291,8 +292,8 @@ const makeFilteredTokens = ({ grouped }) => [
{ type: TOKEN_TYPE_TYPE, value: { data: 'feature', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'bug', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_TYPE, value: { data: 'incident', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: EMOJI_THUMBS_UP, operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: EMOJI_THUMBS_DOWN, operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
@ -332,7 +333,7 @@ export const apiParams = {
labelName: ['cartoon', 'tv'],
releaseTag: ['v3', 'v4'],
types: ['ISSUE', 'FEATURE'],
myReactionEmoji: 'thumbsup',
myReactionEmoji: EMOJI_THUMBS_UP,
confidential: true,
iterationId: ['4', '12'],
epicId: '12',
@ -347,7 +348,7 @@ export const apiParams = {
labelName: ['live action', 'drama'],
releaseTag: ['v20', 'v30'],
types: ['BUG', 'INCIDENT'],
myReactionEmoji: 'thumbsdown',
myReactionEmoji: EMOJI_THUMBS_DOWN,
iterationId: ['20', '42'],
epicId: '34',
weight: '3',
@ -390,8 +391,8 @@ export const urlParams = {
'not[release_tag]': ['v20', 'v30'],
'type[]': ['issue', 'feature'],
'not[type][]': ['bug', 'incident'],
my_reaction_emoji: 'thumbsup',
'not[my_reaction_emoji]': 'thumbsdown',
my_reaction_emoji: EMOJI_THUMBS_UP,
'not[my_reaction_emoji]': EMOJI_THUMBS_DOWN,
confidential: 'yes',
iteration_id: ['4', '12'],
'not[iteration_id]': ['20', '42'],

View File

@ -14,6 +14,7 @@ import {
TOKEN_TYPE_WEIGHT,
TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
export const getServiceDeskIssuesQueryResponse = {
data: {
@ -174,8 +175,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v30', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsup', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: 'thumbsdown', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: EMOJI_THUMBS_UP, operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_MY_REACTION, value: { data: EMOJI_THUMBS_DOWN, operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONFIDENTIAL, value: { data: 'yes', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ITERATION, value: { data: '12', operator: OPERATOR_IS } },
@ -201,8 +202,8 @@ export const urlParams = {
'or[label_name][]': ['comedy', 'sitcom'],
release_tag: ['v3', 'v4'],
'not[release_tag]': ['v20', 'v30'],
my_reaction_emoji: 'thumbsup',
'not[my_reaction_emoji]': 'thumbsdown',
my_reaction_emoji: EMOJI_THUMBS_UP,
'not[my_reaction_emoji]': EMOJI_THUMBS_DOWN,
confidential: 'yes',
iteration_id: ['4', '12'],
'not[iteration_id]': ['20', '42'],
@ -237,8 +238,8 @@ export const locationSearch = [
'release_tag=v4',
'not[release_tag]=v20',
'not[release_tag]=v30',
'my_reaction_emoji=thumbsup',
'not[my_reaction_emoji]=thumbsdown',
`my_reaction_emoji=${EMOJI_THUMBS_UP}`,
`not[my_reaction_emoji]=${EMOJI_THUMBS_DOWN}`,
'confidential=yes',
'iteration_id=4',
'iteration_id=12',

View File

@ -1,47 +0,0 @@
import { GlEmptyState } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { MODEL_ENTITIES } from '~/ml/model_registry/constants';
import EmptyState from '~/ml/model_registry/components/empty_state.vue';
let wrapper;
const createWrapper = (entityType) => {
wrapper = shallowMount(EmptyState, { propsData: { entityType } });
};
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
describe('ml/model_registry/components/empty_state.vue', () => {
describe('when entity type is model', () => {
beforeEach(() => {
createWrapper(MODEL_ENTITIES.model);
});
it('shows the correct empty state', () => {
expect(findEmptyState().props()).toMatchObject({
title: 'Start tracking your machine learning models',
description: 'Store and manage your machine learning models and versions',
primaryButtonText: 'Add a model',
primaryButtonLink:
'/help/user/project/ml/model_registry/index#create-machine-learning-models-by-using-the-ui',
svgPath: 'file-mock',
});
});
});
describe('when entity type is model version', () => {
beforeEach(() => {
createWrapper(MODEL_ENTITIES.modelVersion);
});
it('shows the correct empty state', () => {
expect(findEmptyState().props()).toMatchObject({
title: 'Manage versions of your machine learning model',
description: 'Use versions to track performance, parameters, and metadata',
primaryButtonText: 'Create a model version',
primaryButtonLink:
'/help/user/project/ml/model_registry/index#create-a-model-version-by-using-the-ui',
svgPath: 'file-mock',
});
});
});
});

View File

@ -7,6 +7,7 @@ import { NEVER_TIME_RANGE } from '~/set_status_modal/constants';
import EmojiPicker from '~/emoji/components/picker.vue';
import { timeRanges } from '~/vue_shared/constants';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
const [thirtyMinutes, , , oneDay] = timeRanges;
@ -15,7 +16,7 @@ describe('SetStatusForm', () => {
const defaultPropsData = {
defaultEmoji: 'speech_balloon',
emoji: 'thumbsup',
emoji: EMOJI_THUMBS_UP,
message: 'Foo bar',
availability: false,
};

View File

@ -13,6 +13,7 @@ import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { BV_HIDE_MODAL } from '~/lib/utils/constants';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
jest.mock('~/alert');
@ -103,9 +104,9 @@ describe('SetStatusModalWrapper', () => {
});
it('passes emoji to `SetStatusForm`', async () => {
await getEmojiPicker().vm.$emit('click', 'thumbsup');
await getEmojiPicker().vm.$emit('click', EMOJI_THUMBS_UP);
expect(wrapper.findComponent(SetStatusForm).props('emoji')).toBe('thumbsup');
expect(wrapper.findComponent(SetStatusForm).props('emoji')).toBe(EMOJI_THUMBS_UP);
});
});

View File

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import AwardsList from '~/vue_shared/components/awards_list.vue';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
const createUser = (id, name) => ({ id, name });
const createAward = (name, user) => ({ name, user });
@ -23,8 +24,6 @@ const USERS = {
const EMOJI_SMILE = 'smile';
const EMOJI_OK = 'ok_hand';
const EMOJI_THUMBSUP = 'thumbsup';
const EMOJI_THUMBSDOWN = 'thumbsdown';
const EMOJI_A = 'a';
const EMOJI_B = 'b';
const EMOJI_CACTUS = 'cactus';
@ -34,16 +33,16 @@ const EMOJI_RACEHORSE = 'racehorse';
const TEST_AWARDS = [
createAward(EMOJI_SMILE, USERS.ada),
createAward(EMOJI_OK, USERS.ada),
createAward(EMOJI_THUMBSUP, USERS.ada),
createAward(EMOJI_THUMBSDOWN, USERS.ada),
createAward(EMOJI_THUMBS_UP, USERS.ada),
createAward(EMOJI_THUMBS_DOWN, USERS.ada),
createAward(EMOJI_SMILE, USERS.jane),
createAward(EMOJI_OK, USERS.jane),
createAward(EMOJI_OK, USERS.leonardo),
createAward(EMOJI_THUMBSUP, USERS.leonardo),
createAward(EMOJI_THUMBSUP, USERS.marie),
createAward(EMOJI_THUMBSDOWN, USERS.marie),
createAward(EMOJI_THUMBSDOWN, USERS.root),
createAward(EMOJI_THUMBSDOWN, USERS.donatello),
createAward(EMOJI_THUMBS_UP, USERS.leonardo),
createAward(EMOJI_THUMBS_UP, USERS.marie),
createAward(EMOJI_THUMBS_DOWN, USERS.marie),
createAward(EMOJI_THUMBS_DOWN, USERS.root),
createAward(EMOJI_THUMBS_DOWN, USERS.donatello),
createAward(EMOJI_OK, USERS.root),
// Test that emoji list preserves order of occurrence, not alphabetical order
@ -72,8 +71,8 @@ const TEST_AWARDS = [
const TEST_AWARDS_LENGTH = [
EMOJI_SMILE,
EMOJI_OK,
EMOJI_THUMBSUP,
EMOJI_THUMBSDOWN,
EMOJI_THUMBS_UP,
EMOJI_THUMBS_DOWN,
EMOJI_A,
EMOJI_B,
EMOJI_CACTUS,
@ -125,16 +124,16 @@ describe('vue_shared/components/awards_list', () => {
{
classes: expect.arrayContaining(REACTION_CONTROL_CLASSES),
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
emojiName: EMOJI_THUMBSUP,
html: matchingEmojiTag(EMOJI_THUMBS_UP),
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBS_UP}:`,
emojiName: EMOJI_THUMBS_UP,
},
{
classes: expect.arrayContaining([...REACTION_CONTROL_CLASSES, 'selected']),
count: 4,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: `Ada, Marie, you, and Donatello reacted with :${EMOJI_THUMBSDOWN}:`,
emojiName: EMOJI_THUMBSDOWN,
html: matchingEmojiTag(EMOJI_THUMBS_DOWN),
title: `Ada, Marie, you, and Donatello reacted with :${EMOJI_THUMBS_DOWN}:`,
emojiName: EMOJI_THUMBS_DOWN,
},
{
classes: expect.arrayContaining(REACTION_CONTROL_CLASSES),
@ -261,7 +260,7 @@ describe('vue_shared/components/awards_list', () => {
canAwardEmoji: true,
currentUserId: USERS.root.id,
// Let's assert that it puts thumbsup and thumbsdown in the right order still
defaultAwards: [EMOJI_THUMBSDOWN, EMOJI_100, EMOJI_THUMBSUP],
defaultAwards: [EMOJI_THUMBS_DOWN, EMOJI_100, EMOJI_THUMBS_UP],
});
});
@ -270,16 +269,16 @@ describe('vue_shared/components/awards_list', () => {
{
classes: expect.arrayContaining(REACTION_CONTROL_CLASSES),
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSUP),
html: matchingEmojiTag(EMOJI_THUMBS_UP),
title: '',
emojiName: EMOJI_THUMBSUP,
emojiName: EMOJI_THUMBS_UP,
},
{
classes: expect.arrayContaining(REACTION_CONTROL_CLASSES),
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
html: matchingEmojiTag(EMOJI_THUMBS_DOWN),
title: '',
emojiName: EMOJI_THUMBSDOWN,
emojiName: EMOJI_THUMBS_DOWN,
},
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
{

View File

@ -22,6 +22,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
@ -246,7 +247,7 @@ export const mockGroupCrmOrganizationsQueryResponse = {
};
export const mockEmoji1 = {
name: 'thumbsup',
name: EMOJI_THUMBS_UP,
};
export const mockEmoji2 = {

View File

@ -10,6 +10,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { EMOJI_THUMBS_UP } from '~/emoji/constants';
import {
OPTION_NONE,
@ -152,9 +153,9 @@ describe('EmojiToken', () => {
it('renders token item when value is selected', () => {
const tokenSegments = wrapper.findAllComponents(GlFilteredSearchTokenSegment);
expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbsup"
expect(tokenSegments).toHaveLength(3); // My Reaction, =, "thumbs_up"
expect(tokenSegments.at(2).findComponent(GlEmoji).attributes('data-name')).toEqual(
'thumbsup',
EMOJI_THUMBS_UP,
);
});

View File

@ -13,7 +13,7 @@ import {
mockAwardEmojiThumbsUp,
mockWorkItemNotesResponseWithComments,
} from 'jest/work_items/mock_data';
import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
Vue.use(VueApollo);
@ -93,18 +93,18 @@ describe('Work Item Note Awards List', () => {
createComponent();
await waitForPromises();
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_UP);
expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
awardableId: firstNote.id,
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
});
});
it('emits error if awarding emoji fails', async () => {
createComponent({ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') });
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_UP);
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([['Failed to add emoji. Please try again']]);
@ -114,19 +114,19 @@ describe('Work Item Note Awards List', () => {
const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
createComponent({ removeAwardEmojiMutationHandler });
findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_DOWN);
await waitForPromises();
expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
awardableId: firstNote.id,
name: EMOJI_THUMBSDOWN,
name: EMOJI_THUMBS_DOWN,
});
});
it('restores award if remove fails', async () => {
createComponent({ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no') });
findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_DOWN);
await waitForPromises();
expect(wrapper.emitted('error')).toEqual([['Failed to remove emoji. Please try again']]);

View File

@ -11,11 +11,10 @@ import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vu
import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
import projectWorkItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
import {
EMOJI_THUMBSUP,
EMOJI_THUMBSDOWN,
DEFAULT_PAGE_SIZE_EMOJIS,
I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR,
} from '~/work_items/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
import {
workItemByIidResponseFactory,
@ -70,7 +69,7 @@ describe('WorkItemAwardEmoji component', () => {
.mockRejectedValue(new Error(mutationErrorMessage));
const mockAwardEmojiDifferentUser = {
name: 'thumbsup',
name: EMOJI_THUMBS_UP,
__typename: 'AwardEmoji',
user: {
id: 'gid://gitlab/User/1',
@ -139,7 +138,7 @@ describe('WorkItemAwardEmoji component', () => {
expect(findAwardsList().props()).toEqual({
canAwardEmoji: true,
currentUserId: 5,
defaultAwards: [EMOJI_THUMBSUP, EMOJI_THUMBSDOWN],
defaultAwards: [EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN],
selectedClass: 'selected',
awards: [],
});
@ -148,14 +147,14 @@ describe('WorkItemAwardEmoji component', () => {
it('renders awards-list component with awards present', () => {
expect(findAwardsList().props('awards')).toEqual([
{
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
user: {
id: 5,
name: 'Dave Smith',
},
},
{
name: EMOJI_THUMBSDOWN,
name: EMOJI_THUMBS_DOWN,
user: {
id: 5,
name: 'Dave Smith',
@ -193,14 +192,14 @@ describe('WorkItemAwardEmoji component', () => {
expect(findAwardsList().props('awards')).toEqual([
{
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
user: {
id: 5,
name: 'Dave Smith',
},
},
{
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
user: {
id: 1,
name: 'John Doe',
@ -223,12 +222,12 @@ describe('WorkItemAwardEmoji component', () => {
await waitForPromises();
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_UP);
expect(awardEmojiMutationHandler).toHaveBeenCalledWith({
input: {
awardableId: mockWorkItem.id,
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
},
});
},
@ -242,7 +241,7 @@ describe('WorkItemAwardEmoji component', () => {
await waitForPromises();
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_UP);
await waitForPromises();
@ -276,12 +275,12 @@ describe('WorkItemAwardEmoji component', () => {
await waitForPromises();
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
findAwardsList().vm.$emit('award', EMOJI_THUMBS_UP);
expect(awardEmojiAddSuccessHandler).toHaveBeenCalledWith({
input: {
awardableId: mockWorkItem.id,
name: EMOJI_THUMBSUP,
name: EMOJI_THUMBS_UP,
},
});
});

View File

@ -1,4 +1,5 @@
import { WIDGET_TYPE_LINKED_ITEMS, NEW_WORK_ITEM_IID } from '~/work_items/constants';
import { EMOJI_THUMBS_UP, EMOJI_THUMBS_DOWN } from '~/emoji/constants';
export const mockAssignees = [
{
@ -108,7 +109,7 @@ export const mockMilestone = {
};
export const mockAwardEmojiThumbsUp = {
name: 'thumbsup',
name: EMOJI_THUMBS_UP,
__typename: 'AwardEmoji',
user: {
id: 'gid://gitlab/User/5',
@ -118,7 +119,7 @@ export const mockAwardEmojiThumbsUp = {
};
export const mockAwardEmojiThumbsDown = {
name: 'thumbsdown',
name: EMOJI_THUMBS_DOWN,
__typename: 'AwardEmoji',
user: {
id: 'gid://gitlab/User/5',

View File

@ -27,7 +27,7 @@ RSpec.describe VersionCheckHelper do
stub_application_setting(version_check_enabled: enabled)
allow(User).to receive(:single_user).and_return(double(user, requires_usage_stats_consent?: consent))
allow(helper).to receive(:current_user).and_return(user)
allow(user).to receive(:can_read_all_resources?).and_return(is_admin)
allow(user).to receive(:can_admin_all_resources?).and_return(is_admin)
end
it 'returns correct results' do

View File

@ -1,6 +1,6 @@
# frozen_string_literal: true
require 'fast_spec_helper'
require 'spec_helper'
RSpec.describe Gitlab::Auth::Activity do
describe '.each_counter' do
@ -29,4 +29,28 @@ RSpec.describe Gitlab::Auth::Activity do
end
end
end
describe '#user_csrf_token_mismatch!' do
context 'when GraphQL controller is being used' do
it 'increments correct counter with GraphQL label' do
metrics = described_class.new(controller: GraphqlController.new)
expect(described_class.user_csrf_token_invalid_counter)
.to receive(:increment).with(controller: 'GraphqlController')
metrics.user_csrf_token_mismatch!
end
end
context 'when another controller is being used' do
it 'increments correct count with a non-specific label' do
metrics = described_class.new(controller: ApplicationController.new)
expect(described_class.user_csrf_token_invalid_counter)
.to receive(:increment).with(controller: 'other')
metrics.user_csrf_token_mismatch!
end
end
end
end

View File

@ -0,0 +1,101 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::BackgroundMigration::RestoreOptInToGitlabCom, feature_category: :activation do
describe '#perform' do
let(:connection) { ApplicationRecord.connection }
let(:temporary_table_name) { :_test_temp_user_details_issue18240 }
let(:temporary_table) { table(temporary_table_name) }
let(:users) { table(:users) }
let(:user_details) { table(:user_details) }
let(:start_id) { user_details.minimum(:user_id) }
let(:end_id) { user_details.maximum(:user_id) }
let!(:user_detail_1) do
user = table(:users).create!(username: 'u1', email: 'u1@email.com', projects_limit: 5)
user_details.create!(user_id: user.id, onboarding_status: {})
end
let!(:user_detail_2) do
user = table(:users).create!(username: 'u2', email: 'u2@email.com', projects_limit: 5)
user_details.create!(user_id: user.id, onboarding_status: { step_url: '/users/sign_up/welcome' })
end
let!(:user_detail_3) do
user = table(:users).create!(username: 'u3', email: 'u3@email.com', projects_limit: 5)
user_details.create!(user_id: user.id, onboarding_status: {
step_url: '/users/sign_up/welcome',
email_opt_in: true
})
end
let!(:user_detail_4) do
user = table(:users).create!(username: 'u4', email: 'u4@email.com', projects_limit: 5)
user_details.create!(user_id: user.id, onboarding_status: {})
end
let!(:user_detail_5) do
user = table(:users).create!(username: 'u5', email: 'u5@email.com', projects_limit: 5)
user_details.create!(user_id: user.id, onboarding_status: { email_opt_in: false })
end
before do
# https://gitlab.com/gitlab-com/gl-infra/production/-/issues/18367
connection.execute(<<~SQL)
-- Make sure that the temp table is dropped (in case the after block didn't run)
DROP TABLE IF EXISTS #{temporary_table_name};
CREATE TABLE #{temporary_table_name} (
GITLAB_DOTCOM_ID integer,
RESTORE_VALUE boolean,
RESTORE_VALUE_SOURCE varchar(2550)
);
SQL
temporary_table.create!(gitlab_dotcom_id: user_detail_1.user_id, restore_value: true)
temporary_table.create!(gitlab_dotcom_id: user_detail_2.user_id, restore_value: false)
temporary_table.create!(gitlab_dotcom_id: user_detail_3.user_id, restore_value: false)
temporary_table.create!(gitlab_dotcom_id: user_detail_4.user_id, restore_value: nil)
temporary_table.create!(gitlab_dotcom_id: nil, restore_value: nil)
end
after do
# Make sure that the temp table we created is dropped (it is not removed by the database_cleaner)
connection.execute(<<~SQL)
DROP TABLE IF EXISTS #{temporary_table_name};
SQL
end
subject(:migration) do
described_class.new(
start_id: start_id,
end_id: end_id,
batch_table: :user_details,
batch_column: :user_id,
job_arguments: [temporary_table_name],
sub_batch_size: 2,
pause_ms: 0,
connection: ::ApplicationRecord.connection
)
end
it 'updates user_details from the temporary table' do
expect { migration.perform }.not_to raise_error
# were updated
expect(user_detail_1.reload.onboarding_status).to eq('email_opt_in' => true)
expect(user_detail_2.reload.onboarding_status)
.to eq('step_url' => '/users/sign_up/welcome', 'email_opt_in' => false)
# were NOT updated
expect(user_detail_3.reload.onboarding_status)
.to eq('step_url' => '/users/sign_up/welcome', 'email_opt_in' => true)
expect(user_detail_4.reload.onboarding_status).to eq({})
expect(user_detail_5.reload.onboarding_status).to eq('email_opt_in' => false)
end
end
end

View File

@ -146,19 +146,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"rules:exists:project `invalid/path/[MASKED]xxxx` is not a valid project path"
)
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'raises an error with the variable masked in the old style' do
expect { satisfied_by? }.to raise_error(
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
"rules:exists:project `invalid/path/xxxxxxxxxxxx` is not a valid project path"
)
end
end
end
end
@ -212,20 +199,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists, feature_category:
"in project `#{other_project.full_path}`"
)
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'raises an error with the variable masked in the old style' do
expect { satisfied_by? }.to raise_error(
Gitlab::Ci::Build::Rules::Rule::Clause::ParseError,
"rules:exists:ref `invalid/ref/xxxxxxxxxxxx` is not a valid ref " \
"in project `#{other_project.full_path}`"
)
end
end
end
end
end

View File

@ -149,18 +149,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
end
it_behaves_like 'is invalid'
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
let(:expected_error) do
'File `xxxxxxxxxxxx/generated.yml` is empty!'
end
it_behaves_like 'is invalid'
end
end
context 'when file is not empty' do
@ -204,18 +192,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
end
it_behaves_like 'is invalid'
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
let(:expected_error) do
'Job `xxxxxxxxxxxxxxxxxxxxxxx` not found in parent pipeline or does not have artifacts!'
end
it_behaves_like 'is invalid'
end
end
end
end
@ -252,22 +228,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :
extra: { job_name: '[MASKED]xxxxxxxxxxxxxxx' }
)
}
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it {
is_expected.to eq(
context_project: project.full_path,
context_sha: nil,
type: :artifact,
location: 'generated.yml',
extra: { job_name: 'xxxxxxxxxxxxxxxxxxxxxxx' }
)
}
end
end
end

View File

@ -108,18 +108,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
expect(file.error_message)
.to eq('`some/file/[MASKED]xxxxxxxx.yml`: Invalid configuration format')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'is not a valid file' do
expect(valid?).to be_falsy
expect(file.error_message)
.to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: Invalid configuration format')
end
end
end
context 'when the class has no validate_context!' do

View File

@ -188,20 +188,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip
end
end
describe '#error_message when consistent_ci_variable_masking feature is disabled' do
let(:location) { '/lib/gitlab/ci/templates/secret_file.yml' }
let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) }
before do
stub_feature_flags(consistent_ci_variable_masking: false)
Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([local_file])
end
it 'returns an error message with the variable masked in the old style' do
expect(local_file.error_message).to eq("Local file `lib/gitlab/ci/templates/xxxxxxxxxxx.yml` does not exist!")
end
end
describe '#expand_context' do
let(:location) { 'location.yml' }

View File

@ -158,17 +158,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `#{project.full_path}` file `[MASKED]xxx.yml` is empty!")
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns false with the variable masked in the old style' do
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxx.yml` is empty!")
end
end
end
context 'when non-existing ref is used' do
@ -193,17 +182,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `#{project.full_path}` file `[MASKED]xxxxxxxxxxx.yml` does not exist!")
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns false with the variable masked in the old style' do
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `#{project.full_path}` file `xxxxxxxxxxxxxxxxxxx.yml` does not exist!")
end
end
end
context 'when file is not a yaml file' do
@ -230,17 +208,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `[MASKED]xxxxxxxxxxxxxxx` not found or access denied!")
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns false with project name masked in the old style' do
expect(valid?).to be_falsy
expect(project_file.error_message).to include("Project `xxxxxxxxxxxxxxxxxxxxxxx` not found or access denied!")
end
end
end
context 'when a project contained in an array is used with a masked variable' do
@ -320,24 +287,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p
extra: { project: "#{namespace_path}/[MASKED]xxxxxxx", ref: '[MASKED]xxxxxxxxxxxxxxxxxx' }
)
}
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it {
is_expected.to eq(
context_project: context_project.full_path,
context_sha: project_sha,
type: :file,
location: 'file.yml',
blob: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/blob/#{included_project_sha}/file.yml",
raw: "http://localhost/#{namespace_path}/xxxxxxxxxxxxxxx/-/raw/#{included_project_sha}/file.yml",
extra: { project: "#{namespace_path}/xxxxxxxxxxxxxxx", ref: 'xxxxxxxxxxxxxxxxxxxxxxxxxx' }
)
}
end
end
end

View File

@ -191,16 +191,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
it 'returns an error message describing invalid address' do
expect(subject).to eq('Remote file `not-valid://gitlab.com/gitlab-org/gitlab-foss/blob/1234/?[MASKED]xxx.yml` does not have a valid address!')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns an error message describing invalid address with the variable masked in the old style' do
expect(subject).to eq('Remote file `not-valid://gitlab.com/gitlab-org/gitlab-foss/blob/1234/?xxxxxxxxxxx.yml` does not have a valid address!')
end
end
end
context 'when timeout error has been raised' do
@ -211,16 +201,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
it 'returns error message about a timeout' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.[MASKED]xxx.yml` could not be fetched because of a timeout error!')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns error message about a timeout with the variable masked in the old style' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml` could not be fetched because of a timeout error!')
end
end
end
context 'when HTTP error has been raised' do
@ -231,16 +211,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
it 'returns error message about a HTTP error' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.[MASKED]xxx.yml` could not be fetched because of HTTP error!')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns error message about a HTTP error with the variable masked in the old style' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml` could not be fetched because of HTTP error!')
end
end
end
context 'when response has 404 status' do
@ -251,16 +221,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
it 'returns error message about a timeout' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.[MASKED]xxx.yml` could not be fetched because of HTTP code `404` error!')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns error message about a timeout with the variable masked in the old style' do
expect(subject).to eq('Remote file `https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml` could not be fetched because of HTTP code `404` error!')
end
end
end
context 'when the URL is blocked' do
@ -314,24 +274,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi
extra: {}
)
}
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it {
is_expected.to eq(
context_project: nil,
context_sha: '12345',
type: :remote,
location: 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml',
raw: 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.xxxxxxxxxxx.yml',
blob: nil,
extra: {}
)
}
end
end
describe '#to_hash' do

View File

@ -65,17 +65,6 @@ RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :
expect(valid?).to be_falsy
expect(template_file.error_message).to include('`[MASKED]xxxxxx.yml` is not a valid location!')
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'returns false with the variable masked in the old style' do
expect(valid?).to be_falsy
expect(template_file.error_message).to include('`xxxxxxxxxxxxxx.yml` is not a valid location!')
end
end
end
context 'with a non-existing template' do

View File

@ -59,19 +59,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category:
/`{"invalid":"\[MASKED\]xxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'raises an error with a sentence masked in the old style' do
expect { process }.to raise_error(
Gitlab::Ci::Config::External::Mapper::AmbigiousSpecificationError,
/`{"invalid":"xxxxxxxxxxxxxx.yml"}` does not have a valid subkey for include. Valid subkeys are:/
)
end
end
end
end

View File

@ -7,32 +7,6 @@ RSpec.describe Gitlab::Ci::MaskSecret, feature_category: :ci_variables do
subject { described_class }
describe '#mask' do
context 'when consistent_ci_variable_masking feature is disabled' do
before do
stub_feature_flags(consistent_ci_variable_masking: false)
end
it 'masks exact number of characters' do
expect(mask('token', 'oke')).to eq('txxxn')
end
it 'masks multiple occurrences' do
expect(mask('token token token', 'oke')).to eq('txxxn txxxn txxxn')
end
it 'does not mask if not found' do
expect(mask('token', 'not')).to eq('token')
end
it 'does support null token' do
expect(mask('token', nil)).to eq('token')
end
it 'does not change a bytesize of a value' do
expect(mask('token-ü/unicode', 'token-ü').bytesize).to eq 16
end
end
it 'masks exact number of characters' do
expect(mask('value-to-be-masked', 'be-masked')).to eq('value-to-[MASKED]x')
end

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