Read about our upcoming Code of Conduct on this issue

Commit 4b50532b authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 2c8bb8e4771e
......@@ -30,7 +30,7 @@
policy: pull
.use-pg9:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services:
- name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......@@ -63,7 +63,7 @@
key: "debian-stretch-ruby-2.6.5-pg11-node-12.x"
.use-pg9-ee:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.14-git-2.26-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34"
services:
- name: postgres:9.6.17
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
......
......@@ -81,7 +81,7 @@ review-build-cng:
.review-workflow-base:
extends:
- .default-retry
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-charts-build-base
image: registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-helm3-kubectl1.14
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
......@@ -113,7 +113,6 @@ review-deploy:
script:
- check_kube_domain
- ensure_namespace
- install_tiller
- install_external_dns
- download_chart
- date
......@@ -149,6 +148,7 @@ review-stop-failed-deployment:
stage: prepare
script:
- delete_failed_release
- delete_helm2_release
review-stop:
extends:
......@@ -210,8 +210,11 @@ review-qa-all:
review-performance:
extends:
- .review-docker
- .default-retry
- .review:rules:mr-and-schedule-auto-if-frontend-manual-otherwise
image:
name: sitespeedio/sitespeed.io:6.3.1
entrypoint: [""]
stage: qa
# This is needed so that manual jobs with needs don't block the pipeline.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/199979.
......@@ -224,7 +227,7 @@ review-performance:
- wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
- mkdir -p sitespeed-results
script:
- docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
- /start.sh --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "${CI_ENVIRONMENT_URL}"
after_script:
- mv sitespeed-results/data/performance.json performance.json
artifacts:
......
......@@ -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
......
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';
......
......@@ -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;
},
......
gs
<script>
import { GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import { __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import Tracking from '~/tracking';
import { ASC, DESC } from '../constants';
......@@ -14,16 +16,20 @@ export default {
SORT_OPTIONS,
components: {
GlIcon,
LocalStorageSync,
},
mixins: [Tracking.mixin()],
computed: {
...mapGetters(['sortDirection']),
...mapGetters(['sortDirection', 'noteableType']),
selectedOption() {
return SORT_OPTIONS.find(({ key }) => this.sortDirection === key);
},
dropdownText() {
return this.selectedOption.text;
},
storageKey() {
return `sort_direction_${this.noteableType.toLowerCase()}`;
},
},
methods: {
...mapActions(['setDiscussionSortDirection']),
......@@ -44,6 +50,11 @@ export default {
<template>
<div class="mr-2 d-inline-block align-bottom full-width-mobile">
<local-storage-sync
:value="sortDirection"
:storage-key="storageKey"
@input="setDiscussionSortDirection"
/>
<button class="btn btn-sm js-dropdown-text" data-toggle="dropdown" aria-expanded="false">
{{ dropdownText }}
<gl-icon name="chevron-down" />
......
......@@ -4,6 +4,7 @@ import { SNIPPET_VISIBILITY_PUBLIC } from '../constants';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
import GetSnippetBlobQuery from '../queries/snippet.blob.query.graphql';
import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
......@@ -16,6 +17,7 @@ export default {
BlobHeader,
BlobContent,
GlLoadingIcon,
CloneDropdownButton,
},
apollo: {
blob: {
......@@ -72,6 +74,9 @@ export default {
const { richViewer, simpleViewer } = this.blob;
return this.activeViewerType === RICH_BLOB_VIEWER ? richViewer : simpleViewer;
},
canBeCloned() {
return this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo;
},
},
methods: {
switchViewer(newViewer, respectHash = false) {
......@@ -90,7 +95,15 @@ export default {
class="prepend-top-20 append-bottom-20"
/>
<article v-else class="file-holder snippet-file-content">
<blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer" />
<blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer">
<template #actions>
<clone-dropdown-button
v-if="canBeCloned"
:ssh-link="snippet.sshUrlToRepo"
:http-link="snippet.httpUrlToRepo"
/>
</template>
</blob-header>
<blob-content :loading="isContentLoading" :content="blobContent" :active-viewer="viewer" />
</article>
</div>
......
......@@ -7,8 +7,10 @@ fragment SnippetBase on Snippet {
updatedAt
visibilityLevel
webUrl
httpUrlToRepo
sshUrlToRepo
userPermissions {
adminSnippet
updateSnippet
}
}
\ No newline at end of file
}
<script>
export default {
props: {
storageKey: {
type: String,
required: true,
},
value: {
type: String,
required: false,
default: '',
},
},
watch: {
value(newVal) {
this.saveValue(newVal);
},
},
mounted() {
// On mount, trigger update if we actually have a localStorageValue
const value = this.getValue();
if (value && this.value !== value) {
this.$emit('input', value);
}
},
methods: {
getValue() {
return localStorage.getItem(this.storageKey);
},
saveValue(val) {
localStorage.setItem(this.storageKey, val);
},
},
render() {
return this.$slots.default;
},
};
</script>
.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;
......
......@@ -59,6 +59,7 @@ def self.has_one_cluster_application(name) # rubocop:disable Naming/PredicateNam
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
......
......@@ -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'
......
# 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
......@@ -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