Skip to content
Snippets Groups Projects
Commit c27acb1d authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 18084543
No related branches found
No related tags found
No related merge requests found
Showing
with 320 additions and 153 deletions
Loading
Loading
@@ -350,7 +350,7 @@ end
group :development, :test do
gem 'bullet', '~> 6.0.2', require: !!ENV['ENABLE_BULLET']
gem 'pry-byebug', '~> 3.5.1', platform: :mri
gem 'pry-rails', '~> 0.3.4'
gem 'pry-rails', '~> 0.3.9'
 
gem 'awesome_print', require: false
 
Loading
Loading
Loading
Loading
@@ -778,7 +778,7 @@ GEM
pry-byebug (3.5.1)
byebug (~> 9.1)
pry (~> 0.10)
pry-rails (0.3.6)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
pyu-ruby-sasl (0.0.3.3)
Loading
Loading
@@ -1323,7 +1323,7 @@ DEPENDENCIES
premailer-rails (~> 1.10.3)
prometheus-client-mmap (~> 0.10.0)
pry-byebug (~> 3.5.1)
pry-rails (~> 0.3.4)
pry-rails (~> 0.3.9)
rack (~> 2.0.7)
rack-attack (~> 6.2.0)
rack-cors (~> 1.0.0)
Loading
Loading
<script>
import { GlFormInputGroup, GlButton, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
components: {
GlFormInputGroup,
GlButton,
GlIcon,
},
props: {
url: {
type: String,
required: true,
},
},
data() {
return {
optionValues: [
// eslint-disable-next-line no-useless-escape
{ name: __('Embed'), value: `<script src='${this.url}.js'><\/script>` },
{ name: __('Share'), value: this.url },
],
};
},
};
</script>
<template>
<gl-form-input-group
id="embeddable-text"
:predefined-options="optionValues"
readonly
select-on-click
>
<template #append>
<gl-button new-style data-clipboard-target="#embeddable-text">
<gl-icon name="copy-to-clipboard" :title="__('Copy')" />
</gl-button>
</template>
</gl-form-input-group>
</template>
Loading
Loading
@@ -19,10 +19,10 @@ import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue';
import { getTimeRange } from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
 
import GraphGroup from './graph_group.vue';
Loading
Loading
@@ -31,11 +31,8 @@ import GroupEmptyState from './group_empty_state.vue';
import DashboardsDropdown from './dashboards_dropdown.vue';
 
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getAddMetricTrackingOptions } from '../utils';
import { datePickerTimeWindows, metricStates } from '../constants';
const defaultTimeRange = getTimeRange();
import { getAddMetricTrackingOptions, timeRangeToUrl, timeRangeFromUrl } from '../utils';
import { defaultTimeRange, timeRanges, metricStates } from '../constants';
 
export default {
components: {
Loading
Loading
@@ -197,10 +194,9 @@ export default {
return {
state: 'gettingStarted',
formIsValid: null,
startDate: getParameterValues('start')[0] || defaultTimeRange.start,
endDate: getParameterValues('end')[0] || defaultTimeRange.end,
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
hasValidDates: true,
datePickerTimeWindows,
timeRanges,
isRearrangingPanels: false,
};
},
Loading
Loading
@@ -260,9 +256,11 @@ export default {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
const { start, end } = convertToFixedRange(this.selectedTimeRange);
this.fetchData({
start: this.startDate,
end: this.endDate,
start,
end,
});
}
},
Loading
Loading
@@ -287,8 +285,8 @@ export default {
});
},
 
