Commit c0ed81e5 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents eb26363a d89256f3
...@@ -87,6 +87,7 @@ export default { ...@@ -87,6 +87,7 @@ export default {
v-bind="$attrs" v-bind="$attrs"
:open="isSidebarOpen" :open="isSidebarOpen"
class="boards-sidebar gl-absolute" class="boards-sidebar gl-absolute"
variant="sidebar"
@close="handleClose" @close="handleClose"
> >
<template #title> <template #title>
......
...@@ -87,7 +87,7 @@ export default { ...@@ -87,7 +87,7 @@ export default {
<div> <div>
<header <header
v-show="showHeader" v-show="showHeader"
class="gl-display-flex gl-justify-content-space-between gl-align-items-flex-start gl-mb-3" class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-mb-2"
> >
<span class="gl-vertical-align-middle"> <span class="gl-vertical-align-middle">
<slot name="title"> <slot name="title">
...@@ -97,7 +97,8 @@ export default { ...@@ -97,7 +97,8 @@ export default {
</span> </span>
<gl-button <gl-button
v-if="canUpdate" v-if="canUpdate"
variant="link" category="tertiary"
size="small"
class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link" class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link"
data-testid="edit-button" data-testid="edit-button"
@click="toggle" @click="toggle"
......
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a <a
v-if="editable" v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right" class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#" href="#"
data-test-id="edit-link" data-test-id="edit-link"
data-track-event="click_edit_button" data-track-event="click_edit_button"
......
...@@ -90,7 +90,7 @@ export default { ...@@ -90,7 +90,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }} {{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<a <a
v-if="isEditable" v-if="isEditable"
class="float-right lock-edit" class="float-right lock-edit btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary gl-mr-n2"
href="#" href="#"
data-testid="edit-link" data-testid="edit-link"
data-track-event="click_edit_button" data-track-event="click_edit_button"
......
...@@ -33,6 +33,11 @@ export default { ...@@ -33,6 +33,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
lazy: {
type: Boolean,
required: false,
default: true,
},
}, },
data() { data() {
return { return {
...@@ -95,7 +100,7 @@ export default { ...@@ -95,7 +100,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" /> <gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span> <span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div> </div>
<div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2"> <div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2 gl-line-height-20">
<gl-loading-icon v-if="loading" size="sm" :inline="true" /> <gl-loading-icon v-if="loading" size="sm" :inline="true" />
{{ participantLabel }} {{ participantLabel }}
</div> </div>
...@@ -107,7 +112,7 @@ export default { ...@@ -107,7 +112,7 @@ export default {
> >
<a :href="participant.web_url || participant.webUrl" class="author-link"> <a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image <user-avatar-image
:lazy="true" :lazy="lazy"
:img-src="participant.avatar_url || participant.avatarUrl" :img-src="participant.avatar_url || participant.avatarUrl"
:size="24" :size="24"
:tooltip-text="participant.name" :tooltip-text="participant.name"
......
...@@ -64,6 +64,7 @@ export default { ...@@ -64,6 +64,7 @@ export default {
:loading="isLoading" :loading="isLoading"
:participants="participants" :participants="participants"
:number-of-less-participants="7" :number-of-less-participants="7"
:lazy="false"
class="block participants" class="block participants"
/> />
</template> </template>
...@@ -38,7 +38,7 @@ export default { ...@@ -38,7 +38,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" /> <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a <a
v-if="editable" v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right" class="js-sidebar-dropdown-toggle edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary float-right"
href="#" href="#"
data-track-event="click_edit_button" data-track-event="click_edit_button"
data-track-label="right_sidebar" data-track-label="right_sidebar"
......
...@@ -132,8 +132,9 @@ export default { ...@@ -132,8 +132,9 @@ export default {
<slot name="collapsed-right"></slot> <slot name="collapsed-right"></slot>
<gl-button <gl-button
v-if="canUpdate && !initialLoading && canEdit" v-if="canUpdate && !initialLoading && canEdit"
variant="link" category="tertiary"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed" size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
data-testid="edit-button" data-testid="edit-button"
:data-track-event="tracking.event" :data-track-event="tracking.event"
:data-track-label="tracking.label" :data-track-label="tracking.label"
......
...@@ -28,7 +28,12 @@ export default { ...@@ -28,7 +28,12 @@ export default {
}; };
</script> </script>
<template> <template>
<a v-gl-tooltip :href="authorUrl" :title="author.name" class="author-link inline"> <a
v-gl-tooltip
:href="authorUrl"
:title="showAuthorName ? null : author.name"
class="author-link inline"
>
<img :src="avatarUrl" class="avatar avatar-inline s16" /> <img :src="avatarUrl" class="avatar avatar-inline s16" />
<span v-if="showAuthorName" class="author">{{ author.name }}</span> <span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a> </a>
......
<script> <script>
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui'; import { GlSkeletonLoader, GlIcon, GlButton, GlSprintf } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge'; import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql'; import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import createFlash from '~/flash'; import createFlash from '~/flash';
...@@ -10,7 +10,6 @@ import { AUTO_MERGE_STRATEGIES } from '../../constants'; ...@@ -10,7 +10,6 @@ import { AUTO_MERGE_STRATEGIES } from '../../constants';
import eventHub from '../../event_hub'; import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables'; import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MrWidgetAuthor from '../mr_widget_author.vue'; import MrWidgetAuthor from '../mr_widget_author.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default { export default {
name: 'MRWidgetAutoMergeEnabled', name: 'MRWidgetAutoMergeEnabled',
...@@ -28,9 +27,10 @@ export default { ...@@ -28,9 +27,10 @@ export default {
}, },
components: { components: {
MrWidgetAuthor, MrWidgetAuthor,
statusIcon,
GlLoadingIcon,
GlSkeletonLoader, GlSkeletonLoader,
GlIcon,
GlButton,
GlSprintf,
}, },
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin], mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: { props: {
...@@ -155,54 +155,44 @@ export default { ...@@ -155,54 +155,44 @@ export default {
</gl-skeleton-loader> </gl-skeleton-loader>
</div> </div>
<template v-else> <template v-else>
<status-icon status="success" /> <gl-icon name="status_scheduled" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" />
<div class="media-body"> <div class="media-body">
<h4 class="gl-display-flex"> <h4 class="gl-display-flex">
<span class="gl-mr-3"> <span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{ <gl-sprintf :message="statusText" data-testid="statusText">
statusTextBeforeAuthor <template #merge_author>
}}</span> <mr-widget-author :author="mergeUser" />
<mr-widget-author :author="mergeUser" /> </template>
<span class="js-status-text-after-author" data-testid="afterStatusText">{{ </gl-sprintf>
statusTextAfterAuthor
}}</span>
</span> </span>
<a <gl-button
v-if="mr.canCancelAutomaticMerge" v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge" :loading="isCancellingAutoMerge"
role="button" size="small"
href="#" class="js-cancel-auto-merge"
class="btn btn-sm btn-default js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button" data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton" data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge" @click="cancelAutomaticMerge"
> >
<gl-loading-icon v-if="isCancellingAutoMerge" size="sm" inline class="gl-mr-1" />
{{ cancelButtonText }} {{ cancelButtonText }}
</a> </gl-button>
</h4> </h4>
<section class="mr-info-list"> <section class="mr-info-list">
<p>
{{ s__('mrWidget|The changes will be merged into') }}
<a :href="mr.targetBranchPath" class="label-branch">{{ targetBranch }}</a>
</p>
<p v-if="shouldRemoveSourceBranch"> <p v-if="shouldRemoveSourceBranch">
{{ s__('mrWidget|The source branch will be deleted') }} {{ s__('mrWidget|The source branch will be deleted') }}
</p> </p>
<p v-else class="gl-display-flex"> <p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span> <span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a <gl-button
v-if="canRemoveSourceBranch" v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch" :loading="isRemovingSourceBranch"
role="button" size="small"
class="btn btn-sm btn-default js-remove-source-branch" class="js-remove-source-branch"
href="#"
data-testid="removeSourceBranchButton" data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch" @click="removeSourceBranch"
> >
<gl-loading-icon v-if="isRemovingSourceBranch" size="sm" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }} {{ s__('mrWidget|Delete source branch') }}
</a> </gl-button>
</p> </p>
</section> </section>
</div> </div>
......
...@@ -2,14 +2,13 @@ import { s__ } from '~/locale'; ...@@ -2,14 +2,13 @@ import { s__ } from '~/locale';
export default { export default {
computed: { computed: {
statusTextBeforeAuthor() { statusText() {
return s__('mrWidget|Set by'); return s__(
}, 'mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds',
statusTextAfterAuthor() { );
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
}, },
cancelButtonText() { cancelButtonText() {
return s__('mrWidget|Cancel'); return s__('mrWidget|Cancel auto-merge');
}, },
}, },
}; };
...@@ -23,8 +23,8 @@ export default function deviseState() { ...@@ -23,8 +23,8 @@ export default function deviseState() {
return stateKey.pipelineBlocked; return stateKey.pipelineBlocked;
} else if (this.canMerge && this.isSHAMismatch) { } else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch; return stateKey.shaMismatch;
} else if (this.autoMergeEnabled) { } else if (this.autoMergeEnabled && !this.mergeError) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled; return stateKey.autoMergeEnabled;
} else if (!this.canMerge) { } else if (!this.canMerge) {
return stateKey.notAllowedToMerge; return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) { } else if (this.canBeMerged) {
......
...@@ -28,8 +28,9 @@ export default { ...@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit"> <template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline /> <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button <gl-button
variant="link" category="tertiary"
class="float-right gl-text-gray-900! gl-hover-text-blue-800! js-sidebar-dropdown-toggle" size="small"
class="float-right gl-text-gray-900! js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button" data-qa-selector="labels_edit_button"
@click="toggleDropdownContents" @click="toggleDropdownContents"
> >
......
...@@ -28,8 +28,9 @@ export default { ...@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit"> <template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline /> <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button <gl-button
variant="link" category="tertiary"
class="float-right js-sidebar-dropdown-toggle" size="small"
class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button" data-qa-selector="labels_edit_button"
@click="toggleDropdownContents" @click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button >{{ __('Edit') }}</gl-button
......
...@@ -237,3 +237,7 @@ ...@@ -237,3 +237,7 @@
@include side-panel-toggle; @include side-panel-toggle;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.edit-link {
margin-right: -$gl-spacing-scale-2;
}
...@@ -476,6 +476,10 @@ ...@@ -476,6 +476,10 @@
.gl-drawer-header { .gl-drawer-header {
align-items: flex-start; align-items: flex-start;
} }
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
width: auto;
}
} }
.board-header-collapsed-info-icon:hover { .board-header-collapsed-info-icon:hover {
......
...@@ -197,6 +197,10 @@ ...@@ -197,6 +197,10 @@
margin-bottom: -$gl-spacing-scale-3; margin-bottom: -$gl-spacing-scale-3;
} }
.gl-mr-n2 {
margin-right: -$gl-spacing-scale-2;
}
// Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408 // Will be moved to @gitlab/ui in https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1408
$gl-line-height-42: px-to-rem(42px); $gl-line-height-42: px-to-rem(42px);
......
...@@ -39,5 +39,55 @@ module Postgresql ...@@ -39,5 +39,55 @@ module Postgresql
false false
end end
end end
def self.count
connection
.execute("SELECT COUNT(*) FROM pg_replication_slots;")
.first
.fetch('count')
.to_i
end
def self.unused_slots_count
connection
.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
.first
.fetch('count')
.to_i
end
def self.used_slots_count
connection
.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 't';")
.first
.fetch('count')
.to_i
end
# array of slots and the retained_bytes
# https://www.skillslogic.com/blog/databases/checking-postgres-replication-lag
# http://bdr-project.org/docs/stable/monitoring-peers.html
def self.slots_retained_bytes
connection.execute(<<-SQL.squish).to_a
SELECT slot_name, database,
active, pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)
AS retained_bytes
FROM pg_replication_slots;
SQL
end
# returns the max number WAL space (in bytes) being used across the replication slots
def self.max_retained_wal
connection.execute(<<-SQL.squish).first.fetch('coalesce').to_i
SELECT COALESCE(MAX(pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)), 0)
FROM pg_replication_slots;
SQL
end
def self.max_replication_slots
connection.execute(<<-SQL.squish).first&.fetch('setting').to_i
SELECT setting FROM pg_settings WHERE name = 'max_replication_slots';
SQL
end
end end
end end
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_status_changed
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_assigned
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_reopened
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_closed
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_assigned
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_comment
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_zoom_meeting
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_relate
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_unrelate
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_change_confidential
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,23 @@ value_type: number ...@@ -10,6 +10,23 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
- incident_management_incident_reopened
- incident_management_incident_closed
- incident_management_incident_assigned
- incident_management_incident_todo
- incident_management_incident_comment
- incident_management_incident_zoom_meeting
- incident_management_incident_published
- incident_management_incident_relate
- incident_management_incident_unrelate
- incident_management_incident_change_confidential
- incident_management_alert_status_changed
- incident_management_alert_assigned
- incident_management_alert_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -11,6 +11,10 @@ value_type: number ...@@ -11,6 +11,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_create_incident
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_status_changed
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_assigned
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_reopened
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_closed
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_assigned
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_comment
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_zoom_meeting
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_relate
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_unrelate
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_change_confidential
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -10,6 +10,23 @@ value_type: number ...@@ -10,6 +10,23 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
- incident_management_incident_reopened
- incident_management_incident_closed
- incident_management_incident_assigned
- incident_management_incident_todo
- incident_management_incident_comment
- incident_management_incident_zoom_meeting
- incident_management_incident_published
- incident_management_incident_relate
- incident_management_incident_unrelate
- incident_management_incident_change_confidential
- incident_management_alert_status_changed
- incident_management_alert_assigned
- incident_management_alert_todo
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -11,6 +11,10 @@ value_type: number ...@@ -11,6 +11,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_create_incident
distribution: distribution:
- ce - ce
- ee - ee
......
...@@ -3407,6 +3407,27 @@ Input type: `ProjectSetComplianceFrameworkInput` ...@@ -3407,6 +3407,27 @@ Input type: `ProjectSetComplianceFrameworkInput`
| <a id="mutationprojectsetcomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationprojectsetcomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprojectsetcomplianceframeworkproject"></a>`project` | [`Project`](#project) | Project after mutation. | | <a id="mutationprojectsetcomplianceframeworkproject"></a>`project` | [`Project`](#project) | Project after mutation. |
### `Mutation.projectSetLocked`
Input type: `ProjectSetLockedInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprojectsetlockedclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprojectsetlockedfilepath"></a>`filePath` | [`String!`](#string) | Full path to the file. |
| <a id="mutationprojectsetlockedlock"></a>`lock` | [`Boolean!`](#boolean) | Whether or not to lock the file path. |
| <a id="mutationprojectsetlockedprojectpath"></a>`projectPath` | [`ID!`](#id) | Full path of the project to mutate. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationprojectsetlockedclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationprojectsetlockederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationprojectsetlockedproject"></a>`project` | [`Project`](#project) | Project after mutation. |
### `Mutation.prometheusIntegrationCreate` ### `Mutation.prometheusIntegrationCreate`
Input type: `PrometheusIntegrationCreateInput` Input type: `PrometheusIntegrationCreateInput`
......
...@@ -216,7 +216,7 @@ To set up the GitLab external URL: ...@@ -216,7 +216,7 @@ To set up the GitLab external URL:
1. To prevent the domain name from 1. To prevent the domain name from
[resetting after a reboot](https://docs.bitnami.com/aws/apps/gitlab/configuration/change-default-address/), [resetting after a reboot](https://docs.bitnami.com/aws/apps/gitlab/configuration/change-default-address/),
Rename the utility that Bitnami uses: rename the utility that Bitnami uses:
```shell ```shell
sudo mv /opt/bitnami/apps/gitlab/bnconfig /opt/bitnami/apps/gitlab/bnconfig.bak sudo mv /opt/bitnami/apps/gitlab/bnconfig /opt/bitnami/apps/gitlab/bnconfig.bak
......
...@@ -55,6 +55,7 @@ export default { ...@@ -55,6 +55,7 @@ export default {
v-bind="$attrs" v-bind="$attrs"
class="boards-sidebar gl-absolute" class="boards-sidebar gl-absolute"
:open="isSidebarOpen" :open="isSidebarOpen"
variant="sidebar"
@close="handleClose" @close="handleClose"
> >
<template #title> <template #title>
......
<script>
import getCiMinutesUsage from '../graphql/queries/ci_minutes.graphql';
import MinutesUsageMonthChart from './minutes_usage_month_chart.vue';
import MinutesUsageProjectChart from './minutes_usage_project_chart.vue';
export default {
components: {
MinutesUsageMonthChart,
MinutesUsageProjectChart,
},
data() {
return {
ciMinutesUsage: [],
};
},
apollo: {
ciMinutesUsage: {
query: getCiMinutesUsage,
update(res) {
return res?.ciMinutesUsage?.nodes;
},
},
},
computed: {
minutesUsageDataByMonth() {
return this.ciMinutesUsage.map((cur) => [cur.month, cur.minutes]);
},
},
};
</script>
<template>
<div class="gl-border-b-solid gl-border-gray-200 gl-border-b-1 gl-mb-3">
<minutes-usage-month-chart
class="gl-border-b-solid gl-border-gray-200 gl-border-b-1"
:minutes-usage-data="minutesUsageDataByMonth"
/>
<minutes-usage-project-chart :minutes-usage-data="ciMinutesUsage" />
</div>
</template>
<script>
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { USAGE_BY_MONTH, X_AXIS_MONTH_LABEL, X_AXIS_CATEGORY, Y_AXIS_LABEL } from '../constants';
export default {
USAGE_BY_MONTH,
X_AXIS_MONTH_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
components: {
GlAreaChart,
},
props: {
minutesUsageData: {
type: Array,
required: true,
},
},
computed: {
chartOptions() {
return {
xAxis: {
name: this.$options.X_AXIS_MONTH_LABEL,
type: this.$options.X_AXIS_CATEGORY,
},
yAxis: {
name: this.$options.Y_AXIS_LABEL,
},
};
},
chartData() {
return [
{
data: this.minutesUsageData,
name: this.$options.USAGE_BY_MONTH,
},
];
},
isDataEmpty() {
return this.minutesUsageData.length === 0;
},
},
};
</script>
<template>
<div>
<h5>{{ $options.USAGE_BY_MONTH }}</h5>
<gl-area-chart v-if="!isDataEmpty" class="gl-mb-3" :data="chartData" :option="chartOptions" />
</div>
</template>
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { keyBy } from 'lodash';
import {
USAGE_BY_PROJECT,
X_AXIS_PROJECT_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
} from '../constants';
export default {
USAGE_BY_PROJECT,
X_AXIS_PROJECT_LABEL,
X_AXIS_CATEGORY,
Y_AXIS_LABEL,
components: {
GlColumnChart,
GlDropdown,
GlDropdownItem,
},
props: {
minutesUsageData: {
type: Array,
required: true,
},
},
data() {
return {
selectedMonth: '',
};
},
computed: {
chartData() {
return [
{
data: this.getUsageDataSelectedMonth,
},
];
},
usageDataByMonth() {
return keyBy(this.minutesUsageData, 'month');
},
getUsageDataSelectedMonth() {
return this.usageDataByMonth[this.selectedMonth]?.projects?.nodes.map((cur) => [
cur.name,
cur.minutes,
]);
},
months() {
return this.minutesUsageData.map((cur) => cur.month);
},
isDataEmpty() {
return this.minutesUsageData.length === 0 && this.selectedMonth.length === 0;
},
},
watch: {
months() {
this.setFirstMonthDropdown();
},
},
mounted() {
if (!this.isDataEmpty) {
this.setFirstMonthDropdown();
}
},
methods: {
changeSelectedMonth(month) {
this.selectedMonth = month;
},
setFirstMonthDropdown() {
[this.selectedMonth] = this.months;
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex gl-my-3">
<h5 class="gl-flex-grow-1">{{ $options.USAGE_BY_PROJECT }}</h5>
<gl-dropdown v-if="!isDataEmpty" :text="selectedMonth">
<gl-dropdown-item
v-for="(month, index) in months"
:key="index"
:is-checked="selectedMonth === month"
is-check-item
@click="changeSelectedMonth(month)"
>
{{ month }}
</gl-dropdown-item>
</gl-dropdown>
</div>
<gl-column-chart
v-if="!isDataEmpty"
class="gl-mb-3"
:bars="chartData"
:y-axis-title="$options.Y_AXIS_LABEL"
:x-axis-title="$options.X_AXIS_PROJECT_LABEL"
:x-axis-type="$options.X_AXIS_CATEGORY"
/>
</div>
</template>
import { __, s__ } from '~/locale';
// i18n
export const USAGE_BY_MONTH = s__('UsageQuota|CI minutes usage by month');
export const USAGE_BY_PROJECT = s__('UsageQuota|CI minutes usage by project');
export const X_AXIS_MONTH_LABEL = __('Month');
export const X_AXIS_PROJECT_LABEL = __('Projects');
export const Y_AXIS_LABEL = __('Minutes');
export const X_AXIS_CATEGORY = 'category';
query getCiMinutesUsage {
ciMinutesUsage {
nodes {
month
minutes
projects {
nodes {
name
minutes
}
}
}
}
}
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import CiMinutesUsageApp from './components/app.vue';
const mountCiMinutesUsageApp = (el) => {
Vue.use(VueApollo);
const defaultClient = createDefaultClient();
const apolloProvider = new VueApollo({
defaultClient,
});
return new Vue({
el,
apolloProvider,
name: 'CiMinutesUsageApp',
components: {
CiMinutesUsageApp,
},
render: (createElement) => createElement(CiMinutesUsageApp, {}),
});
};
export default () => {
const el = document.querySelector('.js-ci-minutes-usage');
return !el ? {} : mountCiMinutesUsageApp(el);
};
...@@ -156,10 +156,10 @@ export default { ...@@ -156,10 +156,10 @@ export default {
<template> <template>
<div :class="blockClass" class="block date"> <div :class="blockClass" class="block date">
<collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" /> <collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" />
<div class="title"> <div class="title gl-mb-2">
{{ label }} {{ label }}
<gl-loading-icon v-if="dateSaveInProgress" size="sm" :inline="true" /> <gl-loading-icon v-if="dateSaveInProgress" size="sm" :inline="true" />
<div class="float-right d-flex"> <div class="float-right gl-display-flex gl-align-items-center">
<gl-icon <gl-icon
ref="epicDatePopover" ref="epicDatePopover"
name="question-o" name="question-o"
...@@ -181,8 +181,9 @@ export default { ...@@ -181,8 +181,9 @@ export default {
<gl-button <gl-button
v-show="canUpdate && !editing" v-show="canUpdate && !editing"
ref="editButton" ref="editButton"
variant="link" category="tertiary"
class="btn-sidebar-action" size="small"
class="btn-sidebar-action gl-mr-n2"
@click="startEditing" @click="startEditing"
> >
{{ __('Edit') }} {{ __('Edit') }}
......
import ciMinutesUsage from 'ee/ci_minutes_usage';
import otherStorageCounter from 'ee/other_storage_counter'; import otherStorageCounter from 'ee/other_storage_counter';
import storageCounter from 'ee/storage_counter'; import storageCounter from 'ee/storage_counter';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
...@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) { ...@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) {
hashedTabs: true, hashedTabs: true,
}); });
} }
ciMinutesUsage();
...@@ -73,7 +73,7 @@ export default { ...@@ -73,7 +73,7 @@ export default {
<gl-tooltip :target="() => $refs.sidebarIcon" placement="left" boundary="viewport"> <gl-tooltip :target="() => $refs.sidebarIcon" placement="left" boundary="viewport">
<span v-html="tooltipText"></span> <span v-html="tooltipText"></span>
</gl-tooltip> </gl-tooltip>
<div class="title hide-collapsed">{{ __('Ancestors') }}</div> <div class="title hide-collapsed gl-mb-2">{{ __('Ancestors') }}</div>
<ul v-if="!isFetching && ancestors.length" class="vertical-timeline hide-collapsed"> <ul v-if="!isFetching && ancestors.length" class="vertical-timeline hide-collapsed">
<li v-for="(ancestor, id) in ancestors" :key="id" class="vertical-timeline-row d-flex"> <li v-for="(ancestor, id) in ancestors" :key="id" class="vertical-timeline-row d-flex">
...@@ -87,7 +87,7 @@ export default { ...@@ -87,7 +87,7 @@ export default {
</ul> </ul>
<div v-if="!isFetching && !ancestors.length" class="value hide-collapsed"> <div v-if="!isFetching && !ancestors.length" class="value hide-collapsed">
<span class="no-value">{{ __('None') }}</span> <span class="no-value gl-text-gray-500">{{ __('None') }}</span>
</div> </div>
<gl-loading-icon v-if="isFetching" size="sm" /> <gl-loading-icon v-if="isFetching" size="sm" />
......
...@@ -165,8 +165,9 @@ export default { ...@@ -165,8 +165,9 @@ export default {
> >
<gl-button <gl-button
ref="editButton" ref="editButton"
variant="link" category="tertiary"
class="edit-link btn-link-hover gl-text-gray-900! gl-hover-text-blue-800!" size="small"
class="edit-link btn-link-hover gl-text-gray-900!"
:disabled="!isOpen" :disabled="!isOpen"
@click.stop="toggleFormDropdown" @click.stop="toggleFormDropdown"
@keydown.esc="hideDropdown" @keydown.esc="hideDropdown"
......
...@@ -162,7 +162,7 @@ export default { ...@@ -162,7 +162,7 @@ export default {
/> />
<a <a
v-if="editable" v-if="editable"
class="float-right edit-link js-weight-edit-link" class="float-right edit-link btn gl-text-gray-900! gl-ml-auto hide-collapsed btn-default btn-sm gl-button btn-default-tertiary js-weight-edit-link"
data-qa-selector="edit_weight_link" data-qa-selector="edit_weight_link"
href="#" href="#"
@click.prevent="onEditClick(!shouldShowEditField)" @click.prevent="onEditClick(!shouldShowEditField)"
......
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { import { MT_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
MT_MERGE_STRATEGY,
MTWPS_MERGE_STRATEGY,
MWPS_MERGE_STRATEGY,
} from '~/vue_merge_request_widget/constants';
export default { export default {
computed: { computed: {
statusTextBeforeAuthor() { statusText() {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Added to the merge train by');
}
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr; const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) { if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|to start a merge train when the pipeline succeeds'); return s__('mrWidget|Added to the merge train by %{merge_author}');
} else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) {
return s__(
'mrWidget|Set by %{merge_author} to start a merge train when the pipeline succeeds',
);
} else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount !== 0) { } else if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount !== 0) {
return s__('mrWidget|to be added to the merge train when the pipeline succeeds'); return s__(
} else if (this.autoMergeStrategy === MWPS_MERGE_STRATEGY) { 'mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds',
return s__('mrWidget|to be merged automatically when the pipeline succeeds'); );
} }
return ''; return s__(
'mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds',
);
}, },
cancelButtonText() { cancelButtonText() {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) { if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Remove from merge train'); return s__('mrWidget|Remove from merge train');
} }
return s__('mrWidget|Cancel'); return s__('mrWidget|Cancel auto-merge');
}, },
}, },
}; };
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
} }
} }
.labels-select-dropdown-contents { .labels-select-wrapper.is-embedded {
@include gl-left-0; @include gl-left-0;
@include gl-shadow-x0-y2-b4-s0; @include gl-shadow-x0-y2-b4-s0;
......
...@@ -23,6 +23,7 @@ module EE ...@@ -23,6 +23,7 @@ module EE
mount_mutation ::Mutations::Epics::SetSubscription mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::GitlabSubscriptions::Activate mount_mutation ::Mutations::GitlabSubscriptions::Activate
mount_mutation ::Mutations::Projects::SetLocked
mount_mutation ::Mutations::Iterations::Create mount_mutation ::Mutations::Iterations::Create
mount_mutation ::Mutations::Iterations::Update mount_mutation ::Mutations::Iterations::Update
mount_mutation ::Mutations::Iterations::Delete mount_mutation ::Mutations::Iterations::Delete
......
# frozen_string_literal: true
module Mutations
module Projects
class SetLocked < BaseMutation
include FindsProject
graphql_name 'ProjectSetLocked'
authorize :push_code
argument :project_path, GraphQL::Types::ID,
required: true,
description: 'Full path of the project to mutate.'
argument :file_path, GraphQL::Types::String,
required: true,
description: 'Full path to the file.'
argument :lock,
GraphQL::Types::Boolean,
required: true,
description: 'Whether or not to lock the file path.'
field :project, Types::ProjectType, null: true,
description: 'Project after mutation.'
attr_reader :project
def resolve(project_path:, file_path:, lock:)
@project = authorized_find!(project_path)
unless allowed?
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'FileLocks feature disabled'
end
if lock
lock_path(file_path)
else
unlock_path(file_path)
end
{
project: project,
errors: []
}
rescue PathLocks::UnlockService::AccessDenied => e
{
project: nil,
errors: [e.message]
}
end
private
delegate :repository, to: :project
def fetch_path_lock(file_path)
project.path_locks.find_by(path: file_path) # rubocop: disable CodeReuse/ActiveRecord
end
def lock_path(file_path)
return if fetch_path_lock(file_path)
path_lock = PathLocks::LockService.new(project, current_user).execute(file_path)
if path_lock.persisted? && sync_with_lfs?(file_path)
Lfs::LockFileService.new(project, current_user, path: file_path, create_path_lock: false).execute
end
end
def unlock_path(file_path)
path_lock = fetch_path_lock(file_path)
return unless path_lock
PathLocks::UnlockService.new(project, current_user).execute(path_lock)
if sync_with_lfs?(file_path)
Lfs::UnlockFileService.new(project, current_user, path: file_path, force: true).execute
end
end
def sync_with_lfs?(file_path)
project.lfs_enabled? && lfs_file?(file_path)
end
def lfs_file?(file_path)
blob = repository.blob_at_branch(repository.root_ref, file_path)
return false unless blob
lfs_blob_ids = LfsPointersFinder.new(repository, file_path).execute
lfs_blob_ids.include?(blob.id)
end
def allowed?
project.licensed_feature_available?(:file_locks)
end
end
end
end
...@@ -299,19 +299,19 @@ class GeoNode < ApplicationRecord ...@@ -299,19 +299,19 @@ class GeoNode < ApplicationRecord
def replication_slots_count def replication_slots_count
return unless primary? return unless primary?
PgReplicationSlot.count Postgresql::ReplicationSlot.count
end end
def replication_slots_used_count def replication_slots_used_count
return unless primary? return unless primary?
PgReplicationSlot.used_slots_count Postgresql::ReplicationSlot.used_slots_count
end end
def replication_slots_max_retained_wal_bytes def replication_slots_max_retained_wal_bytes
return unless primary? return unless primary?
PgReplicationSlot.max_retained_wal Postgresql::ReplicationSlot.max_retained_wal
end end
def find_or_build_status def find_or_build_status
......
# frozen_string_literal: true
# `pg_replication_slots` is a PostgreSQL view
class PgReplicationSlot
def self.count
ApplicationRecord.connection.execute("SELECT COUNT(*) FROM pg_replication_slots;")
.first.fetch('count').to_i
end
def self.unused_slots_count
ApplicationRecord.connection.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
.first.fetch('count').to_i
end
def self.used_slots_count
ApplicationRecord.connection.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 't';")
.first.fetch('count').to_i
end
# array of slots and the retained_bytes
# https://www.skillslogic.com/blog/databases/checking-postgres-replication-lag
# http://bdr-project.org/docs/stable/monitoring-peers.html
def self.slots_retained_bytes
ApplicationRecord.connection.execute(<<-SQL.squish)
SELECT slot_name, database,
active, pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)
AS retained_bytes
FROM pg_replication_slots;
SQL
.to_a
end
# returns the max number WAL space (in bytes) being used across the replication slots
def self.max_retained_wal
ApplicationRecord.connection.execute(<<-SQL.squish)
SELECT COALESCE(MAX(pg_wal_lsn_diff(pg_current_wal_insert_lsn(), restart_lsn)), 0)
FROM pg_replication_slots;
SQL
.first.fetch('coalesce').to_i
end
def self.max_replication_slots
ApplicationRecord.connection.execute(<<-SQL.squish)
SELECT setting FROM pg_settings WHERE name = 'max_replication_slots';
SQL
.first&.fetch('setting').to_i
end
end
...@@ -38,6 +38,8 @@ ...@@ -38,6 +38,8 @@
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace = render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
.js-ci-minutes-usage
%table.table.pipeline-project-metrics %table.table.pipeline-project-metrics
%thead %thead
%tr %tr
......
...@@ -17,7 +17,7 @@ module Geo ...@@ -17,7 +17,7 @@ module Geo
def perform def perform
return if Gitlab::Database.read_only? return if Gitlab::Database.read_only?
return unless Gitlab::Database.main.healthy? return if Postgresql::ReplicationSlot.lag_too_great?
unless ::GeoNode.secondary_nodes.any? unless ::GeoNode.secondary_nodes.any?
Geo::PruneEventLogService.new(:all).execute Geo::PruneEventLogService.new(:all).execute
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_published
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -12,6 +12,10 @@ milestone: "13.11" ...@@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606
time_frame: 28d time_frame: 28d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_incident_management_oncall_notification_sent
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -10,6 +10,10 @@ value_type: number ...@@ -10,6 +10,10 @@ value_type: number
status: data_available status: data_available
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_published
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -12,6 +12,10 @@ milestone: "13.11" ...@@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606 introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606
time_frame: 7d time_frame: 7d
data_source: redis_hll data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_incident_management_oncall_notification_sent
distribution: distribution:
- ee - ee
tier: tier:
......
...@@ -6,10 +6,6 @@ module EE ...@@ -6,10 +6,6 @@ module EE
module Connection module Connection
extend ActiveSupport::Concern extend ActiveSupport::Concern
def healthy?
!Postgresql::ReplicationSlot.lag_too_great?
end
def geo_uncached_queries(&block) def geo_uncached_queries(&block)
raise 'No block given' unless block_given? raise 'No block given' unless block_given?
......
...@@ -58,8 +58,8 @@ RSpec.describe 'User adds a merge request to a merge train', :js do ...@@ -58,8 +58,8 @@ RSpec.describe 'User adds a merge request to a merge train', :js do
within('.mr-widget-section') do within('.mr-widget-section') do
expect(page).to have_content("Added to the merge train by #{user.name}") expect(page).to have_content("Added to the merge train by #{user.name}")
expect(page).to have_content('The source branch will not be deleted') expect(page).to have_content('The source branch will not be deleted')
expect(page).to have_link('Remove from merge train') expect(page).to have_button('Remove from merge train')
expect(page).to have_link('Delete source branch') expect(page).to have_button('Delete source branch')
end end
end end
...@@ -84,7 +84,7 @@ RSpec.describe 'User adds a merge request to a merge train', :js do ...@@ -84,7 +84,7 @@ RSpec.describe 'User adds a merge request to a merge train', :js do
context "when user clicks 'Remove from merge train' button" do context "when user clicks 'Remove from merge train' button" do
before do before do
click_link 'Remove from merge train' click_button 'Remove from merge train'
end end
it 'cancels automatic merge' do it 'cancels automatic merge' do
...@@ -97,7 +97,7 @@ RSpec.describe 'User adds a merge request to a merge train', :js do ...@@ -97,7 +97,7 @@ RSpec.describe 'User adds a merge request to a merge train', :js do
context "when user clicks 'Delete source branch" do context "when user clicks 'Delete source branch" do
before do before do
click_link 'Delete source branch' click_button 'Delete source branch'
end end
it 'updates the merge option' do it 'updates the merge option' do
......
...@@ -59,14 +59,14 @@ RSpec.describe 'User adds to merge train when pipeline succeeds', :js do ...@@ -59,14 +59,14 @@ RSpec.describe 'User adds to merge train when pipeline succeeds', :js do
within('.mr-widget-section') do within('.mr-widget-section') do
expect(page).to have_content("Set by #{user.name} to start a merge train when the pipeline succeeds") expect(page).to have_content("Set by #{user.name} to start a merge train when the pipeline succeeds")
expect(page).to have_content('The source branch will not be deleted') expect(page).to have_content('The source branch will not be deleted')
expect(page).to have_link('Cancel') expect(page).to have_button('Cancel auto-merge')
expect(page).to have_link('Delete source branch') expect(page).to have_button('Delete source branch')
end end
end end
context "when user clicks 'Cancel' button" do context "when user clicks 'Cancel' button" do
before do before do
click_link 'Cancel' click_button 'Cancel auto-merge'
end end
it 'cancels automatic merge' do it 'cancels automatic merge' do
...@@ -79,7 +79,7 @@ RSpec.describe 'User adds to merge train when pipeline succeeds', :js do ...@@ -79,7 +79,7 @@ RSpec.describe 'User adds to merge train when pipeline succeeds', :js do
context "when user clicks 'Delete source branch" do context "when user clicks 'Delete source branch" do
before do before do
click_link 'Delete source branch' click_button 'Delete source branch'
end end
it 'updates the merge option' do it 'updates the merge option' do
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import CiMinutesUsageApp from 'ee/ci_minutes_usage/components/app.vue';
import MinutesUsageMonthChart from 'ee/ci_minutes_usage/components/minutes_usage_month_chart.vue';
import MinutesUsageProjectChart from 'ee/ci_minutes_usage/components/minutes_usage_project_chart.vue';
import ciMinutesUsage from 'ee/ci_minutes_usage/graphql/queries/ci_minutes.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { ciMinutesUsageMockData } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('CI minutes usage app', () => {
let wrapper;
function createMockApolloProvider() {
const requestHandlers = [[ciMinutesUsage, jest.fn().mockResolvedValue(ciMinutesUsageMockData)]];
return createMockApollo(requestHandlers);
}
function createComponent(options = {}) {
const { fakeApollo } = options;
return shallowMount(CiMinutesUsageApp, {
localVue,
apolloProvider: fakeApollo,
});
}
const findMinutesUsageMonthChart = () => wrapper.findComponent(MinutesUsageMonthChart);
const findMinutesUsageProjectChart = () => wrapper.findComponent(MinutesUsageProjectChart);
beforeEach(() => {
const fakeApollo = createMockApolloProvider();
wrapper = createComponent({ fakeApollo });
});
afterEach(() => {
wrapper.destroy();
});
it('should contain two charts', () => {
expect(findMinutesUsageMonthChart().exists()).toBe(true);
expect(findMinutesUsageProjectChart().exists()).toBe(true);
});
});
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MinutesUsageMonthChart from 'ee/ci_minutes_usage/components/minutes_usage_month_chart.vue';
import { ciMinutesUsageMockData } from '../mock_data';
describe('Minutes usage by month chart component', () => {
let wrapper;
const findAreaChart = () => wrapper.findComponent(GlAreaChart);
const createComponent = () => {
return shallowMount(MinutesUsageMonthChart, {
propsData: {
minutesUsageData: ciMinutesUsageMockData.data.ciMinutesUsage.nodes.map((cur) => [
cur.month,
cur.minutes,
]),
},
});
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders an area chart component', () => {
expect(findAreaChart().exists()).toBe(true);
});
});
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MinutesUsageProjectChart from 'ee/ci_minutes_usage/components/minutes_usage_project_chart.vue';
import { ciMinutesUsageMockData } from '../mock_data';
describe('Minutes usage by project chart component', () => {
let wrapper;
const findColumnChart = () => wrapper.findComponent(GlColumnChart);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const createComponent = () => {
return shallowMount(MinutesUsageProjectChart, {
propsData: {
minutesUsageData: ciMinutesUsageMockData.data.ciMinutesUsage.nodes,
},
});
};
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a column chart component with axis legends', () => {
expect(findColumnChart().exists()).toBe(true);
expect(findColumnChart().props('xAxisTitle')).toBe('Projects');
expect(findColumnChart().props('yAxisTitle')).toBe('Minutes');
});
it('renders a dropdown component', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdown().props('text')).toBe(
ciMinutesUsageMockData.data.ciMinutesUsage.nodes[0].month,
);
});
it('renders the same amount of dropdown components as the backend response', () => {
expect(findAllDropdownItems().length).toBe(
ciMinutesUsageMockData.data.ciMinutesUsage.nodes.length,
);
});
});
export const ciMinutesUsageMockData = {
data: {
ciMinutesUsage: {
nodes: [
{
month: 'June',
minutes: 5,
projects: {
nodes: [
{
name: 'devcafe-wp-theme',
minutes: 5,
},
],
},
},
],
},
},
};
...@@ -15,6 +15,8 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -15,6 +15,8 @@ describe('MRWidgetAutoMergeEnabled', () => {
poll: () => {}, poll: () => {},
}; };
const getStatusText = () => wrapper.find('[data-testid="statusText"]').attributes('message');
const mr = { const mr = {
shouldRemoveSourceBranch: false, shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true, canRemoveSourceBranch: true,
...@@ -45,34 +47,16 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -45,34 +47,16 @@ describe('MRWidgetAutoMergeEnabled', () => {
}); });
describe('computed', () => { describe('computed', () => {
describe('statusTextBeforeAuthor', () => { describe('status', () => {
it('should return "Added to the merge train by" if the pipeline has been added to the merge train', () => {
factory({ autoMergeStrategy: MT_MERGE_STRATEGY });
expect(vm.statusTextBeforeAuthor).toBe('Added to the merge train by');
});
it('should return "Set by" if the MTWPS is selected', () => {
factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY });
expect(vm.statusTextBeforeAuthor).toBe('Set by');
});
it('should return "Set by" if the MWPS is selected', () => {
factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY });
expect(vm.statusTextBeforeAuthor).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to start a merge train..." if MTWPS is selected and there is no existing merge train', () => { it('should return "to start a merge train..." if MTWPS is selected and there is no existing merge train', () => {
factory({ factory({
autoMergeStrategy: MTWPS_MERGE_STRATEGY, autoMergeStrategy: MTWPS_MERGE_STRATEGY,
mergeTrainsCount: 0, mergeTrainsCount: 0,
}); });
expect(vm.statusTextAfterAuthor).toBe('to start a merge train when the pipeline succeeds'); expect(getStatusText()).toBe(
'Set by %{merge_author} to start a merge train when the pipeline succeeds',
);
}); });
it('should return "to be added to the merge train..." if MTWPS is selected and there is an existing merge train', () => { it('should return "to be added to the merge train..." if MTWPS is selected and there is an existing merge train', () => {
...@@ -81,16 +65,16 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -81,16 +65,16 @@ describe('MRWidgetAutoMergeEnabled', () => {
mergeTrainsCount: 1, mergeTrainsCount: 1,
}); });
expect(vm.statusTextAfterAuthor).toBe( expect(getStatusText()).toBe(
'to be added to the merge train when the pipeline succeeds', 'Set by %{merge_author} to be added to the merge train when the pipeline succeeds',
); );
}); });
it('should return "to be merged automatically..." if MWPS is selected', () => { it('should return "to be merged automatically..." if MWPS is selected', () => {
factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY }); factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY });
expect(vm.statusTextAfterAuthor).toBe( expect(getStatusText()).toBe(
'to be merged automatically when the pipeline succeeds', 'Set by %{merge_author} to be merged automatically when the pipeline succeeds',
); );
}); });
}); });
...@@ -99,7 +83,7 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -99,7 +83,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
it('should return "Cancel start merge train" if MTWPS is selected', () => { it('should return "Cancel start merge train" if MTWPS is selected', () => {
factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY }); factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY });
expect(vm.cancelButtonText).toBe('Cancel'); expect(vm.cancelButtonText).toBe('Cancel auto-merge');
}); });
it('should return "Remove from merge train" if the pipeline has been added to the merge train', () => { it('should return "Remove from merge train" if the pipeline has been added to the merge train', () => {
...@@ -111,40 +95,18 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -111,40 +95,18 @@ describe('MRWidgetAutoMergeEnabled', () => {
it('should return "Cancel" if MWPS is selected', () => { it('should return "Cancel" if MWPS is selected', () => {
factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY }); factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY });
expect(vm.cancelButtonText).toBe('Cancel'); expect(vm.cancelButtonText).toBe('Cancel auto-merge');
}); });
}); });
}); });
describe('template', () => { describe('template', () => {
it('should render the status text as "...to start a merge train" if MTWPS is selected and there is no existing merge train', () => {
factory({
autoMergeStrategy: MTWPS_MERGE_STRATEGY,
mergeTrainsCount: 0,
});
const statusText = wrapper.find('.js-status-text-after-author').text();
expect(statusText).toBe('to start a merge train when the pipeline succeeds');
});
it('should render the status text as "...to be added to the merge train" MTWPS is selected and there is an existing merge train', () => {
factory({
autoMergeStrategy: MTWPS_MERGE_STRATEGY,
mergeTrainsCount: 1,
});
const statusText = wrapper.find('.js-status-text-after-author').text();
expect(statusText).toBe('to be added to the merge train when the pipeline succeeds');
});
it('should render the cancel button as "Cancel" if MTWPS is selected', () => { it('should render the cancel button as "Cancel" if MTWPS is selected', () => {
factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY }); factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY });
const cancelButtonText = wrapper.find('.js-cancel-auto-merge').text(); const cancelButtonText = wrapper.find('.js-cancel-auto-merge').text();
expect(cancelButtonText).toBe('Cancel'); expect(cancelButtonText).toBe('Cancel auto-merge');
}); });
}); });
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Projects::SetLocked do
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
describe '#resolve' do
subject { mutation.resolve(project_path: project.full_path, file_path: file_path, lock: lock) }
let(:file_path) { 'README.md' }
let(:lock) { true }
let(:mutated_path_locks) { subject[:project].path_locks }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can lock the file' do
let(:lock) { true }
before do
project.add_developer(user)
end
context 'when file_locks feature is not available' do
before do
stub_licensed_features(file_locks: false)
end
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when file is not locked' do
it 'sets path locks for the project' do
expect { subject }.to change { project.path_locks.count }.by(1)
expect(mutated_path_locks.first).to have_attributes(path: file_path, user: user)
end
end
context 'when file is already locked' do
before do
create(:path_lock, project: project, path: file_path)
end
it 'does not change the lock' do
expect { subject }.not_to change { project.path_locks.count }
end
end
context 'when LFS is enabled' do
let(:file_path) { 'files/lfs/lfs_object.iso' }
before do
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:lfs_enabled?) { true }
end
end
it 'locks the file in LFS' do
expect { subject }.to change { project.lfs_file_locks.count }.by(1)
end
context 'when file is not tracked in LFS' do
let(:file_path) { 'README.md' }
it 'does not lock the file' do
expect { subject }.not_to change { project.lfs_file_locks.count }
end
end
context 'when locking a directory' do
let(:file_path) { 'lfs/' }
it 'locks the directory' do
expect { subject }.to change { project.path_locks.count }.by(1)
end
it 'does not locks the directory through LFS' do
expect { subject }.not_to change { project.lfs_file_locks.count }
end
end
end
end
context 'when the user can unlock the file' do
let(:lock) { false }
before do
project.add_developer(user)
end
context 'when file is already locked by the same user' do
before do
create(:path_lock, project: project, path: file_path, user: user)
end
it 'unlocks the file' do
expect { subject }.to change { project.path_locks.count }.by(-1)
expect(mutated_path_locks).to be_empty
end
end
context 'when file is already locked by somebody else' do
before do
create(:path_lock, project: project, path: file_path)
end
it 'returns an error' do
expect(subject[:errors]).to eq(['You have no permissions'])
end
end
context 'when file is not locked' do
it 'does nothing' do
expect { subject }.not_to change { project.path_locks.count }
expect(mutated_path_locks).to be_empty
end
end
context 'when LFS is enabled' do
let(:file_path) { 'files/lfs/lfs_object.iso' }
before do
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:lfs_enabled?) { true }
end
end
context 'when file is locked' do
before do
create(:lfs_file_lock, project: project, path: file_path, user: user)
create(:path_lock, project: project, path: file_path, user: user)
end
it 'unlocks the file' do
expect { subject }.to change { project.path_locks.count }.by(-1)
end
it 'unlocks the file in LFS' do
expect { subject }.to change { project.lfs_file_locks.count }.by(-1)
end
context 'when file is not tracked in LFS' do
let(:file_path) { 'README.md' }
it 'does not unlock the file' do
expect { subject }.not_to change { project.lfs_file_locks.count }
end
end
context 'when unlocking a directory' do
let(:file_path) { 'lfs/' }
it 'unlocks the directory' do
expect { subject }.to change { project.path_locks.count }.by(-1)
end
it 'does not call the LFS unlock service' do
expect(Lfs::UnlockFileService).not_to receive(:new)
subject
end
end
end
end
end
end
end
...@@ -7,20 +7,6 @@ RSpec.describe Gitlab::Database::Connection do ...@@ -7,20 +7,6 @@ RSpec.describe Gitlab::Database::Connection do
let(:connection) { described_class.new } let(:connection) { described_class.new }
describe '#healthy?' do
it 'returns true when replication lag is not too great' do
allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(false)
expect(connection.healthy?).to be_truthy
end
it 'returns false when replication lag is too great' do
allow(Postgresql::ReplicationSlot).to receive(:lag_too_great?).and_return(true)
expect(connection.healthy?).to be_falsey
end
end
describe '#geo_uncached_queries' do describe '#geo_uncached_queries' do
context 'when no block is given' do context 'when no block is given' do
it 'raises error' do it 'raises error' do
......
...@@ -22,6 +22,18 @@ RSpec.describe Gitlab::UsageDataMetrics do ...@@ -22,6 +22,18 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject).to include(:license_subscription_id) expect(subject).to include(:license_subscription_id)
end end
it 'includes incident_management_incident_published monthly and weekly keys' do
expect(subject[:redis_hll_counters][:incident_management]).to include(
:incident_management_incident_published_monthly, :incident_management_incident_published_weekly
)
end
it 'includes incident_management_oncall monthly and weekly keys' do
expect(subject[:redis_hll_counters][:incident_management_oncall].keys).to contain_exactly(*[
:i_incident_management_oncall_notification_sent_monthly, :i_incident_management_oncall_notification_sent_weekly
])
end
it 'includes compliance monthly and weekly keys' do it 'includes compliance monthly and weekly keys' do
expect(subject[:redis_hll_counters][:compliance].keys).to contain_exactly(*[ expect(subject[:redis_hll_counters][:compliance].keys).to contain_exactly(*[
:g_compliance_dashboard_monthly, :g_compliance_dashboard_weekly, :g_compliance_dashboard_monthly, :g_compliance_dashboard_weekly,
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PgReplicationSlot do
it '#max_replication_slots' do
expect(described_class.max_replication_slots).to be >= 0
end
skip_examples = described_class.max_replication_slots <= described_class.count
context 'with enough slots available' do
before(:all) do
skip('max_replication_slots too small') if skip_examples
@current_slot_count =
ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM pg_replication_slots;")
.first.fetch('count').to_i
@current_unused_count =
ActiveRecord::Base.connection.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
.first.fetch('count').to_i
ActiveRecord::Base.connection.execute("SELECT * FROM pg_create_physical_replication_slot('test_slot');")
end
after(:all) do
unless skip_examples
ActiveRecord::Base.connection.execute("SELECT pg_drop_replication_slot('test_slot');")
end
end
it '#slots_count' do
expect(described_class.count).to eq(@current_slot_count + 1)
end
it '#unused_slots_count' do
expect(described_class.unused_slots_count).to eq(@current_unused_count + 1)
end
it '#max_retained_wal' do
expect(described_class.max_retained_wal).not_to be_nil
end
it '#slots_retained_bytes' do
slot = described_class.slots_retained_bytes.find {|x| x['slot_name'] == 'test_slot' }
expect(slot).not_to be_nil
expect(slot['retained_bytes']).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Lock/unlock project's file path" do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let(:attributes) { { file_path: file_path, lock: lock } }
let(:file_path) { 'README.md' }
let(:lock) { true }
let(:mutation) do
params = { project_path: project.full_path }.merge(attributes)
graphql_mutation(:project_set_locked, params) do
<<-QL.strip_heredoc
project {
id
pathLocks {
nodes {
path
}
}
}
errors
QL
end
end
def mutation_response
graphql_mutation_response(:project_set_locked)
end
context 'when the user does not have permission' do
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not create requirement' do
expect { post_graphql_mutation(mutation, current_user: current_user) }.not_to change { project.path_locks.count }
end
end
context 'when the user has permission' do
before do
project.add_developer(current_user)
end
it 'creates the path lock' do
post_graphql_mutation(mutation, current_user: current_user)
project_hash = mutation_response['project']
expect(project_hash.dig('pathLocks', 'nodes', 0, 'path')).to eq(file_path)
end
context 'when there are validation errors' do
let(:lock) { false }
before do
create(:path_lock, project: project, path: file_path)
end
it_behaves_like 'a mutation that returns errors in the response',
errors: ['You have no permissions']
end
end
end
...@@ -29,8 +29,11 @@ RSpec.describe Geo::PruneEventLogWorker, :geo do ...@@ -29,8 +29,11 @@ RSpec.describe Geo::PruneEventLogWorker, :geo do
end end
it 'does nothing when database is not feeling healthy' do it 'does nothing when database is not feeling healthy' do
allow(Gitlab::Database.main).to receive(:healthy?).and_return(false) allow(Postgresql::ReplicationSlot)
.to receive(:lag_too_great?)
.and_return(true)
expect(GeoNode).not_to receive(:secondary_nodes)
expect(Geo::PruneEventLogService).not_to receive(:new) expect(Geo::PruneEventLogService).not_to receive(:new)
worker.perform worker.perform
......
...@@ -35635,6 +35635,12 @@ msgstr "" ...@@ -35635,6 +35635,12 @@ msgstr ""
msgid "UsageQuota|Buy additional minutes" msgid "UsageQuota|Buy additional minutes"
msgstr "" msgstr ""
msgid "UsageQuota|CI minutes usage by month"
msgstr ""
msgid "UsageQuota|CI minutes usage by project"
msgstr ""
msgid "UsageQuota|Current period usage" msgid "UsageQuota|Current period usage"
msgstr "" msgstr ""
...@@ -39337,7 +39343,7 @@ msgstr "" ...@@ -39337,7 +39343,7 @@ msgstr ""
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue." msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr "" msgstr ""
msgid "mrWidget|Added to the merge train by" msgid "mrWidget|Added to the merge train by %{merge_author}"
msgstr "" msgstr ""
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged" msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
...@@ -39370,7 +39376,7 @@ msgstr "" ...@@ -39370,7 +39376,7 @@ msgstr ""
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?" msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
msgstr "" msgstr ""
msgid "mrWidget|Cancel" msgid "mrWidget|Cancel auto-merge"
msgstr "" msgstr ""
msgid "mrWidget|Check out branch" msgid "mrWidget|Check out branch"
...@@ -39517,7 +39523,13 @@ msgstr "" ...@@ -39517,7 +39523,13 @@ msgstr ""
msgid "mrWidget|Revoke approval" msgid "mrWidget|Revoke approval"
msgstr "" msgstr ""
msgid "mrWidget|Set by" msgid "mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds"
msgstr ""
msgid "mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds"
msgstr ""
msgid "mrWidget|Set by %{merge_author} to start a merge train when the pipeline succeeds"
msgstr "" msgstr ""
msgid "mrWidget|The changes were merged into" msgid "mrWidget|The changes were merged into"
...@@ -39589,15 +39601,6 @@ msgstr "" ...@@ -39589,15 +39601,6 @@ msgstr ""
msgid "mrWidget|into" msgid "mrWidget|into"
msgstr "" msgstr ""
msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
msgstr ""
msgid "mrWidget|to be merged automatically when the pipeline succeeds"
msgstr ""
msgid "mrWidget|to start a merge train when the pipeline succeeds"
msgstr ""
msgid "must be a Debian package" msgid "must be a Debian package"
msgstr "" msgstr ""
......
...@@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do ...@@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when enabled after it was previously canceled' do context 'when enabled after it was previously canceled' do
before do before do
click_button "Merge when pipeline succeeds" click_button "Merge when pipeline succeeds"
click_link "Cancel" click_button "Cancel auto-merge"
wait_for_requests wait_for_requests
...@@ -87,7 +87,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do ...@@ -87,7 +87,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
before do before do
merge_request.merge_params['force_remove_source_branch'] = '0' merge_request.merge_params['force_remove_source_branch'] = '0'
merge_request.save! merge_request.save!
click_link "Cancel" click_button "Cancel auto-merge"
end end
it_behaves_like 'Merge when pipeline succeeds activator' it_behaves_like 'Merge when pipeline succeeds activator'
...@@ -114,7 +114,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do ...@@ -114,7 +114,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
end end
it 'allows to cancel the automatic merge' do it 'allows to cancel the automatic merge' do
click_link "Cancel" click_button "Cancel auto-merge"
expect(page).to have_button "Merge when pipeline succeeds" expect(page).to have_button "Merge when pipeline succeeds"
...@@ -124,7 +124,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do ...@@ -124,7 +124,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
end end
it 'allows to delete source branch' do it 'allows to delete source branch' do
click_link "Delete source branch" click_button "Delete source branch"
expect(page).to have_content "The source branch will be deleted" expect(page).to have_content "The source branch will be deleted"
end end
......
...@@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', ...@@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when detached merge request pipeline is pending' do context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel') expect(page).to have_button('Cancel auto-merge')
end end
end end
...@@ -178,7 +178,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', ...@@ -178,7 +178,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'waits the head pipeline' do it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel') expect(page).to have_button('Cancel auto-merge')
end end
end end
end end
...@@ -377,7 +377,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', ...@@ -377,7 +377,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when detached merge request pipeline is pending' do context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel') expect(page).to have_button('Cancel auto-merge')
end end
end end
...@@ -403,7 +403,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request', ...@@ -403,7 +403,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'waits the head pipeline' do it 'waits the head pipeline' do
expect(page).to have_content('to be merged automatically when the pipeline succeeds') expect(page).to have_content('to be merged automatically when the pipeline succeeds')
expect(page).to have_link('Cancel') expect(page).to have_button('Cancel auto-merge')
end end
end end
end end
......
...@@ -61,6 +61,7 @@ exports[`Design management design index page renders design index 1`] = ` ...@@ -61,6 +61,7 @@ exports[`Design management design index page renders design index 1`] = `
<participants-stub <participants-stub
class="gl-mb-4" class="gl-mb-4"
lazy="true"
numberoflessparticipants="7" numberoflessparticipants="7"
participants="[object Object]" participants="[object Object]"
/> />
...@@ -221,6 +222,7 @@ exports[`Design management design index page with error GlAlert is rendered in c ...@@ -221,6 +222,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<participants-stub <participants-stub
class="gl-mb-4" class="gl-mb-4"
lazy="true"
numberoflessparticipants="7" numberoflessparticipants="7"
participants="[object Object]" participants="[object Object]"
/> />
......
...@@ -4,8 +4,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have ...@@ -4,8 +4,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<div <div
class="mr-widget-body media" class="mr-widget-body media"
> >
<status-icon-stub <gl-icon-stub
status="success" class="gl-text-blue-500 gl-mr-3 gl-mt-1"
name="status_scheduled"
size="24"
/> />
<div <div
...@@ -17,55 +19,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have ...@@ -17,55 +19,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<span <span
class="gl-mr-3" class="gl-mr-3"
> >
<span <gl-sprintf-stub
class="js-status-text-before-author" data-testid="statusText"
data-testid="beforeStatusText" message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/> />
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span> </span>
<a <gl-button-stub
class="btn btn-sm btn-default js-cancel-auto-merge" buttontextclasses=""
category="primary"
class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button" data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton" data-testid="cancelAutomaticMergeButton"
href="#" icon=""
role="button" size="small"
variant="default"
> >
<!---->
Cancel Cancel auto-merge
</a> </gl-button-stub>
</h4> </h4>
<section <section
class="mr-info-list" class="mr-info-list"
> >
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p <p
class="gl-display-flex" class="gl-display-flex"
> >
...@@ -75,17 +53,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have ...@@ -75,17 +53,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
The source branch will not be deleted The source branch will not be deleted
</span> </span>
<a <gl-button-stub
class="btn btn-sm btn-default js-remove-source-branch" buttontextclasses=""
category="primary"
class="js-remove-source-branch"
data-testid="removeSourceBranchButton" data-testid="removeSourceBranchButton"
href="#" icon=""
role="button" size="small"
variant="default"
> >
<!---->
Delete source branch Delete source branch
</a> </gl-button-stub>
</p> </p>
</section> </section>
</div> </div>
...@@ -96,8 +76,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c ...@@ -96,8 +76,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<div <div
class="mr-widget-body media" class="mr-widget-body media"
> >
<status-icon-stub <gl-icon-stub
status="success" class="gl-text-blue-500 gl-mr-3 gl-mt-1"
name="status_scheduled"
size="24"
/> />
<div <div
...@@ -109,55 +91,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c ...@@ -109,55 +91,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<span <span
class="gl-mr-3" class="gl-mr-3"
> >
<span <gl-sprintf-stub
class="js-status-text-before-author" data-testid="statusText"
data-testid="beforeStatusText" message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
/> />
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span> </span>
<a <gl-button-stub
class="btn btn-sm btn-default js-cancel-auto-merge" buttontextclasses=""
category="primary"
class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button" data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton" data-testid="cancelAutomaticMergeButton"
href="#" icon=""
role="button" size="small"
variant="default"
> >
<!---->
Cancel Cancel auto-merge
</a> </gl-button-stub>
</h4> </h4>
<section <section
class="mr-info-list" class="mr-info-list"
> >
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p <p
class="gl-display-flex" class="gl-display-flex"
> >
...@@ -167,17 +125,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c ...@@ -167,17 +125,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
The source branch will not be deleted The source branch will not be deleted
</span> </span>
<a <gl-button-stub
class="btn btn-sm btn-default js-remove-source-branch" buttontextclasses=""
category="primary"
class="js-remove-source-branch"
data-testid="removeSourceBranchButton" data-testid="removeSourceBranchButton"
href="#" icon=""
role="button" size="small"
variant="default"
> >
<!---->
Delete source branch Delete source branch
</a> </gl-button-stub>
</p> </p>
</section> </section>
</div> </div>
......
...@@ -72,6 +72,8 @@ const defaultMrProps = () => ({ ...@@ -72,6 +72,8 @@ const defaultMrProps = () => ({
autoMergeStrategy: MWPS_MERGE_STRATEGY, autoMergeStrategy: MWPS_MERGE_STRATEGY,
}); });
const getStatusText = () => wrapper.findByTestId('statusText').attributes('message');
describe('MRWidgetAutoMergeEnabled', () => { describe('MRWidgetAutoMergeEnabled', () => {
let oldWindowGl; let oldWindowGl;
...@@ -167,30 +169,6 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -167,30 +169,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
}); });
}); });
describe('statusTextBeforeAuthor', () => {
it('should return "Set by" if the MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.findByTestId('beforeStatusText').text()).toBe('Set by');
});
});
describe('statusTextAfterAuthor', () => {
it('should return "to be merged automatically..." if MWPS is selected', () => {
factory({
...defaultMrProps(),
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
expect(wrapper.findByTestId('afterStatusText').text()).toBe(
'to be merged automatically when the pipeline succeeds',
);
});
});
describe('cancelButtonText', () => { describe('cancelButtonText', () => {
it('should return "Cancel" if MWPS is selected', () => { it('should return "Cancel" if MWPS is selected', () => {
factory({ factory({
...@@ -198,7 +176,9 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -198,7 +176,9 @@ describe('MRWidgetAutoMergeEnabled', () => {
autoMergeStrategy: MWPS_MERGE_STRATEGY, autoMergeStrategy: MWPS_MERGE_STRATEGY,
}); });
expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe('Cancel'); expect(wrapper.findByTestId('cancelAutomaticMergeButton').text()).toBe(
'Cancel auto-merge',
);
}); });
}); });
}); });
...@@ -279,7 +259,7 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -279,7 +259,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
await nextTick(); await nextTick();
expect(wrapper.find('.js-cancel-auto-merge').attributes('disabled')).toBe('disabled'); expect(wrapper.find('.js-cancel-auto-merge').props('loading')).toBe(true);
}); });
it('should show source branch will be deleted text when it source branch set to remove', () => { it('should show source branch will be deleted text when it source branch set to remove', () => {
...@@ -313,7 +293,7 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -313,7 +293,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
await nextTick(); await nextTick();
expect(wrapper.find('.js-remove-source-branch').attributes('disabled')).toBe('disabled'); expect(wrapper.find('.js-remove-source-branch').props('loading')).toBe(true);
}); });
it('should render the status text as "...to merged automatically" if MWPS is selected', () => { it('should render the status text as "...to merged automatically" if MWPS is selected', () => {
...@@ -322,9 +302,9 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -322,9 +302,9 @@ describe('MRWidgetAutoMergeEnabled', () => {
autoMergeStrategy: MWPS_MERGE_STRATEGY, autoMergeStrategy: MWPS_MERGE_STRATEGY,
}); });
const statusText = trimText(wrapper.find('.js-status-text-after-author').text()); expect(getStatusText()).toBe(
'Set by %{merge_author} to be merged automatically when the pipeline succeeds',
expect(statusText).toBe('to be merged automatically when the pipeline succeeds'); );
}); });
it('should render the cancel button as "Cancel" if MWPS is selected', () => { it('should render the cancel button as "Cancel" if MWPS is selected', () => {
...@@ -335,7 +315,7 @@ describe('MRWidgetAutoMergeEnabled', () => { ...@@ -335,7 +315,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text()); const cancelButtonText = trimText(wrapper.find('.js-cancel-auto-merge').text());
expect(cancelButtonText).toBe('Cancel'); expect(cancelButtonText).toBe('Cancel auto-merge');
}); });
}); });
}); });
......
...@@ -41,6 +41,31 @@ RSpec.describe Gitlab::UsageDataMetrics do ...@@ -41,6 +41,31 @@ RSpec.describe Gitlab::UsageDataMetrics do
]) ])
end end
it 'includes incident_management_alerts monthly and weekly keys' do
expect(subject[:redis_hll_counters][:incident_management_alerts].keys).to contain_exactly(*[
:incident_management_alert_create_incident_monthly, :incident_management_alert_create_incident_weekly
])
end
it 'includes incident_management monthly and weekly keys' do
expect(subject[:redis_hll_counters][:incident_management]).to include(
:incident_management_incident_created_monthly, :incident_management_incident_created_weekly,
:incident_management_incident_reopened_monthly, :incident_management_incident_reopened_weekly,
:incident_management_incident_closed_monthly, :incident_management_incident_closed_weekly,
:incident_management_incident_assigned_monthly, :incident_management_incident_assigned_weekly,
:incident_management_incident_todo_monthly, :incident_management_incident_todo_weekly,
:incident_management_incident_comment_monthly, :incident_management_incident_comment_weekly,
:incident_management_incident_zoom_meeting_monthly, :incident_management_incident_zoom_meeting_weekly,
:incident_management_incident_relate_monthly, :incident_management_incident_relate_weekly,
:incident_management_incident_unrelate_monthly, :incident_management_incident_unrelate_weekly,
:incident_management_incident_change_confidential_monthly, :incident_management_incident_change_confidential_weekly,
:incident_management_alert_status_changed_monthly, :incident_management_alert_status_changed_weekly,
:incident_management_alert_assigned_monthly, :incident_management_alert_assigned_weekly,
:incident_management_alert_todo_monthly, :incident_management_alert_todo_weekly,
:incident_management_total_unique_counts_monthly, :incident_management_total_unique_counts_weekly
)
end
it 'includes source_code monthly and weekly keys' do it 'includes source_code monthly and weekly keys' do
expect(subject[:redis_hll_counters][:source_code].keys).to contain_exactly(*[ expect(subject[:redis_hll_counters][:source_code].keys).to contain_exactly(*[
:wiki_action_monthly, :wiki_action_weekly, :wiki_action_monthly, :wiki_action_weekly,
......
...@@ -60,4 +60,71 @@ RSpec.describe Postgresql::ReplicationSlot do ...@@ -60,4 +60,71 @@ RSpec.describe Postgresql::ReplicationSlot do
expect(described_class.lag_too_great?).to eq(false) expect(described_class.lag_too_great?).to eq(false)
end end
end end
describe '#max_replication_slots' do
it 'returns the maximum number of replication slots' do
expect(described_class.max_replication_slots).to be >= 0
end
end
context 'with enough slots available' do
skip_examples = described_class.max_replication_slots <= described_class.count
before(:all) do
skip('max_replication_slots too small') if skip_examples
@current_slot_count = ApplicationRecord
.connection
.execute("SELECT COUNT(*) FROM pg_replication_slots;")
.first
.fetch('count')
.to_i
@current_unused_count = ApplicationRecord
.connection
.execute("SELECT COUNT(*) FROM pg_replication_slots WHERE active = 'f';")
.first
.fetch('count')
.to_i
ApplicationRecord
.connection
.execute("SELECT * FROM pg_create_physical_replication_slot('test_slot');")
end
after(:all) do
unless skip_examples
ApplicationRecord
.connection
.execute("SELECT pg_drop_replication_slot('test_slot');")
end
end
describe '#slots_count' do
it 'returns the number of replication slots' do
expect(described_class.count).to eq(@current_slot_count + 1)
end
end
describe '#unused_slots_count' do
it 'returns the number of unused replication slots' do
expect(described_class.unused_slots_count).to eq(@current_unused_count + 1)
end
end
describe '#max_retained_wal' do
it 'returns the retained WAL size' do
expect(described_class.max_retained_wal).not_to be_nil
end
end
describe '#slots_retained_bytes' do
it 'returns the number of retained bytes' do
slot = described_class.slots_retained_bytes.find {|x| x['slot_name'] == 'test_slot' }
expect(slot).not_to be_nil
expect(slot['retained_bytes']).to be_nil
end
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment