Commit 6cb3e53b authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent c4b517767528
......@@ -102,13 +102,13 @@ rspec migration pg9:
extends:
- .rspec-base-pg9
- .rspec-base-migration
parallel: 4
parallel: 5
rspec migration pg9-foss:
extends:
- .rspec-base-pg9-foss
- .rspec-base-migration
parallel: 4
parallel: 5
rspec unit pg9:
extends: .rspec-base-pg9
......@@ -120,11 +120,11 @@ rspec unit pg9-foss:
rspec integration pg9:
extends: .rspec-base-pg9
parallel: 6
parallel: 8
rspec integration pg9-foss:
extends: .rspec-base-pg9-foss
parallel: 6
parallel: 8
rspec system pg9:
extends: .rspec-base-pg9
......@@ -140,7 +140,7 @@ rspec unit pg10:
rspec integration pg10:
extends: .rspec-base-pg10
parallel: 6
parallel: 8
rspec system pg10:
extends: .rspec-base-pg10
......@@ -170,11 +170,11 @@ rspec-ee unit pg9:
rspec-ee integration pg9:
extends: .rspec-ee-base-pg9
parallel: 3
parallel: 4
rspec-ee system pg9:
extends: .rspec-ee-base-pg9
parallel: 5
parallel: 6
rspec-ee migration pg10:
extends:
......
<script>
import { omit } from 'lodash';
import { omit, throttle } from 'lodash';
import { GlLink, GlButton, GlTooltip, GlResizeObserverDirective } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from 'dateformat';
......@@ -18,6 +18,13 @@ import {
import { makeDataSeries } from '~/helpers/monitor_helper';
import { graphDataValidatorForValues } from '../../utils';
const THROTTLED_DATAZOOM_WAIT = 1000; // miliseconds
const timestampToISODate = timestamp => new Date(timestamp).toISOString();
const events = {
datazoom: 'datazoom',
};
export default {
components: {
GlAreaChart,
......@@ -98,6 +105,7 @@ export default {
height: chartHeight,
svgs: {},
primaryColor: null,
throttledDatazoom: null,
};
},
computed: {
......@@ -245,6 +253,11 @@ export default {
this.setSvg('rocket');
this.setSvg('scroll-handle');
},
destroyed() {
if (this.throttledDatazoom) {
this.throttledDatazoom.cancel();
}
},
methods: {
formatLegendLabel(query) {
return `${query.label}`;
......@@ -287,8 +300,39 @@ export default {
console.error('SVG could not be rendered correctly: ', e);
});
},
onChartUpdated(chart) {
[this.primaryColor] = chart.getOption().color;
onChartUpdated(eChart) {
[this.primaryColor] = eChart.getOption().color;
},
onChartCreated(eChart) {
// Emit a datazoom event that corresponds to the eChart
// `datazoom` event.
if (this.throttledDatazoom) {
// Chart can be created multiple times in this component's
// lifetime, remove previous handlers every time
// chart is created.
this.throttledDatazoom.cancel();
}
// Emitting is throttled to avoid flurries of calls when
// the user changes or scrolls the zoom bar.
this.throttledDatazoom = throttle(
() => {
const { startValue, endValue } = eChart.getOption().dataZoom[0];
this.$emit(events.datazoom, {
start: timestampToISODate(startValue),
end: timestampToISODate(endValue),
});
},
THROTTLED_DATAZOOM_WAIT,
{
leading: false,
},
);
eChart.off('datazoom');
eChart.on('datazoom', this.throttledDatazoom);
},
onResize() {
if (!this.$refs.chart) return;
......@@ -331,6 +375,7 @@ export default {
:height="height"
:average-text="legendAverageText"
:max-text="legendMaxText"
@created="onChartCreated"
@updated="onChartUpdated"
>
<template v-if="tooltip.isDeployment">
......
......@@ -21,7 +21,6 @@ import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
......@@ -102,6 +101,11 @@ export default {
type: String,
required: true,
},
logsPath: {
type: String,
required: false,
default: invalidUrl,
},
defaultBranch: {
type: String,
required: true,
......@@ -247,22 +251,20 @@ export default {
dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard,
projectPath: this.projectPath,
logsPath: this.logsPath,
});
},
mounted() {
if (!this.hasMetrics) {
this.setGettingStartedEmptyState();
} else {
const { start, end } = convertToFixedRange(this.selectedTimeRange);
this.fetchData({
start,
end,
});
this.setTimeRange(this.selectedTimeRange);
this.fetchData();
}
},
methods: {
...mapActions('monitoringDashboard', [
'setTimeRange',
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
......
......@@ -19,14 +19,8 @@ export default {
},
data() {
const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
const { start, end } = convertToFixedRange(timeRange);
const params = {
start,
end,
};
return {
params,
timeRange: convertToFixedRange(timeRange),
elWidth: 0,
};
},
......@@ -49,7 +43,9 @@ export default {
},
mounted() {
this.setInitialState();
this.fetchMetricsData(this.params);
this.setTimeRange(this.timeRange);
this.fetchDashboard();
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true,
......@@ -64,7 +60,8 @@ export default {
},
methods: {
...mapActions('monitoringDashboard', [
'fetchMetricsData',
'setTimeRange',
'fetchDashboard',
'setEndpoints',
'setFeatureFlags',
'setShowErrorBanner',
......
<script>
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import {
GlDropdown,
GlDropdownItem,
......@@ -18,7 +19,7 @@ import MonitorColumnChart from './charts/column.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue';
import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default {
components: {
......@@ -58,8 +59,13 @@ export default {
default: 'panel-type-chart',
},
},
data() {
return {
zoomedTimeRange: null,
};
},
computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']),
...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
},
......@@ -70,6 +76,14 @@ export default {
this.graphData.metrics[0].result.length > 0
);
},
logsPathWithTimeRange() {
const timeRange = this.zoomedTimeRange || this.timeRange;
if (this.logsPath && this.logsPath !== invalidUrl && timeRange) {
return timeRangeToUrl(timeRange, this.logsPath);
}
return null;
},
csvText() {
const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label;
......@@ -107,6 +121,10 @@ export default {
},
downloadCSVOptions,
generateLinkToChartOptions,
onDatazoom({ start, end }) {
this.zoomedTimeRange = { start, end };
},
},
};
</script>
......@@ -130,11 +148,13 @@ export default {
<component
:is="monitorChartComponent"
v-else-if="graphDataHasMetrics"
ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
@datazoom="onDatazoom"
>
<div class="d-flex align-items-center">
<alert-widget
......@@ -157,6 +177,15 @@ export default {
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
<gl-dropdown-item
v-if="logsPathWithTimeRange"
ref="viewLogsLink"
:href="logsPathWithTimeRange"
>
{{ s__('Metrics|View logs') }}
</gl-dropdown-item>
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv"
......
import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
......@@ -32,6 +33,10 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
export const setTimeRange = ({ commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
};
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData');
......@@ -63,19 +68,24 @@ export const receiveEnvironmentsDataSuccess = ({ commit }, data) =>
export const receiveEnvironmentsDataFailure = ({ commit }) =>
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
export const fetchData = ({ dispatch }, params) => {
dispatch('fetchMetricsData', params);
export const fetchData = ({ dispatch }) => {
dispatch('fetchDashboard');
dispatch('fetchDeploymentsData');
dispatch('fetchEnvironmentsData');
};
export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params);
export const fetchDashboard = ({ state, dispatch }, params) => {
export const fetchDashboard = ({ state, dispatch }) => {
dispatch('requestMetricsDashboard');
const params = {};
if (state.timeRange) {
const { start, end } = convertToFixedRange(state.timeRange);
params.start = start;
params.end = end;
}
if (state.currentDashboard) {
// eslint-disable-next-line no-param-reassign
params.dashboard = state.currentDashboard;
}
......
......@@ -14,7 +14,7 @@ export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
......
......@@ -182,6 +182,10 @@ export default {
state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
state.logsPath = endpoints.logsPath || state.logsPath;
},
[types.SET_TIME_RANGE](state, timeRange) {
state.timeRange = timeRange;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
......
import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({
// API endpoints
metricsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,
currentDashboard: null,
// Dashboard data
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
dashboard: {
panel_groups: [],
},
allDashboards: [],
// Other project data
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
environmentsLoading: false,
allDashboards: [],
currentDashboard: null,
// GitLab paths to other pages
projectPath: null,
logsPath: invalidUrl,
});
......@@ -103,8 +103,9 @@ export const graphDataValidatorForAnomalyValues = graphData => {
/**
* Returns a time range from the current URL params
*
* @returns {Object} The time range defined by the
* current URL, reading from `window.location.search`
* @returns {Object|null} The time range defined by the
* current URL, reading from search query or `window.location.search`.
* Returns `null` if no parameters form a time range.
*/
export const timeRangeFromUrl = (search = window.location.search) => {
const params = queryToObject(search);
......
......@@ -102,6 +102,7 @@
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
overflow: hidden;
&:first-child {
margin-top: 0;
......@@ -115,6 +116,7 @@
padding-bottom: 0.3em;
border-bottom: 1px solid $white-dark;
color: $gl-text-color;
overflow: hidden;
}
h3 {
......
......@@ -7,7 +7,7 @@ class Admin::ServicesController < Admin::ApplicationController
before_action :service, only: [:edit, :update]
def index
@services = services_templates
@services = instance_level_services
end
def edit
......@@ -19,7 +19,7 @@ def edit
def update
if service.update(service_params[:service])
PropagateServiceTemplateWorker.perform_async(service.id) if service.active?
PropagateInstanceLevelServiceWorker.perform_async(service.id) if service.active?
redirect_to admin_application_settings_services_path,
notice: 'Application settings saved successfully'
......@@ -31,17 +31,17 @@ def update
private
# rubocop: disable CodeReuse/ActiveRecord
def services_templates
def instance_level_services
Service.available_services_names.map do |service_name|
service_template = "#{service_name}_service".camelize.constantize
service_template.where(template: true).first_or_create
service = "#{service_name}_service".camelize.constantize
service.where(instance: true).first_or_create
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def service
@service ||= Service.where(id: params[:id], template: true).first
@service ||= Service.where(id: params[:id], instance: true).first
end
# rubocop: enable CodeReuse/ActiveRecord
......
......@@ -112,10 +112,6 @@ def build_can_download_code?
has_authentication_ability?(:build_download_code) && can?(user, :build_download_code, project)
end
def storage_project
@storage_project ||= project.lfs_storage_project
end
def objects
@objects ||= (params[:objects] || []).to_a
end
......
......@@ -80,12 +80,13 @@ def create_file!(oid, size)
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
# rubocop: disable CodeReuse/ActiveRecord
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.lfs_objects_projects.create!(project: storage_project)
end
return unless object
LfsObjectsProject.safe_find_or_create_by!(
project: project,
lfs_object: object
)
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
......@@ -8,13 +8,13 @@ module Sortable
extend ActiveSupport::Concern
included do
scope :with_order_id_desc, -> { order(id: :desc) }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
scope :order_created_asc, -> { reorder(created_at: :asc) }
scope :order_updated_desc, -> { reorder(updated_at: :desc) }
scope :order_updated_asc, -> { reorder(updated_at: :asc) }
scope :with_order_id_desc, -> { order(self.arel_table['id'].desc) }
scope :order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :order_id_asc, -> { reorder(self.arel_table['id'].asc) }
scope :order_created_desc, -> { reorder(self.arel_table['created_at'].desc) }
scope :order_created_asc, -> { reorder(self.arel_table['created_at'].asc) }
scope :order_updated_desc, -> { reorder(self.arel_table['updated_at'].desc) }
scope :order_updated_asc, -> { reorder(self.arel_table['updated_at'].asc) }
scope :order_name_asc, -> { reorder(Arel::Nodes::Ascending.new(arel_table[:name].lower)) }
scope :order_name_desc, -> { reorder(Arel::Nodes::Descending.new(arel_table[:name].lower)) }
end
......
......@@ -358,6 +358,8 @@ class Project < ApplicationRecord
project_path: true,
length: { maximum: 255 }
validates :project_feature, presence: true
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
validates :import_url, public_url: { schemes: ->(project) { project.persisted? ? VALID_MIRROR_PROTOCOLS : VALID_IMPORT_PROTOCOLS },
......@@ -395,11 +397,11 @@ class Project < ApplicationRecord
# last_activity_at is throttled every minute, but last_repository_updated_at is updated with every push
scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) }
scope :sorted_by_stars_desc, -> { reorder(star_count: :desc) }
scope :sorted_by_stars_asc, -> { reorder(star_count: :asc) }
scope :sorted_by_stars_desc, -> { reorder(self.arel_table['star_count'].desc) }
scope :sorted_by_stars_asc, -> { reorder(self.arel_table['star_count'].asc) }
scope :sorted_by_name_asc_limited, ->(limit) { reorder(name: :asc).limit(limit) }
# Sometimes queries (e.g. using CTEs) require explicit disambiguation with table name
scope :projects_order_id_desc, -> { reorder("#{table_name}.id DESC") }
scope :projects_order_id_desc, -> { reorder(self.arel_table['id'].desc) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
......@@ -595,9 +597,9 @@ def sort_by_attribute(method)
# pass a string to avoid AR adding the table name
reorder('project_statistics.storage_size DESC, projects.id DESC')
when 'latest_activity_desc'
reorder(last_activity_at: :desc)
reorder(self.arel_table['last_activity_at'].desc)
when 'latest_activity_asc'
reorder(last_activity_at: :asc)
reorder(self.arel_table['last_activity_at'].asc)
when 'stars_desc'
sorted_by_stars_desc
when 'stars_asc'
......@@ -1219,13 +1221,13 @@ def find_or_initialize_service(name)
service = find_service(services, name)
return service if service
# We should check if template for the service exists
template = find_service(services_templates, name)
# We should check if an instance-level service exists
instance_level_service = find_service(instance_level_services, name)
if template
Service.build_from_template(id, template)
if instance_level_service
Service.build_from_instance(id, instance_level_service)
else
# If no template, we should create an instance. Ex `build_gitlab_ci_service`
# If no instance-level service exists, we should create a new service. Ex `build_gitlab_ci_service`
public_send("build_#{name}_service") # rubocop:disable GitlabSecurity/PublicSend
end
end
......@@ -1357,6 +1359,10 @@ def fork_source
forked_from_project || fork_network&.root_project