onDateTimePickerApply(params) {
redirectTo(mergeUrlParams(params, window.location.href));
onDateTimePickerInput(timeRange) {
redirectTo(timeRangeToUrl(timeRange));
},
onDateTimePickerInvalid() {
createFlash(
Loading
Loading
@@ -296,8 +294,8 @@ export default {
'Metrics|Link contains an invalid time window, please verify the link to see the requested time range.',
),
);
this.startDate = defaultTimeRange.start;
this.endDate = defaultTimeRange.end;
// As a fallback, switch to default time range instead
this.selectedTimeRange = defaultTimeRange;
},
 
generateLink(group, title, yLabel) {
Loading
Loading
@@ -447,10 +445,9 @@ export default {
>
<date-time-picker
ref="dateTimePicker"
:start="startDate"
:end="endDate"
:time-windows="datePickerTimeWindows"
@apply="onDateTimePickerApply"
:value="selectedTimeRange"
:options="timeRanges"
@input="onDateTimePickerInput"
@invalid="onDateTimePickerInvalid"
/>
</gl-form-group>
Loading
Loading
<script>
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 { getTimeRange } from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { timeRangeFromUrl, removeTimeRangeParams } from '../utils';
import { sidebarAnimationDuration, defaultTimeRange } from '../constants';
 
let sidebarMutationObserver;
 
Loading
Loading
@@ -18,10 +18,8 @@ export default {
},
},
data() {
const defaultRange = getTimeRange();
const start = getParameterValues('start', this.dashboardUrl)[0] || defaultRange.start;
const end = getParameterValues('end', this.dashboardUrl)[0] || defaultRange.end;
const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
const { start, end } = convertToFixedRange(timeRange);
const params = {
start,
end,
Loading
Loading
@@ -81,7 +79,7 @@ export default {
},
setInitialState() {
this.setEndpoints({
dashboardEndpoint: removeParams(['start', 'end'], this.dashboardUrl),
dashboardEndpoint: removeTimeRangeParams(this.dashboardUrl),
});
this.setShowErrorBanner(false);
},
Loading
Loading
Loading
Loading
@@ -83,34 +83,36 @@ export const dateFormats = {
default: 'dd mmm yyyy, h:MMTT',
};
 
export const datePickerTimeWindows = {
thirtyMinutes: {
export const timeRanges = [
{
label: __('30 minutes'),
seconds: 60 * 30,
duration: { seconds: 60 * 30 },
},
threeHours: {
{
label: __('3 hours'),
seconds: 60 * 60 * 3,
duration: { seconds: 60 * 60 * 3 },
},
eightHours: {
{
label: __('8 hours'),
seconds: 60 * 60 * 8,
duration: { seconds: 60 * 60 * 8 },
default: true,
},
oneDay: {
{
label: __('1 day'),
seconds: 60 * 60 * 24 * 1,
duration: { seconds: 60 * 60 * 24 * 1 },
},
threeDays: {
{
label: __('3 days'),
seconds: 60 * 60 * 24 * 3,
duration: { seconds: 60 * 60 * 24 * 3 },
},
oneWeek: {
{
label: __('1 week'),
seconds: 60 * 60 * 24 * 7 * 1,
duration: { seconds: 60 * 60 * 24 * 7 * 1 },
},
twoWeeks: {
{
label: __('2 weeks'),
seconds: 60 * 60 * 24 * 7 * 2,
duration: { seconds: 60 * 60 * 24 * 7 * 2 },
},
};
];
export const defaultTimeRange = timeRanges.find(tr => tr.default);
Loading
Loading
@@ -2,6 +2,7 @@
import GetSnippetQuery from '../queries/snippet.query.graphql';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
 
export default {
Loading
Loading
@@ -9,6 +10,7 @@ export default {
SnippetHeader,
SnippetTitle,
GlLoadingIcon,
SnippetBlob,
},
apollo: {
snippet: {
Loading
Loading
@@ -50,6 +52,7 @@ export default {
<template v-else>
<snippet-header :snippet="snippet" />
<snippet-title :snippet="snippet" />
<snippet-blob :snippet="snippet" />
</template>
</div>
</template>
<script>
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
export default {
components: {
BlobEmbeddable,
},
props: {
snippet: {
type: Object,
required: true,
},
},
computed: {
embeddable() {
return this.snippet.visibilityLevel === SNIPPET_VISIBILITY_PUBLIC;
},
},
};
</script>
<template>
<div>
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
</div>
</template>
export const SNIPPET_VISIBILITY_PRIVATE = 'private';
export const SNIPPET_VISIBILITY_INTERNAL = 'internal';
export const SNIPPET_VISIBILITY_PUBLIC = 'public';
Loading
Loading
@@ -12,8 +12,7 @@
* css-class="btn-transparent"
* />
*/
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import Icon from '../components/icon.vue';
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
 
export default {
name: 'ClipboardButton',
Loading
Loading
@@ -22,7 +21,7 @@ export default {
},
components: {
GlButton,
Icon,
GlIcon,
},
props: {
text: {
Loading
Loading
@@ -72,6 +71,6 @@ export default {
:title="title"
:data-clipboard-text="clipboardText"
>
<icon name="duplicate" />
<gl-icon name="copy-to-clipboard" />
</gl-button>
</template>
<script>
import { GlButton, GlDropdown, GlDropdownItem, GlFormGroup } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
defaultTimeWindows,
defaultTimeRanges,
defaultTimeRange,
isValidDate,
getTimeRange,
getTimeWindowKey,
stringToISODate,
ISODateToString,
truncateZerosInDateTime,
Loading
Loading
@@ -15,7 +17,7 @@ import {
} from './date_time_picker_lib';
 
const events = {
apply: 'apply',
input: 'input',
invalid: 'invalid',
};
 
Loading
Loading
@@ -29,24 +31,22 @@ export default {
GlDropdownItem,
},
props: {
start: {
type: String,
required: true,
},
end: {
type: String,
required: true,
},
timeWindows: {
value: {
type: Object,
required: false,
default: () => defaultTimeWindows,
default: () => defaultTimeRange,
},
options: {
type: Array,
required: false,
default: () => defaultTimeRanges,
},
},
data() {
return {
startDate: this.start,
endDate: this.end,
timeRange: this.value,
startDate: '',
endDate: '',
};
},
computed: {
Loading
Loading
@@ -67,6 +67,7 @@ export default {
set(val) {
// Attempt to set a formatted date if possible
this.startDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val;
this.timeRange = null;
},
},
endInput: {
Loading
Loading
@@ -76,23 +77,48 @@ export default {
set(val) {
// Attempt to set a formatted date if possible
this.endDate = isDateTimePickerInputValid(val) ? stringToISODate(val) : val;
this.timeRange = null;
},
},
 
timeWindowText() {
const timeWindow = getTimeWindowKey({ start: this.start, end: this.end }, this.timeWindows);
if (timeWindow) {
return this.timeWindows[timeWindow].label;
} else if (isValidDate(this.start) && isValidDate(this.end)) {
return sprintf(__('%{start} to %{end}'), {
start: this.formatDate(this.start),
end: this.formatDate(this.end),
});
try {
const timeRange = findTimeRange(this.value, this.options);
if (timeRange) {
return timeRange.label;
}
const { start, end } = convertToFixedRange(this.value);
if (isValidDate(start) && isValidDate(end)) {
return sprintf(__('%{start} to %{end}'), {
start: this.formatDate(start),
end: this.formatDate(end),
});
}
} catch {
return __('Invalid date range');
}
return '';
},
},
watch: {
value(newValue) {
const { start, end } = convertToFixedRange(newValue);
this.timeRange = this.value;
this.startDate = start;
this.endDate = end;
},
},
mounted() {
try {
const { start, end } = convertToFixedRange(this.timeRange);
this.startDate = start;
this.endDate = end;
} catch {
// when dates cannot be parsed, emit error.
this.$emit(events.invalid);
}
// Validate on mounted, and trigger an update if needed
if (!this.isValid) {
this.$emit(events.invalid);
Loading
Loading
@@ -102,21 +128,22 @@ export default {
formatDate(date) {
return truncateZerosInDateTime(ISODateToString(date));
},
setTimeWindow(key) {
const { start, end } = getTimeRange(key, this.timeWindows);
this.startDate = start;
this.endDate = end;
this.apply();
},
closeDropdown() {
this.$refs.dropdown.hide();
},
apply() {
this.$emit(events.apply, {
isOptionActive(option) {
return isEqualTimeRanges(option, this.timeRange);
},
setQuickRange(option) {
this.timeRange = option;
this.$emit(events.input, this.timeRange);
},
setFixedRange() {
this.timeRange = convertToFixedRange({
start: this.startDate,
end: this.endDate,
});
this.$emit(events.input, this.timeRange);
},
},
};
Loading
Loading
@@ -146,7 +173,7 @@ export default {
</div>
<gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button variant="success" :disabled="!isValid" @click="apply()">
<gl-button variant="success" :disabled="!isValid" @click="setFixedRange()">
{{ __('Apply') }}
</gl-button>
</gl-form-group>
Loading
Loading
@@ -155,19 +182,20 @@ export default {
<template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span>
</template>
<gl-dropdown-item
v-for="(timeWindow, key) in timeWindows"
:key="key"
:active="timeWindow.label === timeWindowText"
v-for="(option, index) in options"
:key="index"
:active="isOptionActive(option)"
active-class="active"
@click="setTimeWindow(key)"
@click="setQuickRange(option)"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: timeWindow.label !== timeWindowText }"
:class="{ invisible: !isOptionActive(option) }"
/>
{{ timeWindow.label }}
{{ option.label }}
</gl-dropdown-item>
</gl-form-group>
</div>
Loading
Loading
import dateformat from 'dateformat';
import { __ } from '~/locale';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
 
/**
* Valid strings for this regex are
Loading
Loading
@@ -9,37 +8,30 @@ import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
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]))?$/;
 
/**
* A key-value pair of "time windows".
*
* A time window is a representation of period of time that starts
* some time in past until now. Keys are only used for easy reference.
*
* It is represented as user friendly `label` and number of `seconds`
* to be substracted from now.
* Default time ranges for the date picker.
* @see app/assets/javascripts/lib/utils/datetime_range.js
*/
export const defaultTimeWindows = {
thirtyMinutes: {
export const defaultTimeRanges = [
{
duration: { seconds: 60 * 30 },
label: __('30 minutes'),
seconds: 60 * 30,
},
threeHours: {
{
duration: { seconds: 60 * 60 * 3 },
label: __('3 hours'),
seconds: 60 * 60 * 3,
},
eightHours: {
{
duration: { seconds: 60 * 60 * 8 },
label: __('8 hours'),
seconds: 60 * 60 * 8,
default: true,
},
oneDay: {
{
duration: { seconds: 60 * 60 * 24 * 1 },
label: __('1 day'),
seconds: 60 * 60 * 24 * 1,
},
threeDays: {
label: __('3 days'),
seconds: 60 * 60 * 24 * 3,
},
};
];
export const defaultTimeRange = defaultTimeRanges.find(tr => tr.default);
 
export const dateFormats = {
ISODate: "yyyy-mm-dd'T'HH:MM:ss'Z'",
Loading
Loading
@@ -67,46 +59,6 @@ export const isValidDate = dateString => {
}
};
 
/**
* For a given time window key (e.g. `threeHours`) and key-value pair
* object of time windows.
*
* Returns a date time range with start and end.
*
* @param {String} timeWindowKey - A key in the object of time windows.
* @param {Object} timeWindows - A key-value pair of time windows,
* with a second duration and a label.
* @returns An object with time range, start and end dates, in ISO format.
*/
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.
*
Loading
Loading
---
title: Allow for relative time ranges in metrics dashboard URLs
merge_request: 23765
author:
type: added
---
title: Remove unused Code Hotspots database tables
merge_request: 23590
author:
type: other
---
title: Fix visibility levels of subgroups to be not higher than their parents' level
merge_request: 22889
author:
type: other
---
title: Updated icon for copy-to-clipboard button
merge_request: 24146
author:
type: other
# frozen_string_literal: true
class RemoveAnalyticsRepositoryTableFksOnProjects < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
# Requires ExclusiveLock on all tables. analytics_* tables are empty
remove_foreign_key :analytics_repository_files, :projects
remove_foreign_key :analytics_repository_file_edits, :projects
remove_foreign_key :analytics_repository_file_commits, :projects
end
end
def down
with_lock_retries do
# rubocop:disable Migration/AddConcurrentForeignKey
add_foreign_key :analytics_repository_files, :projects, on_delete: :cascade
add_foreign_key :analytics_repository_file_edits, :projects, on_delete: :cascade
add_foreign_key :analytics_repository_file_commits, :projects, on_delete: :cascade
# rubocop:enable Migration/AddConcurrentForeignKey
end
end
end
# frozen_string_literal: true
class RemoveAnalyticsRepositoryFilesFkOnOtherAnalyticsTables < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
# Requires ExclusiveLock on all tables. analytics_* tables are empty
remove_foreign_key :analytics_repository_file_edits, :analytics_repository_files
remove_foreign_key :analytics_repository_file_commits, :analytics_repository_files
end
end
def down
with_lock_retries do
# rubocop:disable Migration/AddConcurrentForeignKey
add_foreign_key :analytics_repository_file_edits, :analytics_repository_files, on_delete: :cascade
add_foreign_key :analytics_repository_file_commits, :analytics_repository_files, on_delete: :cascade
# rubocop:enable Migration/AddConcurrentForeignKey
end
end
end
# frozen_string_literal: true
class DropAnalyticsRepositoryFilesTable < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
# Requires ExclusiveLock on the table. Not in use, no records, no FKs.
drop_table :analytics_repository_files
end
def down
create_table :analytics_repository_files do |t|
t.bigint :project_id, null: false
t.string :file_path, limit: 4096, null: false
end
add_index :analytics_repository_files, [:project_id, :file_path], unique: true
end
end
# frozen_string_literal: true
class DropAnalyticsRepositoryFileCommitsTable < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
# Requires ExclusiveLock on the table. Not in use, no records, no FKs.
drop_table :analytics_repository_file_commits
end
def down
create_table :analytics_repository_file_commits do |t|
t.bigint :analytics_repository_file_id, null: false
t.index :analytics_repository_file_id, name: 'index_analytics_repository_file_commits_file_id'
t.bigint :project_id, null: false
t.date :committed_date, null: false
t.integer :commit_count, limit: 2, null: false
end
add_index :analytics_repository_file_commits,
[:project_id, :committed_date, :analytics_repository_file_id],
name: 'index_file_commits_on_committed_date_file_id_and_project_id',
unique: true
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment