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

Add latest changes from gitlab-org/gitlab@master

parent badb9c1d
No related branches found
No related tags found
No related merge requests found
Showing
with 335 additions and 48 deletions
<script>
import icon from '~/vue_shared/components/icon.vue';
import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
Loading
Loading
@@ -8,13 +9,16 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
 
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
GlBadge,
},
mixins: [isProjectPendingRemoval],
props: {
item: {
type: Object,
Loading
Loading
@@ -70,6 +74,9 @@ export default {
css-class="project-stars"
icon-name="star"
/>
<div v-if="isProjectPendingRemoval">
<gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
</div>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div>
Loading
Loading
export default {
computed: {
isProjectPendingRemoval() {
return false;
},
},
};
Loading
Loading
@@ -93,6 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion_at,
};
}
 
Loading
Loading
Loading
Loading
@@ -17,10 +17,18 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT
 
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
commit(types.DISCARD_FILE_CHANGES, file.path);
if (file.tempFile || file.prevPath) dispatch('closeFile', file);
 
if (file.tempFile) {
dispatch('closeFile', file);
dispatch('deleteEntry', file.path);
} else if (file.prevPath) {
dispatch('renameEntry', {
path: file.path,
name: file.prevName,
parentPath: file.prevParentPath,
});
} else {
commit(types.DISCARD_FILE_CHANGES, file.path);
}
});
 
Loading
Loading
Loading
Loading
@@ -20,8 +20,10 @@ import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
import GroupEmptyState from './group_empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
import { metricStates } from '../constants';
 
export default {
components: {
Loading
Loading
@@ -29,6 +31,7 @@ export default {
PanelType,
GraphGroup,
EmptyState,
GroupEmptyState,
Icon,
GlButton,
GlDropdown,
Loading
Loading
@@ -184,7 +187,7 @@ export default {
'allDashboards',
'additionalPanelTypesEnabled',
]),
...mapGetters('monitoringDashboard', ['metricsWithData']),
...mapGetters('monitoringDashboard', ['getMetricStates']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
Loading
Loading
@@ -284,12 +287,35 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
groupHasData(group) {
return this.metricsWithData(group.key).length > 0;
},
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
/**
* Return a single empty state for a group.
*
* If all states are the same a single state is returned to be displayed
* Except if the state is OK, in which case the group is displayed.
*
* @param {String} groupKey - Identifier for group
* @returns {String} state code from `metricStates`
*/
groupSingleEmptyState(groupKey) {
const states = this.getMetricStates(groupKey);
if (states.length === 1 && states[0] !== metricStates.OK) {
return states[0];
}
return null;
},
/**
* A group should be not collapsed if any metric is loaded (OK)
*
* @param {String} groupKey - Identifier for group
* @returns {Boolean} If the group should be collapsed
*/
collapseGroup(groupKey) {
// Collapse group if no data is available
return !this.getMetricStates(groupKey).includes(metricStates.OK);
},
getAddMetricTrackingOptions,
},
addMetric: {
Loading
Loading
@@ -446,9 +472,9 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
:collapse-group="!groupHasData(groupData)"
:collapse-group="collapseGroup(groupData.key)"
>
<div v-if="groupHasData(groupData)">
<div v-if="!groupSingleEmptyState(groupData.key)">
<vue-draggable
:value="groupData.panels"
group="metrics-dashboard"
Loading
Loading
@@ -487,18 +513,12 @@ export default {
</vue-draggable>
</div>
<div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
<empty-state
<group-empty-state
ref="empty-group"
selected-state="noDataGroup"
:documentation-path="documentationPath"
:settings-path="settingsPath"
:clusters-path="clustersPath"
:empty-getting-started-svg-path="emptyGettingStartedSvgPath"
:empty-loading-svg-path="emptyLoadingSvgPath"
:empty-no-data-svg-path="emptyNoDataSvgPath"
:empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
:empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
:compact="true"
:selected-state="groupSingleEmptyState(groupData.key)"
:svg-path="emptyNoDataSmallSvgPath"
/>
</div>
</graph-group>
Loading
Loading
Loading
Loading
@@ -84,11 +84,6 @@ export default {
secondaryButtonText: '',
secondaryButtonPath: '',
},
noDataGroup: {
svgUrl: this.emptyNoDataSmallSvgPath,
title: __('No data to display'),
description: __('The data source is connected, but there is no data to display.'),
},
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
Loading
Loading
<script>
import { __, sprintf } from '~/locale';
import { GlEmptyState } from '@gitlab/ui';
import { metricStates } from '../constants';
export default {
components: {
GlEmptyState,
},
props: {
documentationPath: {
type: String,
required: true,
},
settingsPath: {
type: String,
required: true,
},
selectedState: {
type: String,
required: true,
},
svgPath: {
type: String,
required: true,
},
},
data() {
const documentationLink = `<a href="${this.documentationPath}">${__('More information')}</a>`;
return {
states: {
[metricStates.NO_DATA]: {
title: __('No data to display'),
slottedDescription: sprintf(
__(
'The data source is connected, but there is no data to display. %{documentationLink}',
),
{ documentationLink },
false,
),
},
[metricStates.TIMEOUT]: {
title: __('Connection timed out'),
slottedDescription: sprintf(
__(
"Charts can't be displayed as the request for data has timed out. %{documentationLink}",
),
{ documentationLink },
false,
),
},
[metricStates.CONNECTION_FAILED]: {
title: __('Connection failed'),
description: __(`We couldn't reach the Prometheus server.
Either the server no longer exists or the configuration details need updating.`),
buttonText: __('Verify configuration'),
buttonPath: this.settingsPath,
},
[metricStates.BAD_QUERY]: {
title: __('Query cannot be processed'),
slottedDescription: sprintf(
__(
`The Prometheus server responded with "bad request".
Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}`,
),
{ documentationLink },
false,
),
buttonText: __('Verify configuration'),
buttonPath: this.settingsPath,
},
[metricStates.LOADING]: {
title: __('Waiting for performance data'),
description: __(`Creating graphs uses the data from the Prometheus server.
If this takes a long time, ensure that data is available.`),
},
[metricStates.UNKNOWN_ERROR]: {
title: __('An error has occurred'),
description: __('An error occurred while loading the data. Please try again.'),
},
},
};
},
computed: {
currentState() {
return this.states[this.selectedState] || this.states[metricStates.UNKNOWN_ERROR];
},
},
};
</script>
<template>
<gl-empty-state
:title="currentState.title"
:primary-button-text="currentState.buttonText"
:primary-button-link="currentState.buttonPath"
:description="currentState.description"
:svg-path="svgPath"
:compact="true"
>
<template v-if="currentState.slottedDescription" #description>
<div v-html="currentState.slottedDescription"></div>
</template>
</gl-empty-state>
</template>
Loading
Loading
@@ -3,9 +3,19 @@ import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
 
/**
* Errors in Prometheus Queries (PromQL) for metrics
* States and error states in Prometheus Queries (PromQL) for metrics
*/
export const metricsErrors = {
export const metricStates = {
/**
* Metric data is available
*/
OK: 'OK',
/**
* Metric data is being fetched
*/
LOADING: 'LOADING',
/**
* Connection timed out to prometheus server
* the timeout is set to PROMETHEUS_TIMEOUT
Loading
Loading
@@ -24,12 +34,12 @@ export const metricsErrors = {
CONNECTION_FAILED: 'CONNECTION_FAILED',
 
/**
* The prometheus server was reach but it cannot process
* The prometheus server was reached but it cannot process
* the query. This can happen for several reasons:
* - PromQL syntax is incorrect
* - An operator is not supported
*/
BAD_DATA: 'BAD_DATA',
BAD_QUERY: 'BAD_QUERY',
 
/**
* No specific reason found for error
Loading
Loading
Loading
Loading
@@ -132,7 +132,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
})
.catch(error => {
commit(types.RECEIVE_METRIC_RESULT_ERROR, { metricId: metric.metric_id, error });
commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
});
Loading
Loading
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
 
/**
* Get all state for metric in the dashboard or a group. The
* states are not repeated so the dashboard or group can show
* a global state.
*
* @param {Object} state
* @returns {Function} A function that returns an array of
* states in all the metric in the dashboard or group.
*/
export const getMetricStates = state => groupKey => {
let groups = state.dashboard.panel_groups;
if (groupKey) {
groups = groups.filter(group => group.key === groupKey);
}
const metricStates = groups.reduce((acc, group) => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
if (metric.state) {
acc.push(metric.state);
}
});
});
return acc;
}, []);
// Deduplicate and sort array
return Array.from(new Set(metricStates)).sort();
};
/**
* Getter to obtain the list of metric ids that have data
*
Loading
Loading
Loading
Loading
@@ -12,7 +12,7 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
 
export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
export const RECEIVE_METRIC_RESULT_ERROR = 'RECEIVE_METRIC_RESULT_ERROR';
export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
 
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
Loading
Loading
Loading
Loading
@@ -3,7 +3,7 @@ import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
import { metricsErrors } from '../constants';
import { metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
 
const normalizePanelMetrics = (metrics, defaultLabel) =>
Loading
Loading
@@ -41,39 +41,39 @@ const findMetricInDashboard = (metricId, dashboard) => {
* @param {Object} metric - Metric object as defined in the dashboard
* @param {Object} state - New state
* @param {Array|null} state.result - Array of results
* @param {String} state.error - Error code from metricsErrors
* @param {String} state.error - Error code from metricStates
* @param {Boolean} state.loading - True if the metric is loading
*/
const setMetricState = (metric, { result = null, error = null, loading = false }) => {
const setMetricState = (metric, { result = null, loading = false, state = null }) => {
Vue.set(metric, 'result', result);
Vue.set(metric, 'error', error);
Vue.set(metric, 'loading', loading);
Vue.set(metric, 'state', state);
};
 
/**
* Maps a backened error state to a `metricsErrors` constant
* Maps a backened error state to a `metricStates` constant
* @param {Object} error - Error from backend response
*/
const getMetricError = error => {
const emptyStateFromError = error => {
if (!error) {
return metricsErrors.UNKNOWN_ERROR;
return metricStates.UNKNOWN_ERROR;
}
 
// Special error responses
if (error.message === BACKOFF_TIMEOUT) {
return metricsErrors.TIMEOUT;
return metricStates.TIMEOUT;
}
 
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
return metricsErrors.CONNECTION_FAILED;
return metricStates.CONNECTION_FAILED;
} else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information
return metricsErrors.BAD_DATA;
return metricStates.BAD_QUERY;
}
 
return metricsErrors.UNKNOWN_ERROR;
return metricStates.UNKNOWN_ERROR;
};
 
export default {
Loading
Loading
@@ -132,9 +132,9 @@ export default {
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
loading: true,
state: metricStates.LOADING,
});
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
Loading
Loading
@@ -146,24 +146,24 @@ export default {
 
const metric = findMetricInDashboard(metricId, state.dashboard);
if (!result || result.length === 0) {
// If no data is return we still consider it an error and set it to undefined
setMetricState(metric, {
error: metricsErrors.NO_DATA,
state: metricStates.NO_DATA,
});
} else {
const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults),
state: metricStates.OK,
});
}
},
[types.RECEIVE_METRIC_RESULT_ERROR](state, { metricId, error }) {
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
error: getMetricError(error),
state: emptyStateFromError(error),
});
},
 
