Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-12-14 12:10:37 +00:00
parent 3c0faf1c6b
commit 89a0c1fa66
55 changed files with 651 additions and 373 deletions

View File

@ -563,7 +563,7 @@ gem 'flipper', '~> 0.26.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'flipper-active_record', '~> 0.26.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'flipper-active_support_cache_store', '~> 0.26.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'unleash', '~> 3.2.2' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gitlab-experiment', '~> 0.8.0' # rubocop:todo Gemfile/MissingFeatureCategory
gem 'gitlab-experiment', '~> 0.9.1', feature_category: :shared
# Structured logging
gem 'lograge', '~> 0.5' # rubocop:todo Gemfile/MissingFeatureCategory

View File

@ -210,7 +210,7 @@
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
{"name":"gitlab-dangerfiles","version":"4.6.0","platform":"ruby","checksum":"441b37b17d1dad36268517490a30aaf57e43dffb2e9ebc1da38d3bc9fa20741e"},
{"name":"gitlab-experiment","version":"0.8.0","platform":"ruby","checksum":"b4e2f73e0af19cdd899a745f5a846c1318d44054e068a8f4ac887f6b1017d3f9"},
{"name":"gitlab-experiment","version":"0.9.1","platform":"ruby","checksum":"f230ee742154805a755d5f2539dc44d93cdff08c5bbbb7656018d61f93d01f48"},
{"name":"gitlab-fog-azure-rm","version":"1.8.0","platform":"ruby","checksum":"e4f24b174b273b88849d12fbcfecb79ae1c09f56cbd614998714c7f0a81e6c28"},
{"name":"gitlab-labkit","version":"0.34.0","platform":"ruby","checksum":"ca5c504201390cd07ba1029e6ca3059f4e2e6005eb121ba8a103af1e166a3ecd"},
{"name":"gitlab-license","version":"2.3.0","platform":"ruby","checksum":"60cae3871c46607dde58994faf761c6755adc61133a92e5ab59ab26a8b9b4157"},

View File

@ -677,7 +677,7 @@ GEM
danger (>= 9.3.0)
danger-gitlab (>= 8.0.0)
rake (~> 13.0)
gitlab-experiment (0.8.0)
gitlab-experiment (0.9.1)
activesupport (>= 3.0)
request_store (>= 1.0)
gitlab-fog-azure-rm (1.8.0)
@ -1875,7 +1875,7 @@ DEPENDENCIES
gitlab-backup-cli!
gitlab-chronic (~> 0.10.5)
gitlab-dangerfiles (~> 4.6.0)
gitlab-experiment (~> 0.8.0)
gitlab-experiment (~> 0.9.1)
gitlab-fog-azure-rm (~> 1.8.0)
gitlab-http!
gitlab-labkit (~> 0.34.0)

View File

@ -1,6 +1,7 @@
import {
calculateDeploymentStatus,
calculateStatefulSetStatus,
calculateDaemonSetStatus,
} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
import { STATUS_READY, STATUS_FAILED } from '~/kubernetes_dashboard/constants';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants';
@ -46,16 +47,10 @@ export function getDeploymentsStatuses(items) {
export function getDaemonSetStatuses(items) {
const failed = items.filter((item) => {
return (
item.status?.numberMisscheduled > 0 ||
item.status?.numberReady !== item.status?.desiredNumberScheduled
);
return calculateDaemonSetStatus(item) === STATUS_FAILED;
});
const ready = items.filter((item) => {
return (
item.status?.numberReady === item.status?.desiredNumberScheduled &&
!item.status?.numberMisscheduled
);
return calculateDaemonSetStatus(item) === STATUS_READY;
});
return {

View File

@ -289,7 +289,7 @@ class GfmAutoComplete {
displayTpl({ name }) {
const reviewState = REVIEW_STATES[name];
return `<li><span class="gl-font-weight-bold gl-display-block">${reviewState.header}</span><small class="description gl-display-block gl-w-full gl-float-left! gl-px-0!">${reviewState.description}</small></li>`;
return `<li><span class="name gl-font-weight-bold">${reviewState.header}</span><small class="description"><em>${reviewState.description}</em></small></li>`;
},
});
}

View File

@ -5,6 +5,7 @@ import k8sPodsQuery from './queries/k8s_dashboard_pods.query.graphql';
import k8sDeploymentsQuery from './queries/k8s_dashboard_deployments.query.graphql';
import k8sStatefulSetsQuery from './queries/k8s_dashboard_stateful_sets.query.graphql';
import k8sReplicaSetsQuery from './queries/k8s_dashboard_replica_sets.query.graphql';
import k8sDaemonSetsQuery from './queries/k8s_dashboard_daemon_sets.query.graphql';
import { resolvers } from './resolvers';
export const apolloProvider = () => {
@ -83,6 +84,24 @@ export const apolloProvider = () => {
},
});
cache.writeQuery({
query: k8sDaemonSetsQuery,
data: {
metadata: {
name: null,
namespace: null,
creationTimestamp: null,
labels: null,
annotations: null,
},
status: {
numberMisscheduled: null,
numberReady: null,
desiredNumberScheduled: null,
},
},
});
return new VueApollo({
defaultClient,
});

View File

@ -0,0 +1,16 @@
query getK8sDashboardDaemonSets($configuration: LocalConfiguration) {
k8sDaemonSets(configuration: $configuration) @client {
metadata {
name
namespace
creationTimestamp
labels
annotations
}
status {
numberMisscheduled
numberReady
desiredNumberScheduled
}
}
}

View File

@ -12,6 +12,7 @@ import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql';
import k8sDashboardDeploymentsQuery from '../queries/k8s_dashboard_deployments.query.graphql';
import k8sDashboardStatefulSetsQuery from '../queries/k8s_dashboard_stateful_sets.query.graphql';
import k8sDashboardReplicaSetsQuery from '../queries/k8s_dashboard_replica_sets.query.graphql';
import k8sDaemonSetsQuery from '../queries/k8s_dashboard_daemon_sets.query.graphql';
export default {
k8sPods(_, { configuration }, { client }) {
@ -129,4 +130,40 @@ export default {
}
});
},
k8sDaemonSets(_, { configuration, namespace = '' }, { client }) {
const config = new Configuration(configuration);
const appsV1api = new AppsV1Api(config);
const deploymentsApi = namespace
? appsV1api.listAppsV1NamespacedDaemonSet({ namespace })
: appsV1api.listAppsV1DaemonSetForAllNamespaces();
return deploymentsApi
.then((res) => {
const watchPath = buildWatchPath({
resource: 'daemonsets',
api: 'apis/apps/v1',
namespace,
});
watchWorkloadItems({
client,
query: k8sDaemonSetsQuery,
configuration,
namespace,
watchPath,
queryField: 'k8sDaemonSets',
});
const data = res?.items || [];
return data.map(mapWorkloadItem);
})
.catch(async (err) => {
try {
await handleClusterError(err);
} catch (error) {
throw new Error(error.message);
}
});
},
};

View File

@ -48,3 +48,13 @@ export function calculateStatefulSetStatus(item) {
}
return STATUS_FAILED;
}
export function calculateDaemonSetStatus(item) {
if (
item.status?.numberReady === item.status?.desiredNumberScheduled &&
!item.status?.numberMisscheduled
) {
return STATUS_READY;
}
return STATUS_FAILED;
}

View File

@ -0,0 +1,80 @@
<script>
import { s__ } from '~/locale';
import { getAge, calculateDaemonSetStatus } from '../helpers/k8s_integration_helper';
import WorkloadLayout from '../components/workload_layout.vue';
import k8sDaemonSetsQuery from '../graphql/queries/k8s_dashboard_daemon_sets.query.graphql';
import { STATUS_FAILED, STATUS_READY, STATUS_LABELS } from '../constants';
export default {
components: {
WorkloadLayout,
},
inject: ['configuration'],
apollo: {
k8sDaemonSets: {
query: k8sDaemonSetsQuery,
variables() {
return {
configuration: this.configuration,
};
},
update(data) {
return (
data?.k8sDaemonSets?.map((daemonSet) => {
return {
name: daemonSet.metadata?.name,
namespace: daemonSet.metadata?.namespace,
status: calculateDaemonSetStatus(daemonSet),
age: getAge(daemonSet.metadata?.creationTimestamp),
labels: daemonSet.metadata?.labels,
annotations: daemonSet.metadata?.annotations,
kind: s__('KubernetesDashboard|DaemonSet'),
};
}) || []
);
},
error(err) {
this.errorMessage = err?.message;
},
},
},
data() {
return {
k8sDaemonSets: [],
errorMessage: '',
};
},
computed: {
daemonSetsStats() {
return [
{
value: this.countDaemonSetsByStatus(STATUS_READY),
title: STATUS_LABELS[STATUS_READY],
},
{
value: this.countDaemonSetsByStatus(STATUS_FAILED),
title: STATUS_LABELS[STATUS_FAILED],
},
];
},
loading() {
return this.$apollo.queries.k8sDaemonSets.loading;
},
},
methods: {
countDaemonSetsByStatus(status) {
const filteredDaemonSets = this.k8sDaemonSets.filter((item) => item.status === status) || [];
return filteredDaemonSets.length;
},
},
};
</script>
<template>
<workload-layout
:loading="loading"
:error-message="errorMessage"
:stats="daemonSetsStats"
:items="k8sDaemonSets"
/>
</template>

View File

@ -2,8 +2,10 @@ export const PODS_ROUTE_NAME = 'pods';
export const DEPLOYMENTS_ROUTE_NAME = 'deployments';
export const STATEFUL_SETS_ROUTE_NAME = 'statefulSets';
export const REPLICA_SETS_ROUTE_NAME = 'replicaSets';
export const DAEMON_SETS_ROUTE_NAME = 'daemonSets';
export const PODS_ROUTE_PATH = '/pods';
export const DEPLOYMENTS_ROUTE_PATH = '/deployments';
export const STATEFUL_SETS_ROUTE_PATH = '/statefulsets';
export const REPLICA_SETS_ROUTE_PATH = '/replicasets';
export const DAEMON_SETS_ROUTE_PATH = '/daemonsets';

View File

@ -3,6 +3,7 @@ import PodsPage from '../pages/pods_page.vue';
import DeploymentsPage from '../pages/deployments_page.vue';
import StatefulSetsPage from '../pages/stateful_sets_page.vue';
import ReplicaSetsPage from '../pages/replica_sets_page.vue';
import DaemonSetsPage from '../pages/daemon_sets_page.vue';
import {
PODS_ROUTE_NAME,
PODS_ROUTE_PATH,
@ -12,6 +13,8 @@ import {
STATEFUL_SETS_ROUTE_PATH,
REPLICA_SETS_ROUTE_NAME,
REPLICA_SETS_ROUTE_PATH,
DAEMON_SETS_ROUTE_NAME,
DAEMON_SETS_ROUTE_PATH,
} from './constants';
export default [
@ -47,4 +50,12 @@ export default [
title: s__('KubernetesDashboard|ReplicaSets'),
},
},
{
name: DAEMON_SETS_ROUTE_NAME,
path: DAEMON_SETS_ROUTE_PATH,
component: DaemonSetsPage,
meta: {
title: s__('KubernetesDashboard|DaemonSets'),
},
},
];

View File

@ -69,9 +69,15 @@ export default {
<template>
<section>
<div class="gl-lg-display-flex gl-flex-direction-row gl-justify-content-space-between gl-pt-5">
<div
class="gl-lg-display-flex gl-flex-direction-row gl-py-5"
:class="{
'gl-justify-content-space-between': showSyntaxOptions,
'gl-justify-content-end': !showSyntaxOptions,
}"
>
<template v-if="showSyntaxOptions">
<div class="gl-pb-6">
<div>
<gl-button
category="tertiary"
variant="link"

View File

@ -1,23 +0,0 @@
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
export function getUpdateWorkItemMutation({ input, workItemParentId }) {
let mutation = updateWorkItemMutation;
const variables = {
input,
};
if (workItemParentId) {
mutation = updateWorkItemTaskMutation;
variables.input = {
id: workItemParentId,
taskData: input,
};
}
return {
mutation,
variables,
};
}

View File

