Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-17 21:09:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-17 21:09:20 +0300
commitec72da1833d94bb1556af94193ccf2a93c9cb939 (patch)
tree6227669a11aaf8370186a7aa6591d5fa9d853bb0 /app/assets/javascripts/deploy_freeze
parent283fb71e02992b6687e3264d53bbc718b7567109 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/deploy_freeze')
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_modal.vue147
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_settings.vue18
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_table.vue84
-rw-r--r--app/assets/javascripts/deploy_freeze/components/deploy_freeze_timezone_dropdown.vue107
-rw-r--r--app/assets/javascripts/deploy_freeze/constants.js5
-rw-r--r--app/assets/javascripts/deploy_freeze/index.js22
-rw-r--r--app/assets/javascripts/deploy_freeze/store/actions.js77
-rw-r--r--app/assets/javascripts/deploy_freeze/store/index.js14
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/deploy_freeze/store/mutations.js45
-rw-r--r--app/assets/javascripts/deploy_freeze/store/state.js17
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,
+});