Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ff09936938
commit
9742ffd06b
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
80b47e3c58c2e52495d579d2c4f2071b1a793e21
|
||||
92eaec235ff4801ec059fa79fa891d67ae765c11
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
fc8928109f97c8239cde31aafc11c6b84bbd24c3
|
||||
001ded581e320bcdae4294e1dda41b0c25cf00b2
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 || [];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
];
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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[
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class CreatePipelineWorker # rubocop:disable Scalability/IdempotentWorker
|
||||
include ApplicationWorker
|
||||
|
||||
data_consistency :always
|
||||
data_consistency :sticky
|
||||
|
||||
sidekiq_options retry: 3
|
||||
include PipelineQueue
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
08147a6cb23f2c47c51540f86da313b5e5e7d2102bdc53797b67f9d697773a81
|
||||
|
|
@ -0,0 +1 @@
|
|||
f8dc10977e72d248d218f95cffb772dc16dd73bd1ed5b153c16dd5a600642fcf
|
||||
|
|
@ -0,0 +1 @@
|
|||
7e8133287cec74e687a8f4949e96505f6f7f90745407195d16fa8602aad5b467
|
||||
|
|
@ -0,0 +1 @@
|
|||
852bea9dfd5a2074dc3eab8cbf865b572c373f6373831d66356edaa4edb5c7db
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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. |
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ describe('gl_emoji', () => {
|
|||
await waitForPromises();
|
||||
|
||||
expect(glEmojiElement.outerHTML).toBe(
|
||||
'<gl-emoji data-name=""x="y" onload="alert(document.location.href)"" data-unicode-version="x"><img class="emoji" title=":"x="y" onload="alert(document.location.href)":" alt=":"x="y" onload="alert(document.location.href)":" src="/-/emojis/3/grey_question.png" align="absmiddle"></gl-emoji>',
|
||||
`<gl-emoji data-name=""x="y" onload="alert(document.location.href)"" data-unicode-version="x"><img class="emoji" title=":"x="y" onload="alert(document.location.href)":" alt=":"x="y" onload="alert(document.location.href)":" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" align="absmiddle"></gl-emoji>`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -33,9 +33,6 @@ describe('Pipeline Url Component', () => {
|
|||
const createComponent = (props) => {
|
||||
wrapper = shallowMountExtended(PipelineUrlComponent, {
|
||||
propsData: { ...defaultProps, ...props },
|
||||
provide: {
|
||||
targetProjectFullPath: projectPath,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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]],
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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]]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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']]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue