diff options
Diffstat (limited to 'app/assets')
6 files changed, 325 insertions, 45 deletions
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 2e35ef8d4b0..b4ea415bb51 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -12,23 +12,19 @@ import { GlTooltipDirective, } from '@gitlab/ui'; import { __, s__ } from '~/locale'; +import createFlash from '~/flash'; import Icon from '~/vue_shared/components/icon.vue'; -import { getParameterValues, mergeUrlParams } from '~/lib/utils/url_utility'; +import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import invalidUrl from '~/lib/utils/invalid_url'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; +import DateTimePicker from './date_time_picker/date_time_picker.vue'; import MonitorTimeSeriesChart from './charts/time_series.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import GraphGroup from './graph_group.vue'; import EmptyState from './empty_state.vue'; -import { sidebarAnimationDuration, timeWindows } from '../constants'; +import { sidebarAnimationDuration } from '../constants'; import TrackEventDirective from '~/vue_shared/directives/track_event'; - -import { - getTimeDiff, - getTimeWindow, - downloadCSVOptions, - generateLinkToChartOptions, -} from '../utils'; +import { getTimeDiff, isValidDate, downloadCSVOptions, generateLinkToChartOptions } from '../utils'; let sidebarMutationObserver; @@ -46,6 +42,7 @@ export default { GlDropdownItem, GlFormGroup, GlModal, + DateTimePicker, }, directives: { GlModal: GlModalDirective, @@ -171,10 +168,8 @@ export default { return { state: 'gettingStarted', elWidth: 0, - selectedTimeWindow: '', - selectedTimeWindowKey: '', formIsValid: null, - timeWindows: {}, + selectedTimeWindow: {}, isRearrangingPanels: false, }; }, @@ -237,11 +232,13 @@ export default { end, }; - this.timeWindows = timeWindows; - this.selectedTimeWindowKey = getTimeWindow(range); - this.selectedTimeWindow = this.timeWindows[this.selectedTimeWindowKey]; + this.selectedTimeWindow = range; - this.fetchData(range); + if (!isValidDate(start) || !isValidDate(end)) { + this.showInvalidDateError(); + } else { + this.fetchData(range); + } sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver.observe(document.querySelector('.layout-page'), { @@ -298,6 +295,9 @@ export default { // See https://gitlab.com/gitlab-org/gitlab/issues/27835 metrics.splice(graphIndex, 1); }, + showInvalidDateError() { + createFlash(s__('Metrics|Link contains an invalid time window.')); + }, generateLink(group, title, yLabel) { const dashboard = this.currentDashboard || this.firstDashboard.path; const params = _.pick({ dashboard, group, title, y_label: yLabel }, value => value != null); @@ -320,16 +320,12 @@ export default { submitCustomMetricsForm() { this.$refs.customMetricsForm.submit(); }, - activeTimeWindow(key) { - return this.timeWindows[key] === this.selectedTimeWindow; - }, - setTimeWindowParameter(key) { - const { start, end } = getTimeDiff(key); - return `?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`; - }, groupHasData(group) { return this.chartsWithData(group.metrics).length > 0; }, + onDateTimePickerApply(timeWindowUrlParams) { + return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href)); + }, downloadCSVOptions, generateLinkToChartOptions, }, @@ -342,14 +338,14 @@ export default { <template> <div class="prometheus-graphs"> - <div class="gl-p-3 pb-0 border-bottom bg-gray-light"> + <div class="prometheus-graphs-header gl-p-3 pb-0 border-bottom bg-gray-light"> <div class="row"> <template v-if="environmentsEndpoint"> <gl-form-group :label="__('Dashboard')" label-size="sm" label-for="monitor-dashboards-dropdown" - class="col-sm-12 col-md-4 col-lg-2" + class="col-sm-12 col-md-6 col-lg-2" > <gl-dropdown id="monitor-dashboards-dropdown" @@ -372,7 +368,7 @@ export default { :label="s__('Metrics|Environment')" label-size="sm" label-for="monitor-environments-dropdown" - class="col-sm-6 col-md-4 col-lg-2" + class="col-sm-6 col-md-6 col-lg-2" > <gl-dropdown id="monitor-environments-dropdown" @@ -397,30 +393,19 @@ export default { :label="s__('Metrics|Show last')" label-size="sm" label-for="monitor-time-window-dropdown" - class="col-sm-6 col-md-4 col-lg-2" + class="col-sm-6 col-md-6 col-lg-4" > - <gl-dropdown - id="monitor-time-window-dropdown" - class="mb-0 d-flex js-time-window-dropdown" - toggle-class="dropdown-menu-toggle" - :text="selectedTimeWindow" - > - <gl-dropdown-item - v-for="(value, key) in timeWindows" - :key="key" - :active="activeTimeWindow(key)" - :href="setTimeWindowParameter(key)" - active-class="active" - >{{ value }}</gl-dropdown-item - > - </gl-dropdown> + <date-time-picker + :selected-time-window="selectedTimeWindow" + @onApply="onDateTimePickerApply" + /> </gl-form-group> </template> <gl-form-group v-if="addingMetricsAvailable || showRearrangePanelsBtn || externalDashboardUrl.length" label-for="prometheus-graphs-dropdown-buttons" - class="dropdown-buttons col-lg d-lg-flex align-items-end" + class="dropdown-buttons col-md d-md-flex col-lg d-lg-flex align-items-end" > <div id="prometheus-graphs-dropdown-buttons"> <gl-button 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 new file mode 100644 index 00000000000..4616a767295 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker.vue @@ -0,0 +1,151 @@ +<script> +import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui'; +import { s__, sprintf } from '~/locale'; +import Icon from '~/vue_shared/components/icon.vue'; +import DateTimePickerInput from './date_time_picker_input.vue'; +import { + getTimeDiff, + getTimeWindow, + stringToISODate, + ISODateToString, + truncateZerosInDateTime, + isDateTimePickerInputValid, +} from '~/monitoring/utils'; +import { timeWindows } from '~/monitoring/constants'; + +export default { + components: { + Icon, + DateTimePickerInput, + GlFormGroup, + GlButton, + GlDropdown, + GlDropdownItem, + }, + props: { + timeWindows: { + type: Object, + required: false, + default: () => timeWindows, + }, + selectedTimeWindow: { + type: Object, + required: false, + default: () => {}, + }, + }, + data() { + return { + selectedTimeWindowText: '', + customTime: { + from: null, + to: null, + }, + }; + }, + computed: { + applyEnabled() { + return Boolean(this.inputState.from && this.inputState.to); + }, + inputState() { + const { from, to } = this.customTime; + return { + from: from && isDateTimePickerInputValid(from), + to: to && isDateTimePickerInputValid(to), + }; + }, + }, + mounted() { + const range = getTimeWindow(this.selectedTimeWindow); + if (range) { + this.selectedTimeWindowText = this.timeWindows[range]; + } else { + this.customTime = { + from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)), + to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)), + }; + this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime); + } + }, + methods: { + activeTimeWindow(key) { + return this.timeWindows[key] === this.selectedTimeWindowText; + }, + setCustomTimeWindowParameter() { + this.$emit('onApply', { + start: stringToISODate(this.customTime.from), + end: stringToISODate(this.customTime.to), + }); + }, + setTimeWindowParameter(key) { + const { start, end } = getTimeDiff(key); + this.$emit('onApply', { + start, + end, + }); + }, + closeDropdown() { + this.$refs.dropdown.hide(); + }, + }, +}; +</script> +<template> + <gl-dropdown + ref="dropdown" + :text="selectedTimeWindowText" + menu-class="time-window-dropdown-menu" + class="js-time-window-dropdown" + > + <div class="d-flex justify-content-between time-window-dropdown-menu-container"> + <gl-form-group + :label="__('Custom range')" + label-for="custom-from-time" + class="custom-time-range-form-group col-md-7 p-0 m-0" + > + <date-time-picker-input + id="custom-time-from" + v-model="customTime.from" + :label="__('From')" + :state="inputState.from" + /> + <date-time-picker-input + id="custom-time-to" + v-model="customTime.to" + :label="__('To')" + :state="inputState.to" + /> + <gl-form-group> + <gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button> + <gl-button + variant="success" + :disabled="!applyEnabled" + @click="setCustomTimeWindowParameter" + >{{ __('Apply') }}</gl-button + > + </gl-form-group> + </gl-form-group> + <gl-form-group + :label="__('Quick range')" + label-for="group-id-dropdown" + label-align="center" + class="col-md-4 p-0 m-0" + > + <gl-dropdown-item + v-for="(value, key) in timeWindows" + :key="key" + :active="activeTimeWindow(key)" + active-class="active" + @click="setTimeWindowParameter(key)" + > + <icon + name="mobile-issue-close" + class="align-bottom" + :class="{ invisible: !activeTimeWindow(key) }" + /> + {{ value }} + </gl-dropdown-item> + </gl-form-group> + </div> + </gl-dropdown> +</template> 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 new file mode 100644 index 00000000000..0388a6190d9 --- /dev/null +++ b/app/assets/javascripts/monitoring/components/date_time_picker/date_time_picker_input.vue @@ -0,0 +1,77 @@ +<script> +import _ from 'underscore'; +import { s__, sprintf } from '~/locale'; +import { GlFormGroup, GlFormInput } from '@gitlab/ui'; +import { dateFormats } from '~/monitoring/constants'; + +const inputGroupText = { + invalidFeedback: sprintf(s__('Format: %{dateFormat}'), { + dateFormat: dateFormats.dateTimePicker.format, + }), + placeholder: dateFormats.dateTimePicker.format, +}; + +export default { + components: { + GlFormGroup, + GlFormInput, + }, + props: { + state: { + default: null, + required: true, + validator: prop => typeof prop === 'boolean' || prop === null, + }, + value: { + default: null, + required: false, + validator: prop => typeof prop === 'string' || prop === null, + }, + label: { + type: String, + default: '', + required: true, + }, + id: { + type: String, + required: false, + default: () => _.uniqueId('dateTimePicker_'), + }, + }, + data() { + return { + inputGroupText, + }; + }, + computed: { + invalidFeedback() { + return this.state ? '' : this.inputGroupText.invalidFeedback; + }, + inputState() { + // When the state is valid we want to show no + // green outline. Hence passing null and not true. + if (this.state === true) { + return null; + } + return this.state; + }, + }, + methods: { + onInputBlur(e) { + this.$emit('input', e.target.value.trim() || null); + }, + }, +}; +</script> + +<template> + <gl-form-group :label="label" label-size="sm" :label-for="id" :invalid-feedback="invalidFeedback"> + <gl-form-input + :id="id" + :value="value" + :state="inputState" + :placeholder="inputGroupText.placeholder" + @blur="onInputBlur" + /> + </gl-form-group> +</template> diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js index 13aba3d9f44..2836fe4fc26 100644 --- a/app/assets/javascripts/monitoring/constants.js +++ b/app/assets/javascripts/monitoring/constants.js @@ -3,6 +3,11 @@ import { __ } from '~/locale'; 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', @@ -28,6 +33,11 @@ export const timeWindows = { 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 = { diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 9049695b992..4c72f5226b7 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -1,4 +1,5 @@ -import { secondsIn, timeWindowsKeyNames } from './constants'; +import dateformat from 'dateformat'; +import { secondsIn, dateTimePickerRegex, dateFormats } from './constants'; const secondsToMilliseconds = seconds => seconds * 1000; @@ -19,7 +20,49 @@ export const getTimeWindow = ({ start, end }) => return timeRange; } return acc; - }, timeWindowsKeyNames.eightHours); + }, 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 { + 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 diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss index ceafff94719..154e505f7a4 100644 --- a/app/assets/stylesheets/pages/prometheus.scss +++ b/app/assets/stylesheets/pages/prometheus.scss @@ -46,6 +46,20 @@ } } +.prometheus-graphs-header { + .time-window-dropdown-menu { + padding: $gl-padding $gl-padding 0 $gl-padding-12; + } + + .time-window-dropdown-menu-container { + width: 360px; + } + + .custom-time-range-form-group > label { + padding-bottom: $gl-padding; + } +} + .prometheus-panel { margin-top: 20px; } |