Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-01-23 09:09:07 +00:00
parent 0f8c2334f0
commit f47c768fad
16 changed files with 571 additions and 417 deletions

View file

@ -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" />

View file

@ -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"
/>

View file

@ -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>

View file

@ -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 {

View file

@ -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 {};

View file

@ -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;

View file

@ -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 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 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 timeWindowsKeyNames = Object.keys(secondsIn).reduce(
(otherTimeWindows, timeWindow) => ({
...otherTimeWindows,
[timeWindow]: timeWindow,
}),
{},
);

View file

@ -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

View file

@ -0,0 +1,5 @@
---
title: Track usage of merge request file header buttons
merge_request:
author: Oregand
type: other

View file

@ -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

View file

@ -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 ""

View file

@ -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(),
);

View file

@ -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);
});
});
});
});

View file

@ -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();
});
});
});
});

View file

@ -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;

View file

@ -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);