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 {
v-bind="$attrs"
:open="isSidebarOpen"
class="boards-sidebar gl-absolute"
variant="sidebar"
@close="handleClose"
>
<template #title>
......
......@@ -87,7 +87,7 @@ export default {
<div>
<header
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">
<slot name="title">
......@@ -97,7 +97,8 @@ export default {
</span>
<gl-button
v-if="canUpdate"
variant="link"
category="tertiary"
size="small"
class="gl-text-gray-900! gl-ml-5 js-sidebar-dropdown-toggle edit-link"
data-testid="edit-button"
@click="toggle"
......
......@@ -50,7 +50,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
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="#"
data-test-id="edit-link"
data-track-event="click_edit_button"
......
......@@ -90,7 +90,7 @@ export default {
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<a
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="#"
data-testid="edit-link"
data-track-event="click_edit_button"
......
......@@ -33,6 +33,11 @@ export default {
required: false,
default: true,
},
lazy: {
type: Boolean,
required: false,
default: true,
},
},
data() {
return {
......@@ -95,7 +100,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</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" />
{{ participantLabel }}
</div>
......@@ -107,7 +112,7 @@ export default {
>
<a :href="participant.web_url || participant.webUrl" class="author-link">
<user-avatar-image
:lazy="true"
:lazy="lazy"
:img-src="participant.avatar_url || participant.avatarUrl"
:size="24"
:tooltip-text="participant.name"
......
......@@ -64,6 +64,7 @@ export default {
:loading="isLoading"
:participants="participants"
:number-of-less-participants="7"
:lazy="false"
class="block participants"
/>
</template>
......@@ -38,7 +38,7 @@ export default {
<gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
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="#"
data-track-event="click_edit_button"
data-track-label="right_sidebar"
......
......@@ -132,8 +132,9 @@ export default {
<slot name="collapsed-right"></slot>
<gl-button
v-if="canUpdate && !initialLoading && canEdit"
variant="link"
class="gl-text-gray-900! gl-hover-text-blue-800! gl-ml-auto hide-collapsed"
category="tertiary"
size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2"
data-testid="edit-button"
:data-track-event="tracking.event"
:data-track-label="tracking.label"
......
......@@ -28,7 +28,12 @@ export default {
};
</script>
<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" />
<span v-if="showAuthorName" class="author">{{ author.name }}</span>
</a>
......
<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 autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
import createFlash from '~/flash';
......@@ -10,7 +10,6 @@ import { AUTO_MERGE_STRATEGIES } from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import MrWidgetAuthor from '../mr_widget_author.vue';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetAutoMergeEnabled',
......@@ -28,9 +27,10 @@ export default {
},
components: {
MrWidgetAuthor,
statusIcon,
GlLoadingIcon,
GlSkeletonLoader,
GlIcon,
GlButton,
GlSprintf,
},
mixins: [autoMergeMixin, glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
......@@ -155,54 +155,44 @@ export default {
</gl-skeleton-loader>
</div>
<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">
<h4 class="gl-display-flex">
<span class="gl-mr-3">
<span class="js-status-text-before-author" data-testid="beforeStatusText">{{
statusTextBeforeAuthor
}}</span>
<gl-sprintf :message="statusText" data-testid="statusText">
<template #merge_author>
<mr-widget-author :author="mergeUser" />
<span class="js-status-text-after-author" data-testid="afterStatusText">{{
statusTextAfterAuthor
}}</span>
</template>
</gl-sprintf>
</span>
<a
<gl-button
v-if="mr.canCancelAutomaticMerge"
:disabled="isCancellingAutoMerge"
role="button"
href="#"
class="btn btn-sm btn-default js-cancel-auto-merge"
:loading="isCancellingAutoMerge"
size="small"
class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
@click="cancelAutomaticMerge"
>
<gl-loading-icon v-if="isCancellingAutoMerge" size="sm" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</gl-button>
</h4>
<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">
{{ s__('mrWidget|The source branch will be deleted') }}
</p>
<p v-else class="gl-display-flex">
<span class="gl-mr-3">{{ s__('mrWidget|The source branch will not be deleted') }}</span>
<a
<gl-button
v-if="canRemoveSourceBranch"
:disabled="isRemovingSourceBranch"
role="button"
class="btn btn-sm btn-default js-remove-source-branch"
href="#"
:loading="isRemovingSourceBranch"
size="small"
class="js-remove-source-branch"
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') }}
</a>
</gl-button>
</p>
</section>
</div>
......
......@@ -2,14 +2,13 @@ import { s__ } from '~/locale';
export default {
computed: {
statusTextBeforeAuthor() {
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
statusText() {
return s__(
'mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds',
);
},
cancelButtonText() {
return s__('mrWidget|Cancel');
return s__('mrWidget|Cancel auto-merge');
},
},
};
......@@ -23,8 +23,8 @@ export default function deviseState() {
return stateKey.pipelineBlocked;
} else if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
} else if (this.autoMergeEnabled) {
return this.mergeError ? stateKey.autoMergeFailed : stateKey.autoMergeEnabled;
} else if (this.autoMergeEnabled && !this.mergeError) {
return stateKey.autoMergeEnabled;
} else if (!this.canMerge) {
return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
......
......@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
variant="link"
class="float-right gl-text-gray-900! gl-hover-text-blue-800! js-sidebar-dropdown-toggle"
category="tertiary"
size="small"
class="float-right gl-text-gray-900! js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>
......
......@@ -28,8 +28,9 @@ export default {
<template v-if="allowLabelEdit">
<gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
variant="link"
class="float-right js-sidebar-dropdown-toggle"
category="tertiary"
size="small"
class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>{{ __('Edit') }}</gl-button
......
......@@ -237,3 +237,7 @@
@include side-panel-toggle;
border-bottom: 1px solid $border-color;
}
.edit-link {
margin-right: -$gl-spacing-scale-2;
}
......@@ -476,6 +476,10 @@
.gl-drawer-header {
align-items: flex-start;
}
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
width: auto;
}
}
.board-header-collapsed-info-icon:hover {
......
......@@ -197,6 +197,10 @@
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
$gl-line-height-42: px-to-rem(42px);
......
......@@ -39,5 +39,55 @@ module Postgresql
false
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
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_status_changed
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_assigned
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_todo
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_reopened
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_closed
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_assigned
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_todo
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_comment
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_zoom_meeting
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_relate
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_unrelate
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_change_confidential
distribution:
- ce
- ee
......
......@@ -10,6 +10,23 @@ value_type: number
status: data_available
time_frame: 28d
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:
- ce
- ee
......
......@@ -11,6 +11,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_create_incident
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_status_changed
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_assigned
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_todo
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_created
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_reopened
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_closed
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_assigned
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_todo
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_comment
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_zoom_meeting
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_relate
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_unrelate
distribution:
- ce
- ee
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_change_confidential
distribution:
- ce
- ee
......
......@@ -10,6 +10,23 @@ value_type: number
status: data_available
time_frame: 7d
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:
- ce
- ee
......
......@@ -11,6 +11,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_alert_create_incident
distribution:
- ce
- ee
......
......@@ -3407,6 +3407,27 @@ Input type: `ProjectSetComplianceFrameworkInput`
| <a id="mutationprojectsetcomplianceframeworkerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the 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`
Input type: `PrometheusIntegrationCreateInput`
......
......@@ -216,7 +216,7 @@ To set up the GitLab external URL:
1. To prevent the domain name from
[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
sudo mv /opt/bitnami/apps/gitlab/bnconfig /opt/bitnami/apps/gitlab/bnconfig.bak
......
......@@ -55,6 +55,7 @@ export default {
v-bind="$attrs"
class="boards-sidebar gl-absolute"
:open="isSidebarOpen"
variant="sidebar"
@close="handleClose"
>
<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 {
<template>
<div :class="blockClass" class="block date">
<collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" />
<div class="title">
<div class="title gl-mb-2">
{{ label }}
<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
ref="epicDatePopover"
name="question-o"
......@@ -181,8 +181,9 @@ export default {
<gl-button
v-show="canUpdate && !editing"
ref="editButton"
variant="link"
class="btn-sidebar-action"
category="tertiary"
size="small"
class="btn-sidebar-action gl-mr-n2"
@click="startEditing"
>
{{ __('Edit') }}
......
import ciMinutesUsage from 'ee/ci_minutes_usage';
import otherStorageCounter from 'ee/other_storage_counter';
import storageCounter from 'ee/storage_counter';
import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs';
......@@ -23,3 +24,5 @@ if (document.querySelector('#js-other-storage-counter-app')) {
hashedTabs: true,
});
}
ciMinutesUsage();
......@@ -73,7 +73,7 @@ export default {
<gl-tooltip :target="() => $refs.sidebarIcon" placement="left" boundary="viewport">
<span v-html="tooltipText"></span>
</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">
<li v-for="(ancestor, id) in ancestors" :key="id" class="vertical-timeline-row d-flex">
......@@ -87,7 +87,7 @@ export default {
</ul>
<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>
<gl-loading-icon v-if="isFetching" size="sm" />
......
......@@ -165,8 +165,9 @@ export default {
>
<gl-button
ref="editButton"
variant="link"
class="edit-link btn-link-hover gl-text-gray-900! gl-hover-text-blue-800!"
category="tertiary"
size="small"
class="edit-link btn-link-hover gl-text-gray-900!"
:disabled="!isOpen"
@click.stop="toggleFormDropdown"
@keydown.esc="hideDropdown"
......
......@@ -162,7 +162,7 @@ export default {
/>
<a
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"
href="#"
@click.prevent="onEditClick(!shouldShowEditField)"
......
import { s__ } from '~/locale';
import {
MT_MERGE_STRATEGY,
MTWPS_MERGE_STRATEGY,
MWPS_MERGE_STRATEGY,
} from '~/vue_merge_request_widget/constants';
import { MT_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants';
export default {
computed: {
statusTextBeforeAuthor() {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Added to the merge train by');
}
return s__('mrWidget|Set by');
},
statusTextAfterAuthor() {
statusText() {
const { mergeTrainsCount } = this.glFeatures.mergeRequestWidgetGraphql ? this.state : this.mr;
if (this.autoMergeStrategy === MTWPS_MERGE_STRATEGY && mergeTrainsCount === 0) {
return s__('mrWidget|to start a merge train when the pipeline succeeds');
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
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) {
return s__('mrWidget|to be added to the merge train when the pipeline succeeds');
} else if (this.autoMergeStrategy === MWPS_MERGE_STRATEGY) {
return s__('mrWidget|to be merged automatically when the pipeline succeeds');
return s__(
'mrWidget|Set by %{merge_author} to be added to the merge train when the pipeline succeeds',
);
}
return '';
return s__(
'mrWidget|Set by %{merge_author} to be merged automatically when the pipeline succeeds',
);
},
cancelButtonText() {
if (this.autoMergeStrategy === MT_MERGE_STRATEGY) {
return s__('mrWidget|Remove from merge train');
}
return s__('mrWidget|Cancel');
return s__('mrWidget|Cancel auto-merge');
},
},
};
......@@ -25,7 +25,7 @@
}
}
.labels-select-dropdown-contents {
.labels-select-wrapper.is-embedded {
@include gl-left-0;
@include gl-shadow-x0-y2-b4-s0;
......
......@@ -23,6 +23,7 @@ module EE
mount_mutation ::Mutations::Epics::SetSubscription
mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::GitlabSubscriptions::Activate
mount_mutation ::Mutations::Projects::SetLocked
mount_mutation ::Mutations::Iterations::Create
mount_mutation ::Mutations::Iterations::Update
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
def replication_slots_count
return unless primary?
PgReplicationSlot.count
Postgresql::ReplicationSlot.count
end
def replication_slots_used_count
return unless primary?
PgReplicationSlot.used_slots_count
Postgresql::ReplicationSlot.used_slots_count
end
def replication_slots_max_retained_wal_bytes
return unless primary?
PgReplicationSlot.max_retained_wal
Postgresql::ReplicationSlot.max_retained_wal
end
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 @@
= render 'namespaces/pipelines_quota/extra_shared_runners_minutes_quota', namespace: namespace
.js-ci-minutes-usage
%table.table.pipeline-project-metrics
%thead
%tr
......
......@@ -17,7 +17,7 @@ module Geo
def perform
return if Gitlab::Database.read_only?
return unless Gitlab::Database.main.healthy?
return if Postgresql::ReplicationSlot.lag_too_great?
unless ::GeoNode.secondary_nodes.any?
Geo::PruneEventLogService.new(:all).execute
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_published
distribution:
- ee
tier:
......
......@@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606
time_frame: 28d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_incident_management_oncall_notification_sent
distribution:
- ee
tier:
......
......@@ -10,6 +10,10 @@ value_type: number
status: data_available
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- incident_management_incident_published
distribution:
- ee
tier:
......
......@@ -12,6 +12,10 @@ milestone: "13.11"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/58606
time_frame: 7d
data_source: redis_hll
instrumentation_class: RedisHLLMetric
options:
events:
- i_incident_management_oncall_notification_sent
distribution:
- ee
tier:
......
......@@ -6,10 +6,6 @@ module EE
module Connection
extend ActiveSupport::Concern
def healthy?
!Postgresql::ReplicationSlot.lag_too_great?
end
def geo_uncached_queries(&block)
raise 'No block given' unless block_given?
......
......@@ -58,8 +58,8 @@ RSpec.describe 'User adds a merge request to a merge train', :js do
within('.mr-widget-section') do
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_link('Remove from merge train')
expect(page).to have_link('Delete source branch')
expect(page).to have_button('Remove from merge train')
expect(page).to have_button('Delete source branch')
end
end
......@@ -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
before do
click_link 'Remove from merge train'
click_button 'Remove from merge train'
end
it 'cancels automatic merge' 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
before do
click_link 'Delete source branch'
click_button 'Delete source branch'
end
it 'updates the merge option' do
......
......@@ -59,14 +59,14 @@ RSpec.describe 'User adds to merge train when pipeline succeeds', :js 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('The source branch will not be deleted')
expect(page).to have_link('Cancel')
expect(page).to have_link('Delete source branch')
expect(page).to have_button('Cancel auto-merge')
expect(page).to have_button('Delete source branch')
end
end
context "when user clicks 'Cancel' button" do
before do
click_link 'Cancel'
click_button 'Cancel auto-merge'
end
it 'cancels automatic merge' 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
before do
click_link 'Delete source branch'
click_button 'Delete source branch'
end
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', () => {
poll: () => {},
};
const getStatusText = () => wrapper.find('[data-testid="statusText"]').attributes('message');
const mr = {
shouldRemoveSourceBranch: false,
canRemoveSourceBranch: true,
......@@ -45,34 +47,16 @@ describe('MRWidgetAutoMergeEnabled', () => {
});
describe('computed', () => {
describe('statusTextBeforeAuthor', () => {
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', () => {
describe('status', () => {
it('should return "to start a merge train..." if MTWPS is selected and there is no existing merge train', () => {
factory({
autoMergeStrategy: MTWPS_MERGE_STRATEGY,
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', () => {
......@@ -81,16 +65,16 @@ describe('MRWidgetAutoMergeEnabled', () => {
mergeTrainsCount: 1,
});
expect(vm.statusTextAfterAuthor).toBe(
'to be added to the merge train when the pipeline succeeds',
expect(getStatusText()).toBe(
'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', () => {
factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY });
expect(vm.statusTextAfterAuthor).toBe(
'to be merged automatically when the pipeline succeeds',
expect(getStatusText()).toBe(
'Set by %{merge_author} to be merged automatically when the pipeline succeeds',
);
});
});
......@@ -99,7 +83,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
it('should return "Cancel start merge train" if MTWPS is selected', () => {
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', () => {
......@@ -111,40 +95,18 @@ describe('MRWidgetAutoMergeEnabled', () => {
it('should return "Cancel" if MWPS is selected', () => {
factory({ autoMergeStrategy: MWPS_MERGE_STRATEGY });
expect(vm.cancelButtonText).toBe('Cancel');
expect(vm.cancelButtonText).toBe('Cancel auto-merge');
});
});
});
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', () => {
factory({ autoMergeStrategy: MTWPS_MERGE_STRATEGY });
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
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
context 'when no block is given' do
it 'raises error' do
......
......@@ -22,6 +22,18 @@ RSpec.describe Gitlab::UsageDataMetrics do
expect(subject).to include(:license_subscription_id)
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
expect(subject[:redis_hll_counters][:compliance].keys).to contain_exactly(*[
: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
end
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)
worker.perform
......
......@@ -35635,6 +35635,12 @@ msgstr ""
msgid "UsageQuota|Buy additional minutes"
msgstr ""
msgid "UsageQuota|CI minutes usage by month"
msgstr ""
msgid "UsageQuota|CI minutes usage by project"
msgstr ""
msgid "UsageQuota|Current period usage"
msgstr ""
......@@ -39337,7 +39343,7 @@ msgstr ""
msgid "mrWidget|A new merge train has started and this merge request is the first of the queue."
msgstr ""
msgid "mrWidget|Added to the merge train by"
msgid "mrWidget|Added to the merge train by %{merge_author}"
msgstr ""
msgid "mrWidget|Added to the merge train. There are %{mergeTrainPosition} merge requests waiting to be merged"
......@@ -39370,7 +39376,7 @@ msgstr ""
msgid "mrWidget|Are you adding technical debt or code vulnerabilities?"
msgstr ""
msgid "mrWidget|Cancel"
msgid "mrWidget|Cancel auto-merge"
msgstr ""
msgid "mrWidget|Check out branch"
......@@ -39517,7 +39523,13 @@ msgstr ""
msgid "mrWidget|Revoke approval"
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 ""
msgid "mrWidget|The changes were merged into"
......@@ -39589,15 +39601,6 @@ msgstr ""
msgid "mrWidget|into"
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"
msgstr ""
......
......@@ -64,7 +64,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
context 'when enabled after it was previously canceled' do
before do
click_button "Merge when pipeline succeeds"
click_link "Cancel"
click_button "Cancel auto-merge"
wait_for_requests
......@@ -87,7 +87,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
before do
merge_request.merge_params['force_remove_source_branch'] = '0'
merge_request.save!
click_link "Cancel"
click_button "Cancel auto-merge"
end
it_behaves_like 'Merge when pipeline succeeds activator'
......@@ -114,7 +114,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
end
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"
......@@ -124,7 +124,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do
end
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"
end
......
......@@ -151,7 +151,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
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
......@@ -178,7 +178,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'waits the head pipeline' do
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
......@@ -377,7 +377,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
context 'when detached merge request pipeline is pending' do
it 'waits the head pipeline' do
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
......@@ -403,7 +403,7 @@ RSpec.describe 'Merge request > User sees pipelines triggered by merge request',
it 'waits the head pipeline' do
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
......
......@@ -61,6 +61,7 @@ exports[`Design management design index page renders design index 1`] = `
<participants-stub
class="gl-mb-4"
lazy="true"
numberoflessparticipants="7"
participants="[object Object]"
/>
......@@ -221,6 +222,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<participants-stub
class="gl-mb-4"
lazy="true"
numberoflessparticipants="7"
participants="[object Object]"
/>
......
......@@ -4,8 +4,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
<gl-icon-stub
class="gl-text-blue-500 gl-mr-3 gl-mt-1"
name="status_scheduled"
size="24"
/>
<div
......@@ -17,55 +19,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
<gl-sprintf-stub
data-testid="statusText"
message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
icon=""
size="small"
variant="default"
>
<!---->
Cancel
Cancel auto-merge
</a>
</gl-button-stub>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
......@@ -75,17 +53,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is disabled template should have
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
icon=""
size="small"
variant="default"
>
<!---->
Delete source branch
</a>
</gl-button-stub>
</p>
</section>
</div>
......@@ -96,8 +76,10 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<div
class="mr-widget-body media"
>
<status-icon-stub
status="success"
<gl-icon-stub
class="gl-text-blue-500 gl-mr-3 gl-mt-1"
name="status_scheduled"
size="24"
/>
<div
......@@ -109,55 +91,31 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
<span
class="gl-mr-3"
>
<span
class="js-status-text-before-author"
data-testid="beforeStatusText"
>
Set by
</span>
<mr-widget-author-stub
author="[object Object]"
showauthorname="true"
<gl-sprintf-stub
data-testid="statusText"
message="Set by %{merge_author} to be merged automatically when the pipeline succeeds"
/>
<span
class="js-status-text-after-author"
data-testid="afterStatusText"
>
to be merged automatically when the pipeline succeeds
</span>
</span>
<a
class="btn btn-sm btn-default js-cancel-auto-merge"
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-cancel-auto-merge"
data-qa-selector="cancel_auto_merge_button"
data-testid="cancelAutomaticMergeButton"
href="#"
role="button"
icon=""
size="small"
variant="default"
>
<!---->
Cancel
Cancel auto-merge
</a>
</gl-button-stub>
</h4>
<section
class="mr-info-list"
>
<p>
The changes will be merged into
<a
class="label-branch"
href="/foo/bar"
>
foo
</a>
</p>
<p
class="gl-display-flex"
>
......@@ -167,17 +125,19 @@ exports[`MRWidgetAutoMergeEnabled when graphql is enabled template should have c
The source branch will not be deleted
</span>
<a
class="btn btn-sm btn-default js-remove-source-branch"
<gl-button-stub
buttontextclasses=""
category="primary"
class="js-remove-source-branch"
data-testid="removeSourceBranchButton"
href="#"
role="button"
icon=""
size="small"
variant="default"
>
<!---->
Delete source branch
</a>
</gl-button-stub>
</p>
</section>
</div>
......
......@@ -72,6 +72,8 @@ const defaultMrProps = () => ({
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
const getStatusText = () => wrapper.findByTestId('statusText').attributes('message');
describe('MRWidgetAutoMergeEnabled', () => {
let oldWindowGl;
......@@ -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', () => {
it('should return "Cancel" if MWPS is selected', () => {
factory({
......@@ -198,7 +176,9 @@ describe('MRWidgetAutoMergeEnabled', () => {
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', () => {
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', () => {
......@@ -313,7 +293,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
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', () => {
......@@ -322,9 +302,9 @@ describe('MRWidgetAutoMergeEnabled', () => {
autoMergeStrategy: MWPS_MERGE_STRATEGY,
});
const statusText = trimText(wrapper.find('.js-status-text-after-author').text());
expect(statusText).toBe('to be merged automatically when the pipeline succeeds');
expect(getStatusText()).toBe(
'Set by %{merge_author} to be merged automatically when the pipeline succeeds',
);
});
it('should render the cancel button as "Cancel" if MWPS is selected', () => {
......@@ -335,7 +315,7 @@ describe('MRWidgetAutoMergeEnabled', () => {
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
])
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
expect(subject[:redis_hll_counters][:source_code].keys).to contain_exactly(*[
:wiki_action_monthly, :wiki_action_weekly,
......
......@@ -60,4 +60,71 @@ RSpec.describe Postgresql::ReplicationSlot do
expect(described_class.lag_too_great?).to eq(false)
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
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