diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-17 15:08:15 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-12-17 15:08:15 +0000 |
commit | c2b98d3dbd47ab92c79c702276fe9130d9a28036 (patch) | |
tree | bf4071f551fdc12c22b23b2bb66483064e7b9ea9 /app | |
parent | badb9c1deacbea601b02f88811b7e123589d9251 (diff) | |
download | gitlab-ce-c2b98d3dbd47ab92c79c702276fe9130d9a28036.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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", |