summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 15:08:15 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 15:08:15 +0000
commitc2b98d3dbd47ab92c79c702276fe9130d9a28036 (patch)
treebf4071f551fdc12c22b23b2bb66483064e7b9ea9 /app
parentbadb9c1deacbea601b02f88811b7e123589d9251 (diff)
downloadgitlab-ce-c2b98d3dbd47ab92c79c702276fe9130d9a28036.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue7
-rw-r--r--app/assets/javascripts/groups/mixins/is_project_pending_removal.js7
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions.js12
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue105
-rw-r--r--app/assets/javascripts/monitoring/constants.js18
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js30
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js32
-rw-r--r--app/controllers/projects/hook_logs_controller.rb10
-rw-r--r--app/controllers/projects/service_hook_logs_controller.rb20
-rw-r--r--app/controllers/projects/services_controller.rb7
-rw-r--r--app/graphql/mutations/snippets/mark_as_spam.rb39
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/blob.rb1
-rw-r--r--app/models/concerns/blob_active_model.rb19
-rw-r--r--app/models/concerns/safe_url.rb15
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/web_hook_log.rb11
-rw-r--r--app/models/readme_blob.rb2
-rw-r--r--app/models/remote_mirror.rb9
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/policies/blob_policy.rb7
-rw-r--r--app/policies/wiki_page_policy.rb7
-rw-r--r--app/presenters/hooks/project_hook_presenter.rb13
-rw-r--r--app/presenters/hooks/service_hook_presenter.rb13
-rw-r--r--app/presenters/web_hook_log_presenter.rb13
-rw-r--r--app/serializers/group_child_entity.rb2
-rw-r--r--app/services/web_hook_service.rb3
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml2
-rw-r--r--app/views/admin/projects/_archived.html.haml3
-rw-r--r--app/views/admin/projects/_projects.html.haml3
-rw-r--r--app/views/projects/_archived_notice.html.haml5
-rw-r--r--app/views/projects/_remove.html.haml10
-rw-r--r--app/views/projects/edit.html.haml27
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/hook_logs/show.html.haml3
-rw-r--r--app/views/projects/services/edit.html.haml4
-rw-r--r--app/views/projects/settings/_archive.html.haml18
-rw-r--r--app/views/projects/show.html.haml7
-rw-r--r--app/views/shared/projects/_archived.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml3
46 files changed, 464 insertions, 96 deletions
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 734a9a89c72..675552e6c2b 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,5 +1,6 @@
<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,
@@ -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,
@@ -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>
diff --git a/app/assets/javascripts/groups/mixins/is_project_pending_removal.js b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
new file mode 100644
index 00000000000..e44e5780199
--- /dev/null
+++ b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ isProjectPendingRemoval() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 16f95d5a0cc..214ac5e3db5 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -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,
};
}
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 2e7bf9a7d5a..dd69e2d6f1f 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -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);
}
});
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2a9321f6733..c1ca5449ba3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -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: {
@@ -29,6 +31,7 @@ export default {
PanelType,
GraphGroup,
EmptyState,
+ GroupEmptyState,
Icon,
GlButton,
GlDropdown,
@@ -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]
@@ -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: {
@@ -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"
@@ -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>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 728910dd633..d3157b731b2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -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'),
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
new file mode 100644
index 00000000000..dee4e5998ee
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -0,0 +1,105 @@
+<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>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index e613351e524..398b45b9012 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -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
@@ -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
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a655191b2b4..1cb82ce0083 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -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;
});
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 3eddd52705d..a13157c6f87 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -2,6 +2,36 @@ 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
*
* Useful to understand which parts of the dashboard should
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index e4e467f3d68..74068e1d846 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -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';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index f04c12c2ac8..16a34a6c026 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -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) =>
@@ -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 {
@@ -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 }) {
@@ -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),
});
},
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
index a7afc3d77a5..ed7e7b68acb 100644
--- a/app/controllers/projects/hook_logs_controller.rb
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -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
diff --git a/app/controllers/projects/service_hook_logs_controller.rb b/app/controllers/projects/service_hook_logs_controller.rb
new file mode 100644
index 00000000000..5c814ea139f
--- /dev/null
+++ b/app/controllers/projects/service_hook_logs_controller.rb
@@ -0,0 +1,20 @@
+# 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
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index c9f680a4696..daaca9e1268 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -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
@@ -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
diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb
new file mode 100644
index 00000000000..260a9753f76
--- /dev/null
+++ b/app/graphql/mutations/snippets/mark_as_spam.rb
@@ -0,0 +1,39 @@
+# 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
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 998dfdc7815..0a9c0143945 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -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
diff --git a/app/models/blob.rb b/app/models/blob.rb
index c0f26ee64f8..0a425f2b961 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -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
diff --git a/app/models/concerns/blob_active_model.rb b/app/models/concerns/blob_active_model.rb
new file mode 100644
index 00000000000..89157e90e34
--- /dev/null
+++ b/app/models/concerns/blob_active_model.rb
@@ -0,0 +1,19 @@
+# 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
diff --git a/app/models/concerns/safe_url.rb b/app/models/concerns/safe_url.rb
new file mode 100644
index 00000000000..febca7d241f
--- /dev/null
+++ b/app/models/concerns/safe_url.rb
@@ -0,0 +1,15 @@
+# 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
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 9ae697b9e59..a5f68831f34 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@
class ProjectHook < WebHook
include TriggerableHooks
+ include Presentable
triggerable_hooks [
:push_hooks,
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 8f305dd7c22..4caa45a13d4 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ServiceHook < WebHook
+ include Presentable
+
belongs_to :service
validates :service, presence: true
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index cfb1f3ec63b..df0e7b30f84 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class WebHookLog < ApplicationRecord
+ include SafeUrl
+ include Presentable
+
belongs_to :web_hook
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -9,6 +12,8 @@ class WebHookLog < ApplicationRecord
validates :web_hook, presence: true
+ before_save :obfuscate_basic_auth
+
def self.recent
where('created_at >= ?', 2.days.ago.beginning_of_day)
.order(created_at: :desc)
@@ -17,4 +22,10 @@ class WebHookLog < ApplicationRecord
def success?
response_status =~ /^2/
end
+
+ private
+
+ def obfuscate_basic_auth
+ self.url = safe_url
+ end
end
diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb
index 7b49fa632f6..695b4e3ffe3 100644
--- a/app/models/readme_blob.rb
+++ b/app/models/readme_blob.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ReadmeBlob < SimpleDelegator
+ include BlobActiveModel
+
attr_reader :repository
def initialize(blob, repository)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c165a1a9b0d..1e5c93cd913 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -3,6 +3,7 @@
class RemoteMirror < ApplicationRecord
include AfterCommitQueue
include MirrorAuthentication
+ include SafeUrl
MAX_FIRST_RUNTIME = 3.hours
MAX_INCREMENTAL_RUNTIME = 1.hour
@@ -194,13 +195,7 @@ class RemoteMirror < ApplicationRecord
end
def safe_url
- return if url.nil?
-
- result = URI.parse(url)
- result.password = '*****' if result.password
- result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user
- result.to_s
- rescue URI::Error
+ super(usernames_whitelist: %w[git])
end
def ensure_remote!
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index f9c562364cb..c6867e48cbf 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -274,6 +274,10 @@ class WikiPage
@attributes.merge!(attrs)
end
+ def to_ability_name
+ 'wiki_page'
+ end
+
private
# Process and format the title based on the user input.
diff --git a/app/policies/blob_policy.rb b/app/policies/blob_policy.rb
new file mode 100644
index 00000000000..639b9dfeea7
--- /dev/null
+++ b/app/policies/blob_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class BlobPolicy < BasePolicy
+ delegate { @subject.project }
+
+ rule { can?(:download_code) }.enable :read_blob
+end
diff --git a/app/policies/wiki_page_policy.rb b/app/policies/wiki_page_policy.rb
new file mode 100644
index 00000000000..468632c9085
--- /dev/null
+++ b/app/policies/wiki_page_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class WikiPagePolicy < BasePolicy
+ delegate { @subject.wiki.project }
+
+ rule { can?(:read_wiki) }.enable :read_wiki_page
+end
diff --git a/app/presenters/hooks/project_hook_presenter.rb b/app/presenters/hooks/project_hook_presenter.rb
new file mode 100644
index 00000000000..a65c7221b5a
--- /dev/null
+++ b/app/presenters/hooks/project_hook_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
+ presents :project_hook
+
+ def logs_details_path(log)
+ project_hook_hook_log_path(project, self, log)
+ end
+
+ def logs_retry_path(log)
+ retry_project_hook_hook_log_path(project, self, log)
+ end
+end
diff --git a/app/presenters/hooks/service_hook_presenter.rb b/app/presenters/hooks/service_hook_presenter.rb
new file mode 100644
index 00000000000..bc20d5b1a3b
--- /dev/null
+++ b/app/presenters/hooks/service_hook_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
+ presents :service_hook
+
+ def logs_details_path(log)
+ project_service_hook_log_path(service.project, service, log)
+ end
+
+ def logs_retry_path(log)
+ retry_project_service_hook_log_path(service.project, service, log)
+ end
+end
diff --git a/app/presenters/web_hook_log_presenter.rb b/app/presenters/web_hook_log_presenter.rb
new file mode 100644
index 00000000000..fca03ddb5d7
--- /dev/null
+++ b/app/presenters/web_hook_log_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
+ presents :web_hook_log
+
+ def details_path
+ web_hook.present.logs_details_path(self)
+ end
+
+ def retry_path
+ web_hook.present.logs_retry_path(self)
+ end
+end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index 20d7032c970..a7fe4d3f9b9 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
end
end
end
+
+GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 8c294218708..87edac36e33 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -92,9 +92,6 @@ class WebHookService
end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
- # logging for ServiceHook's is not available
- return if hook.is_a?(ServiceHook)
-
WebHookLog.create(
web_hook: hook,
trigger: trigger,
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index be5f1f4f9a8..ae90ffd9efc 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
+ = render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
@@ -53,6 +54,7 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.form-text.text-muted#clone-protocol-help
= _('Allow only the selected protocols to be used for Git access.')
+
.form-group
= f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold'
= f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block'
diff --git a/app/views/admin/projects/_archived.html.haml b/app/views/admin/projects/_archived.html.haml
new file mode 100644
index 00000000000..8b4d5806c47
--- /dev/null
+++ b/app/views/admin/projects/_archived.html.haml
@@ -0,0 +1,3 @@
+- if project.archived
+ %span.badge.badge-warning
+ = _('archived')
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 2f7ad35eb3e..f842ab2d009 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -14,8 +14,7 @@
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- - if project.archived
- %span.badge.badge-warning archived
+ = render_if_exists 'admin/projects/archived', project: project
.title
= link_to(admin_project_path(project)) do
.dash-project-avatar
diff --git a/app/views/projects/_archived_notice.html.haml b/app/views/projects/_archived_notice.html.haml
new file mode 100644
index 00000000000..522693ae24a
--- /dev/null
+++ b/app/views/projects/_archived_notice.html.haml
@@ -0,0 +1,5 @@
+- if project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ = _('Archived project! Repository and other project resources are read only')
diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml
new file mode 100644
index 00000000000..6c84fbfeeb3
--- /dev/null
+++ b/app/views/projects/_remove.html.haml
@@ -0,0 +1,10 @@
+- return unless can?(current_user, :remove_project, project)
+
+.sub-section
+ %h4.danger-title= _('Remove project')
+ %p
+ %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
+ = form_tag(project_path(project), method: :delete) do
+ %p
+ %strong= _('Removed projects cannot be restored!')
+ = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 7ad52673137..1c18487f688 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -73,23 +73,7 @@
= render 'export', project: @project
- - if can? current_user, :archive_project, @project
- .sub-section
- %h4.warning-title
- - if @project.archived?
- = _('Unarchive project')
- - else
- = _('Archive project')
- - if @project.archived?
- %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>").html_safe
- = link_to _('Unarchive project'), unarchive_project_path(@project),
- data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
- method: :post, class: "btn btn-success"
- - else
- %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>").html_safe
- = link_to _('Archive project'), archive_project_path(@project),
- data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
- method: :post, class: "btn btn-warning"
+ = render_if_exists 'projects/settings/archive'
.sub-section.rename-repository
%h4.warning-title= _('Change path')
= render 'projects/errors'
@@ -135,14 +119,7 @@
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
= button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
- - if can?(current_user, :remove_project, @project)
- .sub-section
- %h4.danger-title= _('Remove project')
- %p= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
- = form_tag(project_path(@project), method: :delete) do
- %p
- %strong= _('Removed projects cannot be restored!')
- = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
+ = render 'remove', project: @project
.save-project-loader.hide
.center
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 3e54c3ca9f8..ada986dd969 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -28,7 +28,7 @@
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
- = link_to 'View details', project_hook_hook_log_path(project, hook, hook_log)
+ = link_to 'View details', hook_log.present.details_path
= paginate hook_logs, theme: 'gitlab'
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index bd8ca5e7d70..a8796cd7b1c 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -3,7 +3,6 @@
%h4.prepend-top-0
Request details
.col-lg-9
-
- = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10"
+ = link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 1e7903535c6..e3e8a312431 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,8 +1,10 @@
- breadcrumb_title @service.title
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
-- add_to_breadcrumbs(s_("ProjectService|Integrations"), namespace_project_settings_integrations_path)
+- add_to_breadcrumbs(s_("ProjectService|Integrations"), project_settings_integrations_path(@project))
= render 'deprecated_message' if @service.deprecation_message
= render 'form'
+- if @web_hook_logs
+ = render partial: 'projects/hook_logs/index', locals: { hook: @service.service_hook, hook_logs: @web_hook_logs, project: @project }
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
new file mode 100644
index 00000000000..3307c3775ec
--- /dev/null
+++ b/app/views/projects/settings/_archive.html.haml
@@ -0,0 +1,18 @@
+- return unless can?(current_user, :archive_project, @project)
+
+.sub-section
+ %h4.warning-title
+ - if @project.archived?
+ = _('Unarchive project')
+ - else
+ = _('Archive project')
+ - if @project.archived?
+ %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = link_to _('Unarchive project'), unarchive_project_path(@project),
+ data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
+ method: :post, class: "btn btn-success"
+ - else
+ %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = link_to _('Archive project'), archive_project_path(@project),
+ data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
+ method: :post, class: "btn btn-warning"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index c5653c3dd5a..8f13806e8cd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -18,11 +18,8 @@
- if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages)
- - if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- #{ _('Archived project! Repository and other project resources are read-only') }
+ = render "archived_notice", project: @project
+ = render_if_exists "projects/marked_for_deletion_notice", project: @project
- view_path = @project.default_view
diff --git a/app/views/shared/projects/_archived.html.haml b/app/views/shared/projects/_archived.html.haml
new file mode 100644
index 00000000000..fad93d14390
--- /dev/null
+++ b/app/views/shared/projects/_archived.html.haml
@@ -0,0 +1,3 @@
+- if project.archived
+ %span.d-flex.badge.badge-warning
+ = _('archived')
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 5b9af0267cc..45e95685677 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -67,8 +67,7 @@
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
- - if project.archived
- %span.d-flex.icon-wrapper.badge.badge-warning archived
+ = render_if_exists 'shared/projects/archived', project: project
- if stars
= link_to project_starrers_path(project),
class: "d-flex align-items-center icon-wrapper stars has-tooltip",