Commit 925b9264 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 1b19205e a5514584
......@@ -40,18 +40,23 @@ export default {
data() {
return {
isLoading: false,
isSharedRunnerEnabled: false,
isSharedRunnerEnabled: this.isEnabled,
errorMessage: null,
isCcValidationRequired: false,
successfulValidation: false,
};
},
created() {
this.isSharedRunnerEnabled = this.isEnabled;
this.isCcValidationRequired = this.isCreditCardValidationRequired;
computed: {
showCreditCardValidation() {
return (
this.isCreditCardValidationRequired &&
!this.isSharedRunnerEnabled &&
!this.successfulValidation
);
},
},
methods: {
creditCardValidated() {
this.isCcValidationRequired = false;
this.successfulValidation = true;
},
toggleSharedRunners() {
this.isLoading = true;
......@@ -62,7 +67,6 @@ export default {
.then(() => {
this.isLoading = false;
this.isSharedRunnerEnabled = !this.isSharedRunnerEnabled;
this.isCcValidationRequired = this.isCreditCardValidationRequired;
})
.catch((error) => {
this.isLoading = false;
......@@ -81,7 +85,7 @@ export default {
</gl-alert>
<cc-validation-required-alert
v-if="isCcValidationRequired && !isSharedRunnerEnabled"
v-if="showCreditCardValidation"
class="gl-pb-5"
:custom-message="$options.i18n.REQUIRES_VALIDATION_TEXT"
@verifiedCreditCard="creditCardValidated"
......
- navbar_links = links.sort_by(&:title)
- all_paths = navbar_links.map(&:path)
- analytics_link = navbar_links.find { |link| link.title == _('Value Stream') } || navbar_links.first
- analytics_link = navbar_links.find { |link| link.title == _('Value stream') } || navbar_links.first
- if navbar_links.any?
= nav_link(path: all_paths) do
......
......@@ -76,13 +76,13 @@ export default {
this.rules = this.form.rules.map((rule) => {
const {
status,
elapsedTimeSeconds,
elapsedTimeMinutes,
oncallSchedule: { iid: oncallScheduleIid },
} = rule;
return {
status,
elapsedTimeSeconds,
elapsedTimeMinutes,
action: DEFAULT_ACTION,
oncallScheduleIid,
key: uniqueId(),
......
......@@ -9,7 +9,7 @@ import {
import createEscalationPolicyMutation from '../graphql/mutations/create_escalation_policy.mutation.graphql';
import updateEscalationPolicyMutation from '../graphql/mutations/update_escalation_policy.mutation.graphql';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import { isNameFieldValid, getRulesValidationState } from '../utils';
import { isNameFieldValid, getRulesValidationState, serializeRule } from '../utils';
import AddEditEscalationPolicyForm from './add_edit_escalation_policy_form.vue';
export const i18n = {
......@@ -91,7 +91,7 @@ export default {
},
requestParams() {
const id = this.isEditMode ? { id: this.escalationPolicy.id } : {};
return { ...this.form, ...id, rules: this.getRules(this.form.rules) };
return { ...this.form, ...id, rules: this.getRules(this.form.rules).map(serializeRule) };
},
},
methods: {
......@@ -186,9 +186,9 @@ export default {
},
getRules(rules) {
return rules.map(
({ status, elapsedTimeSeconds, oncallScheduleIid, oncallSchedule: { iid } = {} }) => ({
({ status, elapsedTimeMinutes, oncallScheduleIid, oncallSchedule: { iid } = {} }) => ({
status,
elapsedTimeSeconds,
elapsedTimeMinutes,
oncallScheduleIid: oncallScheduleIid || iid,
}),
);
......
......@@ -4,6 +4,7 @@ import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { addEscalationPolicyModalId } from '../constants';
import getEscalationPoliciesQuery from '../graphql/queries/get_escalation_policies.query.graphql';
import { parsePolicy } from '../utils';
import AddEscalationPolicyModal from './add_edit_escalation_policy_modal.vue';
import EscalationPolicy from './escalation_policy.vue';
......@@ -47,7 +48,7 @@ export default {
};
},
update({ project }) {
return project?.incidentManagementEscalationPolicies?.nodes ?? [];
return project?.incidentManagementEscalationPolicies?.nodes.map(parsePolicy) ?? [];
},
error(error) {
Sentry.captureException(error);
......
......@@ -29,9 +29,9 @@ export const i18n = {
minutes: s__('EscalationPolicies|mins'),
};
const isRuleValid = ({ status, elapsedTimeSeconds, oncallSchedule: { name } }) =>
const isRuleValid = ({ status, elapsedTimeMinutes, oncallSchedule: { name } }) =>
Object.keys(ALERT_STATUSES).includes(status) &&
typeof elapsedTimeSeconds === 'number' &&
typeof elapsedTimeMinutes === 'number' &&
typeof name === 'string';
export default {
......@@ -145,7 +145,7 @@ export default {
</template>
<template #minutes>
<span class="gl-font-weight-bold">
{{ rule.elapsedTimeSeconds }} {{ $options.i18n.minutes }}
{{ rule.elapsedTimeMinutes }} {{ $options.i18n.minutes }}
</span>
</template>
<template #then>
......
......@@ -76,10 +76,10 @@ export default {
},
},
data() {
const { status, elapsedTimeSeconds, action, oncallScheduleIid } = this.rule;
const { status, elapsedTimeMinutes, action, oncallScheduleIid } = this.rule;
return {
status,
elapsedTimeSeconds,
elapsedTimeMinutes,
action,
oncallScheduleIid,
};
......@@ -119,7 +119,7 @@ export default {
oncallScheduleIid: parseInt(this.oncallScheduleIid, 10),
action: this.action,
status: this.status,
elapsedTimeSeconds: parseInt(this.elapsedTimeSeconds, 10),
elapsedTimeMinutes: this.elapsedTimeMinutes,
},
});
},
......@@ -169,9 +169,9 @@ export default {
</template>
<template #minutes>
<gl-form-input
v-model="elapsedTimeSeconds"
v-model="elapsedTimeMinutes"
class="gl-mx-3 gl-inset-border-1-gray-200! gl-w-12"
type="number"
number
min="0"
@input="emitUpdate"
/>
......
......@@ -13,7 +13,7 @@ export const ACTIONS = {
export const DEFAULT_ESCALATION_RULE = {
status: 'ACKNOWLEDGED',
elapsedTimeSeconds: 0,
elapsedTimeMinutes: 0,
action: 'EMAIL_ONCALL_SCHEDULE_USER',
oncallScheduleIid: null,
};
......
......@@ -17,8 +17,33 @@ export const isNameFieldValid = (name) => {
export const getRulesValidationState = (rules) => {
return rules.map((rule) => {
return {
isTimeValid: parseInt(rule.elapsedTimeSeconds, 10) >= 0,
isTimeValid: parseInt(rule.elapsedTimeMinutes, 10) >= 0,
isScheduleValid: Boolean(rule.oncallScheduleIid),
};
});
};
/**
* Serializes a rule by converting elapsed minutes to seconds
* @param {Object} rule
*
* @returns {Object} rule
*/
export const serializeRule = ({ elapsedTimeMinutes, ...ruleParams }) => ({
...ruleParams,
elapsedTimeSeconds: elapsedTimeMinutes * 60,
});
/**
* Parses a policy by converting elapsed seconds to minutes
* @param {Object} policy
*
* @returns {Object} policy
*/
export const parsePolicy = (policy) => ({
...policy,
rules: policy.rules.map(({ elapsedTimeSeconds, ...ruleParams }) => ({
...ruleParams,
elapsedTimeMinutes: elapsedTimeSeconds / 60,
})),
});
......@@ -28,7 +28,7 @@ module EE
return unless group_sidebar_link?(:merge_request_analytics) # rubocop: disable Lint/UnreachableCode
navbar_sub_item(
title: _('Merge Request'),
title: _('Merge request'),
path: 'groups/analytics/merge_request_analytics#show',
link: group_analytics_merge_request_analytics_path(group)
)
......@@ -38,7 +38,7 @@ module EE
return unless group_sidebar_link?(:cycle_analytics)
navbar_sub_item(
title: _('Value Stream'),
title: _('Value stream'),
path: 'groups/analytics/cycle_analytics#show',
link: group_analytics_cycle_analytics_path(group)
)
......@@ -48,7 +48,7 @@ module EE
return unless group_sidebar_link?(:group_devops_adoption)
navbar_sub_item(
title: _('DevOps Adoption'),
title: _('DevOps adoption'),
path: 'groups/analytics/devops_adoption#show',
link: group_analytics_devops_adoption_path(group)
)
......
......@@ -44,7 +44,7 @@ module EE
end
::Sidebars::MenuItem.new(
title: _('Code Review'),
title: _('Code review'),
link: project_analytics_code_reviews_path(context.project),
active_routes: { path: 'projects/analytics/code_reviews#index' },
item_id: :code_review
......@@ -76,7 +76,7 @@ module EE
end
::Sidebars::MenuItem.new(
title: _('Merge Request'),
title: _('Merge request'),
link: project_analytics_merge_request_analytics_path(context.project),
active_routes: { path: 'projects/analytics/merge_request_analytics#show' },
item_id: :merge_requests
......
......@@ -52,7 +52,7 @@ RSpec.describe Projects::Analytics::MergeRequestAnalyticsController do
end
it 'renders the side navigation with the correct submenu set as active' do
expect(response.body).to have_active_sub_navigation('Merge Request')
expect(response.body).to have_active_sub_navigation('Merge request')
end
end
end
......
......@@ -28,7 +28,7 @@ RSpec.describe 'Group navbar' do
insert_after_sub_nav_item(
_('Contribution'),
within: _('Analytics'),
new_sub_nav_item_name: _('DevOps Adoption')
new_sub_nav_item_name: _('DevOps adoption')
)
visit group_path(group)
......@@ -60,7 +60,7 @@ RSpec.describe 'Group navbar' do
insert_after_sub_nav_item(
_('Contribution'),
within: _('Analytics'),
new_sub_nav_item_name: _('Value Stream')
new_sub_nav_item_name: _('Value stream')
)
visit group_path(group)
......
......@@ -30,7 +30,7 @@ RSpec.describe 'Project active tab' do
end
it_behaves_like 'page has active tab', _('Analytics')
it_behaves_like 'page has active sub tab', _('Code Review')
it_behaves_like 'page has active sub tab', _('Code review')
end
context 'on project CI/CD' do
......
......@@ -23,7 +23,7 @@ RSpec.describe 'Project navbar' do
stub_licensed_features(issues_analytics: true)
insert_after_sub_nav_item(
_('Code Review'),
_('Code review'),
within: _('Analytics'),
new_sub_nav_item_name: _('Issue')
)
......
......@@ -38,7 +38,7 @@ exports[`EscalationPolicy renders policy with rules 1`] = `
class="gl-font-weight-bold"
>
10 mins
1 mins
</span>
......@@ -81,7 +81,7 @@ exports[`EscalationPolicy renders policy with rules 1`] = `
class="gl-font-weight-bold"
>
20 mins
2 mins
</span>
......
......@@ -82,7 +82,7 @@ describe('AddEscalationPolicyForm', () => {
it('on rule update emitted should update rules array and emit updates up', () => {
const updatedRule = {
status: 'TRIGGERED',
elapsedTimeSeconds: 30,
elapsedTimeMinutes: 3,
oncallScheduleIid: 2,
};
findRules().at(0).vm.$emit('update-escalation-rule', { index: 0, rule: updatedRule });
......
......@@ -23,7 +23,8 @@ describe('AddEditsEscalationPolicyModal', () => {
const mockEscalationPolicy = cloneDeep(mockPolicies[0]);
const updatedName = 'Policy name';
const updatedDescription = 'Policy description';
const updatedRules = [{ status: 'RESOLVED', elapsedTimeSeconds: 10, oncallScheduleIid: 1 }];
const updatedRules = [{ status: 'RESOLVED', elapsedTimeMinutes: 1, oncallScheduleIid: 1 }];
const serializedRules = [{ status: 'RESOLVED', elapsedTimeSeconds: 60, oncallScheduleIid: 1 }];
const createComponent = ({ escalationPolicy, isEditMode = false, modalId, data } = {}) => {
wrapper = shallowMount(AddEscalationPolicyModal, {
......@@ -99,7 +100,7 @@ describe('AddEditsEscalationPolicyModal', () => {
projectPath,
name: updatedName,
description: updatedDescription,
rules: updatedRules,
rules: serializedRules,
},
},
update: expect.any(Function),
......@@ -168,7 +169,7 @@ describe('AddEditsEscalationPolicyModal', () => {
input: {
name: updatedName,
description: updatedDescription,
rules: updatedRules,
rules: serializedRules,
id: mockEscalationPolicy.id,
},
},
......@@ -264,7 +265,7 @@ describe('AddEditsEscalationPolicyModal', () => {
});
form.vm.$emit('update-escalation-policy-form', {
field: 'rules',
value: [{ status: 'RESOLVED', elapsedTimeSeconds: 10, oncallScheduleIid: 1 }],
value: [{ status: 'RESOLVED', elapsedTimeMinutes: 1, oncallScheduleIid: 1 }],
});
await wrapper.vm.$nextTick();
expect(findModal().props('actionPrimary').attributes).toContainEqual({ disabled: false });
......
......@@ -9,11 +9,12 @@ import {
deleteEscalationPolicyModalId,
editEscalationPolicyModalId,
} from 'ee/escalation_policies/constants';
import { parsePolicy } from 'ee/escalation_policies/utils';
import mockPolicies from './mocks/mockPolicies.json';
describe('EscalationPolicy', () => {
let wrapper;
const escalationPolicy = cloneDeep(mockPolicies[0]);
const escalationPolicy = parsePolicy(cloneDeep(mockPolicies[0]));
const createComponent = () => {
wrapper = shallowMount(EscalationPolicy, {
......
import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import EscalationPoliciesWrapper from 'ee/escalation_policies/components/escalation_policies_wrapper.vue';
import EscalationPolicy from 'ee/escalation_policies/components/escalation_policy.vue';
import { parsePolicy } from 'ee/escalation_policies/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import mockEscalationPolicies from './mocks/mockPolicies.json';
......@@ -55,7 +56,7 @@ describe('Escalation Policies Wrapper', () => {
beforeEach(() => {
mountComponent({
loading,
escalationPolicies,
escalationPolicies: escalationPolicies.map(parsePolicy),
});
});
......
......@@ -7,7 +7,7 @@
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/22",
"status": "ACKNOWLEDGED",
"elapsedTimeSeconds": 10,
"elapsedTimeSeconds": 60,
"oncallSchedule": {
"iid": "3",
"name": "Schedule to fill in"
......@@ -16,7 +16,7 @@
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/23",
"status": "RESOLVED",
"elapsedTimeSeconds": 20,
"elapsedTimeSeconds": 120,
"oncallSchedule": {
"iid": "4",
"name": "Monitor schedule"
......@@ -32,7 +32,7 @@
{
"id": "gid://gitlab/IncidentManagement::EscalationRule/48",
"status": "ACKNOWLEDGED",
"elapsedTimeSeconds": 30,
"elapsedTimeSeconds": 180,
"oncallSchedule": {
"iid": "3",
"name": "Schedule to fill in"
......
import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import MockAxiosAdapter from 'axios-mock-adapter';
import CcValidationRequiredAlert from 'ee_component/billings/components/cc_validation_required_alert.vue';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
const TEST_UPDATE_PATH = '/test/update_shared_runners';
describe('projects/settings/components/shared_runners', () => {
let wrapper;
let mockAxios;
const createComponent = (props = {}) => {
wrapper = shallowMount(SharedRunnersToggleComponent, {
......@@ -28,6 +31,11 @@ describe('projects/settings/components/shared_runners', () => {
const getToggleValue = () => findSharedRunnersToggle().props('value');
const isToggleDisabled = () => findSharedRunnersToggle().props('disabled');
beforeEach(() => {
mockAxios = new MockAxiosAdapter(axios);
mockAxios.onPost(TEST_UPDATE_PATH).reply(200);
});
afterEach(() => {
wrapper.destroy();
});
......@@ -57,14 +65,31 @@ describe('projects/settings/components/shared_runners', () => {
});
describe('when credit card is validated', () => {
it('should show the toggle button', async () => {
beforeEach(() => {
findCcValidationRequiredAlert().vm.$emit('verifiedCreditCard');
await waitForPromises();
});
it('should show the toggle button', () => {
expect(findSharedRunnersToggle().exists()).toBe(true);
expect(getToggleValue()).toBe(false);
expect(isToggleDisabled()).toBe(false);
});
it('should not show credit card alert after toggling on and off', async () => {
findSharedRunnersToggle().vm.$emit('change', true);
await waitForPromises();
expect(mockAxios.history.post[0].data).toBeUndefined();
expect(mockAxios.history.post).toHaveLength(1);
expect(findCcValidationRequiredAlert().exists()).toBe(false);
findSharedRunnersToggle().vm.$emit('change', false);
await waitForPromises();
expect(mockAxios.history.post[1].data).toBeUndefined();
expect(mockAxios.history.post).toHaveLength(2);
expect(findCcValidationRequiredAlert().exists()).toBe(false);
});
});
});
});
......@@ -93,7 +93,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
it 'is visible' do
render
expect(rendered).to have_text 'DevOps Adoption'
expect(rendered).to have_text 'DevOps adoption'
end
end
......@@ -105,7 +105,7 @@ RSpec.describe 'layouts/nav/sidebar/_group' do
it 'is not visible' do
render
expect(rendered).not_to have_text 'DevOps Adoption'
expect(rendered).not_to have_text 'DevOps adoption'
end
end
end
......
......@@ -282,7 +282,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'has a link to the Code Review analytics page' do
render
expect(rendered).to have_link('Code Review', href: project_analytics_code_reviews_path(project))
expect(rendered).to have_link('Code review', href: project_analytics_code_reviews_path(project))
end
context 'when user does not have access' do
......@@ -291,7 +291,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'does not have a link to the Code Review analytics page' do
render
expect(rendered).not_to have_link('Code Review', href: project_analytics_code_reviews_path(project))
expect(rendered).not_to have_link('Code review', href: project_analytics_code_reviews_path(project))
end
end
end
......@@ -340,7 +340,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
describe 'Merge Request' do
describe 'Merge request' do
before do
stub_licensed_features(project_merge_request_analytics: true)
end
......@@ -348,7 +348,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'has a link to the merge request analytics page' do
render
expect(rendered).to have_link('Merge Request', href: project_analytics_merge_request_analytics_path(project))
expect(rendered).to have_link('Merge request', href: project_analytics_merge_request_analytics_path(project))
end
context 'when user does not have access' do
......@@ -357,7 +357,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'does not have a link to the merge request analytics page' do
render
expect(rendered).not_to have_link('Merge Request', href: project_analytics_merge_request_analytics_path(project))
expect(rendered).not_to have_link('Merge request', href: project_analytics_merge_request_analytics_path(project))
end
end
end
......
......@@ -79,7 +79,7 @@ module Sidebars
end
::Sidebars::MenuItem.new(
title: _('Value Stream'),
title: _('Value stream'),
link: project_cycle_analytics_path(context.project),
container_html_options: { class: 'shortcuts-project-cycle-analytics' },
active_routes: { path: 'cycle_analytics#show' },
......
......@@ -7741,6 +7741,9 @@ msgstr ""
msgid "Code owners"
msgstr ""
msgid "Code review"
msgstr ""
msgid "Code snippet copied. Insert it in the correct location in the YAML file."
msgstr ""
......@@ -11069,6 +11072,9 @@ msgstr ""
msgid "DevOps Report"
msgstr ""
msgid "DevOps adoption"
msgstr ""
msgid "DevopsAdoption|Add Group"
msgstr ""
......@@ -35673,9 +35679,6 @@ msgstr ""
msgid "Value"
msgstr ""
msgid "Value Stream"
msgstr ""
msgid "Value Stream Analytics"
msgstr ""
......@@ -35685,6 +35688,9 @@ msgstr ""
msgid "Value Stream Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
msgid "Value stream"
msgstr ""
msgid "ValueStreamAnalyticsStage|We don't have enough data to show this stage."
msgstr ""
......
......@@ -138,9 +138,9 @@ RSpec.describe 'Project active tab' do
visit project_cycle_analytics_path(project)
end
context 'on project Analytics/Value Stream Analytics' do
context 'on project Analytics/Value stream Analytics' do
it_behaves_like 'page has active tab', _('Analytics')
it_behaves_like 'page has active sub tab', _('Value Stream')
it_behaves_like 'page has active sub tab', _('Value stream')
end
context 'on project Analytics/"CI/CD"' do
......
......@@ -95,10 +95,10 @@ RSpec.shared_context 'project navbar structure' do
nav_item: _('Analytics'),
nav_sub_items: [
_('CI/CD'),
(_('Code Review') if Gitlab.ee?),
(_('Merge Request') if Gitlab.ee?),
(_('Code review') if Gitlab.ee?),
(_('Merge request') if Gitlab.ee?),
_('Repository'),
_('Value Stream')
_('Value stream')
]
},
{
......
......@@ -695,11 +695,11 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
end
end
describe 'Value Stream' do
describe 'Value stream' do
it 'has a link to the value stream page' do
render
expect(rendered).to have_link('Value Stream', href: project_cycle_analytics_path(project))
expect(rendered).to have_link('Value stream', href: project_cycle_analytics_path(project))
end
context 'when user does not have access' do
......@@ -708,7 +708,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
it 'does not have a link to the value stream page' do
render
expect(rendered).not_to have_link('Value Stream', href: project_cycle_analytics_path(project))
expect(rendered).not_to have_link('Value stream', href: project_cycle_analytics_path(project))
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