diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-17 21:09:20 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-17 21:09:20 +0300 |
commit | ec72da1833d94bb1556af94193ccf2a93c9cb939 (patch) | |
tree | 6227669a11aaf8370186a7aa6591d5fa9d853bb0 /app/assets/javascripts/deploy_freeze | |
parent | 283fb71e02992b6687e3264d53bbc718b7567109 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/deploy_freeze')
11 files changed, 548 insertions, 0 deletions
diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue new file mode 100644 index 00000000000..05ee77b932a --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue @@ -0,0 +1,147 @@ +<script> +import { GlFormGroup, GlFormInput, GlModal, GlSprintf, GlLink } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import { mapComputed } from '~/vuex_shared/bindings'; +import { __ } from '~/locale'; +import { MODAL_ID } from '../constants'; +import DeployFreezeTimezoneDropdown from './deploy_freeze_timezone_dropdown.vue'; +import { isValidCron } from 'cron-validator'; + +export default { + components: { + GlFormGroup, + GlFormInput, + GlModal, + GlSprintf, + GlLink, + DeployFreezeTimezoneDropdown, + }, + modalOptions: { + ref: 'modal', + modalId: MODAL_ID, + title: __('Add deploy freeze'), + actionCancel: { + text: __('Cancel'), + }, + static: true, + lazy: true, + }, + translations: { + cronPlaceholder: __('* * * * *'), + cronSyntaxInstructions: __( + 'Define a custom deploy freeze pattern with %{cronSyntaxStart}cron syntax%{cronSyntaxEnd}', + ), + }, + computed: { + ...mapState([ + 'projectId', + 'selectedTimezone', + 'timezoneData', + 'freezeStartCron', + 'freezeEndCron', + 'selectedTimezone', + ]), + ...mapComputed([ + { key: 'freezeStartCron', updateFn: 'setFreezeStartCron' }, + { key: 'freezeEndCron', updateFn: 'setFreezeEndCron' }, + ]), + addDeployFreezeButton() { + return { + text: __('Add deploy freeze'), + attributes: [ + { variant: 'success' }, + { + disabled: + !isValidCron(this.freezeStartCron) || + !isValidCron(this.freezeEndCron) || + !this.selectedTimezone, + }, + ], + }; + }, + invalidFreezeStartCron() { + return this.invalidCronMessage(this.freezeStartCronState); + }, + freezeStartCronState() { + return Boolean(!this.freezeStartCron || isValidCron(this.freezeStartCron)); + }, + invalidFreezeEndCron() { + return this.invalidCronMessage(this.freezeEndCronState); + }, + freezeEndCronState() { + return Boolean(!this.freezeEndCron || isValidCron(this.freezeEndCron)); + }, + }, + methods: { + ...mapActions(['addFreezePeriod', 'setSelectedTimezone', 'resetModal']), + resetModalHandler() { + this.resetModal(); + }, + invalidCronMessage(validCronState) { + if (!validCronState) { + return __('This Cron pattern is invalid'); + } + return ''; + }, + }, +}; +</script> + +<template> + <gl-modal + v-bind="$options.modalOptions" + :action-primary="addDeployFreezeButton" + @primary="addFreezePeriod" + @canceled="resetModalHandler" + > + <p> + <gl-sprintf :message="$options.translations.cronSyntaxInstructions"> + <template #cronSyntax="{ content }"> + <gl-link href="https://crontab.guru/" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </p> + + <gl-form-group + :label="__('Freeze start')" + label-for="deploy-freeze-start" + :invalid-feedback="invalidFreezeStartCron" + :state="freezeStartCronState" + > + <gl-form-input + id="deploy-freeze-start" + v-model="freezeStartCron" + class="gl-font-monospace!" + data-qa-selector="deploy_freeze_start_field" + :placeholder="this.$options.translations.cronPlaceholder" + :state="freezeStartCronState" + trim + /> + </gl-form-group> + + <gl-form-group + :label="__('Freeze end')" + label-for="deploy-freeze-end" + :invalid-feedback="invalidFreezeEndCron" + :state="freezeEndCronState" + > + <gl-form-input + id="deploy-freeze-end" + v-model="freezeEndCron" + class="gl-font-monospace!" + data-qa-selector="deploy_freeze_end_field" + :placeholder="this.$options.translations.cronPlaceholder" + :state="freezeEndCronState" + trim + /> + </gl-form-group> + + <gl-form-group :label="__('Cron time zone')" label-for="cron-time-zone-dropdown"> + <deploy-freeze-timezone-dropdown + :timezone-data="timezoneData" + :value="selectedTimezone" + @selectTimezone="setSelectedTimezone" + /> + </gl-form-group> + </gl-modal> +</template> diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue new file mode 100644 index 00000000000..fc2ed10f3ca --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue @@ -0,0 +1,18 @@ +<script> +import DeployFreezeTable from './deploy_freeze_table.vue'; +import DeployFreezeModal from './deploy_freeze_modal.vue'; + +export default { + components: { + DeployFreezeTable, + DeployFreezeModal, + }, +}; +</script> + +<template> + <div> + <deploy-freeze-table /> + <deploy-freeze-modal /> + </div> +</template> diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue new file mode 100644 index 00000000000..b80df5d4f1e --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue @@ -0,0 +1,84 @@ +<script> +import { GlTable, GlButton, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { s__, __ } from '~/locale'; +import { mapState, mapActions } from 'vuex'; +import { MODAL_ID } from '../constants'; + +export default { + modalId: MODAL_ID, + fields: [ + { + key: 'freezeStart', + label: s__('DeployFreeze|Freeze start'), + }, + { + key: 'freezeEnd', + label: s__('DeployFreeze|Freeze end'), + }, + { + key: 'cronTimezone', + label: s__('DeployFreeze|Time zone'), + }, + ], + translations: { + addDeployFreeze: __('Add deploy freeze'), + }, + components: { + GlTable, + GlButton, + GlSprintf, + }, + directives: { + GlModalDirective, + }, + computed: { + ...mapState(['freezePeriods']), + tableIsNotEmpty() { + return this.freezePeriods?.length > 0; + }, + }, + mounted() { + this.fetchFreezePeriods(); + }, + methods: { + ...mapActions(['fetchFreezePeriods']), + }, +}; +</script> + +<template> + <div class="deploy-freeze-table"> + <gl-table + data-testid="deploy-freeze-table" + :items="freezePeriods" + :fields="$options.fields" + show-empty + > + <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}', + ) + " + > + <template #strong="{ content }"> + <strong>{{ content }}</strong> + </template> + </gl-sprintf> + </p> + </template> + </gl-table> + <div class="gl-display-flex gl-justify-content-center"> + <gl-button + v-gl-modal-directive="$options.modalId" + data-testid="add-deploy-freeze" + category="primary" + variant="success" + > + {{ $options.translations.addDeployFreeze }} + </gl-button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/deploy_freeze/components/deploy_freeze_timezone_dropdown.vue b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_timezone_dropdown.vue new file mode 100644 index 00000000000..09f6d9460ea --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/components/deploy_freeze_timezone_dropdown.vue @@ -0,0 +1,107 @@ +<script> +import { GlNewDropdown, GlDropdownItem, GlSearchBoxByType, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import autofocusonshow from '~/vue_shared/directives/autofocusonshow'; + +export default { + name: 'DeployFreezeTimezoneDropdown', + components: { + GlNewDropdown, + GlDropdownItem, + GlSearchBoxByType, + GlIcon, + }, + directives: { + autofocusonshow, + }, + props: { + value: { + type: String, + required: false, + default: '', + }, + timezoneData: { + type: Array, + required: true, + default: () => [], + }, + }, + data() { + return { + searchTerm: this.value || '', + }; + }, + tranlations: { + noResultsText: __('No matching results'), + }, + computed: { + timezones() { + return this.timezoneData.map(timezone => ({ + formattedTimezone: this.formatTimezone(timezone), + identifier: timezone.identifier, + })); + }, + filteredResults() { + const lowerCasedSearchTerm = this.searchTerm.toLowerCase(); + return this.timezones.filter(timezone => + timezone.formattedTimezone.toLowerCase().includes(lowerCasedSearchTerm), + ); + }, + selectTimezoneLabel() { + return this.value || __('Select timezone'); + }, + }, + watch: { + value(newVal) { + this.searchTerm = newVal; + }, + }, + methods: { + selectTimezone(selected) { + this.$emit('selectTimezone', selected); + this.searchTerm = ''; + }, + isSelected(timezone) { + return this.value === timezone.formattedTimezone; + }, + formatUtcOffset(offset) { + const parsed = parseInt(offset, 10); + if (Number.isNaN(parsed) || parsed === 0) { + return `0`; + } + const prefix = offset > 0 ? '+' : '-'; + return `${prefix}${Math.abs(offset / 3600)}`; + }, + formatTimezone(item) { + return `[UTC ${this.formatUtcOffset(item.offset)}] ${item.name}`; + }, + }, +}; +</script> +<template> + <gl-new-dropdown :text="value" block lazy menu-class="gl-w-full!"> + <template #button-content> + <span ref="buttonText" class="gl-flex-grow-1" :class="{ 'gl-text-gray-500': !value }">{{ + selectTimezoneLabel + }}</span> + <gl-icon name="chevron-down" /> + </template> + + <gl-search-box-by-type v-model.trim="searchTerm" v-autofocusonshow autofocus class="gl-m-3" /> + <gl-dropdown-item + v-for="timezone in filteredResults" + :key="timezone.formattedTimezone" + @click="selectTimezone(timezone)" + > + <gl-icon + :class="{ invisible: !isSelected(timezone) }" + name="mobile-issue-close" + class="gl-vertical-align-middle" + /> + {{ timezone.formattedTimezone }} + </gl-dropdown-item> + <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults"> + {{ $options.tranlations.noResultsText }} + </gl-dropdown-item> + </gl-new-dropdown> +</template> diff --git a/app/assets/javascripts/deploy_freeze/constants.js b/app/assets/javascripts/deploy_freeze/constants.js new file mode 100644 index 00000000000..79e556e0b55 --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/constants.js @@ -0,0 +1,5 @@ +export const MODAL_ID = 'deploy-freeze-modal'; + +export default { + MODAL_ID, +}; diff --git a/app/assets/javascripts/deploy_freeze/index.js b/app/assets/javascripts/deploy_freeze/index.js new file mode 100644 index 00000000000..fd3f52b6da1 --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/index.js @@ -0,0 +1,22 @@ +import Vue from 'vue'; +import DeployFreezeSettings from './components/deploy_freeze_settings.vue'; +import createStore from './store'; + +export default () => { + const el = document.getElementById('js-deploy-freeze-table'); + + const { projectId, timezoneData } = el.dataset; + + const store = createStore({ + projectId, + timezoneData: JSON.parse(timezoneData), + }); + + return new Vue({ + el, + store, + render(createElement) { + return createElement(DeployFreezeSettings); + }, + }); +}; diff --git a/app/assets/javascripts/deploy_freeze/store/actions.js b/app/assets/javascripts/deploy_freeze/store/actions.js new file mode 100644 index 00000000000..e4c649ac4c3 --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/store/actions.js @@ -0,0 +1,77 @@ +import * as types from './mutation_types'; +import Api from '~/api'; +import createFlash from '~/flash'; +import { __ } from '~/locale'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; + +export const requestAddFreezePeriod = ({ commit }) => { + commit(types.REQUEST_ADD_FREEZE_PERIOD); +}; + +export const receiveAddFreezePeriodSuccess = ({ commit }) => { + commit(types.RECEIVE_ADD_FREEZE_PERIOD_SUCCESS); +}; + +export const receiveAddFreezePeriodError = ({ commit }, error) => { + commit(types.RECEIVE_ADD_FREEZE_PERIOD_ERROR, error); +}; + +export const addFreezePeriod = ({ state, dispatch, commit }) => { + dispatch('requestAddFreezePeriod'); + + return Api.createFreezePeriod(state.projectId, { + freeze_start: state.freezeStartCron, + freeze_end: state.freezeEndCron, + cron_timezone: state.selectedTimezoneIdentifier, + }) + .then(() => { + dispatch('receiveAddFreezePeriodSuccess'); + commit(types.RESET_MODAL); + dispatch('fetchFreezePeriods'); + }) + .catch(error => { + createFlash(__('Error: Unable to create deploy freeze')); + dispatch('receiveAddFreezePeriodError', error); + }); +}; + +export const requestFreezePeriods = ({ commit }) => { + commit(types.REQUEST_FREEZE_PERIODS); +}; +export const receiveFreezePeriodsSuccess = ({ state, commit }, freezePeriods) => { + const addTimezoneIdentifier = freezePeriod => + convertObjectPropsToCamelCase({ + ...freezePeriod, + cron_timezone: state.timezoneData.find(tz => tz.identifier === freezePeriod.cron_timezone) + ?.name, + }); + + commit(types.RECEIVE_FREEZE_PERIODS_SUCCESS, freezePeriods.map(addTimezoneIdentifier)); +}; + +export const fetchFreezePeriods = ({ dispatch, state }) => { + dispatch('requestFreezePeriods'); + + return Api.freezePeriods(state.projectId) + .then(({ data }) => { + dispatch('receiveFreezePeriodsSuccess', convertObjectPropsToCamelCase(data)); + }) + .catch(() => { + createFlash(__('There was an error fetching the deploy freezes.')); + }); +}; + +export const setSelectedTimezone = ({ commit }, timezone) => { + commit(types.SET_SELECTED_TIMEZONE, timezone); +}; +export const setFreezeStartCron = ({ commit }, { freezeStartCron }) => { + commit(types.SET_FREEZE_START_CRON, freezeStartCron); +}; + +export const setFreezeEndCron = ({ commit }, { freezeEndCron }) => { + commit(types.SET_FREEZE_END_CRON, freezeEndCron); +}; + +export const resetModal = ({ commit }) => { + commit(types.RESET_MODAL); +}; diff --git a/app/assets/javascripts/deploy_freeze/store/index.js b/app/assets/javascripts/deploy_freeze/store/index.js new file mode 100644 index 00000000000..ca7ea8c783c --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/store/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import mutations from './mutations'; +import createState from './state'; + +Vue.use(Vuex); + +export default initialState => + new Vuex.Store({ + actions, + mutations, + state: createState(initialState), + }); diff --git a/app/assets/javascripts/deploy_freeze/store/mutation_types.js b/app/assets/javascripts/deploy_freeze/store/mutation_types.js new file mode 100644 index 00000000000..47a4874a5cf --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/store/mutation_types.js @@ -0,0 +1,12 @@ +export const REQUEST_FREEZE_PERIODS = 'REQUEST_FREEZE_PERIODS'; +export const RECEIVE_FREEZE_PERIODS_SUCCESS = 'RECEIVE_FREEZE_PERIODS_SUCCESS'; + +export const REQUEST_ADD_FREEZE_PERIOD = 'REQUEST_ADD_FREEZE_PERIOD'; +export const RECEIVE_ADD_FREEZE_PERIOD_SUCCESS = 'RECEIVE_ADD_FREEZE_PERIOD_SUCCESS'; +export const RECEIVE_ADD_FREEZE_PERIOD_ERROR = 'RECEIVE_ADD_FREEZE_PERIOD_ERROR'; + +export const SET_SELECTED_TIMEZONE = 'SET_SELECTED_TIMEZONE'; +export const SET_FREEZE_START_CRON = 'SET_FREEZE_START_CRON'; +export const SET_FREEZE_END_CRON = 'SET_FREEZE_END_CRON'; + +export const RESET_MODAL = 'RESET_MODAL'; diff --git a/app/assets/javascripts/deploy_freeze/store/mutations.js b/app/assets/javascripts/deploy_freeze/store/mutations.js new file mode 100644 index 00000000000..57b4b226b16 --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/store/mutations.js @@ -0,0 +1,45 @@ +import * as types from './mutation_types'; + +export default { + [types.REQUEST_FREEZE_PERIODS](state) { + state.isLoading = true; + }, + + [types.RECEIVE_FREEZE_PERIODS_SUCCESS](state, freezePeriods) { + state.isLoading = false; + state.freezePeriods = freezePeriods; + }, + + [types.REQUEST_ADD_FREEZE_PERIOD](state) { + state.isLoading = true; + }, + + [types.RECEIVE_ADD_FREEZE_PERIOD_SUCCESS](state) { + state.isLoading = false; + }, + + [types.RECEIVE_ADD_FREEZE_PERIOD_ERROR](state, error) { + state.isLoading = false; + state.error = error; + }, + + [types.SET_SELECTED_TIMEZONE](state, timezone) { + state.selectedTimezone = timezone.formattedTimezone; + state.selectedTimezoneIdentifier = timezone.identifier; + }, + + [types.SET_FREEZE_START_CRON](state, freezeStartCron) { + state.freezeStartCron = freezeStartCron; + }, + + [types.SET_FREEZE_END_CRON](state, freezeEndCron) { + state.freezeEndCron = freezeEndCron; + }, + + [types.RESET_MODAL](state) { + state.freezeStartCron = ''; + state.freezeEndCron = ''; + state.selectedTimezone = ''; + state.selectedTimezoneIdentifier = ''; + }, +}; diff --git a/app/assets/javascripts/deploy_freeze/store/state.js b/app/assets/javascripts/deploy_freeze/store/state.js new file mode 100644 index 00000000000..4cc38c097b6 --- /dev/null +++ b/app/assets/javascripts/deploy_freeze/store/state.js @@ -0,0 +1,17 @@ +export default ({ + projectId, + freezePeriods = [], + timezoneData = [], + selectedTimezone = '', + selectedTimezoneIdentifier = '', + freezeStartCron = '', + freezeEndCron = '', +}) => ({ + projectId, + freezePeriods, + timezoneData, + selectedTimezone, + selectedTimezoneIdentifier, + freezeStartCron, + freezeEndCron, +}); |