Loading
Loading
Loading
Loading
@@ -16,15 +16,17 @@ class Projects::HookLogsController < Projects::ApplicationController
end
 
def retry
result = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(result)
execute_hook
redirect_to edit_project_hook_path(@project, @hook)
end
 
private
 
def execute_hook
result = hook.execute(hook_log.request_data, hook_log.trigger)
set_hook_execution_notice(result)
end
def hook
@hook ||= @project.hooks.find(params[:hook_id])
end
Loading
Loading
# frozen_string_literal: true
class Projects::ServiceHookLogsController < Projects::HookLogsController
before_action :service, only: [:show, :retry]
def retry
execute_hook
redirect_to edit_project_service_path(@project, @service)
end
private
def hook
@hook ||= service.service_hook
end
def service
@service ||= @project.find_or_initialize_service(params[:service_id])
end
end
Loading
Loading
@@ -7,6 +7,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :ensure_service_enabled
before_action :service
before_action :web_hook_logs, only: [:edit, :update]
 
respond_to :html
 
Loading
Loading
@@ -77,6 +78,12 @@ class Projects::ServicesController < Projects::ApplicationController
@service ||= @project.find_or_initialize_service(params[:id])
end
 
def web_hook_logs
return unless @service.service_hook.present?
@web_hook_logs ||= @service.service_hook.web_hook_logs.recent.page(params[:page])
end
def ensure_service_enabled
render_404 unless service
end
Loading
Loading
# frozen_string_literal: true
module Mutations
module Snippets
class MarkAsSpam < Base
graphql_name 'MarkAsSpamSnippet'
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the snippet to update'
def resolve(id:)
snippet = authorized_find!(id: id)
result = mark_as_spam(snippet)
errors = result ? [] : ['Error with Akismet. Please check the logs for more info.']
{
errors: errors
}
end
private
def mark_as_spam(snippet)
SpamService.new(snippet).mark_as_spam!
end
def authorized_resource?(snippet)
super && snippet.submittable_as_spam_by?(context[:current_user])
end
def ability_name
"admin"
end
end
end
end
Loading
Loading
@@ -28,6 +28,7 @@ module Types
mount_mutation Mutations::Snippets::Destroy
mount_mutation Mutations::Snippets::Update
mount_mutation Mutations::Snippets::Create
mount_mutation Mutations::Snippets::MarkAsSpam
end
end
 
Loading
Loading
Loading
Loading
@@ -4,6 +4,7 @@
class Blob < SimpleDelegator
include Presentable
include BlobLanguageFromGitAttributes
include BlobActiveModel
 
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
Loading
Loading
# frozen_string_literal: true
# To be included in blob classes which are to be
# treated as ActiveModel.
#
# The blob class must respond_to `project`
module BlobActiveModel
extend ActiveSupport::Concern
class_methods do
def declarative_policy_class
'BlobPolicy'
end
end
def to_ability_name
'blob'
end
end
# frozen_string_literal: true
module SafeUrl
extend ActiveSupport::Concern
def safe_url(usernames_whitelist: [])
return if url.nil?
uri = URI.parse(url)
uri.password = '*****' if uri.password
uri.user = '*****' if uri.user && !usernames_whitelist.include?(uri.user)
uri.to_s
rescue URI::Error
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