Commit 2fcac2b2 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents f2182208 3ac72085
......@@ -47,6 +47,7 @@ module MergeRequests
handle_draft_status_change(merge_request, changed_fields)
track_title_and_desc_edits(changed_fields)
track_discussion_lock_toggle(merge_request, changed_fields)
notify_if_labels_added(merge_request, old_labels)
notify_if_mentions_added(merge_request, old_mentioned_users)
......@@ -95,6 +96,16 @@ module MergeRequests
end
end
def track_discussion_lock_toggle(merge_request, changed_fields)
return unless changed_fields.include?('discussion_locked')
if merge_request.discussion_locked
merge_request_activity_counter.track_discussion_locked_action(user: current_user)
else
merge_request_activity_counter.track_discussion_unlocked_action(user: current_user)
end
end
def notify_if_labels_added(merge_request, old_labels)
added_labels = merge_request.labels - old_labels
......
---
title: Track usage pings when MR gets locked/unlocked
merge_request: 55069
author:
type: other
---
title: Expose container_registry_image_prefix to project API
merge_request: 54090
author: Mathieu Parent
type: added
---
name: usage_data_i_code_review_user_mr_discussion_locked
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true
---
name: usage_data_i_code_review_user_mr_discussion_unlocked
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
rollout_issue_url:
milestone: '13.10'
type: development
group: group::code review
default_enabled: true
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly
description: Count of unique users per month who locked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly
description: Count of unique users per month who unlocked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 28d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly
description: Count of unique users per week who unlocked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
---
key_path: redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly
description: Count of unique users per week who locked a MR
product_section: dev
product_stage: create
product_group: group::code review
product_category: code_review
value_type: number
status: implemented
milestone: "13.10"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069
time_frame: 7d
data_source: redis_hll
distribution:
- ce
- ee
tier:
- free
- premium
- ultimate
......@@ -179,6 +179,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -284,6 +285,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -439,6 +441,7 @@ GET /users/:user_id/projects
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -544,6 +547,7 @@ GET /users/:user_id/projects
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -658,6 +662,7 @@ Example response:
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -758,6 +763,7 @@ Example response:
"lfs_objects_size": 0,
"job_artifacts_size": 0
},
"container_registry_image_prefix": "registry.example.com/brightbox/puppet",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -921,6 +927,7 @@ GET /projects/:id
"packages_size": 0,
"snippets_size": 0
},
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-client",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1373,6 +1380,7 @@ Example responses:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1467,6 +1475,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1559,6 +1568,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1745,6 +1755,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1858,6 +1869,7 @@ Example response:
"merge_method": "merge",
"autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"container_registry_image_prefix": "registry.example.com/diaspora/diaspora-project-site",
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -2354,6 +2366,7 @@ Example response:
"avatar_url": null,
"web_url": "https://gitlab.example.com/groups/cute-cats"
},
"container_registry_image_prefix": "registry.example.com/cute-cats/hello-world",
"_links": {
"self": "https://gitlab.example.com/api/v4/projects/7",
"issues": "https://gitlab.example.com/api/v4/projects/7/issues",
......
......@@ -13384,6 +13384,86 @@ Count of unique users per week|month who merged a MR
| `tier` | |
| `skip_validation` | true |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly`
Count of unique users per month who locked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly`
Count of unique users per week who locked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_locked_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly`
Count of unique users per month who unlocked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_monthly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 28d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly`
Count of unique users per week who unlocked a MR
| field | value |
| --- | --- |
| `key_path` | **`redis_hll_counters.code_review.i_code_review_user_mr_discussion_unlocked_weekly`** |
| `product_section` | dev |
| `product_stage` | create |
| `product_group` | `group::code review` |
| `product_category` | `code_review` |
| `value_type` | number |
| `status` | implemented |
| `milestone` | 13.10 |
| `introduced_by_url` | [Introduced by](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55069) |
| `time_frame` | 7d |
| `data_source` | Redis_hll |
| `distribution` | ce, ee |
| `tier` | free, premium, ultimate |
## `redis_hll_counters.code_review.i_code_review_user_publish_review_monthly`
Missing description
......
......@@ -40,9 +40,10 @@ export const i18n = {
title: __('Starts on'),
error: s__('OnCallSchedules|Rotation start date cannot be empty'),
},
endsOn: {
endsAt: {
enableToggle: s__('OnCallSchedules|Enable end date'),
title: __('Ends on'),
error: s__('OnCallSchedules|Rotation end date/time must come after start date/time'),
},
restrictToTime: {
enableToggle: s__('OnCallSchedules|Restrict to time intervals'),
......@@ -234,7 +235,7 @@ export default {
<div class="gl-display-inline-block">
<gl-toggle
v-model="endDateEnabled"
:label="$options.i18n.fields.endsOn.enableToggle"
:label="$options.i18n.fields.endsAt.enableToggle"
label-position="left"
class="gl-mb-5"
/>
......@@ -245,28 +246,43 @@ export default {
class="gl-border-gray-400 gl-bg-gray-10"
>
<gl-form-group
:label="$options.i18n.fields.endsOn.title"
:label="$options.i18n.fields.endsAt.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
:state="validationState.endsAt"
:invalid-feedback="$options.i18n.fields.endsAt.error"
class="gl-mb-0"
>
<div class="gl-display-flex gl-align-items-center">
<gl-datepicker
class="gl-mr-3"
@input="$emit('update-rotation-form', { type: 'endsOn.date', value: $event })"
/>
@input="$emit('update-rotation-form', { type: 'endsAt.date', value: $event })"
>
<template #default="{ formattedDate }">
<gl-form-input
class="gl-w-full"
:value="formattedDate"
:placeholder="__(`YYYY-MM-DD`)"
@blur="
$emit('update-rotation-form', {
type: 'endsAt.date',
value: $event.target.value,
})
"
/>
</template>
</gl-datepicker>
<span> {{ __('at') }} </span>
<gl-dropdown
data-testid="rotation-end-time"
:text="format24HourTimeStringFromInt(form.endsOn.time)"
:text="format24HourTimeStringFromInt(form.endsAt.time)"
class="gl-px-3"
>
<gl-dropdown-item
v-for="time in $options.HOURS_IN_DAY"
:key="time"
:is-checked="form.endsOn.time === time"
:is-checked="form.endsAt.time === time"
is-check-item
@click="$emit('update-rotation-form', { type: 'endsOn.time', value: time })"
@click="$emit('update-rotation-form', { type: 'endsAt.time', value: time })"
>
<span class="gl-white-space-nowrap">
{{ format24HourTimeStringFromInt(time) }}</span
......@@ -294,7 +310,7 @@ export default {
<gl-form-group
:label="$options.i18n.fields.restrictToTime.title"
label-size="sm"
:invalid-feedback="$options.i18n.fields.endsOn.error"
:invalid-feedback="$options.i18n.fields.endsAt.error"
class="gl-mb-0"
>
<div class="gl-display-flex gl-align-items-center">
......
......@@ -78,7 +78,7 @@ export default {
date: null,
time: 0,
},
endsOn: {
endsAt: {
date: null,
time: 0,
},
......@@ -92,6 +92,7 @@ export default {
name: true,
participants: true,
startsAt: true,
endsAt: true,
},
};
},
......@@ -129,7 +130,8 @@ export default {
name,
rotationLength,
participants,
startsAt: { date, time },
startsAt: { date: startDate, time: startTime },
endsAt: { date: endDate, time: endTime },
} = this.form;
return {
......@@ -137,9 +139,15 @@ export default {
scheduleIid: this.schedule.iid,
name,
startsAt: {
date: formatDate(date, 'yyyy-mm-dd'),
time: format24HourTimeStringFromInt(time),
date: formatDate(startDate, 'yyyy-mm-dd'),
time: format24HourTimeStringFromInt(startTime),
},
endsAt: endDate
? {
date: formatDate(endDate, 'yyyy-mm-dd'),
time: format24HourTimeStringFromInt(endTime),
}
: null,
rotationLength: {
...rotationLength,
length: parseInt(rotationLength.length, 10),
......@@ -150,6 +158,20 @@ export default {
title() {
return this.isEditMode ? this.$options.i18n.editRotation : this.$options.i18n.addRotation;
},
isEndDateValid() {
const startsAt = this.form.startsAt.date?.getTime();
const endsAt = this.form.endsAt.date?.getTime();
if (!startsAt || !endsAt) {
// If start or end is not present, we consider the end date valid
return true;
} else if (startsAt < endsAt) {
return true;
} else if (startsAt === endsAt) {
return this.form.startsAt.time < this.form.endsAt.time;
}
return false;
},
},
methods: {
createRotation() {
......@@ -244,8 +266,11 @@ export default {
this.validationState.name = isNameFieldValid(this.form.name);
} else if (key === 'participants') {
this.validationState.participants = this.form.participants.length > 0;
} else if (key === 'startsAt.date') {
} else if (key === 'startsAt.date' || key === 'startsAt.time') {
this.validationState.startsAt = Boolean(this.form.startsAt.date);
this.validationState.endsAt = this.isEndDateValid;
} else if (key === 'endsAt.date' || key === 'endsAt.time') {
this.validationState.endsAt = this.isEndDateValid;
}
},
},
......
......@@ -4,6 +4,7 @@ fragment OnCallRotation on IncidentManagementOncallRotation {
id
name
startsAt
endsAt
length
lengthUnit
participants {
......
......@@ -171,7 +171,13 @@ export default {
</gl-sprintf>
</gl-alert>
<gl-table ref="securityControlTable" :items="features" :fields="fields" stacked="md">
<gl-table
ref="securityControlTable"
:items="features"
:fields="fields"
stacked="md"
:tbody-tr-attr="{ 'data-testid': 'security-scanner-row' }"
>
<template #cell(feature)="{ item }">
<div class="gl-text-gray-900">{{ item.name }}</div>
<div>
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'User sees Security Configuration table', :js do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :repository) }
before_all do
project.add_developer(user)
end
before do
sign_in(user)
end
context 'with security_dashboard feature available' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'with no SAST report' do
it 'shows SAST is not enabled' do
visit(project_security_configuration_path(project))
within_sast_row do
expect(page).to have_text('SAST')
expect(page).to have_text('Not enabled')
expect(page).to have_css('[data-testid="enableButton"]')
end
end
end
context 'with SAST report' do
before do
pipeline = create(:ci_pipeline, project: project)
create(:ci_build, :sast, pipeline: pipeline, status: 'success')
end
it 'shows SAST is enabled' do
visit(project_security_configuration_path(project))
within_sast_row do
expect(page).to have_text('SAST')
expect(page).to have_text('Enabled')
expect(page).to have_css('[data-testid="configureButton"]')
end
end
end
end
def within_sast_row
within '[data-testid="security-scanner-row"]:nth-of-type(1)' do
yield
end
end
end
......@@ -138,7 +138,8 @@ export const createRotationResponse = {
oncallRotation: {
id: '44',
name: 'Test',
startsAt: '2020-12-17T12:00:00Z',
startsAt: '2020-12-20T12:00:00Z',
endsAt: '2021-03-17T12:00:00Z',
length: 5,
lengthUnit: 'WEEKS',
participants: {
......@@ -171,7 +172,8 @@ export const createRotationResponseWithErrors = {
oncallRotation: {
id: '44',
name: 'Test',
startsAt: '2020-12-17T12:00:00Z',
startsAt: '2020-12-20T12:00:00Z',
endsAt: '2021-03-17T12:00:00Z',
length: 5,
lengthUnit: 'WEEKS',
participants: {
......
......@@ -2,6 +2,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/2",
"name": "Rotation 242",
"startsAt": "2021-01-13T10:04:56.333Z",
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
......@@ -54,6 +55,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/55",
"name": "Rotation 242",
"startsAt": "2021-01-13T10:04:56.333Z",
"endsAt": "2021-03-13T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
......@@ -102,6 +104,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/3",
"name": "Rotation 244",
"startsAt": "2021-01-06T10:04:56.333Z",
"endsAt": "2021-01-10T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
......@@ -150,6 +153,7 @@
"id": "gid://gitlab/IncidentManagement::OncallRotation/5",
"name": "Rotation 247",
"startsAt": "2021-01-06T10:04:56.333Z",
"endsAt": "2021-01-11T10:04:56.333Z",
"length": 1,
"lengthUnit": "WEEKS",
"participants": {
......
......@@ -40,7 +40,7 @@ describe('AddEditRotationForm', () => {
date: null,
time: 0,
},
endsOn: {
endsAt: {
date: null,
time: 0,
},
......@@ -160,7 +160,7 @@ describe('AddEditRotationForm', () => {
await wrapper.vm.$nextTick();
const emittedEvent = wrapper.emitted('update-rotation-form');
expect(emittedEvent).toHaveLength(1);
expect(emittedEvent[0][0]).toEqual({ type: 'endsOn.time', value: option + 1 });
expect(emittedEvent[0][0]).toEqual({ type: 'endsAt.time', value: option + 1 });
});
it('should add a checkmark to a selected end time', async () => {
......@@ -168,7 +168,7 @@ describe('AddEditRotationForm', () => {
const time = 5;
wrapper.setProps({
form: {
endsOn: {
endsAt: {
time,
},
startsAt: {
......@@ -221,7 +221,7 @@ describe('AddEditRotationForm', () => {
wrapper.setProps({
form: {
endsOn: {
endsAt: {
time: 0,
},
startsAt: {
......
import { GlModal, GlAlert } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import AddEditRotationForm from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_form.vue';
import AddEditRotationModal, {
i18n,
} from 'ee/oncall_schedules/components/rotations/components/add_edit_rotation_modal.vue';
......@@ -129,8 +130,9 @@ describe('AddEditRotationModal', () => {
wrapper = null;
});
const findModal = () => wrapper.find(GlModal);
const findAlert = () => wrapper.find(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const findAlert = () => wrapper.findComponent(GlAlert);
const findForm = () => wrapper.findComponent(AddEditRotationForm);
it('renders rotation modal layout', () => {
expect(wrapper.element).toMatchSnapshot();
......@@ -155,6 +157,149 @@ describe('AddEditRotationModal', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toContain(error);
});
describe('Validation', () => {
describe('name', () => {
it('is valid when name is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: '' });
expect(form.props('validationState').name).toBe(false);
});
it('is NOT valid when name is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'Some value' });
expect(form.props('validationState').name).toBe(true);
});
});
describe('participants', () => {
it('is valid when participants array is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'participants',
value: ['user1', 'user2'],
});
expect(form.props('validationState').participants).toBe(true);
});
it('is NOT valid when participants array is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'participants', value: [] });
expect(form.props('validationState').participants).toBe(false);
});
});
describe('startsAt date', () => {
it('is valid when date is NOT empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('10/12/2021'),
});
expect(form.props('validationState').startsAt).toBe(true);
});
it('is NOT valid when date is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: null });
expect(form.props('validationState').startsAt).toBe(false);
});
});
describe('endsAt date', () => {
it('is valid when date is empty', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'endsAt.date', value: null });
expect(form.props('validationState').endsAt).toBe(true);
});
it('is valid when start date is smaller then end date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('9/11/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('10/11/2021'),
});
expect(form.props('validationState').endsAt).toBe(true);
});
it('is invalid when start date is larger then end date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('10/11/2021'),
});
expect(form.props('validationState').endsAt).toBe(false);
});
it('is valid when start and end dates are equal but time is smaller on start date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: 10 });
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'endsAt.time', value: 22 });
expect(form.props('validationState').endsAt).toBe(true);
});
it('is invalid when start and end dates are equal but time is larger on start date', () => {
const form = findForm();
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'startsAt.time', value: 10 });
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('11/11/2021'),
});
form.vm.$emit('update-rotation-form', { type: 'endsAt.time', value: 8 });
expect(form.props('validationState').endsAt).toBe(false);
});
});
describe('Toggle primary button state', () => {
it('should disable primary button when any of the fields is invalid', async () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'lalal' });
await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.arrayContaining([{ disabled: true }]),
);
});
it('should enable primary button when all fields are valid', async () => {
const form = findForm();
form.vm.$emit('update-rotation-form', { type: 'name', value: 'Value' });
form.vm.$emit('update-rotation-form', { type: 'participants', value: [1, 2, 3] });
form.vm.$emit('update-rotation-form', {
type: 'startsAt.date',
value: new Date('11/10/2021'),
});
form.vm.$emit('update-rotation-form', {
type: 'endsAt.date',
value: new Date('12/10/2021'),
});
await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toEqual(
expect.arrayContaining([{ disabled: false }]),
);
});
});
});
});
describe('with mocked Apollo client', () => {
......
......@@ -5,6 +5,8 @@ module API
class Project < BasicProjectDetails
include ::API::Helpers::RelatedResourcesHelpers
expose :container_registry_url, as: :container_registry_image_prefix, if: -> (_, _) { Gitlab.config.registry.enabled }
expose :_links do
expose :self do |project|
expose_url(api_v4_projects_path(id: project.id))
......
......@@ -42,7 +42,9 @@
'i_code_review_user_approval_rule_edited',
'i_code_review_user_vs_code_api_request',
'i_code_review_user_toggled_task_item_status',
'i_code_review_user_create_mr_from_issue'
'i_code_review_user_create_mr_from_issue',
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked'
]
- name: code_review_category_monthly_active_users
operator: OR
......@@ -78,7 +80,9 @@
'i_code_review_user_approval_rule_deleted',
'i_code_review_user_approval_rule_edited',
'i_code_review_user_toggled_task_item_status',
'i_code_review_user_create_mr_from_issue'
'i_code_review_user_create_mr_from_issue',
'i_code_review_user_mr_discussion_locked',
'i_code_review_user_mr_discussion_unlocked'
]
- name: code_review_extension_category_monthly_active_users
operator: OR
......
......@@ -164,3 +164,13 @@
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_create_mr_from_issue
- name: i_code_review_user_mr_discussion_locked
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_mr_discussion_locked
- name: i_code_review_user_mr_discussion_unlocked
redis_slot: code_review
category: code_review
aggregation: weekly
feature_flag: usage_data_i_code_review_user_mr_discussion_unlocked
......@@ -35,6 +35,8 @@ module Gitlab
MR_EDIT_MR_TITLE_ACTION = 'i_code_review_edit_mr_title'
MR_EDIT_MR_DESC_ACTION = 'i_code_review_edit_mr_desc'
MR_CREATE_FROM_ISSUE_ACTION = 'i_code_review_user_create_mr_from_issue'
MR_DISCUSSION_LOCKED_ACTION = 'i_code_review_user_mr_discussion_locked'
MR_DISCUSSION_UNLOCKED_ACTION = 'i_code_review_user_mr_discussion_unlocked'
class << self
def track_mr_diffs_action(merge_request:)
......@@ -153,6 +155,14 @@ module Gitlab
track_unique_action_by_user(MR_CREATE_FROM_ISSUE_ACTION, user)
end
def track_discussion_locked_action(user:)
track_unique_action_by_user(MR_DISCUSSION_LOCKED_ACTION, user)
end
def track_discussion_unlocked_action(user:)
track_unique_action_by_user(MR_DISCUSSION_UNLOCKED_ACTION, user)
end
private
def track_unique_action_by_merge_request(action, merge_request)
......
......@@ -21028,6 +21028,9 @@ msgstr ""
msgid "OnCallSchedules|Restrict to time intervals"
msgstr ""
msgid "OnCallSchedules|Rotation end date/time must come after start date/time"
msgstr ""
msgid "OnCallSchedules|Rotation length"
msgstr ""
......
......@@ -284,4 +284,20 @@ RSpec.describe Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter, :cl
let(:action) { described_class::MR_CREATE_FROM_ISSUE_ACTION }
end
end
describe '.track_discussion_locked_action' do
subject { described_class.track_discussion_locked_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_DISCUSSION_LOCKED_ACTION }
end
end
describe '.track_discussion_unlocked_action' do
subject { described_class.track_discussion_unlocked_action(user: user) }
it_behaves_like 'a tracked merge request unique event' do
let(:action) { described_class::MR_DISCUSSION_UNLOCKED_ACTION }
end
end
end
......@@ -56,6 +56,7 @@ itself: # project
- can_create_merge_request_in
- compliance_frameworks
- container_expiration_policy
- container_registry_image_prefix
- default_branch
- empty_repo
- forks_count
......
......@@ -1540,6 +1540,10 @@ RSpec.describe API::Projects do
end
context 'when authenticated as an admin' do
before do
stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
end
let(:project_attributes_file) { 'spec/requests/api/project_attributes.yml' }
let(:project_attributes) { YAML.load_file(project_attributes_file) }
......@@ -1569,7 +1573,7 @@ RSpec.describe API::Projects do
keys
end
it 'returns a project by id' do
it 'returns a project by id', :aggregate_failures do
project
project_member
group = create(:group)
......@@ -1587,6 +1591,7 @@ RSpec.describe API::Projects do
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
......@@ -1644,9 +1649,10 @@ RSpec.describe API::Projects do
before do
project
project_member
stub_container_registry_config(enabled: true, host_port: 'registry.example.org:5000')
end
it 'returns a project by id' do
it 'returns a project by id', :aggregate_failures do
group = create(:group)
link = create(:project_group_link, project: project, group: group)
......@@ -1662,6 +1668,7 @@ RSpec.describe API::Projects do
expect(json_response['ssh_url_to_repo']).to be_present
expect(json_response['http_url_to_repo']).to be_present
expect(json_response['web_url']).to be_present
expect(json_response['container_registry_image_prefix']).to eq("registry.example.org:5000/#{project.full_path}")
expect(json_response['owner']).to be_a Hash
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to be_present
......
......@@ -48,6 +48,8 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
end
context 'valid params' do
let(:locked) { true }
let(:opts) do
{
title: 'New title',
......@@ -58,7 +60,7 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
label_ids: [label.id],
target_branch: 'target',
force_remove_source_branch: '1',
discussion_locked: true
discussion_locked: locked
}
end
......@@ -117,6 +119,56 @@ RSpec.describe MergeRequests::UpdateService, :mailer do
MergeRequests::UpdateService.new(project, user, opts).execute(draft_merge_request)
end
context 'when MR is locked' do
context 'when locked again' do
it 'does not track discussion locking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_discussion_locked_action)
opts[:discussion_locked] = true
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when unlocked' do
it 'tracks dicussion unlocking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_discussion_unlocked_action).once.with(user: user)
opts[:discussion_locked] = false
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
context 'when MR is unlocked' do
let(:locked) { false }
context 'when unlocked again' do
it 'does not track discussion unlocking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.not_to receive(:track_discussion_unlocked_action)
opts[:discussion_locked] = false
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
context 'when locked' do
it 'tracks dicussion locking' do
expect(Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter)
.to receive(:track_discussion_locked_action).once.with(user: user)
opts[:discussion_locked] = true
MergeRequests::UpdateService.new(project, user, opts).execute(merge_request)
end
end
end
end
context 'updating milestone' do
......
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