From 3290d46655f07d7ae3dca788d6df9f326972ffd8 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Tue, 7 Apr 2020 18:09:19 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../javascripts/monitoring/stores/actions.js | 186 ++++++++++++--------- .../monitoring/stores/mutation_types.js | 11 +- .../javascripts/monitoring/stores/mutations.js | 6 +- .../notes/components/sort_discussion.vue | 13 +- .../snippets/components/snippet_blob_view.vue | 15 +- .../fragments/snippetBase.fragment.graphql | 4 +- .../vue_shared/components/local_storage_sync.vue | 39 +++++ app/assets/stylesheets/framework/dropdowns.scss | 8 + app/models/clusters/cluster.rb | 1 + app/models/environment.rb | 1 + app/models/metrics/dashboard/annotation.rb | 33 ++++ app/policies/group_policy.rb | 4 + .../metrics/dashboard/annotation_policy.rb | 9 + app/policies/project_policy.rb | 4 + .../dashboard/annotations/create_service.rb | 80 +++++++++ .../dashboard/annotations/delete_service.rb | 43 +++++ 16 files changed, 369 insertions(+), 88 deletions(-) create mode 100644 app/assets/javascripts/vue_shared/components/local_storage_sync.vue create mode 100644 app/models/metrics/dashboard/annotation.rb create mode 100644 app/policies/metrics/dashboard/annotation_policy.rb create mode 100644 app/services/metrics/dashboard/annotations/create_service.rb create mode 100644 app/services/metrics/dashboard/annotations/delete_service.rb (limited to 'app') diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index 2e4987b7349..acc09fa6305 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -12,6 +12,20 @@ import { s__, sprintf } from '../../locale'; import { PROMETHEUS_TIMEOUT } from '../constants'; +function prometheusMetricQueryParams(timeRange) { + const { start, end } = convertToFixedRange(timeRange); + + const timeDiff = (new Date(end) - new Date(start)) / 1000; + const minStep = 60; + const queryDataPoints = 600; + + return { + start_time: start, + end_time: end, + step: Math.max(minStep, Math.ceil(timeDiff / queryDataPoints)), + }; +} + function backOffRequest(makeRequestCallback) { return backOff((next, stop) => { makeRequestCallback() @@ -26,6 +40,20 @@ function backOffRequest(makeRequestCallback) { }, PROMETHEUS_TIMEOUT); } +function getPrometheusMetricResult(prometheusEndpoint, params) { + return backOffRequest(() => axios.get(prometheusEndpoint, { params })) + .then(res => res.data) + .then(response => { + if (response.status === 'error') { + throw new Error(response.error); + } + + return response.data.result; + }); +} + +// Setup + export const setGettingStartedEmptyState = ({ commit }) => { commit(types.SET_GETTING_STARTED_EMPTY_STATE); }; @@ -47,56 +75,26 @@ export const setShowErrorBanner = ({ commit }, enabled) => { commit(types.SET_SHOW_ERROR_BANNER, enabled); }; -export const requestMetricsDashboard = ({ commit }) => { - commit(types.REQUEST_METRICS_DATA); -}; -export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => { - const { all_dashboards, dashboard, metrics_data } = response; - - commit(types.SET_ALL_DASHBOARDS, all_dashboards); - commit(types.RECEIVE_METRICS_DATA_SUCCESS, dashboard); - commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data)); - - return dispatch('fetchPrometheusMetrics', params); -}; -export const receiveMetricsDashboardFailure = ({ commit }, error) => { - commit(types.RECEIVE_METRICS_DATA_FAILURE, error); -}; - -export const receiveDeploymentsDataSuccess = ({ commit }, data) => - commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data); -export const receiveDeploymentsDataFailure = ({ commit }) => - commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE); -export const requestEnvironmentsData = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS_DATA); -export const receiveEnvironmentsDataSuccess = ({ commit }, data) => - commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data); -export const receiveEnvironmentsDataFailure = ({ commit }) => - commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); +// All Data export const fetchData = ({ dispatch }) => { - dispatch('fetchDashboard'); - dispatch('fetchDeploymentsData'); dispatch('fetchEnvironmentsData'); + dispatch('fetchDashboard'); }; +// Metrics dashboard + export const fetchDashboard = ({ state, commit, dispatch }) => { dispatch('requestMetricsDashboard'); const params = {}; - - if (state.timeRange) { - const { start, end } = convertToFixedRange(state.timeRange); - params.start_time = start; - params.end_time = end; - } - if (state.currentDashboard) { params.dashboard = state.currentDashboard; } return backOffRequest(() => axios.get(state.dashboardEndpoint, { params })) .then(resp => resp.data) - .then(response => dispatch('receiveMetricsDashboardSuccess', { response, params })) + .then(response => dispatch('receiveMetricsDashboardSuccess', { response })) .catch(error => { Sentry.captureException(error); @@ -120,61 +118,43 @@ export const fetchDashboard = ({ state, commit, dispatch }) => { }); }; -function fetchPrometheusResult(prometheusEndpoint, params) { - return backOffRequest(() => axios.get(prometheusEndpoint, { params })) - .then(res => res.data) - .then(response => { - if (response.status === 'error') { - throw new Error(response.error); - } - - return response.data.result; - }); -} - -/** - * Returns list of metrics in data.result - * {"status":"success", "data":{"resultType":"matrix","result":[]}} - * - * @param {metric} metric - */ -export const fetchPrometheusMetric = ({ commit }, { metric, params }) => { - const { start_time, end_time } = params; - const timeDiff = (new Date(end_time) - new Date(start_time)) / 1000; +export const requestMetricsDashboard = ({ commit }) => { + commit(types.REQUEST_METRICS_DASHBOARD); +}; +export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response }) => { + const { all_dashboards, dashboard, metrics_data } = response; - const minStep = 60; - const queryDataPoints = 600; - const step = metric.step ? metric.step : Math.max(minStep, Math.ceil(timeDiff / queryDataPoints)); + commit(types.SET_ALL_DASHBOARDS, all_dashboards); + commit(types.RECEIVE_METRICS_DASHBOARD_SUCCESS, dashboard); + commit(types.SET_ENDPOINTS, convertObjectPropsToCamelCase(metrics_data)); - const queryParams = { - start_time, - end_time, - step, - }; + return dispatch('fetchPrometheusMetrics'); +}; +export const receiveMetricsDashboardFailure = ({ commit }, error) => { + commit(types.RECEIVE_METRICS_DASHBOARD_FAILURE, error); +}; - commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId }); +// Metrics - return fetchPrometheusResult(metric.prometheusEndpointPath, queryParams) - .then(result => { - commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, result }); - }) - .catch(error => { - Sentry.captureException(error); +/** + * Loads timeseries data: Prometheus data points and deployment data from the project + * @param {Object} Vuex store + */ +export const fetchPrometheusMetrics = ({ state, dispatch, getters }) => { + dispatch('fetchDeploymentsData'); - commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error }); - // Continue to throw error so the dashboard can notify using createFlash - throw error; - }); -}; + if (!state.timeRange) { + createFlash(s__(`Metrics|Invalid time range, please verify.`), 'warning'); + return Promise.reject(); + } -export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, params) => { - commit(types.REQUEST_METRICS_DATA); + const defaultQueryParams = prometheusMetricQueryParams(state.timeRange); const promises = []; state.dashboard.panelGroups.forEach(group => { group.panels.forEach(panel => { panel.metrics.forEach(metric => { - promises.push(dispatch('fetchPrometheusMetric', { metric, params })); + promises.push(dispatch('fetchPrometheusMetric', { metric, defaultQueryParams })); }); }); }); @@ -192,6 +172,35 @@ export const fetchPrometheusMetrics = ({ state, commit, dispatch, getters }, par }); }; +/** + * Returns list of metrics in data.result + * {"status":"success", "data":{"resultType":"matrix","result":[]}} + * + * @param {metric} metric + */ +export const fetchPrometheusMetric = ({ commit }, { metric, defaultQueryParams }) => { + const queryParams = { ...defaultQueryParams }; + if (metric.step) { + queryParams.step = metric.step; + } + + commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId }); + + return getPrometheusMetricResult(metric.prometheusEndpointPath, queryParams) + .then(result => { + commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, result }); + }) + .catch(error => { + Sentry.captureException(error); + + commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metricId, error }); + // Continue to throw error so the dashboard can notify using createFlash + throw error; + }); +}; + +// Deployments + export const fetchDeploymentsData = ({ state, dispatch }) => { if (!state.deploymentsEndpoint) { return Promise.resolve([]); @@ -212,6 +221,14 @@ export const fetchDeploymentsData = ({ state, dispatch }) => { createFlash(s__('Metrics|There was an error getting deployment information.')); }); }; +export const receiveDeploymentsDataSuccess = ({ commit }, data) => { + commit(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, data); +}; +export const receiveDeploymentsDataFailure = ({ commit }) => { + commit(types.RECEIVE_DEPLOYMENTS_DATA_FAILURE); +}; + +// Environments export const fetchEnvironmentsData = ({ state, dispatch }) => { dispatch('requestEnvironmentsData'); @@ -241,6 +258,17 @@ export const fetchEnvironmentsData = ({ state, dispatch }) => { createFlash(s__('Metrics|There was an error getting environments information.')); }); }; +export const requestEnvironmentsData = ({ commit }) => { + commit(types.REQUEST_ENVIRONMENTS_DATA); +}; +export const receiveEnvironmentsDataSuccess = ({ commit }, data) => { + commit(types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS, data); +}; +export const receiveEnvironmentsDataFailure = ({ commit }) => { + commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); +}; + +// Dashboard manipulation /** * Set a new array of metrics to a panel group diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 09eb7dc1673..9a3489d53d7 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -1,19 +1,24 @@ -export const REQUEST_METRICS_DATA = 'REQUEST_METRICS_DATA'; -export const RECEIVE_METRICS_DATA_SUCCESS = 'RECEIVE_METRICS_DATA_SUCCESS'; -export const RECEIVE_METRICS_DATA_FAILURE = 'RECEIVE_METRICS_DATA_FAILURE'; +// Dashboard "skeleton", groups, panels and metrics +export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD'; +export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS'; +export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE'; +// Git project deployments export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA'; export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS'; export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILURE'; +// Environments export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; +// Metric data points 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'; +// Parameters and other information export const SET_TIME_RANGE = 'SET_TIME_RANGE'; export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index 2e10d189087..0a7bb47d533 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -74,18 +74,18 @@ export default { /** * Dashboard panels structure and global state */ - [types.REQUEST_METRICS_DATA](state) { + [types.REQUEST_METRICS_DASHBOARD](state) { state.emptyState = 'loading'; state.showEmptyState = true; }, - [types.RECEIVE_METRICS_DATA_SUCCESS](state, dashboard) { + [types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, dashboard) { state.dashboard = mapToDashboardViewModel(dashboard); if (!state.dashboard.panelGroups.length) { state.emptyState = 'noData'; } }, - [types.RECEIVE_METRICS_DATA_FAILURE](state, error) { + [types.RECEIVE_METRICS_DASHBOARD_FAILURE](state, error) { state.emptyState = error ? 'unableToConnect' : 'noData'; state.showEmptyState = true; }, diff --git a/app/assets/javascripts/notes/components/sort_discussion.vue b/app/assets/javascripts/notes/components/sort_discussion.vue index 3f82ddde3ef..4a7543819eb 100644 --- a/app/assets/javascripts/notes/components/sort_discussion.vue +++ b/app/assets/javascripts/notes/components/sort_discussion.vue @@ -1,7 +1,9 @@ +gs diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index a56505ee6e2..b6edadb05a9 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -1,6 +1,14 @@ .dropdown { position: relative; + // Once the new design (https://gitlab.com/gitlab-org/gitlab-foss/-/issues/63499/designs) + // for Snippets is introduced and Clone button is relocated, we won't + // need this style. + // Issue for the refactoring: https://gitlab.com/gitlab-org/gitlab/-/issues/213327 + &.gl-new-dropdown button.dropdown-toggle { + @include gl-display-inline-flex; + } + .btn-link { &:hover { cursor: pointer; diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb index 78efe2b4337..42771eaa82a 100644 --- a/app/models/clusters/cluster.rb +++ b/app/models/clusters/cluster.rb @@ -59,6 +59,7 @@ module Clusters has_one_cluster_application :elastic_stack has_many :kubernetes_namespaces + has_many :metrics_dashboard_annotations, class_name: 'Metrics::Dashboard::Annotation', inverse_of: :cluster accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :provider_aws, update_only: true diff --git a/app/models/environment.rb b/app/models/environment.rb index fecf13f349e..23c2296688d 100644 --- a/app/models/environment.rb +++ b/app/models/environment.rb @@ -18,6 +18,7 @@ class Environment < ApplicationRecord has_many :successful_deployments, -> { success }, class_name: 'Deployment' has_many :active_deployments, -> { active }, class_name: 'Deployment' has_many :prometheus_alerts, inverse_of: :environment + has_many :metrics_dashboard_annotations, class_name: 'Metrics::Dashboard::Annotation', inverse_of: :environment has_many :self_managed_prometheus_alert_events, inverse_of: :environment has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb new file mode 100644 index 00000000000..2f1b6527742 --- /dev/null +++ b/app/models/metrics/dashboard/annotation.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Metrics + module Dashboard + class Annotation < ApplicationRecord + self.table_name = 'metrics_dashboard_annotations' + + belongs_to :environment, inverse_of: :metrics_dashboard_annotations + belongs_to :cluster, class_name: 'Clusters::Cluster', inverse_of: :metrics_dashboard_annotations + + validates :starting_at, presence: true + validates :description, presence: true, length: { maximum: 255 } + validates :dashboard_path, presence: true, length: { maximum: 255 } + validates :panel_xid, length: { maximum: 255 } + validate :single_ownership + validate :orphaned_annotation + + private + + def single_ownership + return if cluster.nil? ^ environment.nil? + + errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation can't belong to both a cluster and an environment at the same time")) + end + + def orphaned_annotation + return if cluster.present? || environment.present? + + errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation must belong to a cluster or an environment")) + end + end + end +end diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index abd63753908..5e252c8e564 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -75,6 +75,9 @@ class GroupPolicy < BasePolicy rule { developer }.policy do enable :admin_milestone enable :read_package + enable :create_metrics_dashboard_annotation + enable :delete_metrics_dashboard_annotation + enable :update_metrics_dashboard_annotation end rule { reporter }.policy do @@ -82,6 +85,7 @@ class GroupPolicy < BasePolicy enable :admin_label enable :admin_list enable :admin_issue + enable :read_metrics_dashboard_annotation end rule { maintainer }.policy do diff --git a/app/policies/metrics/dashboard/annotation_policy.rb b/app/policies/metrics/dashboard/annotation_policy.rb new file mode 100644 index 00000000000..25b78e104c4 --- /dev/null +++ b/app/policies/metrics/dashboard/annotation_policy.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true +module Metrics + module Dashboard + class AnnotationPolicy < BasePolicy + delegate { @subject.cluster } + delegate { @subject.environment } + end + end +end diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e694963eac0..0f5e4ac378e 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -224,6 +224,7 @@ class ProjectPolicy < BasePolicy enable :read_sentry_issue enable :update_sentry_issue enable :read_prometheus + enable :read_metrics_dashboard_annotation end # We define `:public_user_access` separately because there are cases in gitlab-ee @@ -276,6 +277,9 @@ class ProjectPolicy < BasePolicy enable :update_deployment enable :create_release enable :update_release + enable :create_metrics_dashboard_annotation + enable :delete_metrics_dashboard_annotation + enable :update_metrics_dashboard_annotation end rule { can?(:developer_access) & user_confirmed? }.policy do diff --git a/app/services/metrics/dashboard/annotations/create_service.rb b/app/services/metrics/dashboard/annotations/create_service.rb new file mode 100644 index 00000000000..c04f4c56b51 --- /dev/null +++ b/app/services/metrics/dashboard/annotations/create_service.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +# Create Metrics::Dashboard::Annotation entry based on matched dashboard_path, environment, cluster +module Metrics + module Dashboard + module Annotations + class CreateService < ::BaseService + include Stepable + + steps :authorize_environment_access, + :authorize_cluster_access, + :parse_dashboard_path, + :create + + def initialize(user, params) + @user, @params = user, params + end + + def execute + execute_steps + end + + private + + attr_reader :user, :params + + def authorize_environment_access(options) + if environment.nil? || Ability.allowed?(user, :create_metrics_dashboard_annotation, project) + options[:environment] = environment + success(options) + else + error(s_('Metrics::Dashboard::Annotation|You are not authorized to create annotation for selected environment')) + end + end + + def authorize_cluster_access(options) + if cluster.nil? || Ability.allowed?(user, :create_metrics_dashboard_annotation, cluster) + options[:cluster] = cluster + success(options) + else + error(s_('Metrics::Dashboard::Annotation|You are not authorized to create annotation for selected cluster')) + end + end + + def parse_dashboard_path(options) + dashboard_path = params[:dashboard_path] + + Gitlab::Metrics::Dashboard::Finder.find_raw(project, dashboard_path: dashboard_path) + options[:dashboard_path] = dashboard_path + + success(options) + rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError + error(s_('Metrics::Dashboard::Annotation|Dashboard with requested path can not be found')) + end + + def create(options) + annotation = Annotation.new(options.slice(:environment, :cluster, :dashboard_path).merge(params.slice(:description, :starting_at, :ending_at))) + + if annotation.save + success(annotation: annotation) + else + error(annotation.errors) + end + end + + def environment + params[:environment] + end + + def cluster + params[:cluster] + end + + def project + (environment || cluster)&.project + end + end + end + end +end diff --git a/app/services/metrics/dashboard/annotations/delete_service.rb b/app/services/metrics/dashboard/annotations/delete_service.rb new file mode 100644 index 00000000000..c6a6c4f5fbf --- /dev/null +++ b/app/services/metrics/dashboard/annotations/delete_service.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# Delete Metrics::Dashboard::Annotation entry +module Metrics + module Dashboard + module Annotations + class DeleteService < ::BaseService + include Stepable + + steps :authorize_action, + :delete + + def initialize(user, annotation) + @user, @annotation = user, annotation + end + + def execute + execute_steps + end + + private + + attr_reader :user, :annotation + + def authorize_action(_options) + if Ability.allowed?(user, :delete_metrics_dashboard_annotation, annotation) + success + else + error(s_('Metrics::Dashboard::Annotation|You are not authorized to delete this annotation')) + end + end + + def delete(_options) + if annotation.destroy + success + else + error(s_('Metrics::Dashboard::Annotation|Annotation has not been deleted')) + end + end + end + end + end +end -- cgit v1.2.3