Commit e2644335 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 0810b3bf d69809a3
......@@ -79,6 +79,7 @@ const Api = {
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
freezePeriodPath: '/api/:version/projects/:id/freeze_periods/:freeze_period_id',
usageDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
......@@ -832,6 +833,14 @@ const Api = {
return axios.post(url, freezePeriod);
},
updateFreezePeriod(id, freezePeriod = {}) {
const url = Api.buildUrl(this.freezePeriodPath)
.replace(':id', encodeURIComponent(id))
.replace(':freeze_period_id', encodeURIComponent(freezePeriod.id));
return axios.put(url, freezePeriod);
},
trackRedisCounterEvent(event) {
if (!gon.features?.usageDataApi) {
return null;
......
......@@ -3,8 +3,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import CiVariableSettings from './components/ci_variable_settings.vue';
import createStore from './store';
export default (containerId = 'js-ci-project-variables') => {
const containerEl = document.getElementById(containerId);
const mountCiVariableListApp = (containerEl) => {
const {
endpoint,
projectId,
......@@ -43,3 +42,8 @@ export default (containerId = 'js-ci-project-variables') => {
},
});
};
export default () => {
const el = document.querySelector('#js-ci-project-variables');
return !el ? {} : mountCiVariableListApp(el);
};
......@@ -18,7 +18,6 @@ export default {
modalOptions: {
ref: 'modal',
modalId: 'deploy-freeze-modal',
title: __('Add deploy freeze'),
actionCancel: {
text: __('Cancel'),
},
......@@ -30,10 +29,13 @@ export default {
cronSyntaxInstructions: __(
'Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}',
),
addTitle: __('Add deploy freeze'),
editTitle: __('Edit deploy freeze'),
},
computed: {
...mapState([
'projectId',
'selectedId',
'selectedTimezone',
'timezoneData',
'freezeStartCron',
......@@ -45,9 +47,9 @@ export default {
]),
addDeployFreezeButton() {
return {
text: __('Add deploy freeze'),
text: this.isEditing ? __('Save deploy freeze') : __('Add deploy freeze'),
attributes: [
{ variant: 'success' },
{ variant: 'confirm' },
{
disabled:
!isValidCron(this.freezeStartCron) ||
......@@ -77,9 +79,17 @@ export default {
this.setSelectedTimezone(selectedTimezone);
},
},
isEditing() {
return Boolean(this.selectedId);
},
modalTitle() {
return this.isEditing
? this.$options.translations.editTitle
: this.$options.translations.addTitle;
},
},
methods: {
...mapActions(['addFreezePeriod', 'setSelectedTimezone', 'resetModal']),
...mapActions(['addFreezePeriod', 'updateFreezePeriod', 'setSelectedTimezone', 'resetModal']),
resetModalHandler() {
this.resetModal();
},
......@@ -89,6 +99,13 @@ export default {
}
return '';
},
submit() {
if (this.isEditing) {
this.updateFreezePeriod();
} else {
this.addFreezePeriod();
}
},
},
};
</script>
......@@ -96,8 +113,9 @@ export default {
<template>
<gl-modal
v-bind="$options.modalOptions"
:title="modalTitle"
:action-primary="addDeployFreezeButton"
@primary="addFreezePeriod"
@primary="submit"
@canceled="resetModalHandler"
>
<p>
......
<script>
import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { s__, __ } from '~/locale';
import { s__ } from '~/locale';
export default {
fields: [
......@@ -17,9 +17,16 @@ export default {
key: 'cronTimezone',
label: s__('DeployFreeze|Time zone'),
},
{
key: 'edit',
label: s__('DeployFreeze|Edit'),
},
],
translations: {
addDeployFreeze: __('Add deploy freeze'),
addDeployFreeze: s__('DeployFreeze|Add deploy freeze'),
emptyStateText: s__(
'DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}',
),
},
components: {
GlTable,
......@@ -39,7 +46,7 @@ export default {
this.fetchFreezePeriods();
},
methods: {
...mapActions(['fetchFreezePeriods']),
...mapActions(['fetchFreezePeriods', 'setFreezePeriod']),
},
};
</script>
......@@ -53,15 +60,20 @@ export default {
show-empty
stacked="lg"
>
<template #cell(cronTimezone)="{ item }">
{{ item.cronTimezone.formattedTimezone }}
</template>
<template #cell(edit)="{ item }">
<gl-button
v-gl-modal.deploy-freeze-modal
icon="pencil"
data-testid="edit-deploy-freeze"
@click="setFreezePeriod(item)"
/>
</template>
<template #empty>
<p data-testid="empty-freeze-periods" class="gl-text-center text-plain">
<gl-sprintf
:message="
s__(
'DeployFreeze|No deploy freezes exist for this project. To add one, click %{strongStart}Add deploy freeze%{strongEnd}',
)
"
>
<gl-sprintf :message="$options.translations.emptyStateText">
<template #strong="{ content }">
<strong>{{ content }}</strong>
</template>
......@@ -73,7 +85,7 @@ export default {
v-gl-modal.deploy-freeze-modal
data-testid="add-deploy-freeze"
category="primary"
variant="success"
variant="confirm"
>
{{ $options.translations.addDeployFreeze }}
</gl-button>
......
......@@ -3,37 +3,53 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const requestAddFreezePeriod = ({ commit }) => {
export const requestFreezePeriod = ({ commit }) => {
commit(types.REQUEST_ADD_FREEZE_PERIOD);
};
export const receiveAddFreezePeriodSuccess = ({ commit }) => {
export const receiveFreezePeriodSuccess = ({ commit }) => {
commit(types.RECEIVE_ADD_FREEZE_PERIOD_SUCCESS);
};
export const receiveAddFreezePeriodError = ({ commit }, error) => {
export const receiveFreezePeriodError = ({ commit }, error) => {
commit(types.RECEIVE_ADD_FREEZE_PERIOD_ERROR, error);
};
export const addFreezePeriod = ({ state, dispatch, commit }) => {
dispatch('requestAddFreezePeriod');
const receiveFreezePeriod = (store, request) => {
const { dispatch, commit } = store;
dispatch('requestFreezePeriod');
return Api.createFreezePeriod(state.projectId, {
freeze_start: state.freezeStartCron,
freeze_end: state.freezeEndCron,
cron_timezone: state.selectedTimezoneIdentifier,
})
request(store)
.then(() => {
dispatch('receiveAddFreezePeriodSuccess');
dispatch('receiveFreezePeriodSuccess');
commit(types.RESET_MODAL);
dispatch('fetchFreezePeriods');
})
.catch((error) => {
createFlash(__('Error: Unable to create deploy freeze'));
dispatch('receiveAddFreezePeriodError', error);
dispatch('receiveFreezePeriodError', error);
});
};
export const addFreezePeriod = (store) =>
receiveFreezePeriod(store, ({ state }) =>
Api.createFreezePeriod(state.projectId, {
freeze_start: state.freezeStartCron,
freeze_end: state.freezeEndCron,
cron_timezone: state.selectedTimezoneIdentifier,
}),
);
export const updateFreezePeriod = (store) =>
receiveFreezePeriod(store, ({ state }) =>
Api.updateFreezePeriod(state.projectId, {
id: state.selectedId,
freeze_start: state.freezeStartCron,
freeze_end: state.freezeEndCron,
cron_timezone: state.selectedTimezoneIdentifier,
}),
);
export const fetchFreezePeriods = ({ commit, state }) => {
commit(types.REQUEST_FREEZE_PERIODS);
......@@ -46,6 +62,13 @@ export const fetchFreezePeriods = ({ commit, state }) => {
});
};
export const setFreezePeriod = ({ commit }, freezePeriod) => {
commit(types.SET_SELECTED_ID, freezePeriod.id);
commit(types.SET_SELECTED_TIMEZONE, freezePeriod.cronTimezone);
commit(types.SET_FREEZE_START_CRON, freezePeriod.freezeStart);
commit(types.SET_FREEZE_END_CRON, freezePeriod.freezeEnd);
};
export const setSelectedTimezone = ({ commit }, timezone) => {
commit(types.SET_SELECTED_TIMEZONE, timezone);
};
......
......@@ -6,6 +6,7 @@ export const RECEIVE_ADD_FREEZE_PERIOD_SUCCESS = 'RECEIVE_ADD_FREEZE_PERIOD_SUCC
export const RECEIVE_ADD_FREEZE_PERIOD_ERROR = 'RECEIVE_ADD_FREEZE_PERIOD_ERROR';
export const SET_SELECTED_TIMEZONE = 'SET_SELECTED_TIMEZONE';
export const SET_SELECTED_ID = 'SET_SELECTED_ID';
export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON';
export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON';
......
......@@ -4,7 +4,11 @@ import * as types from './mutation_types';
const formatTimezoneName = (freezePeriod, timezoneList) =>
convertObjectPropsToCamelCase({
...freezePeriod,
cron_timezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)?.name,
cron_timezone: {
formattedTimezone: timezoneList.find((tz) => tz.identifier === freezePeriod.cron_timezone)
?.name,
identifier: freezePeriod.cronTimezone,
},
});
export default {
......@@ -45,10 +49,15 @@ export default {
state.freezeEndCron = freezeEndCron;
},
[types.SET_SELECTED_ID](state, id) {
state.selectedId = id;
},
[types.RESET_MODAL](state) {
state.freezeStartCron = '';
state.freezeEndCron = '';
state.selectedTimezone = '';
state.selectedTimezoneIdentifier = '';
state.selectedId = '';
},
};
......@@ -6,6 +6,7 @@ export default ({
selectedTimezoneIdentifier = '',
freezeStartCron = '',
freezeEndCron = '',
selectedId = '',
}) => ({
projectId,
freezePeriods,
......@@ -14,4 +15,5 @@ export default ({
selectedTimezoneIdentifier,
freezeStartCron,
freezeEndCron,
selectedId,
});
......@@ -4,9 +4,7 @@ import initSearchSettings from '~/search_settings';
import selfMonitor from '~/self_monitor';
import initSettingsPanels from '~/settings_panels';
if (gon.features?.ciInstanceVariablesUi) {
initVariableList('js-instance-variables');
}
initVariableList('js-instance-variables');
selfMonitor();
// Initialize expandable settings panels
initSettingsPanels();
......
......@@ -13,10 +13,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :disable_query_limiting, only: [:usage_data]
before_action only: [:ci_cd] do
push_frontend_feature_flag(:ci_instance_variables_ui, default_enabled: true)
end
feature_category :not_owned, [
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token
......
......@@ -2,16 +2,15 @@
- page_title _("CI/CD")
- @content_class = "limit-container-width" unless fluid_layout
- if ::Gitlab::Ci::Features.instance_variables_ui_enabled?
%section.settings.no-animate#js-ci-cd-variables{ class: ('expanded' if expanded_by_default?) }
.settings-header
= render 'admin/application_settings/ci/header', expanded: expanded_by_default?
.settings-content
- if ci_variable_protected_by_default?
%p.settings-message.text-center
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') }
= s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
#js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
%section.settings.no-animate#js-ci-cd-variables{ class: ('expanded' if expanded_by_default?) }
.settings-header
= render 'admin/application_settings/ci/header', expanded: expanded_by_default?
.settings-content
- if ci_variable_protected_by_default?
%p.settings-message.text-center
- link_start = '<a href="%{url}">'.html_safe % { url: help_page_path('ci/variables/README', anchor: 'protect-a-custom-variable') }
= s_('Environment variables on this GitLab instance are configured to be %{link_start}protected%{link_end} by default.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
#js-instance-variables{ data: { endpoint: admin_ci_variables_path, group: 'true', maskable_regex: ci_variable_maskable_regex, protected_by_default: ci_variable_protected_by_default?.to_s} }
%section.settings.as-ci-cd.no-animate#js-ci-cd-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
......
......@@ -2,7 +2,7 @@
%section.settings.js-service-desk-setting-wrapper.no-animate#js-service-desk{ class: ('expanded' if expanded), data: { qa_selector: 'service_desk_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Service Desk')
%button.btn.gl-button.js-settings-toggle
%button.btn.gl-button.btn-default.js-settings-toggle
= expanded ? _('Collapse') : _('Expand')
- link_start = "<a href='#{help_page_path('user/project/service_desk')}' target='_blank' rel='noopener noreferrer'>".html_safe
%p= _('Enable and disable Service Desk. Some additional configuration might be required. %{link_start}Learn more%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
......
---
title: Add Ability to Edit Freeze Periods
merge_request: 56407
author:
type: added
---
title: Add btn-default class for Service Desk toggle in settings
merge_request: 56195
author: Yogi (@yo)
type: changed
---
title: Enable the instance variables UI
merge_request: 56255
author:
type: other
---
name: ci_instance_variables_ui
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33510
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/299879
milestone: '13.1'
type: development
group: group::continuous integration
default_enabled: true
......@@ -217,11 +217,11 @@ To set a deploy freeze window in the UI, complete these steps:
1. Click **Add deploy freeze** to open the deploy freeze modal.
1. Enter the start time, end time, and timezone of the desired deploy freeze period.
1. Click **Add deploy freeze** in the modal.
![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v13_2.png)
1. After the deploy freeze is saved, you can edit it by selecting the edit button (**{pencil}**).
![Deploy freeze modal for setting a deploy freeze period](img/deploy_freeze_v13_10.png)
WARNING:
To edit or delete a deploy freeze, use the [Freeze Periods API](../../../api/freeze_periods.md).
To delete a deploy freeze, use the [Freeze Periods API](../../../api/freeze_periods.md).
If a project contains multiple freeze periods, all periods apply. If they overlap, the freeze covers the
complete overlapping period.
......
......@@ -24,13 +24,9 @@ RSpec.describe 'GFM autocomplete', :js do
end
it 'opens quick action autocomplete in the description field' do
find('#epic-description').native.send_keys('/la')
fill_in 'Description', with: '/la'
expect(page).to have_selector('.atwho-container')
page.within '.atwho-container #at-view-commands' do
expect(find('li', match: :first)).to have_content('/label')
end
expect(find_highlighted_autocomplete_item).to have_text('/label')
end
end
......@@ -42,11 +38,11 @@ RSpec.describe 'GFM autocomplete', :js do
end
it 'opens quick action autocomplete when updating description' do
find('.js-issuable-edit').click
click_button 'Edit title and description'
find('#issue-description').native.send_keys('/')
fill_in 'Description', with: '/'
expect(page).to have_selector('.atwho-container')
expect(find_autocomplete_menu).to be_visible
end
context 'issuables' do
......@@ -57,7 +53,7 @@ RSpec.describe 'GFM autocomplete', :js do
issue_1 = create(:issue, project: project)
issue_2 = create(:issue, project: project)
type(find('#note-body'), '#')
fill_in 'Comment', with: '#'
expect_resources(shown: [issue_1, issue_2])
end
......@@ -68,7 +64,7 @@ RSpec.describe 'GFM autocomplete', :js do
mr_1 = create(:merge_request, source_project: project)
mr_2 = create(:merge_request, source_project: project, source_branch: 'other-branch')
type(find('#note-body'), '!')
fill_in 'Comment', with: '!'
expect_resources(shown: [mr_1, mr_2])
end
......@@ -79,10 +75,8 @@ RSpec.describe 'GFM autocomplete', :js do
let!(:epic2) { create(:epic, group: group, title: 'make tea') }
it 'shows epics' do
note = find('#note-body')
fill_in 'Comment', with: '&'
# It should show all the epics on "&".
type(note, '&')
expect_resources(shown: [epic, epic2])
end
end
......@@ -93,51 +87,46 @@ RSpec.describe 'GFM autocomplete', :js do
milestone_1 = create(:milestone, title: 'milestone_1', group: group)
milestone_2 = create(:milestone, title: 'milestone_2', group: group)
milestone_3 = create(:milestone, title: 'milestone_3', project: project)
note = find('#note-body')
type(note, '%')
fill_in 'Comment', with: '%'
expect_resources(shown: [milestone_1, milestone_2], not_shown: [milestone_3])
end
end
context 'labels' do
let_it_be(:backend) { create(:group_label, group: group, title: 'backend') }
let_it_be(:bug) { create(:group_label, group: group, title: 'bug') }
let_it_be(:backend) { create(:group_label, group: group, title: 'backend') }
let_it_be(:bug) { create(:group_label, group: group, title: 'bug') }
let_it_be(:feature_proposal) { create(:group_label, group: group, title: 'feature proposal') }
context 'when no labels are assigned' do
it 'shows all labels for ~' do
note = find('#note-body')
fill_in 'Comment', with: '~'
type(note, '~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows all labels for /label ~' do
note = find('#note-body')
fill_in 'Comment', with: '/label ~'
type(note, '/label ~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows all labels for /relabel ~' do
note = find('#note-body')
fill_in 'Comment', with: '/relabel ~'
type(note, '/relabel ~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows no labels for /unlabel ~' do
note = find('#note-body')
fill_in 'Comment', with: '/unlabel ~'
type(note, '/unlabel ~')
wait_for_requests
expect_resources(not_shown: [backend, bug, feature_proposal])
......@@ -150,36 +139,32 @@ RSpec.describe 'GFM autocomplete', :js do
end
it 'shows all labels for ~' do
note = find('#note-body')
fill_in 'Comment', with: '~'
type(note, '~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows only unset labels for /label ~' do
note = find('#note-body')
fill_in 'Comment', with: '/label ~'
type(note, '/label ~')
wait_for_requests
expect_resources(shown: [bug, feature_proposal], not_shown: [backend])
end
it 'shows all labels for /relabel ~' do
note = find('#note-body')
fill_in 'Comment', with: '/relabel ~'
type(note, '/relabel ~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows only set labels for /unlabel ~' do
note = find('#note-body')
fill_in 'Comment', with: '/unlabel ~'
type(note, '/unlabel ~')
wait_for_requests
expect_resources(shown: [backend], not_shown: [bug, feature_proposal])
......@@ -192,36 +177,31 @@ RSpec.describe 'GFM autocomplete', :js do
end
it 'shows all labels for ~' do
note = find('#note-body')
fill_in 'Comment', with: '~'
type(note, '~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows no labels for /label ~' do
note = find('#note-body')
fill_in 'Comment', with: '/label ~'
type(note, '/label ~')
wait_for_requests
expect_resources(not_shown: [backend, bug, feature_proposal])
end
it 'shows all labels for /relabel ~' do
note = find('#note-body')
fill_in 'Comment', with: '/relabel ~'
type(note, '/relabel ~')
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
end
it 'shows all labels for /unlabel ~' do
note = find('#note-body')
type(note, '/unlabel ~')
fill_in 'Comment', with: '/unlabel ~'
wait_for_requests
expect_resources(shown: [backend, bug, feature_proposal])
......@@ -232,19 +212,6 @@ RSpec.describe 'GFM autocomplete', :js do
private
def expect_to_wrap(should_wrap, item, note, value)
expect(item).to have_content(value)
expect(item).not_to have_content("\"#{value}\"")
item.click
if should_wrap
expect(note.value).to include("\"#{value}\"")
else
expect(note.value).not_to include("\"#{value}\"")
end
end
def expect_resources(shown: nil, not_shown: nil)
page.within('.atwho-container') do
if shown
......@@ -259,12 +226,11 @@ RSpec.describe 'GFM autocomplete', :js do
end
end
# `note` is a textarea where the given text should be typed.
# We don't want to find it each time this function gets called.
def type(note, text)
page.within('.timeline-content-form') do
note.set('')
note.native.send_keys(text)
end
def find_autocomplete_menu
find('.atwho-view ul', visible: true)
end
def find_highlighted_autocomplete_item
find('.atwho-view li.cur', visible: true)
end
end
......@@ -10,10 +10,6 @@ module Gitlab
::Feature.enabled?(:ci_artifacts_exclude, default_enabled: true)
end
def self.instance_variables_ui_enabled?
::Feature.enabled?(:ci_instance_variables_ui, default_enabled: true)
end
def self.pipeline_latest?
::Feature.enabled?(:ci_pipeline_latest, default_enabled: true)
end
......
......@@ -10245,13 +10245,19 @@ msgstr ""
msgid "DeployFreeze|Add a freeze period to prevent unintended releases during a period of time for a given environment. You must update the deployment jobs in %{filename} according to the deploy freezes added here. %{freeze_period_link_start}Learn more.%{freeze_period_link_end}"
msgstr ""
msgid "DeployFreeze|Add deploy freeze"
msgstr ""
msgid "DeployFreeze|Edit"
msgstr ""
msgid "DeployFreeze|Freeze end"
msgstr ""
msgid "DeployFreeze|Freeze start"
msgstr ""
msgid "DeployFreeze|No deploy freezes exist for this project. To add one, click %{strongStart}Add deploy freeze%{strongEnd}"
msgid "DeployFreeze|No deploy freezes exist for this project. To add one, select %{strongStart}Add deploy freeze%{strongEnd}"
msgstr ""
msgid "DeployFreeze|Specify deploy freezes using %{cron_syntax_link_start}cron syntax%{cron_syntax_link_end}."
......@@ -11191,6 +11197,9 @@ msgstr ""
msgid "Edit comment"
msgstr ""
msgid "Edit deploy freeze"
msgstr ""
msgid "Edit description"
msgstr ""
......@@ -26436,6 +26445,9 @@ msgstr ""
msgid "Save comment"
msgstr ""
msgid "Save deploy freeze"
msgstr ""
msgid "Save password"
msgstr ""
......
......@@ -1382,6 +1382,38 @@ describe('Api', () => {
});
});
describe('updateFreezePeriod', () => {
const options = {
id: 10,
freeze_start: '* * * * *',
freeze_end: '* * * * *',
cron_timezone: 'America/Juneau',
created_at: '2020-07-11T07:04:50.153Z',
updated_at: '2020-07-11T07:04:50.153Z',
};
const projectId = 8;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/freeze_periods/${options.id}`;
const expectedResult = {
id: 10,
freeze_start: '* * * * *',
freeze_end: '* * * * *',
cron_timezone: 'America/Juneau',
created_at: '2020-07-11T07:04:50.153Z',
updated_at: '2020-07-11T07:04:50.153Z',
};
describe('when the freeze period is successfully updated', () => {
it('resolves the Promise', () => {
mock.onPut(expectedUrl, options).replyOnce(httpStatus.OK, expectedResult);
return Api.updateFreezePeriod(projectId, options).then(({ data }) => {
expect(data).toStrictEqual(expectedResult);
});
});
});
});
describe('createPipeline', () => {
it('creates new pipeline', () => {
const redirectUrl = 'ci-project/-/pipelines/95';
......
import { GlButton, GlModal } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import Api from '~/api';
import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
import createStore from '~/deploy_freeze/store';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/api');
Vue.use(Vuex);
describe('Deploy freeze modal', () => {
let wrapper;
......@@ -23,18 +26,19 @@ describe('Deploy freeze modal', () => {
stubs: {
GlModal,
},
localVue,
store,
});
});
const findModal = () => wrapper.find(GlModal);
const addDeployFreezeButton = () => findModal().findAll(GlButton).at(1);
const findModal = () => wrapper.findComponent(GlModal);
const submitDeployFreezeButton = () => findModal().findAllComponents(GlButton).at(1);
const setInput = (freezeStartCron, freezeEndCron, selectedTimezone) => {
const setInput = (freezeStartCron, freezeEndCron, selectedTimezone, id = '') => {
store.state.freezeStartCron = freezeStartCron;
store.state.freezeEndCron = freezeEndCron;
store.state.selectedTimezone = selectedTimezone;
store.state.selectedTimezoneIdentifier = selectedTimezone;
store.state.selectedId = id;
wrapper.find('#deploy-freeze-start').trigger('input');
wrapper.find('#deploy-freeze-end').trigger('input');
......@@ -48,18 +52,36 @@ describe('Deploy freeze modal', () => {
describe('Basic interactions', () => {
it('button is disabled when freeze period is invalid', () => {
expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy();
expect(submitDeployFreezeButton().attributes('disabled')).toBeTruthy();
});
});
describe('Adding a new deploy freeze', () => {
const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0];
beforeEach(() => {
const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0];
setInput(freeze_start, freeze_end, cron_timezone);
});
it('button is enabled when valid freeze period settings are present', () => {
expect(addDeployFreezeButton().attributes('disabled')).toBeUndefined();
expect(submitDeployFreezeButton().attributes('disabled')).toBeUndefined();
});
it('should display Add deploy freeze', () => {
expect(findModal().props('title')).toBe('Add deploy freeze');
expect(submitDeployFreezeButton().text()).toBe('Add deploy freeze');
});
it('should call the add deploy freze API', () => {
Api.createFreezePeriod.mockResolvedValue();
findModal().vm.$emit('primary');
expect(Api.createFreezePeriod).toHaveBeenCalledTimes(1);
expect(Api.createFreezePeriod).toHaveBeenCalledWith(store.state.projectId, {
freeze_start,
freeze_end,
cron_timezone,
});
});
});
......@@ -70,7 +92,7 @@ describe('Deploy freeze modal', () => {
});
it('disables the add deploy freeze button', () => {
expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy();
expect(submitDeployFreezeButton().attributes('disabled')).toBeTruthy();
});
});
......@@ -81,7 +103,32 @@ describe('Deploy freeze modal', () => {
});
it('does not disable the submit button', () => {
expect(addDeployFreezeButton().attributes('disabled')).toBeFalsy();
expect(submitDeployFreezeButton().attributes('disabled')).toBeFalsy();
});
});
});
describe('Editing an existing deploy freeze', () => {
const { freeze_start, freeze_end, cron_timezone, id } = freezePeriodsFixture[0];
beforeEach(() => {
setInput(freeze_start, freeze_end, cron_timezone, id);
});
it('should display Edit deploy freeze', () => {
expect(findModal().props('title')).toBe('Edit deploy freeze');
expect(submitDeployFreezeButton().text()).toBe('Save deploy freeze');
});
it('should call the update deploy freze API', () => {
Api.updateFreezePeriod.mockResolvedValue();
findModal().vm.$emit('primary');
expect(Api.updateFreezePeriod).toHaveBeenCalledTimes(1);
expect(Api.updateFreezePeriod).toHaveBeenCalledWith(store.state.projectId, {
id,
freeze_start,
freeze_end,
cron_timezone,
});
});
});
......
......@@ -2,6 +2,7 @@ import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
import createStore from '~/deploy_freeze/store';
import { RECEIVE_FREEZE_PERIODS_SUCCESS } from '~/deploy_freeze/store/mutation_types';
import { freezePeriodsFixture, timezoneDataFixture } from '../helpers';
const localVue = createLocalVue();
......@@ -26,6 +27,7 @@ describe('Deploy freeze table', () => {
const findEmptyFreezePeriods = () => wrapper.find('[data-testid="empty-freeze-periods"]');
const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
const findEditDeployFreezeButton = () => wrapper.find('[data-testid="edit-deploy-freeze"]');
const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
beforeEach(() => {
......@@ -45,17 +47,31 @@ describe('Deploy freeze table', () => {
it('displays empty', () => {
expect(findEmptyFreezePeriods().exists()).toBe(true);
expect(findEmptyFreezePeriods().text()).toBe(
'No deploy freezes exist for this project. To add one, click Add deploy freeze',
'No deploy freezes exist for this project. To add one, select Add deploy freeze',
);
});
it('displays data', () => {
store.state.freezePeriods = freezePeriodsFixture;
describe('with data', () => {
beforeEach(async () => {
store.commit(RECEIVE_FREEZE_PERIODS_SUCCESS, freezePeriodsFixture);
await wrapper.vm.$nextTick();
});
return wrapper.vm.$nextTick(() => {
it('displays data', () => {
const tableRows = findDeployFreezeTable().findAll('tbody tr');
expect(tableRows.length).toBe(freezePeriodsFixture.length);
expect(findEmptyFreezePeriods().exists()).toBe(false);
expect(findEditDeployFreezeButton().exists()).toBe(true);
});
it('allows user to edit deploy freeze', async () => {
findEditDeployFreezeButton().trigger('click');
await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith(
'setFreezePeriod',
store.state.freezePeriods[0],
);
});
});
});
......
......@@ -23,12 +23,46 @@ describe('deploy freeze store actions', () => {
});
Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
Api.createFreezePeriod.mockResolvedValue();
Api.updateFreezePeriod.mockResolvedValue();
});
afterEach(() => {
mock.restore();
});
describe('setSelectedFreezePeriod', () => {
it('commits SET_SELECTED_TIMEZONE mutation', () => {
testAction(
actions.setFreezePeriod,
{
id: 3,
cronTimezone: 'UTC',
freezeStart: 'start',
freezeEnd: 'end',
},
{},
[
{
payload: 3,
type: types.SET_SELECTED_ID,
},
{
payload: 'UTC',
type: types.SET_SELECTED_TIMEZONE,
},
{
payload: 'start',
type: types.SET_FREEZE_START_CRON,
},
{
payload: 'end',
type: types.SET_FREEZE_END_CRON,
},
],
);
});
});
describe('setSelectedTimezone', () => {
it('commits SET_SELECTED_TIMEZONE mutation', () => {
testAction(actions.setSelectedTimezone, {}, {}, [
......@@ -68,10 +102,16 @@ describe('deploy freeze store actions', () => {
state,
[{ type: 'RESET_MODAL' }],
[
{ type: 'requestAddFreezePeriod' },
{ type: 'receiveAddFreezePeriodSuccess' },
{ type: 'requestFreezePeriod' },
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
() =>
expect(Api.createFreezePeriod).toHaveBeenCalledWith(state.projectId, {
freeze_start: state.freezeStartCron,
freeze_end: state.freezeEndCron,
cron_timezone: state.selectedTimezoneIdentifier,
}),
);
});
......@@ -83,7 +123,43 @@ describe('deploy freeze store actions', () => {
{},
state,
[],
[{ type: 'requestAddFreezePeriod' }, { type: 'receiveAddFreezePeriodError' }],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
() => expect(createFlash).toHaveBeenCalled(),
);
});
});
describe('updateFreezePeriod', () => {
it('dispatch correct actions on updating a freeze period', () => {
testAction(
actions.updateFreezePeriod,
{},
state,
[{ type: 'RESET_MODAL' }],
[
{ type: 'requestFreezePeriod' },
{ type: 'receiveFreezePeriodSuccess' },
{ type: 'fetchFreezePeriods' },
],
() =>
expect(Api.updateFreezePeriod).toHaveBeenCalledWith(state.projectId, {
id: state.selectedId,
freeze_start: state.freezeStartCron,
freeze_end: state.freezeEndCron,
cron_timezone: state.selectedTimezoneIdentifier,
}),
);
});
it('should show flash error and set error in state on add failure', () => {
Api.updateFreezePeriod.mockRejectedValue();
testAction(
actions.updateFreezePeriod,
{},
state,
[],
[{ type: 'requestFreezePeriod' }, { type: 'receiveFreezePeriodError' }],
() => expect(createFlash).toHaveBeenCalled(),
);
});
......
......@@ -33,7 +33,10 @@ describe('Deploy freeze mutations', () => {
const expectedFreezePeriods = freezePeriodsFixture.map((freezePeriod, index) => ({
...convertObjectPropsToCamelCase(freezePeriod),
cronTimezone: timezoneNames[index],
cronTimezone: {
formattedTimezone: timezoneNames[index],
identifier: freezePeriod.cronTimezone,
},
}));
expect(stateCopy.freezePeriods).toMatchObject(expectedFreezePeriods);
......@@ -62,11 +65,19 @@ describe('Deploy freeze mutations', () => {
});
});
describe('SET_FREEZE_ENDT_CRON', () => {
describe('SET_FREEZE_END_CRON', () => {
it('should set freezeEndCron', () => {
mutations[types.SET_FREEZE_END_CRON](stateCopy, '5 0 * 8 *');
expect(stateCopy.freezeEndCron).toBe('5 0 * 8 *');
});
});
describe('SET_SELECTED_ID', () => {
it('should set selectedId', () => {
mutations[types.SET_SELECTED_ID](stateCopy, 5);
expect(stateCopy.selectedId).toBe(5);
});
});
});
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