diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-23 09:09:07 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-01-23 09:09:07 +0000 |
commit | f47c768fad17d4c876e96524f83f8306f071db66 (patch) | |
tree | 9350146fdfb67f001716357d745436338621b590 | |
parent | 0f8c2334f0e57a22bf10e4485c17f856289e4fb4 (diff) | |
download | gitlab-ce-f47c768fad17d4c876e96524f83f8306f071db66.tar.gz |
Add latest changes from gitlab-org/gitlab@master
16 files changed, 570 insertions, 416 deletions
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue index 5d27c6eb865..ee10a1e92fc 100644 --- a/app/assets/javascripts/diffs/components/diff_file_header.vue +++ b/app/assets/javascripts/diffs/components/diff_file_header.vue @@ -210,6 +210,9 @@ export default { :text="diffFile.file_path" :gfm="gfmCopyText" css-class="btn-default btn-transparent btn-clipboard" + data-track-event="click_copy_file_button" + data-track-label="diff_copy_file_path_button" + data-track-property="diff_copy_file" /> <small v-if="isModeChanged" ref="fileMode" class="mr-1"> @@ -233,6 +236,9 @@ export default { :class="{ active: diffHasExpandedDiscussions(diffFile) }" class="js-btn-vue-toggle-comments btn" data-qa-selector="toggle_comments_button" + data-track-event="click_toggle_comments_button" + data-track-label="diff_toggle_comments_button" + data-track-property="diff_toggle_comments" type="button" @click="toggleFileDiscussionWrappers(diffFile)" > @@ -245,6 +251,9 @@ export default { :can-current-user-fork="canCurrentUserFork" :edit-path="diffFile.edit_path" :can-modify-blob="diffFile.can_modify_blob" + data-track-event="click_toggle_edit_button" + data-track-label="diff_toggle_edit_button" + data-track-property="diff_toggle_edit" @showForkMessage="showForkMessage" /> </template> @@ -263,6 +272,9 @@ export default { v-gl-tooltip.hover :title="expandDiffToFullFileTitle" class="expand-file" + data-track-event="click_toggle_view_full_button" + data-track-label="diff_toggle_view_full_button" + data-track-property="diff_toggle_view_full" @click="toggleFullDiff(diffFile.file_path)" > <gl-loading-icon v-if="diffFile.isLoadingFullFile" color="dark" inline /> @@ -275,6 +287,9 @@ export default { :href="diffFile.view_path" target="blank" class="view-file" + data-track-event="click_toggle_view_sha_button" + data-track-label="diff_toggle_view_sha_button" + data-track-property="diff_toggle_view_sha" :title="viewFileButtonText" > <icon name="doc-text" /> @@ -288,6 +303,9 @@ export default { :title="`View on ${diffFile.formatted_external_url}`" target="_blank" rel="noopener noreferrer" + data-track-event="click_toggle_external_button" + data-track-label="diff_toggle_external_button" + data-track-property="diff_toggle_external" class="btn btn-file-option" > <icon name="external-link" /> diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index f6f119d4463..211990f3d7c 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -27,10 +27,12 @@ import GroupEmptyState from './group_empty_state.vue'; import DashboardsDropdown from './dashboards_dropdown.vue'; import TrackEventDirective from '~/vue_shared/directives/track_event'; -import { getTimeDiff, getAddMetricTrackingOptions } from '../utils'; -import { metricStates } from '../constants'; +import { getAddMetricTrackingOptions } from '../utils'; +import { getTimeRange } from './date_time_picker/date_time_picker_lib'; -const defaultTimeDiff = getTimeDiff(); +import { datePickerTimeWindows, metricStates } from '../constants'; + +const defaultTimeDiff = getTimeRange(); export default { components: { @@ -191,6 +193,7 @@ export default { startDate: getParameterValues('start')[0] || defaultTimeDiff.start, endDate: getParameterValues('end')[0] || defaultTimeDiff.end, hasValidDates: true, + datePickerTimeWindows, isRearrangingPanels: false, }; }, @@ -426,6 +429,7 @@ export default { <date-time-picker :start="startDate" :end="endDate" + :time-windows="datePickerTimeWindows" @apply="onDateTimePickerApply" @invalid="onDateTimePickerInvalid" /> diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue index 0aa710b1b3a..3a18a494cad 100644 --- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue +++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue @@ -1,19 +1,18 @@ <script> import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; -import { s__, sprintf } from '~/locale'; +import { __, sprintf } from '~/locale'; import Icon from '~/vue_shared/components/icon.vue'; import DateTimePickerInput from './date_time_picker_input.vue'; import { - getTimeDiff, + defaultTimeWindows, isValidDate, - getTimeWindow, + getTimeRange, + getTimeWindowKey, stringToISODate, ISODateToString, truncateZerosInDateTime, isDateTimePickerInputValid, -} from '~/monitoring/utils'; - -import { timeWindows } from '~/monitoring/constants'; +} from './date_time_picker_lib'; const events = { apply: 'apply', @@ -41,7 +40,7 @@ export default { timeWindows: { type: Object, required: false, - default: () => timeWindows, + default: () => defaultTimeWindows, }, }, data() { @@ -81,11 +80,11 @@ export default { }, timeWindowText() { - const timeWindow = getTimeWindow({ start: this.start, end: this.end }); + const timeWindow = getTimeWindowKey({ start: this.start, end: this.end }, this.timeWindows); if (timeWindow) { - return this.timeWindows[timeWindow]; + return this.timeWindows[timeWindow].label; } else if (isValidDate(this.start) && isValidDate(this.end)) { - return sprintf(s__('%{start} to %{end}'), { + return sprintf(__('%{start} to %{end}'), { start: this.formatDate(this.start), end: this.formatDate(this.end), }); @@ -104,7 +103,7 @@ export default { return truncateZerosInDateTime(ISODateToString(date)); }, setTimeWindow(key) { - const { start, end } = getTimeDiff(key); + const { start, end } = getTimeRange(key, this.timeWindows); this.startDate = start; this.endDate = end; @@ -161,18 +160,18 @@ export default { class="col-md-4 p-0 m-0" > <gl-dropdown-item - v-for="(value, key) in timeWindows" + v-for="(timeWindow, key) in timeWindows" :key="key" - :active="value === timeWindowText" + :active="timeWindow.label === timeWindowText" active-class="active" @click="setTimeWindow(key)" > <icon name="mobile-issue-close" class="align-bottom" - :class="{ invisible: value !== timeWindowText }" + :class="{ invisible: timeWindow.label !== timeWindowText }" /> - {{ value }} + {{ timeWindow.label }} </gl-dropdown-item> </gl-form-group> </div> diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue index c3beae18726..b27a379c46a 100644 --- a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue +++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue @@ -1,14 +1,14 @@ <script> import _ from 'underscore'; import { GlFormGroup, GlFormInput } from '@gitlab/ui'; -import { s__, sprintf } from '~/locale'; -import { dateFormats } from '~/monitoring/constants'; +import { __, sprintf } from '~/locale'; +import { dateFormats } from './date_time_picker_lib'; const inputGroupText = { - invalidFeedback: sprintf(s__('Format: %{dateFormat}'), { - dateFormat: dateFormats.dateTimePicker.format, + invalidFeedback: sprintf(__('Format: %{dateFormat}'), { + dateFormat: dateFormats.stringDate, }), - placeholder: dateFormats.dateTimePicker.format, + placeholder: dateFormats.stringDate, }; export default { diff --git a/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_lib.js b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_lib.js new file mode 100644 index 00000000000..604b17baab9 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_lib.js @@ -0,0 +1,112 @@ +import dateformat from 'dateformat'; +import { __ } from '~/locale'; +import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; + +/** + * Valid strings for this regex are + * 2019-10-01 and 2019-10-01 01:02:03 + */ +const dateTimePickerRegex = /^(\d{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])(?: (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))?$/; + +export const defaultTimeWindows = { + thirtyMinutes: { + label: __('30 minutes'), + seconds: 60 * 30, + }, + threeHours: { + label: __('3 hours'), + seconds: 60 * 60 * 3, + }, + eightHours: { + label: __('8 hours'), + seconds: 60 * 60 * 8, + default: true, + }, + oneDay: { + label: __('1 day'), + seconds: 60 * 60 * 24 * 1, + }, + threeDays: { + label: __('3 days'), + seconds: 60 * 60 * 24 * 3, + }, +}; + +export const dateFormats = { + ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'", + stringDate: 'yyyy-mm-dd HH:MM:ss', +}; + +/** + * The URL params start and end need to be validated + * before passing them down to other components. + * + * @param {string} dateString + * @returns true if the string is a valid date, false otherwise + */ +export const isValidDate = dateString => { + try { + // dateformat throws error that can be caught. + // This is better than using `new Date()` + if (dateString && dateString.trim()) { + dateformat(dateString, 'isoDateTime'); + return true; + } + return false; + } catch (e) { + return false; + } +}; + +export const getTimeRange = (timeWindowKey, timeWindows = defaultTimeWindows) => { + let difference; + if (timeWindows[timeWindowKey]) { + difference = timeWindows[timeWindowKey].seconds; + } else { + const [defaultEntry] = Object.entries(timeWindows).filter( + ([, timeWindow]) => timeWindow.default, + ); + // find default time window + difference = defaultEntry[1].seconds; + } + + const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds + const start = end - difference; + + return { + start: new Date(secondsToMilliseconds(start)).toISOString(), + end: new Date(secondsToMilliseconds(end)).toISOString(), + }; +}; + +export const getTimeWindowKey = ({ start, end }, timeWindows = defaultTimeWindows) => + Object.entries(timeWindows).reduce((acc, [timeWindowKey, timeWindow]) => { + if (new Date(end) - new Date(start) === secondsToMilliseconds(timeWindow.seconds)) { + return timeWindowKey; + } + return acc; + }, null); + +/** + * Convert the input in Time picker component to ISO date. + * + * @param {string} val + * @returns {string} + */ +export const stringToISODate = val => + dateformat(new Date(val.replace(/-/g, '/')), dateFormats.ISODate, true); + +/** + * Convert the ISO date received from the URL to string + * for the Time picker component. + * + * @param {Date} date + * @returns {string} + */ +export const ISODateToString = date => dateformat(date, dateFormats.stringDate); + +export const truncateZerosInDateTime = datetime => datetime.replace(' 00:00:00', ''); + +export const isDateTimePickerInputValid = val => dateTimePickerRegex.test(val); + +export default {}; diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue index 2f562071764..10c081070d7 100644 --- a/app/assets/javascripts/monitoring/components/embed.vue +++ b/app/assets/javascripts/monitoring/components/embed.vue @@ -3,7 +3,7 @@ import { mapActions, mapState, mapGetters } from 'vuex'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import { getParameterValues, removeParams } from '~/lib/utils/url_utility'; import { sidebarAnimationDuration } from '../constants'; -import { getTimeDiff } from '../utils'; +import { getTimeRange } from './date_time_picker/date_time_picker_lib'; let sidebarMutationObserver; @@ -18,7 +18,7 @@ export default { }, }, data() { - const defaultRange = getTimeDiff(); + const defaultRange = getTimeRange(); const start = getParameterValues('start', this.dashboardUrl)[0] || defaultRange.start; const end = getParameterValues('end', this.dashboardUrl)[0] || defaultRange.end; diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 398b45b9012..2d5361a5029 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -50,11 +50,6 @@ export const metricStates = { export const sidebarAnimationDuration = 300; // milliseconds. export const chartHeight = 300; -/** - * Valid strings for this regex are - * 2019-10-01 and 2019-10-01 01:02:03 - */ -export const dateTimePickerRegex = /^(\d{4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])(?: (0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]))?$/; export const graphTypes = { deploymentData: 'scatter', @@ -83,38 +78,39 @@ export const lineWidths = { default: 2, }; -export const timeWindows = { - thirtyMinutes: __('30 minutes'), - threeHours: __('3 hours'), - eightHours: __('8 hours'), - oneDay: __('1 day'), - threeDays: __('3 days'), - oneWeek: __('1 week'), -}; - export const dateFormats = { timeOfDay: 'h:MM TT', default: 'dd mmm yyyy, h:MMTT', - dateTimePicker: { - format: 'yyyy-mm-dd hh:mm:ss', - ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'", - stringDate: 'yyyy-mm-dd HH:MM:ss', - }, }; -export const secondsIn = { - thirtyMinutes: 60 * 30, - threeHours: 60 * 60 * 3, - eightHours: 60 * 60 * 8, - oneDay: 60 * 60 * 24 * 1, - threeDays: 60 * 60 * 24 * 3, - oneWeek: 60 * 60 * 24 * 7 * 1, +export const datePickerTimeWindows = { + thirtyMinutes: { + label: __('30 minutes'), + seconds: 60 * 30, + }, + threeHours: { + label: __('3 hours'), + seconds: 60 * 60 * 3, + }, + eightHours: { + label: __('8 hours'), + seconds: 60 * 60 * 8, + default: true, + }, + oneDay: { + label: __('1 day'), + seconds: 60 * 60 * 24 * 1, + }, + threeDays: { + label: __('3 days'), + seconds: 60 * 60 * 24 * 3, + }, + oneWeek: { + label: __('1 week'), + seconds: 60 * 60 * 24 * 7 * 1, + }, + twoWeeks: { + label: __('2 weeks'), + seconds: 60 * 60 * 24 * 7 * 2, + }, }; - -export const timeWindowsKeyNames = Object.keys(secondsIn).reduce( - (otherTimeWindows, timeWindow) => ({ - ...otherTimeWindows, - [timeWindow]: timeWindow, - }), - {}, -); diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index c824d6d4ddb..3847d885f9a 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -1,68 +1,3 @@ -import dateformat from 'dateformat'; -import { secondsIn, dateTimePickerRegex, dateFormats } from './constants'; -import { secondsToMilliseconds } from '~/lib/utils/datetime_utility'; - -export const getTimeDiff = timeWindow => { - const end = Math.floor(Date.now() / 1000); // convert milliseconds to seconds - const difference = secondsIn[timeWindow] || secondsIn.eightHours; - const start = end - difference; - - return { - start: new Date(secondsToMilliseconds(start)).toISOString(), - end: new Date(secondsToMilliseconds(end)).toISOString(), - }; -}; - -export const getTimeWindow = ({ start, end }) => - Object.entries(secondsIn).reduce((acc, [timeRange, value]) => { - if (new Date(end) - new Date(start) === secondsToMilliseconds(value)) { - return timeRange; - } - return acc; - }, null); - -export const isDateTimePickerInputValid = val => dateTimePickerRegex.test(val); - -export const truncateZerosInDateTime = datetime => datetime.replace(' 00:00:00', ''); - -/** - * The URL params start and end need to be validated - * before passing them down to other components. - * - * @param {string} dateString - */ -export const isValidDate = dateString => { - try { - // dateformat throws error that can be caught. - // This is better than using `new Date()` - if (dateString && dateString.trim()) { - dateformat(dateString, 'isoDateTime'); - return true; - } - return false; - } catch (e) { - return false; - } -}; - -/** - * Convert the input in Time picker component to ISO date. - * - * @param {string} val - * @returns {string} - */ -export const stringToISODate = val => - dateformat(new Date(val.replace(/-/g, '/')), dateFormats.dateTimePicker.ISODate, true); - -/** - * Convert the ISO date received from the URL to string - * for the Time picker component. - * - * @param {Date} date - * @returns {string} - */ -export const ISODateToString = date => dateformat(date, dateFormats.dateTimePicker.stringDate); - /** * This method is used to validate if the graph data format for a chart component * that needs a time series as a response from a prometheus query (query_range) is diff --git a/changelogs/unreleased/197412.yml b/changelogs/unreleased/197412.yml new file mode 100644 index 00000000000..5118cbdd2a2 --- /dev/null +++ b/changelogs/unreleased/197412.yml @@ -0,0 +1,5 @@ +--- +title: Track usage of merge request file header buttons +merge_request: +author: Oregand +type: other diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md index 4a79fc33da3..c6389ee5093 100644 --- a/doc/ci/yaml/README.md +++ b/doc/ci/yaml/README.md @@ -1342,8 +1342,7 @@ In its simplest form, the `environment` keyword can be defined like: deploy to production: stage: deploy script: git push production HEAD:master - environment: - name: production + environment: production ``` In the above example, the `deploy to production` job will be marked as doing a diff --git a/locale/gitlab.pot b/locale/gitlab.pot index e06754b9db5..65f2a948f20 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6801,6 +6801,9 @@ msgstr "" msgid "Email address" msgstr "" +msgid "Email display name" +msgstr "" + msgid "Email domain is not editable in subgroups. Value inherited from top-level parent group." msgstr "" @@ -6846,6 +6849,9 @@ msgstr "" msgid "Emails" msgstr "" +msgid "Emails sent from Service Desk will have this name" +msgstr "" + msgid "Emails separated by comma" msgstr "" @@ -8918,6 +8924,9 @@ msgstr "" msgid "GitLab Shared Runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com)." msgstr "" +msgid "GitLab Support Bot" +msgstr "" + msgid "GitLab User" msgstr "" diff --git a/spec/frontend/monitoring/components/dashboard_time_window_spec.js b/spec/frontend/monitoring/components/dashboard_time_window_spec.js index 8e2f52a3183..29cca695093 100644 --- a/spec/frontend/monitoring/components/dashboard_time_window_spec.js +++ b/spec/frontend/monitoring/components/dashboard_time_window_spec.js @@ -42,7 +42,7 @@ describe('dashboard time window', () => { mock.restore(); }); - it('shows an error message if invalid url parameters are passed', done => { + it('shows an active quick range option', done => { mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsDashboardPayload); createComponentWrapperMounted({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); @@ -55,6 +55,7 @@ describe('dashboard time window', () => { const timeWindowDropdownItems = wrapper .find('.js-time-window-dropdown') .findAll(GlDropdownItem); + const activeItem = timeWindowDropdownItems.wrappers.filter(itemWrapper => itemWrapper.find('.active').exists(), ); diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_lib_spec.js new file mode 100644 index 00000000000..9c0f66427ae --- /dev/null +++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_lib_spec.js @@ -0,0 +1,264 @@ +import * as dateTimePickerLib from '~/monitoring/components/date_time_picker/date_time_picker_lib'; + +describe('date time picker lib', () => { + describe('isValidDate', () => { + [ + { + input: '2019-09-09T00:00:00.000Z', + output: true, + }, + { + input: '2019-09-09T000:00.000Z', + output: false, + }, + { + input: 'a2019-09-09T000:00.000Z', + output: false, + }, + { + input: '2019-09-09T', + output: false, + }, + { + input: '2019-09-09', + output: true, + }, + { + input: '2019-9-9', + output: true, + }, + { + input: '2019-9-', + output: true, + }, + { + input: '2019--', + output: false, + }, + { + input: '2019', + output: true, + }, + { + input: '', + output: false, + }, + { + input: null, + output: false, + }, + ].forEach(({ input, output }) => { + it(`isValidDate return ${output} for ${input}`, () => { + expect(dateTimePickerLib.isValidDate(input)).toBe(output); + }); + }); + }); + + describe('getTimeWindow', () => { + [ + { + args: [ + { + start: '2019-10-01T18:27:47.000Z', + end: '2019-10-01T21:27:47.000Z', + }, + dateTimePickerLib.defaultTimeWindows, + ], + expected: 'threeHours', + }, + { + args: [ + { + start: '2019-10-01T28:27:47.000Z', + end: '2019-10-01T21:27:47.000Z', + }, + dateTimePickerLib.defaultTimeWindows, + ], + expected: null, + }, + { + args: [ + { + start: '', + end: '', + }, + dateTimePickerLib.defaultTimeWindows, + ], + expected: null, + }, + { + args: [ + { + start: null, + end: null, + }, + dateTimePickerLib.defaultTimeWindows, + ], + expected: null, + }, + { + args: [{}, dateTimePickerLib.defaultTimeWindows], + expected: null, + }, + ].forEach(({ args, expected }) => { + it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => { + expect(dateTimePickerLib.getTimeWindowKey(...args)).toEqual(expected); + }); + }); + }); + + describe('getTimeRange', () => { + function secondsBetween({ start, end }) { + return (new Date(end) - new Date(start)) / 1000; + } + + function minutesBetween(timeRange) { + return secondsBetween(timeRange) / 60; + } + + function hoursBetween(timeRange) { + return minutesBetween(timeRange) / 60; + } + + it('defaults to an 8 hour (28800s) difference', () => { + const params = dateTimePickerLib.getTimeRange(); + + expect(hoursBetween(params)).toEqual(8); + }); + + it('accepts time window as an argument', () => { + const params = dateTimePickerLib.getTimeRange('thirtyMinutes'); + + expect(minutesBetween(params)).toEqual(30); + }); + + it('returns a value for every defined time window', () => { + const nonDefaultWindows = Object.entries(dateTimePickerLib.defaultTimeWindows).filter( + ([, timeWindow]) => !timeWindow.default, + ); + nonDefaultWindows.forEach(timeWindow => { + const params = dateTimePickerLib.getTimeRange(timeWindow[0]); + + // Ensure we're not returning the default + expect(hoursBetween(params)).not.toEqual(8); + }); + }); + }); + + describe('stringToISODate', () => { + ['', 'null', undefined, 'abc'].forEach(input => { + it(`throws error for invalid input like ${input}`, done => { + try { + dateTimePickerLib.stringToISODate(input); + } catch (e) { + expect(e).toBeDefined(); + done(); + } + }); + }); + [ + { + input: '2019-09-09 01:01:01', + output: '2019-09-09T01:01:01Z', + }, + { + input: '2019-09-09 00:00:00', + output: '2019-09-09T00:00:00Z', + }, + { + input: '2019-09-09 23:59:59', + output: '2019-09-09T23:59:59Z', + }, + { + input: '2019-09-09', + output: '2019-09-09T00:00:00Z', + }, + ].forEach(({ input, output }) => { + it(`returns ${output} from ${input}`, () => { + expect(dateTimePickerLib.stringToISODate(input)).toBe(output); + }); + }); + }); + + describe('truncateZerosInDateTime', () => { + [ + { + input: '', + output: '', + }, + { + input: '2019-10-10', + output: '2019-10-10', + }, + { + input: '2019-10-10 00:00:01', + output: '2019-10-10 00:00:01', + }, + { + input: '2019-10-10 00:00:00', + output: '2019-10-10', + }, + ].forEach(({ input, output }) => { + it(`truncateZerosInDateTime return ${output} for ${input}`, () => { + expect(dateTimePickerLib.truncateZerosInDateTime(input)).toBe(output); + }); + }); + }); + + describe('isDateTimePickerInputValid', () => { + [ + { + input: null, + output: false, + }, + { + input: '', + output: false, + }, + { + input: 'xxxx-xx-xx', + output: false, + }, + { + input: '9999-99-19', + output: false, + }, + { + input: '2019-19-23', + output: false, + }, + { + input: '2019-09-23', + output: true, + }, + { + input: '2019-09-23 x', + output: false, + }, + { + input: '2019-09-29 0:0:0', + output: false, + }, + { + input: '2019-09-29 00:00:00', + output: true, + }, + { + input: '2019-09-29 24:24:24', + output: false, + }, + { + input: '2019-09-29 23:24:24', + output: true, + }, + { + input: '2019-09-29 23:24:24 ', + output: false, + }, + ].forEach(({ input, output }) => { + it(`returns ${output} for ${input}`, () => { + expect(dateTimePickerLib.isDateTimePickerInputValid(input)).toBe(output); + }); + }); + }); +}); diff --git a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js index 340143a6b53..3b37da5bcd6 100644 --- a/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js +++ b/spec/frontend/monitoring/components/date_time_picker/date_time_picker_spec.js @@ -1,8 +1,8 @@ import { mount } from '@vue/test-utils'; import DateTimePicker from '~/monitoring/components/date_time_picker/date_time_picker.vue'; -import { timeWindows } from '~/monitoring/constants'; +import { defaultTimeWindows } from '~/monitoring/components/date_time_picker/date_time_picker_lib'; -const timeWindowsCount = Object.keys(timeWindows).length; +const timeWindowsCount = Object.entries(defaultTimeWindows).length; const start = '2019-10-10T07:00:00.000Z'; const end = '2019-10-13T07:00:00.000Z'; const selectedTimeWindowText = `3 days`; @@ -13,6 +13,7 @@ describe('DateTimePicker', () => { const dropdownToggle = () => dateTimePicker.find('.dropdown-toggle'); const dropdownMenu = () => dateTimePicker.find('.dropdown-menu'); const applyButtonElement = () => dateTimePicker.find('button.btn-success').element; + const findQuickRangeItems = () => dateTimePicker.findAll('.dropdown-item'); const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element; const fillInputAndBlur = (input, val) => { dateTimePicker.find(input).setValue(val); @@ -25,7 +26,6 @@ describe('DateTimePicker', () => { const createComponent = props => { dateTimePicker = mount(DateTimePicker, { propsData: { - timeWindows, start, end, ...props, @@ -52,16 +52,6 @@ describe('DateTimePicker', () => { }); }); - it('renders dropdown without a selectedTimeWindow set', done => { - createComponent({ - selectedTimeWindow: {}, - }); - dateTimePicker.vm.$nextTick(() => { - expect(dateTimePicker.findAll('input').length).toBe(2); - done(); - }); - }); - it('renders inputs with h/m/s truncated if its all 0s', done => { createComponent({ start: '2019-10-10T00:00:00.000Z', @@ -74,11 +64,11 @@ describe('DateTimePicker', () => { }); }); - it(`renders dropdown with ${timeWindowsCount} items in quick range`, done => { + it(`renders dropdown with ${timeWindowsCount} (default) items in quick range`, done => { createComponent(); dropdownToggle().trigger('click'); dateTimePicker.vm.$nextTick(() => { - expect(dateTimePicker.findAll('.dropdown-item').length).toBe(timeWindowsCount); + expect(findQuickRangeItems().length).toBe(timeWindowsCount); done(); }); }); @@ -167,4 +157,77 @@ describe('DateTimePicker', () => { }); }); }); + + describe('when using non-default time windows', () => { + const otherTimeWindows = { + oneMinute: { + label: '1 minute', + seconds: 60, + }, + twoMinutes: { + label: '2 minutes', + seconds: 60 * 2, + default: true, + }, + fiveMinutes: { + label: '5 minutes', + seconds: 60 * 5, + }, + }; + + it('renders dropdown with a label in the quick range', done => { + createComponent({ + // 2 minutes range + start: '2020-01-21T15:00:00.000Z', + end: '2020-01-21T15:02:00.000Z', + timeWindows: otherTimeWindows, + }); + dropdownToggle().trigger('click'); + dateTimePicker.vm.$nextTick(() => { + expect(dropdownToggle().text()).toBe('2 minutes'); + + done(); + }); + }); + + it('renders dropdown with quick range items', done => { + createComponent({ + // 2 minutes range + start: '2020-01-21T15:00:00.000Z', + end: '2020-01-21T15:02:00.000Z', + timeWindows: otherTimeWindows, + }); + dropdownToggle().trigger('click'); + dateTimePicker.vm.$nextTick(() => { + const items = findQuickRangeItems(); + + expect(items.length).toBe(Object.keys(otherTimeWindows).length); + expect(items.at(0).text()).toBe('1 minute'); + expect(items.at(0).is('.active')).toBe(false); + + expect(items.at(1).text()).toBe('2 minutes'); + expect(items.at(1).is('.active')).toBe(true); + + expect(items.at(2).text()).toBe('5 minutes'); + expect(items.at(2).is('.active')).toBe(false); + + done(); + }); + }); + + it('renders dropdown with a label not in the quick range', done => { + createComponent({ + // 10 minutes range + start: '2020-01-21T15:00:00.000Z', + end: '2020-01-21T15:10:00.000Z', + timeWindows: otherTimeWindows, + }); + dropdownToggle().trigger('click'); + dateTimePicker.vm.$nextTick(() => { + expect(dropdownToggle().text()).toBe('2020-01-21 15:00:00 to 2020-01-21 15:10:00'); + + done(); + }); + }); + }); }); diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 9b1a331e3b5..9df48eb0ad3 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -1,5 +1,4 @@ import * as monitoringUtils from '~/monitoring/utils'; -import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import { graphDataPrometheusQuery, graphDataPrometheusQueryRange, @@ -58,92 +57,6 @@ describe('monitoring/utils', () => { }); }); - describe('getTimeDiff', () => { - function secondsBetween({ start, end }) { - return (new Date(end) - new Date(start)) / 1000; - } - - function minutesBetween(timeRange) { - return secondsBetween(timeRange) / 60; - } - - function hoursBetween(timeRange) { - return minutesBetween(timeRange) / 60; - } - - it('defaults to an 8 hour (28800s) difference', () => { - const params = monitoringUtils.getTimeDiff(); - - expect(hoursBetween(params)).toEqual(8); - }); - - it('accepts time window as an argument', () => { - const params = monitoringUtils.getTimeDiff('thirtyMinutes'); - - expect(minutesBetween(params)).toEqual(30); - }); - - it('returns a value for every defined time window', () => { - const nonDefaultWindows = Object.keys(timeWindows).filter(window => window !== 'eightHours'); - - nonDefaultWindows.forEach(timeWindow => { - const params = monitoringUtils.getTimeDiff(timeWindow); - - // Ensure we're not returning the default - expect(hoursBetween(params)).not.toEqual(8); - }); - }); - }); - - describe('getTimeWindow', () => { - [ - { - args: [ - { - start: '2019-10-01T18:27:47.000Z', - end: '2019-10-01T21:27:47.000Z', - }, - ], - expected: timeWindowsKeyNames.threeHours, - }, - { - args: [ - { - start: '2019-10-01T28:27:47.000Z', - end: '2019-10-01T21:27:47.000Z', - }, - ], - expected: null, - }, - { - args: [ - { - start: '', - end: '', - }, - ], - expected: null, - }, - { - args: [ - { - start: null, - end: null, - }, - ], - expected: null, - }, - { - args: [{}], - expected: null, - }, - ].forEach(({ args, expected }) => { - it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => { - expect(monitoringUtils.getTimeWindow(...args)).toEqual(expected); - }); - }); - }); - describe('graphDataValidatorForValues', () => { /* * When dealing with a metric using the query format, e.g. @@ -174,193 +87,6 @@ describe('monitoring/utils', () => { }); }); - describe('stringToISODate', () => { - ['', 'null', undefined, 'abc'].forEach(input => { - it(`throws error for invalid input like ${input}`, done => { - try { - monitoringUtils.stringToISODate(input); - } catch (e) { - expect(e).toBeDefined(); - done(); - } - }); - }); - [ - { - input: '2019-09-09 01:01:01', - output: '2019-09-09T01:01:01Z', - }, - { - input: '2019-09-09 00:00:00', - output: '2019-09-09T00:00:00Z', - }, - { - input: '2019-09-09 23:59:59', - output: '2019-09-09T23:59:59Z', - }, - { - input: '2019-09-09', - output: '2019-09-09T00:00:00Z', - }, - ].forEach(({ input, output }) => { - it(`returns ${output} from ${input}`, () => { - expect(monitoringUtils.stringToISODate(input)).toBe(output); - }); - }); - }); - - describe('ISODateToString', () => { - [ - { - input: new Date('2019-09-09T00:00:00.000Z'), - output: '2019-09-09 00:00:00', - }, - { - input: new Date('2019-09-09T07:00:00.000Z'), - output: '2019-09-09 07:00:00', - }, - ].forEach(({ input, output }) => { - it(`ISODateToString return ${output} for ${input}`, () => { - expect(monitoringUtils.ISODateToString(input)).toBe(output); - }); - }); - }); - - describe('truncateZerosInDateTime', () => { - [ - { - input: '', - output: '', - }, - { - input: '2019-10-10', - output: '2019-10-10', - }, - { - input: '2019-10-10 00:00:01', - output: '2019-10-10 00:00:01', - }, - { - input: '2019-10-10 00:00:00', - output: '2019-10-10', - }, - ].forEach(({ input, output }) => { - it(`truncateZerosInDateTime return ${output} for ${input}`, () => { - expect(monitoringUtils.truncateZerosInDateTime(input)).toBe(output); - }); - }); - }); - - describe('isValidDate', () => { - [ - { - input: '2019-09-09T00:00:00.000Z', - output: true, - }, - { - input: '2019-09-09T000:00.000Z', - output: false, - }, - { - input: 'a2019-09-09T000:00.000Z', - output: false, - }, - { - input: '2019-09-09T', - output: false, - }, - { - input: '2019-09-09', - output: true, - }, - { - input: '2019-9-9', - output: true, - }, - { - input: '2019-9-', - output: true, - }, - { - input: '2019--', - output: false, - }, - { - input: '2019', - output: true, - }, - { - input: '', - output: false, - }, - { - input: null, - output: false, - }, - ].forEach(({ input, output }) => { - it(`isValidDate return ${output} for ${input}`, () => { - expect(monitoringUtils.isValidDate(input)).toBe(output); - }); - }); - }); - - describe('isDateTimePickerInputValid', () => { - [ - { - input: null, - output: false, - }, - { - input: '', - output: false, - }, - { - input: 'xxxx-xx-xx', - output: false, - }, - { - input: '9999-99-19', - output: false, - }, - { - input: '2019-19-23', - output: false, - }, - { - input: '2019-09-23', - output: true, - }, - { - input: '2019-09-23 x', - output: false, - }, - { - input: '2019-09-29 0:0:0', - output: false, - }, - { - input: '2019-09-29 00:00:00', - output: true, - }, - { - input: '2019-09-29 24:24:24', - output: false, - }, - { - input: '2019-09-29 23:24:24', - output: true, - }, - { - input: '2019-09-29 23:24:24 ', - output: false, - }, - ].forEach(({ input, output }) => { - it(`returns ${output} for ${input}`, () => { - expect(monitoringUtils.isDateTimePickerInputValid(input)).toBe(output); - }); - }); - }); - describe('graphDataValidatorForAnomalyValues', () => { let oneMetric; let threeMetrics; diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index b4425b8e8a2..eab4f4fb17f 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -1,6 +1,7 @@ import Vue from 'vue'; import { createStore } from 'ee_else_ce/mr_notes/stores'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { mockTracking, triggerEvent } from 'spec/helpers/tracking_helper'; import DiffFileComponent from '~/diffs/components/diff_file.vue'; import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import diffFileMockDataReadable from '../mock_data/diff_file'; @@ -8,12 +9,14 @@ import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable'; describe('DiffFile', () => { let vm; + let trackingSpy; beforeEach(() => { vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), { file: JSON.parse(JSON.stringify(diffFileMockDataReadable)), canCurrentUserFork: false, }).$mount(); + trackingSpy = mockTracking('_category_', vm.$el, spyOn); }); afterEach(() => { @@ -30,6 +33,7 @@ describe('DiffFile', () => { expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0); expect(el.querySelector('.js-file-title')).toBeDefined(); + expect(el.querySelector('.btn-clipboard')).toBeDefined(); expect(el.querySelector('.file-title-name').innerText.indexOf(file_path)).toBeGreaterThan(-1); expect(el.querySelector('.js-syntax-highlight')).toBeDefined(); @@ -39,6 +43,25 @@ describe('DiffFile', () => { .then(() => { expect(el.querySelectorAll('.line_content').length).toBe(5); expect(el.querySelectorAll('.js-line-expansion-content').length).toBe(1); + triggerEvent('.btn-clipboard'); + }) + .then(done) + .catch(done.fail); + }); + + it('should track a click event on copy to clip board button', done => { + const el = vm.$el; + + expect(el.querySelector('.btn-clipboard')).toBeDefined(); + vm.file.renderIt = true; + vm.$nextTick() + .then(() => { + triggerEvent('.btn-clipboard'); + + expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_copy_file_button', { + label: 'diff_copy_file_path_button', + property: 'diff_copy_file', + }); }) .then(done) .catch(done.fail); |