@ -134,11 +134,6 @@ export default {
required: false,
default: false,
},
workItemParentId: {
type: String,
required: false,
default: null,
},
},
apollo: {
workItemTypes: {
@ -328,7 +323,6 @@ export default {
:data-testid="$options.stateToggleTestId"
:work-item-id="workItemId"
:work-item-state="workItemState"
:work-item-parent-id="workItemParentId"
:work-item-type="workItemType"
show-as-dropdown-item
/>

View File

@ -1,7 +1,6 @@
<script>
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
sprintfWorkItem,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_HIERARCHY,
@ -51,11 +50,6 @@ export default {
type: Object,
required: true,
},
workItemParentId: {
type: String,
required: false,
default: null,
},
},
computed: {
workItemType() {
@ -67,15 +61,6 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
canSetWorkItemMetadata() {
return this.workItem?.userPermissions?.setWorkItemMetadata;
},
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
confidentialTooltip() {
return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
},
workItemAssignees() {
return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},

View File

@ -17,7 +17,6 @@ import {
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_NOTES,
WIDGET_TYPE_LINKED_ITEMS,
@ -25,7 +24,6 @@ import {
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
@ -83,11 +81,6 @@ export default {
required: false,
default: null,
},
workItemParentId: {
type: String,
required: false,
default: null,
},
},
data() {
return {
@ -163,9 +156,6 @@ export default {
workItemTypeId() {
return this.workItem.workItemType?.id;
},
workItemBreadcrumbReference() {
return this.workItemType ? `#${this.workItem.iid}` : '';
},
canUpdate() {
return this.workItem.userPermissions?.updateWorkItem;
},
@ -184,26 +174,9 @@ export default {
parentWorkItem() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
},
parentWorkItemType() {
return this.parentWorkItem?.workItemType?.name;
},
parentWorkItemIconName() {
return this.parentWorkItem?.workItemType?.iconName;
},
parentWorkItemConfidentiality() {
return this.parentWorkItem?.confidential;
},
parentWorkItemReference() {
return this.parentWorkItem ? `${this.parentWorkItem.title} #${this.parentWorkItem.iid}` : '';
},
parentUrl() {
// Once more types are moved to have Work Items involved
// we need to handle this properly.
if (this.parentWorkItemType === WORK_ITEM_TYPE_VALUE_ISSUE) {
return `../../-/issues/${this.parentWorkItem?.iid}`;
}
return this.parentWorkItem?.webUrl;
},
workItemIconName() {
return this.workItem.workItemType?.iconName;
},
@ -290,34 +263,21 @@ export default {
},
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
let updateMutation = updateWorkItemMutation;
let inputVariables = {
id: this.workItem.id,
confidential: confidentialStatus,
};
if (this.parentWorkItem) {
updateMutation = updateWorkItemTaskMutation;
inputVariables = {
id: this.parentWorkItem.id,
taskData: {
id: this.workItem.id,
confidential: confidentialStatus,
},
};
}
this.$apollo
.mutate({
mutation: updateMutation,
mutation: updateWorkItemMutation,
variables: {
input: inputVariables,
input: {
id: this.workItem.id,
confidential: confidentialStatus,
},
},
})
.then(
({
data: {
workItemUpdate: { errors, workItem, task },
workItemUpdate: { errors, workItem },
},
}) => {
if (errors?.length) {
@ -325,7 +285,7 @@ export default {
}
this.$emit('workItemUpdated', {
confidential: workItem?.confidential || task?.confidential,
confidential: workItem?.confidential,
});
},
)
@ -435,7 +395,6 @@ export default {
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
@error="updateError = $event"
/>
@ -465,7 +424,6 @@ export default {
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
:work-item-state="workItem.state"
:work-item-parent-id="workItemParentId"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@ -490,7 +448,6 @@ export default {
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
:work-item-parent-id="workItemParentId"
:can-update="canUpdate"
:use-h1="!isModal"
@error="updateError = $event"
@ -511,7 +468,6 @@ export default {
:is-modal="isModal"
:work-item="workItem"
:is-sticky-header-showing="isStickyHeaderShowing"
:work-item-parent-id="workItemParentId"
:work-item-notifications-subscribed="workItemNotificationsSubscribed"
@hideStickyHeader="hideStickyHeader"
@showStickyHeader="showStickyHeader"
@ -530,7 +486,6 @@ export default {
class="gl-border-b"
:full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
<work-item-description
@ -605,7 +560,6 @@ export default {
<work-item-attributes-wrapper
:full-path="fullPath"
:work-item="workItem"
:work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
</aside>

View File

@ -3,7 +3,6 @@ import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { __ } from '~/locale';
import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
@ -12,6 +11,7 @@ import {
STATE_EVENT_REOPEN,
TRACKING_CATEGORY_SHOW,
} from '../constants';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
export default {
components: {
@ -33,11 +33,6 @@ export default {
type: String,
required: true,
},
workItemParentId: {
type: String,
required: false,
default: null,
},
showAsDropdownItem: {
type: Boolean,
required: false,
@ -75,24 +70,19 @@ export default {
},
methods: {
async updateWorkItem() {
const input = {
id: this.workItemId,
stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
};
this.updateInProgress = true;
try {
this.track('updated_state');
const { mutation, variables } = getUpdateWorkItemMutation({
workItemParentId: this.workItemParentId,
input,
});
const { data } = await this.$apollo.mutate({
mutation,
variables,
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
},
},
});
const errors = data.workItemUpdate?.errors;
@ -102,7 +92,6 @@ export default {
}
} catch (error) {
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
this.$emit('error', msg);
Sentry.captureException(error);
}

View File

@ -30,11 +30,6 @@ export default {
type: Boolean,
required: true,
},
workItemParentId: {
type: String,
required: false,
default: null,
},
updateInProgress: {
type: Boolean,
required: false,
@ -126,7 +121,6 @@ export default {
:work-item-reference="workItem.reference"
:work-item-create-note-email="workItem.createNoteEmail"
:work-item-state="workItem.state"
:work-item-parent-id="workItemParentId"
:is-modal="isModal"
@deleteWorkItem="$emit('deleteWorkItem')"
@toggleWorkItemConfidentiality="

View File

@ -8,7 +8,7 @@ import {
WORK_ITEM_TITLE_MAX_LENGTH,
I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE,
} from '../constants';
import { getUpdateWorkItemMutation } from './update_work_item';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import ItemTitle from './item_title.vue';
export default {
@ -32,11 +32,6 @@ export default {
required: false,
default: '',
},
workItemParentId: {
type: String,
required: false,
default: null,
},
canUpdate: {
type: Boolean,
required: false,
@ -68,24 +63,19 @@ export default {
return;
}
const input = {
id: this.workItemId,
title: updatedTitle,
};
this.updateInProgress = true;
try {
this.track('updated_title');
const { mutation, variables } = getUpdateWorkItemMutation({
workItemParentId: this.workItemParentId,
input,
});
const { data } = await this.$apollo.mutate({
mutation,
variables,
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
title: updatedTitle,
},
},
});
const errors = data.workItemUpdate?.errors;

View File

@ -54,9 +54,6 @@ export const i18n = {
"WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
confidentialTooltip: s__(
'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this %{workItemType}.',
),
};
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(

View File

@ -1,13 +0,0 @@
#import "./work_item.fragment.graphql"
mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
workItemCreateFromTask(input: $input) {
workItem {
...WorkItem
}
newWorkItem {
...WorkItem
}
errors
}
}

View File

@ -1,14 +0,0 @@
#import "./work_item.fragment.graphql"
mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
workItemUpdate: workItemUpdateTask(input: $input) {
errors
workItem {
id
descriptionHtml
}
task {
...WorkItem
}
}
}

View File

@ -234,7 +234,7 @@
color: $skype;
}
.twitter-icon {
.x-icon {
color: var(--gl-text-color, $gl-text-color);
}

View File

@ -65,24 +65,7 @@ class Issue < ApplicationRecord
belongs_to :moved_to, class_name: 'Issue', inverse_of: :moved_from
has_one :moved_from, class_name: 'Issue', foreign_key: :moved_to_id, inverse_of: :moved_to
has_internal_id :iid, scope: :namespace, track_if: -> { !importing? }, init: ->(issue, scope) do
# we need this init for the case where the IID allocation in internal_ids#last_value
# is higher than the actual issues.max(iid) value for a given project. For instance
# in case of an import where a batch of IIDs may be prealocated
#
# TODO: remove this once the UpdateIssuesInternalIdScope migration completes
if issue
[
InternalId.where(project: issue.project, usage: :issues).pick(:last_value).to_i,
issue.namespace&.issues&.maximum(:iid).to_i
].max
else
[
InternalId.where(**scope, usage: :issues).pick(:last_value).to_i,
where(**scope).maximum(:iid).to_i
].max
end
end
has_internal_id :iid, scope: :namespace, track_if: -> { !importing? }
has_many :events, as: :target, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent

View File

@ -23,7 +23,7 @@
- if Feature.enabled?(:restyle_login_page, @project) && Gitlab::CurrentSettings.current_application_settings.terms
%p.gl-px-5
= html_escape(s_("SignUp|By signing in you accept the %{link_start}Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}.")) % { link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe,
= html_escape(s_("SignUp|By signing in you accept the %{link_start}Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}.")) % { link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe,
link_end: '</a>'.html_safe }
- if allow_signup?

View File

@ -3,15 +3,15 @@
%p.gl-text-gray-500.gl-mt-5.gl-mb-0
- if Feature.enabled?(:restyle_login_page, @project)
- if Gitlab.com?
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}")) % { button_text: button_text,
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}")) % { button_text: button_text,
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
- else
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}")) % { button_text: button_text,
= html_escape(s_("SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}")) % { button_text: button_text,
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
- else
- if Gitlab.com?
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Statement%{link_end}")) % { button_text: button_text,
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }
- else
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}")) % { button_text: button_text,
= html_escape(s_("SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Statement%{link_end}")) % { button_text: button_text,
link_start: "<a href='#{terms_path}' target='_blank' rel='noreferrer noopener'>".html_safe, link_end: '</a>'.html_safe }

View File

@ -88,7 +88,7 @@
- if @user.twitter.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: _("X (formerly Twitter)"), target: '_blank', rel: 'noopener noreferrer nofollow' do
= sprite_icon('twitter', css_class: 'twitter-icon')
= sprite_icon('x', css_class: 'x-icon')
- if @user.discord.present?
= render 'middle_dot_divider', breakpoint: 'sm' do
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do

View File

@ -0,0 +1,8 @@
---
name: gitlab_duo_chat_requests_to_ai_gateway
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/138274
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/433213
milestone: '16.7'
type: development
group: group::ai framework
default_enabled: false

View File

@ -0,0 +1,21 @@
# frozen_string_literal: true
class AddSourcePackageNameToSbomComponentVersions < Gitlab::Database::Migration[2.2]
disable_ddl_transaction!
milestone '16.7'
def up
with_lock_retries do
add_column :sbom_component_versions, :source_package_name, :text, if_not_exists: true
end
add_text_limit :sbom_component_versions, :source_package_name, 255
end
def down
with_lock_retries do
remove_column :sbom_component_versions, :source_package_name, if_exists: true
end
end
end

View File

@ -0,0 +1 @@
557e640d30119599a2ca50cbe2b4e36f01b888df5a4679de362ae000ee23072b

View File

@ -23117,6 +23117,8 @@ CREATE TABLE sbom_component_versions (
updated_at timestamp with time zone NOT NULL,
component_id bigint NOT NULL,
version text NOT NULL,
source_package_name text,
CONSTRAINT check_39636b9a8a CHECK ((char_length(source_package_name) <= 255)),
CONSTRAINT check_e71cad08d3 CHECK ((char_length(version) <= 255))
);

View File

@ -860,7 +860,7 @@ component under test, with the `computed` property, for example). Remember to us
We should test for events emitted in response to an action in our component. This testing
verifies the correct events are being fired with the correct arguments.
For any DOM events we should use [`trigger`](https://v1.test-utils.vuejs.org/api/wrapper/#trigger)
For any native DOM events we should use [`trigger`](https://v1.test-utils.vuejs.org/api/wrapper/#trigger)
to fire out event.
```javascript
@ -892,6 +892,20 @@ it('should fire the itemClicked event', () => {
We should verify an event has been fired by asserting against the result of the
[`emitted()`](https://v1.test-utils.vuejs.org/api/wrapper/#emitted) method.
It is a good practice to prefer to use `vm.$emit` over `trigger` when emitting events from child components.
Using `trigger` on the component means we treat it as a white box: we assume that the root element of child component has a native `click` event. Also, some tests fail in Vue3 mode when using `trigger` on child components.
```javascript
const findButton = () => wrapper.findComponent(GlButton);
// bad
findButton().trigger('click');
// good
findButton().vm.$emit('click');
```
## Vue.js Expert Role
You should only apply to be a Vue.js expert when your own merge requests and your reviews show:

View File

@ -54,11 +54,6 @@ Only GitLab administrators can change enterprise users' primary email address to
Providing the ability to group Owners to change their enterprise users' primary email to an email with a non-verified domain is proposed in [issue 412966](https://gitlab.com/gitlab-org/gitlab/-/issues/412966).
## Dissociation of the user from their enterprise group
Changing an enterprise user's primary email to an email with a non-verified domain automatically disassociates them from their enterprise group.
However, there are [primary email change restrictions](#primary-email-change).
## Verified domains for groups
The following automated processes use [verified domains](../project/pages/custom_domains_ssl_tls_certification/index.md) to run:
@ -213,6 +208,10 @@ this information includes users' email addresses.
[Issue 391453](https://gitlab.com/gitlab-org/gitlab/-/issues/391453) proposes to change the criteria for access to email addresses from provisioned users to enterprise users.
### Remove enterprise management features from an account
Changing an enterprise user's primary email to any email with a non-verified domain automatically removes the enterprise badge from the account. This does not alter any account roles or permissions for the user, but does limit the group Owner's ability to manage this account.
## Troubleshooting
### Cannot disable two-factor authentication for an enterprise user

View File

@ -76,6 +76,7 @@ module Bitbucket
merge_commit_sha: merge_commit_sha,
target_branch_name: target_branch_name,
target_branch_sha: target_branch_sha,
source_and_target_project_different: source_and_target_project_different,
reviewers: reviewers
}
end
@ -89,6 +90,18 @@ module Bitbucket
def target_branch
raw['destination']
end
def source_repo_uuid
source_branch&.dig('repository', 'uuid')
end
def target_repo_uuid
target_branch&.dig('repository', 'uuid')
end
def source_and_target_project_different
source_repo_uuid != target_repo_uuid
end
end
end
end

View File

@ -15,6 +15,8 @@ module Gitlab
end
def execute
return if skip
log_info(import_stage: 'import_pull_request', message: 'starting', iid: object[:iid])
description = ''
@ -58,6 +60,15 @@ module Gitlab
attr_reader :object, :project, :formatter, :user_finder
def skip
return false unless object[:source_and_target_project_different]
message = 'skipping because source and target projects are different'
log_info(import_stage: 'import_pull_request', message: message, iid: object[:iid])
true
end
def author_line
return '' if find_user_id

View File

@ -27874,6 +27874,12 @@ msgstr ""
msgid "KubernetesDashboard|Annotations"
msgstr ""
msgid "KubernetesDashboard|DaemonSet"
msgstr ""
msgid "KubernetesDashboard|DaemonSets"
msgstr ""
msgid "KubernetesDashboard|Dashboard"
msgstr ""
@ -45646,19 +45652,19 @@ msgstr ""
msgid "Sign-up restrictions"
msgstr ""
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}"
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}"
msgstr ""
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}"
msgid "SignUp|By clicking %{button_text} or registering through a third party you accept the%{link_start} Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}"
msgstr ""
msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Policy%{link_end}"
msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the %{link_start}Terms of Use and Privacy Statement%{link_end}"
msgstr ""
msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Policy%{link_end}"
msgid "SignUp|By clicking %{button_text}, I agree that I have read and accepted the GitLab %{link_start}Terms of Use and Privacy Statement%{link_end}"
msgstr ""
msgid "SignUp|By signing in you accept the %{link_start}Terms of Use and acknowledge the Privacy Policy and Cookie Policy%{link_end}."
msgid "SignUp|By signing in you accept the %{link_start}Terms of Use and acknowledge the Privacy Statement and Cookie Policy%{link_end}."
msgstr ""
msgid "SignUp|First name is too long (maximum is %{max_length} characters)."
@ -55129,9 +55135,6 @@ msgstr ""
msgid "WorkItem|Only %{MAX_WORK_ITEMS} items can be added at a time."
msgstr ""
msgid "WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this %{workItemType}."
msgstr ""
msgid "WorkItem|Open"
msgstr ""

View File

@ -227,6 +227,8 @@ function handle_retry_rspec_in_new_process() {
}
function rspec_paralellized_job() {
echo "[$(date '+%H:%M:%S')] Starting rspec_paralellized_job"
read -ra job_name <<< "${CI_JOB_NAME}"
local test_tool="${job_name[0]}"
local test_level="${job_name[1]}"

View File

@ -62,7 +62,7 @@ RSpec.describe 'Signup', :js, feature_category: :user_management do
let(:terms_text) do
<<~TEXT.squish
By clicking Register or registering through a third party you accept the
Terms of Use and acknowledge the Privacy Policy and Cookie Policy
Terms of Use and acknowledge the Privacy Statement and Cookie Policy
TEXT
end
@ -383,7 +383,7 @@ RSpec.describe 'Signup', :js, feature_category: :user_management do
let(:terms_text) do
<<~TEXT.squish
By clicking Register, I agree that I have read and accepted the Terms of
Use and Privacy Policy
Use and Privacy Statement
TEXT
end

View File

@ -295,3 +295,59 @@ export const k8sReplicaSetsMock = [readyStatefulSet, readyStatefulSet, failedSta
export const mockReplicaSetsTableItems = mockStatefulSetsTableItems.map((item) => {
return { ...item, kind: 'ReplicaSet' };
});
const readyDaemonSet = {
status: { numberMisscheduled: 0, numberReady: 2, desiredNumberScheduled: 2 },
metadata: {
name: 'daemonSet-1',
namespace: 'default',
creationTimestamp: '2023-07-31T11:50:17Z',
labels: {},
annotations: {},
},
};
const failedDaemonSet = {
status: { numberMisscheduled: 1, numberReady: 1, desiredNumberScheduled: 2 },
metadata: {
name: 'daemonSet-2',
namespace: 'default',
creationTimestamp: '2023-11-21T11:50:59Z',
labels: {},
annotations: {},
},
};
export const mockDaemonSetsStats = [
{
title: 'Ready',
value: 1,
},
{
title: 'Failed',
value: 1,
},
];
export const mockDaemonSetsTableItems = [
{
name: 'daemonSet-1',
namespace: 'default',
status: 'Ready',
age: '114d',
labels: {},
annotations: {},
kind: 'DaemonSet',
},
{
name: 'daemonSet-2',
namespace: 'default',
status: 'Failed',
age: '1d',
labels: {},
annotations: {},
kind: 'DaemonSet',
},
];
export const k8sDaemonSetsMock = [readyDaemonSet, failedDaemonSet];

View File

@ -4,11 +4,13 @@ import k8sDashboardPodsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_da
import k8sDashboardDeploymentsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql';
import k8sDashboardStatefulSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql';
import k8sDashboardReplicaSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql';
import k8sDashboardDaemonSetsQuery from '~/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql';
import {
k8sPodsMock,
k8sDeploymentsMock,
k8sStatefulSetsMock,
k8sReplicaSetsMock,
k8sDaemonSetsMock,
} from '../mock_data';
describe('~/frontend/environments/graphql/resolvers', () => {
@ -370,4 +372,88 @@ describe('~/frontend/environments/graphql/resolvers', () => {
).rejects.toThrow('API error');
});
});
describe('k8sDaemonSets', () => {
const client = { writeQuery: jest.fn() };
const mockWatcher = WatchApi.prototype;
const mockDaemonSetsListWatcherFn = jest.fn().mockImplementation(() => {
return Promise.resolve(mockWatcher);
});
const mockOnDataFn = jest.fn().mockImplementation((eventName, callback) => {
if (eventName === 'data') {
callback([]);
}
});
const mockDaemonSetsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
items: k8sDaemonSetsMock,
});
});
const mockAllDaemonSetsListFn = jest.fn().mockImplementation(mockDaemonSetsListFn);
describe('when the DaemonSets data is present', () => {
beforeEach(() => {
jest
.spyOn(AppsV1Api.prototype, 'listAppsV1DaemonSetForAllNamespaces')
.mockImplementation(mockAllDaemonSetsListFn);
jest
.spyOn(mockWatcher, 'subscribeToStream')
.mockImplementation(mockDaemonSetsListWatcherFn);
jest.spyOn(mockWatcher, 'on').mockImplementation(mockOnDataFn);
});
it('should request all DaemonSets from the cluster_client library and watch the events', async () => {
const DaemonSets = await mockResolvers.Query.k8sDaemonSets(
null,
{
configuration,
},
{ client },
);
expect(mockAllDaemonSetsListFn).toHaveBeenCalled();
expect(mockDaemonSetsListWatcherFn).toHaveBeenCalled();
expect(DaemonSets).toEqual(k8sDaemonSetsMock);
});
it('should update cache with the new data when received from the library', async () => {
await mockResolvers.Query.k8sDaemonSets(null, { configuration, namespace: '' }, { client });
expect(client.writeQuery).toHaveBeenCalledWith({
query: k8sDashboardDaemonSetsQuery,
variables: { configuration, namespace: '' },
data: { k8sDaemonSets: [] },
});
});
});
it('should not watch DaemonSets from the cluster_client library when the DaemonSets data is not present', async () => {
jest.spyOn(AppsV1Api.prototype, 'listAppsV1DaemonSetForAllNamespaces').mockImplementation(
jest.fn().mockImplementation(() => {
return Promise.resolve({
items: [],
});
}),
);
await mockResolvers.Query.k8sDaemonSets(null, { configuration }, { client });
expect(mockDaemonSetsListWatcherFn).not.toHaveBeenCalled();
});
it('should throw an error if the API call fails', async () => {
jest
.spyOn(AppsV1Api.prototype, 'listAppsV1DaemonSetForAllNamespaces')
.mockRejectedValue(new Error('API error'));
await expect(
mockResolvers.Query.k8sDaemonSets(null, { configuration }, { client }),
).rejects.toThrow('API error');
});
});
});

View File

@ -2,6 +2,7 @@ import {
getAge,
calculateDeploymentStatus,
calculateStatefulSetStatus,
calculateDaemonSetStatus,
} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
import { useFakeDate } from 'helpers/fake_date';
@ -72,4 +73,21 @@ describe('k8s_integration_helper', () => {
expect(calculateStatefulSetStatus(item)).toBe(expected);
});
});
describe('calculateDaemonSetStatus', () => {
const ready = {
status: { numberMisscheduled: 0, numberReady: 2, desiredNumberScheduled: 2 },
};
const failed = {
status: { numberMisscheduled: 1, numberReady: 1, desiredNumberScheduled: 2 },
};
it.each`
condition | item | expected
${'there are less numberReady than desiredNumberScheduled or the numberMisscheduled is present'} | ${failed} | ${'Failed'}
${'there are the same amount of numberReady and desiredNumberScheduled'} | ${ready} | ${'Ready'}
`('returns status as $expected when $condition', ({ item, expected }) => {
expect(calculateDaemonSetStatus(item)).toBe(expected);
});
});
});

View File

@ -0,0 +1,106 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import DaemonSetsPage from '~/kubernetes_dashboard/pages/daemon_sets_page.vue';
import WorkloadLayout from '~/kubernetes_dashboard/components/workload_layout.vue';
import { useFakeDate } from 'helpers/fake_date';
import {
k8sDaemonSetsMock,
mockDaemonSetsStats,
mockDaemonSetsTableItems,
} from '../graphql/mock_data';
Vue.use(VueApollo);
describe('Kubernetes dashboard daemonSets page', () => {
let wrapper;
const configuration = {
basePath: 'kas/tunnel/url',
baseOptions: {
headers: { 'GitLab-Agent-Id': '1' },
},
};
const findWorkloadLayout = () => wrapper.findComponent(WorkloadLayout);
const createApolloProvider = () => {
const mockResolvers = {
Query: {
k8sDaemonSets: jest.fn().mockReturnValue(k8sDaemonSetsMock),
},
};
return createMockApollo([], mockResolvers);
};
const createWrapper = (apolloProvider = createApolloProvider()) => {
wrapper = shallowMount(DaemonSetsPage, {
provide: { configuration },
apolloProvider,
});
};
describe('mounted', () => {
it('renders WorkloadLayout component', () => {
createWrapper();
expect(findWorkloadLayout().exists()).toBe(true);
});
it('sets loading prop for the WorkloadLayout', () => {
createWrapper();
expect(findWorkloadLayout().props('loading')).toBe(true);
});
it('removes loading prop from the WorkloadLayout when the list of pods loaded', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('loading')).toBe(false);
});
});
describe('when gets pods data', () => {
useFakeDate(2023, 10, 23, 10, 10);
it('sets correct stats object for the WorkloadLayout', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('stats')).toEqual(mockDaemonSetsStats);
});
it('sets correct table items object for the WorkloadLayout', async () => {
createWrapper();
await waitForPromises();
expect(findWorkloadLayout().props('items')).toMatchObject(mockDaemonSetsTableItems);
});
});
describe('when gets an error from the cluster_client API', () => {
const error = new Error('Error from the cluster_client API');
const createErroredApolloProvider = () => {
const mockResolvers = {
Query: {
k8sDaemonSets: jest.fn().mockRejectedValueOnce(error),
},
};
return createMockApollo([], mockResolvers);
};
beforeEach(async () => {
createWrapper(createErroredApolloProvider());
await waitForPromises();
});
it('sets errorMessage prop for the WorkloadLayout', () => {
expect(findWorkloadLayout().props('errorMessage')).toBe(error.message);
});
});
});

View File

@ -1,8 +1,8 @@
import { GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import DropdownWidget from '~/vue_shared/components/dropdown/dropdown_widget/dropdown_widget.vue';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
describe('DropdownWidget component', () => {
let wrapper;
@ -27,11 +27,14 @@ describe('DropdownWidget component', () => {
...props,
},
stubs: {
GlDropdown,
GlDropdown: stubComponent(GlDropdown, {
methods: {
hide: jest.fn(),
},
template: RENDER_ALL_SLOTS_TEMPLATE,
}),
},
});
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
};
beforeEach(() => {

View File

@ -61,7 +61,6 @@ describe('WorkItemDetailModal component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: true,
workItemIid: '1',
workItemParentId: null,
});
});

View File

@ -25,7 +25,6 @@ import { i18n } from '~/work_items/constants';
import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
@ -89,7 +88,7 @@ describe('WorkItemDetail component', () => {
updateInProgress = false,
workItemIid = '1',
handler = successHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
mutationHandler,
error = undefined,
workItemsMvc2Enabled = false,
linkedWorkItemsEnabled = false,
@ -98,8 +97,8 @@ describe('WorkItemDetail component', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, handler],
[groupWorkItemByIidQuery, groupSuccessHandler],
[updateWorkItemMutation, mutationHandler],
[workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
confidentialityMock,
]),
isLoggedIn: isLoggedIn(),
propsData: {
@ -230,119 +229,52 @@ describe('WorkItemDetail component', () => {
describe('confidentiality', () => {
const errorMessage = 'Mutation failed';
const confidentialWorkItem = workItemByIidResponseFactory({
confidential: true,
});
const workItem = confidentialWorkItem.data.workspace.workItems.nodes[0];
// Mocks for work item without parent
const withoutParentExpectedInputVars = { id, confidential: true };
const toggleConfidentialityWithoutParentHandler = jest.fn().mockResolvedValue({
const confidentialWorkItem = workItemByIidResponseFactory({ confidential: true });
const mutationHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
workItem,
workItem: confidentialWorkItem.data.workspace.workItems.nodes[0],
errors: [],
},
},
});
const withoutParentHandlerMock = jest
.fn()
.mockResolvedValue(workItemQueryResponseWithoutParent);
const confidentialityWithoutParentMock = [
updateWorkItemMutation,
toggleConfidentialityWithoutParentHandler,
];
const confidentialityWithoutParentFailureMock = [
updateWorkItemMutation,
jest.fn().mockRejectedValue(new Error(errorMessage)),
];
// Mocks for work item with parent
const withParentExpectedInputVars = {
id: mockParent.parent.id,
taskData: { id, confidential: true },
};
const toggleConfidentialityWithParentHandler = jest.fn().mockResolvedValue({
data: {
workItemUpdate: {
workItem: {
id: workItem.id,
descriptionHtml: workItem.description,
},
task: {
workItem,
confidential: true,
},
errors: [],
},
},
it('sends updateInProgress props to child component', async () => {
createComponent({ mutationHandler });
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await nextTick();
expect(findCreatedUpdated().props('updateInProgress')).toBe(true);
});
const confidentialityWithParentMock = [
updateWorkItemTaskMutation,
toggleConfidentialityWithParentHandler,
];
const confidentialityWithParentFailureMock = [
updateWorkItemTaskMutation,
jest.fn().mockRejectedValue(new Error(errorMessage)),
];
describe.each`
context | handlerMock | confidentialityMock | confidentialityFailureMock | inputVariables
${'no parent'} | ${withoutParentHandlerMock} | ${confidentialityWithoutParentMock} | ${confidentialityWithoutParentFailureMock} | ${withoutParentExpectedInputVars}
${'parent'} | ${successHandler} | ${confidentialityWithParentMock} | ${confidentialityWithParentFailureMock} | ${withParentExpectedInputVars}
`(
'when work item has $context',
({ handlerMock, confidentialityMock, confidentialityFailureMock, inputVariables }) => {
it('sends updateInProgress props to child component', async () => {
createComponent({
handler: handlerMock,
confidentialityMock,
});
it('emits workItemUpdated when mutation is successful', async () => {
createComponent({ mutationHandler });
await waitForPromises();
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]);
expect(mutationHandler).toHaveBeenCalledWith({
input: {
id: 'gid://gitlab/WorkItem/1',
confidential: true,
},
});
});
await nextTick();
it('shows an alert when mutation fails', async () => {
createComponent({ mutationHandler: jest.fn().mockRejectedValue(new Error(errorMessage)) });
await waitForPromises();
expect(findCreatedUpdated().props('updateInProgress')).toBe(true);
});
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
it('emits workItemUpdated when mutation is successful', async () => {
createComponent({
handler: handlerMock,
confidentialityMock,
});
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
expect(wrapper.emitted('workItemUpdated')).toEqual([[{ confidential: true }]]);
expect(confidentialityMock[1]).toHaveBeenCalledWith({
input: inputVariables,
});
});
it('shows an alert when mutation fails', async () => {
createComponent({
handler: handlerMock,
confidentialityMock: confidentialityFailureMock,
});
await waitForPromises();
findWorkItemActions().vm.$emit('toggleWorkItemConfidentiality', true);
await waitForPromises();
expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
await nextTick();
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(errorMessage);
});
},
);
expect(wrapper.emitted('workItemUpdated')).toBeUndefined();
expect(findAlert().text()).toBe(errorMessage);
});
});
describe('description', () => {

View File

@ -21,7 +21,6 @@ describe('WorkItemStickyHeader', () => {
fullPath: '/test',
isStickyHeaderShowing: true,
workItemNotificationsSubscribed: true,
workItemParentId: null,
updateInProgress: false,
parentWorkItemConfidentiality: false,
showWorkItemCurrentUserTodos: true,

View File

@ -8,7 +8,6 @@ import ItemTitle from '~/work_items/components/item_title.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import { updateWorkItemMutationResponse, workItemQueryResponse } from '../mock_data';
describe('WorkItemTitle component', () => {
@ -20,22 +19,14 @@ describe('WorkItemTitle component', () => {
const findItemTitle = () => wrapper.findComponent(ItemTitle);
const createComponent = ({
workItemParentId,
mutationHandler = mutationSuccessHandler,
canUpdate = true,
} = {}) => {
const createComponent = ({ mutationHandler = mutationSuccessHandler, canUpdate = true } = {}) => {
const { id, title, workItemType } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemTitle, {
apolloProvider: createMockApollo([
[updateWorkItemMutation, mutationHandler],
[updateWorkItemTaskMutation, mutationHandler],
]),
apolloProvider: createMockApollo([[updateWorkItemMutation, mutationHandler]]),
propsData: {
workItemId: id,
workItemTitle: title,
workItemType: workItemType.name,
workItemParentId,
canUpdate,
},
});
@ -77,27 +68,6 @@ describe('WorkItemTitle component', () => {
});
});
it('calls WorkItemTaskUpdate if passed workItemParentId prop', () => {
const title = 'new title!';
const workItemParentId = '1234';
createComponent({
workItemParentId,
});
findItemTitle().vm.$emit('title-changed', title);
expect(mutationSuccessHandler).toHaveBeenCalledWith({
input: {
id: workItemParentId,
taskData: {
id: workItemQueryResponse.data.workItem.id,
title,
},
},
});
});
it('does not call a mutation when the title has not changed', () => {
createComponent();

View File

@ -8,7 +8,6 @@ import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import ItemTitle from '~/work_items/components/item_title.vue';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import { projectWorkItemTypesQueryResponse, createWorkItemMutationResponse } from '../mock_data';
jest.mock('~/lib/utils/uuids', () => ({ uuids: () => ['testuuid'] }));
@ -42,7 +41,6 @@ describe('Create work item component', () => {
[
[projectWorkItemTypesQuery, queryHandler],
[createWorkItemMutation, mutationHandler],
[createWorkItemFromTaskMutation, mutationHandler],
],
{},
{ typePolicies: { Project: { merge: true } } },

View File

@ -49,7 +49,6 @@ describe('Work items root component', () => {
expect(findWorkItemDetail().props()).toEqual({
isModal: false,
workItemParentId: null,
workItemIid: '1',
});
});

View File

@ -74,11 +74,13 @@ RSpec.describe Bitbucket::Representation::PullRequest, feature_category: :import
'title' => 'title',
'source' => {
'branch' => { 'name' => 'source-branch-name' },
'commit' => { 'hash' => 'source-commit-hash' }
'commit' => { 'hash' => 'source-commit-hash' },
'repository' => { 'uuid' => 'uuid' }
},
'destination' => {
'branch' => { 'name' => 'destination-branch-name' },
'commit' => { 'hash' => 'destination-commit-hash' }
'commit' => { 'hash' => 'destination-commit-hash' },
'repository' => { 'uuid' => 'uuid' }
},
'merge_commit' => { 'hash' => 'merge-commit-hash' },
'reviewers' => [
@ -101,6 +103,7 @@ RSpec.describe Bitbucket::Representation::PullRequest, feature_category: :import
target_branch_sha: 'destination-commit-hash',
title: 'title',
updated_at: 'updated-at',
source_and_target_project_different: false,
reviewers: ['user-2']
}

View File

@ -77,6 +77,18 @@ RSpec.describe Gitlab::BitbucketImport::Importers::PullRequestImporter, :clean_g
end
end
context 'when the source and target projects are different' do
let(:importer) { described_class.new(project, hash.merge(source_and_target_project_different: true)) }
it 'skips the import' do
expect(Gitlab::BitbucketImport::Logger)
.to receive(:info)
.with(include(message: 'skipping because source and target projects are different', iid: anything))
expect { importer.execute }.not_to change { project.merge_requests.count }
end
end
context 'when the author does not have a bitbucket identity' do
before do
identity.update!(provider: :github)

View File

@ -35,6 +35,17 @@ RSpec.describe Gitlab::Tracking::EventDefinition do
expect { described_class.definitions }.not_to raise_error
end
it 'has no duplicated actions in InternalEventTracking events', :aggregate_failures do
definitions_by_action = described_class.definitions
.each_value.select { |d| d.category == 'InternalEventTracking' }
.group_by(&:action)
definitions_by_action.each do |action, definitions|
expect(definitions.size).to eq(1),
"Multiple definitions use the action '#{action}': #{definitions.map(&:path).join(', ')}"
end
end
it 'has event definitions for all events used in Internal Events metric definitions', :aggregate_failures do
from_metric_definitions = Gitlab::Usage::MetricDefinition.definitions
.values

View File

@ -8,13 +8,13 @@ RSpec.describe 'devise/shared/_signup_box' do
let(:translation_com) do
s_("SignUp|By clicking %{button_text} or registering through a third party you "\
"accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Policy "\
"accept the GitLab%{link_start} Terms of Use and acknowledge the Privacy Statement "\
"and Cookie Policy%{link_end}")
end
let(:translation_non_com) do
s_("SignUp|By clicking %{button_text} or registering through a third party you "\
"accept the%{link_start} Terms of Use and acknowledge the Privacy Policy and "\
"accept the%{link_start} Terms of Use and acknowledge the Privacy Statement and "\
"Cookie Policy%{link_end}")
end