Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-12-17 15:08:15 +00:00
parent badb9c1dea
commit c2b98d3dbd
91 changed files with 1509 additions and 192 deletions

View file

@ -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>

View file

@ -0,0 +1,7 @@
export default {
computed: {
isProjectPendingRemoval() {
return false;
},
},
};

View file

@ -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,
};
}

View file

@ -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);
}
});

View file

@ -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>

View file

@ -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'),

View file

@ -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>

View file

@ -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

View file

@ -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;
});

View file

@ -1,6 +1,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
*

View file

@ -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';

View file

@ -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),
});
},

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -2,6 +2,7 @@
class ProjectHook < WebHook
include TriggerableHooks
include Presentable
triggerable_hooks [
:push_hooks,

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ServiceHook < WebHook
include Presentable
belongs_to :service
validates :service, presence: true

View file

@ -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

View file

@ -1,6 +1,8 @@
# frozen_string_literal: true
class ReadmeBlob < SimpleDelegator
include BlobActiveModel
attr_reader :repository
def initialize(blob, repository)

View file

@ -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!

View file

@ -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.

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class BlobPolicy < BasePolicy
delegate { @subject.project }
rule { can?(:download_code) }.enable :read_blob
end

View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class WikiPagePolicy < BasePolicy
delegate { @subject.wiki.project }
rule { can?(:read_wiki) }.enable :read_wiki_page
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
end
end
end
GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')

View file

@ -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,

View file

@ -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'

View file

@ -0,0 +1,3 @@
- if project.archived
%span.badge.badge-warning
= _('archived')

View file

@ -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

View file

@ -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')

View file

@ -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) }

View file

@ -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

View file

@ -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'

View file

@ -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 }

View file

@ -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 }

View file

@ -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"

View file

@ -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

View file

@ -0,0 +1,3 @@
- if project.archived
%span.d-flex.badge.badge-warning
= _('archived')

View file

@ -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",

View file

@ -0,0 +1,5 @@
---
title: Fix "Discard all" for new and renamed files
merge_request: 21854
author:
type: fixed

View file

@ -0,0 +1,5 @@
---
title: Add specific error states to dashboard
merge_request: 21618
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add mark as spam snippet mutation
merge_request: 21912
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Added WebHookLogs for ServiceHooks
merge_request: 20976
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Update auto-deploy-image to v0.8.3
merge_request: 21696
author:
type: fixed

View file

@ -475,6 +475,9 @@ Gitlab.ee do
Settings.cron_jobs['clear_shared_runners_minutes_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['clear_shared_runners_minutes_worker']['cron'] ||= '0 0 1 * *'
Settings.cron_jobs['clear_shared_runners_minutes_worker']['job_class'] = 'ClearSharedRunnersMinutesWorker'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['cron'] ||= '0 4 * * *'
Settings.cron_jobs['adjourned_projects_deletion_cron_worker']['job_class'] = 'AdjournedProjectsDeletionCronWorker'
Settings.cron_jobs['geo_file_download_dispatch_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['geo_file_download_dispatch_worker']['cron'] ||= '*/1 * * * *'
Settings.cron_jobs['geo_file_download_dispatch_worker']['job_class'] ||= 'Geo::FileDownloadDispatchWorker'

View file

@ -159,6 +159,12 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
member do
put :test
end
resources :hook_logs, only: [:show], controller: :service_hook_logs do
member do
post :retry
end
end
end
resources :boards, only: [:index, :show, :create, :update, :destroy], constraints: { id: /\d+/ } do

View file

@ -124,4 +124,4 @@
- [design_management_new_version, 1]
- [epics, 2]
- [personal_access_tokens, 1]
- [adjourned_project_deletion, 1]

View file

@ -90,7 +90,6 @@ The following metrics can be controlled by feature flags:
| Metric | Feature Flag |
|:---------------------------------------------------------------|:-------------------------------------------------------------------|
| `gitlab_method_call_duration_seconds` | `prometheus_metrics_method_instrumentation` |
| `gitlab_transaction_allocated_memory_bytes` | `prometheus_metrics_transaction_allocated_memory` |
| `gitlab_view_rendering_duration_seconds` | `prometheus_metrics_view_instrumentation` |
## Sidekiq Metrics available for Geo **(PREMIUM)**

View file

@ -3069,6 +3069,41 @@ type LabelEdge {
node: Label
}
"""
Autogenerated input type of MarkAsSpamSnippet
"""
input MarkAsSpamSnippetInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the snippet to update
"""
id: ID!
}
"""
Autogenerated return type of MarkAsSpamSnippet
"""
type MarkAsSpamSnippetPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The snippet after mutation
"""
snippet: Snippet
}
type MergeRequest implements Noteable {
"""
Indicates if members of the target project can push to the fork
@ -3941,6 +3976,7 @@ type Mutation {
issueSetConfidential(input: IssueSetConfidentialInput!): IssueSetConfidentialPayload
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
markAsSpamSnippet(input: MarkAsSpamSnippetInput!): MarkAsSpamSnippetPayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetLabels(input: MergeRequestSetLabelsInput!): MergeRequestSetLabelsPayload
mergeRequestSetLocked(input: MergeRequestSetLockedInput!): MergeRequestSetLockedPayload

View file

@ -16121,6 +16121,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "markAsSpamSnippet",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "MarkAsSpamSnippetInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "MarkAsSpamSnippetPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequestSetAssignees",
"description": null,
@ -19662,6 +19689,108 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MarkAsSpamSnippetPayload",
"description": "Autogenerated return type of MarkAsSpamSnippet",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippet",
"description": "The snippet after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Snippet",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "MarkAsSpamSnippetInput",
"description": "Autogenerated input type of MarkAsSpamSnippet",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the snippet to update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DesignManagementUploadPayload",

View file

@ -429,6 +429,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `color` | String! | Background color of the label |
| `textColor` | String! | Text color of the label |
### MarkAsSpamSnippetPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `snippet` | Snippet | The snippet after mutation |
### MergeRequest
| Name | Type | Description |

View file

@ -1713,7 +1713,12 @@ Example response:
## Remove project
Removes a project including all associated resources (issues, merge requests etc).
This endpoint either:
- Removes a project including all associated resources (issues, merge requests etc).
- From GitLab 12.6 on Premium or higher tiers, marks a project for deletion. Actual
deletion happens after number of days specified in
[instance settings](../user/admin_area/settings/visibility_and_access_controls.md#project-deletion-adjourned-period-premium-only).
```
DELETE /projects/:id
@ -1723,6 +1728,18 @@ DELETE /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Restore project marked for deletion **(PREMIUM)**
Restores project marked for deletion.
```
POST /projects/:id/restore
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
## Upload a file
Uploads a file to the specified project to be used in an issue or merge request description, or a comment.

View file

@ -72,14 +72,15 @@ Example response:
```
Users on GitLab [Premium or Ultimate](https://about.gitlab.com/pricing/) may also see
the `file_template_project_id` or the `geo_node_allowed_ips` parameters:
the `file_template_project_id`, `deletion_adjourned_period`, or the `geo_node_allowed_ips` parameters:
```json
{
"id" : 1,
"signup_enabled" : true,
"file_template_project_id": 1,
"geo_node_allowed_ips": "0.0.0.0/0, ::/0"
"geo_node_allowed_ips": "0.0.0.0/0, ::/0",
"deletion_adjourned_period": 7,
...
}
```
@ -162,6 +163,7 @@ these parameters:
- `file_template_project_id`
- `geo_node_allowed_ips`
- `geo_status_timeout`
- `deletion_adjourned_period`
Example responses: **(PREMIUM ONLY)**
@ -292,6 +294,7 @@ are listed in the descriptions of the relevant settings.
| `plantuml_enabled` | boolean | no | (**If enabled, requires:** `plantuml_url`) Enable PlantUML integration. Default is `false`. |
| `plantuml_url` | string | required by: `plantuml_enabled` | The PlantUML instance URL for integration. |
| `polling_interval_multiplier` | decimal | no | Interval multiplier used by endpoints that perform polling. Set to `0` to disable polling. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM ONLY)** How many days after marking project for deletion it is actually removed. Value between 0 and 90.
| `project_export_enabled` | boolean | no | Enable project export. |
| `prometheus_metrics_enabled` | boolean | no | Enable Prometheus metrics. |
| `protected_ci_variables` | boolean | no | Environment variables are protected by default. |

View file

@ -48,6 +48,17 @@ To ensure only admin users can delete projects:
1. Check the **Default project deletion protection** checkbox.
1. Click **Save changes**.
## Project deletion adjourned period **(PREMIUM ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/32935) in GitLab 12.6.
By default, project marked for deletion will be permanently removed after 7 days. This period may be changed.
To change this period:
1. Select the desired option.
1. Click **Save changes**.
## Default project visibility
To set the default visibility levels for new projects:

View file

@ -26,6 +26,14 @@ module API
def verify_update_project_attrs!(project, attrs)
end
def delete_project(user_project)
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
end
end
helpers do
@ -404,11 +412,7 @@ module API
delete ":id" do
authorize! :remove_project, user_project
destroy_conditionally!(user_project) do
::Projects::DestroyService.new(user_project, current_user, {}).async_execute
end
accepted!
delete_project(user_project)
end
desc 'Mark this project as forked from another'

View file

@ -1,5 +1,5 @@
.auto-deploy:
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.0"
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.8.3"
review:
extends: .auto-deploy

View file

@ -164,7 +164,6 @@ module Gitlab
docstring 'Transaction allocated memory bytes'
base_labels BASE_LABELS
buckets [100, 1000, 10000, 100000, 1000000, 10000000]
with_feature :prometheus_metrics_transaction_allocated_memory
end
def self.transaction_metric(name, type, prefix: nil, tags: {})

View file

@ -1720,6 +1720,9 @@ msgstr ""
msgid "An error occurred while loading issues"
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
msgid "An error occurred while loading the file"
msgstr ""
@ -2031,13 +2034,16 @@ msgstr ""
msgid "Archive project"
msgstr ""
msgid "Archived project! Repository and other project resources are read only"
msgstr ""
msgid "Archived project! Repository and other project resources are read-only"
msgstr ""
msgid "Archived projects"
msgstr ""
msgid "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>"
msgid "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}"
msgstr ""
msgid "Are you setting up GitLab for a company?"
@ -3148,6 +3154,9 @@ msgstr ""
msgid "Charts"
msgstr ""
msgid "Charts can't be displayed as the request for data has timed out. %{documentationLink}"
msgstr ""
msgid "Chat"
msgstr ""
@ -4713,9 +4722,15 @@ msgstr ""
msgid "Connecting..."
msgstr ""
msgid "Connection failed"
msgstr ""
msgid "Connection failure"
msgstr ""
msgid "Connection timed out"
msgstr ""
msgid "Contact an owner of group %{namespace_name} to upgrade the plan."
msgstr ""
@ -5554,6 +5569,9 @@ msgstr ""
msgid "Default classification label"
msgstr ""
msgid "Default deletion adjourned period"
msgstr ""
msgid "Default description template for issues"
msgstr ""
@ -5665,6 +5683,9 @@ msgstr ""
msgid "Deleting the license failed. You are not permitted to perform this action."
msgstr ""
msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only."
msgstr ""
msgid "Denied authorization of chat nickname %{user_name}."
msgstr ""
@ -9339,6 +9360,9 @@ msgstr ""
msgid "How it works"
msgstr ""
msgid "How many days need to pass between marking entity for deletion and actual removing it."
msgstr ""
msgid "How many replicas each Elasticsearch shard has."
msgstr ""
@ -12242,6 +12266,9 @@ msgstr ""
msgid "Only Project Members"
msgstr ""
msgid "Only active this projects shows up in the search and on the dashboard."
msgstr ""
msgid "Only admins"
msgstr ""
@ -13640,6 +13667,9 @@ msgstr ""
msgid "Project '%{project_name}' is in the process of being deleted."
msgstr ""
msgid "Project '%{project_name}' is restored."
msgstr ""
msgid "Project '%{project_name}' queued for deletion."
msgstr ""
@ -13649,6 +13679,9 @@ msgstr ""
msgid "Project '%{project_name}' was successfully updated."
msgstr ""
msgid "Project '%{project_name}' will be deleted on %{date}"
msgstr ""
msgid "Project Badges"
msgstr ""
@ -13670,6 +13703,9 @@ msgstr ""
msgid "Project already created"
msgstr ""
msgid "Project already deleted"
msgstr ""
msgid "Project and wiki repositories"
msgstr ""
@ -14588,6 +14624,9 @@ msgstr ""
msgid "Query"
msgstr ""
msgid "Query cannot be processed"
msgstr ""
msgid "Query is valid"
msgstr ""
@ -14931,6 +14970,12 @@ msgstr ""
msgid "Removes time estimate."
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanantly removed. Are you ABSOLUTELY sure?"
msgstr ""
msgid "Removing a project places it into a read-only state until %{date}, at which point the project will be permanently removed."
msgstr ""
msgid "Removing group will cause all child projects and resources to be removed."
msgstr ""
@ -15226,6 +15271,12 @@ msgstr ""
msgid "Restart Terminal"
msgstr ""
msgid "Restore project"
msgstr ""
msgid "Restoring the project will prevent the project from being removed on this date and restore people's ability to make changes to it."
msgstr ""
msgid "Restrict access by IP address"
msgstr ""
@ -17681,6 +17732,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
@ -17711,7 +17765,7 @@ msgstr ""
msgid "The content of this page is not encoded in UTF-8. Edits can only be made via the Git repository."
msgstr ""
msgid "The data source is connected, but there is no data to display."
msgid "The data source is connected, but there is no data to display. %{documentationLink}"
msgstr ""
msgid "The default CI configuration path for new projects."
@ -17881,6 +17935,9 @@ msgstr ""
msgid "The remote repository is being updated..."
msgstr ""
msgid "The repository can be commited to, and issues, comments and other entities can be created."
msgstr ""
msgid "The repository for this project does not exist."
msgstr ""
@ -18397,6 +18454,9 @@ msgstr ""
msgid "This project path either does not exist or is private."
msgstr ""
msgid "This project will be removed on %{date}"
msgstr ""
msgid "This repository"
msgstr ""
@ -19157,7 +19217,7 @@ msgstr ""
msgid "Unarchive project"
msgstr ""
msgid "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>"
msgid "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}"
msgstr ""
msgid "Unblock"
@ -19268,6 +19328,9 @@ msgstr ""
msgid "Until"
msgstr ""
msgid "Until that time, the project can be restored."
msgstr ""
msgid "Unverified"
msgstr ""
@ -19886,6 +19949,9 @@ msgstr ""
msgid "Verify SAML Configuration"
msgstr ""
msgid "Verify configuration"
msgstr ""
msgid "Version"
msgstr ""
@ -20095,6 +20161,9 @@ msgstr ""
msgid "We could not determine the path to remove the issue"
msgstr ""
msgid "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating."
msgstr ""
msgid "We created a short guided tour that will help you learn the basics of GitLab and how it will help you be better at your job. It should only take a couple of minutes. You will be guided by two types of helpers, best recognized by their color."
msgstr ""
@ -20949,6 +21018,9 @@ msgstr ""
msgid "among other things"
msgstr ""
msgid "archived"
msgstr ""
msgid "assign yourself"
msgstr ""
@ -21893,6 +21965,9 @@ msgstr ""
msgid "pending comment"
msgstr ""
msgid "pending removal"
msgstr ""
msgid "pipeline"
msgstr ""

View file

@ -12,6 +12,9 @@ module QA
element :project_path_field
element :change_path_button
element :transfer_button
end
view 'app/views/projects/settings/_archive.html.haml' do
element :archive_project_link
element :unarchive_project_link
end

View file

@ -1,42 +0,0 @@
#!/usr/bin/env ruby
require_relative 'ee_specific_check/ee_specific_check'
include EESpecificCheck # rubocop:disable Style/MixinUsage
git_version
base = find_compare_base
current_numstat = updated_diff_numstat(base.ce_base, base.ee_base)
updated_numstat = updated_diff_numstat(base.ce_head, base.ee_head)
offenses = updated_numstat.select do |file, updated_delta|
current_delta = current_numstat[file]
more_lines = updated_delta > current_delta
more_lines &&
!WHITELIST.any? { |pattern| Dir.glob(pattern, File::FNM_DOTMATCH).include?(file) }
end
if offenses.empty?
say "🎉 All good, congrats! 🎉"
else
puts
offenses.each do |(file, delta)|
puts "* 💥 #{file} has #{delta - current_numstat[file]} updated lines that differ between EE and CE! 💥"
end
say <<~MESSAGE
Make sure all lines in shared files have been updated in your backport merge request and the branch name includes #{minimal_ce_branch_name}.
Consider using an EE module to add the features you want.
See this for detail: https://docs.gitlab.com/ee/development/ee_features.html#ee-features-based-on-ce-features
MESSAGE
end
remove_remotes
say " For more information on why, see https://gitlab.com/gitlab-org/gitlab/issues/2952"
exit(offenses.size)

View file

@ -1,6 +1,12 @@
#!/usr/bin/env bash
karma_files=$(find spec/javascripts ee/spec/javascripts -type f -name '*_spec.js' -not -path '*/helpers/*')
karma_directory=spec/javascripts
if [ -d ee ]; then
karma_directory="$karma_directory ee/$karma_directory"
fi
karma_files=$(find $karma_directory -type f -name '*_spec.js' -not -path '*/helpers/*')
violations=""
for karma_file in $karma_files; do

View file

@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'spec_helper'
describe Projects::ServiceHookLogsController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
let(:service) { create(:drone_ci_service, project: project) }
let(:log) { create(:web_hook_log, web_hook: service.service_hook) }
let(:log_params) do
{
namespace_id: project.namespace,
project_id: project,
service_id: service.to_param,
id: log.id
}
end
before do
sign_in(user)
project.add_maintainer(user)
end
describe 'GET #show' do
subject { get :show, params: log_params }
it do
expect(response).to be_successful
end
end
describe 'POST #retry' do
subject { post :retry, params: log_params }
it 'executes the hook and redirects to the service form' do
expect_any_instance_of(ServiceHook).to receive(:execute)
expect_any_instance_of(described_class).to receive(:set_hook_execution_notice)
expect(subject).to redirect_to(edit_project_service_path(project, service))
end
end
end

View file

@ -44,6 +44,13 @@ FactoryBot.define do
end
end
factory :drone_ci_service do
project
active { true }
drone_url { 'https://bamboo.example.com' }
token { 'test' }
end
factory :jira_service do
project
active { true }

View file

@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GroupEmptyState Renders an empty state for BAD_QUERY 1`] = `
<glemptystate-stub
compact="true"
primarybuttonlink="/path/to/settings"
primarybuttontext="Verify configuration"
svgpath="/path/to/empty-group-illustration.svg"
title="Query cannot be processed"
/>
`;
exports[`GroupEmptyState Renders an empty state for BAD_QUERY 2`] = `"The Prometheus server responded with \\"bad request\\". Please check your queries are correct and are supported in your Prometheus version. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for CONNECTION_FAILED 1`] = `
<glemptystate-stub
compact="true"
description="We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating."
primarybuttonlink="/path/to/settings"
primarybuttontext="Verify configuration"
svgpath="/path/to/empty-group-illustration.svg"
title="Connection failed"
/>
`;
exports[`GroupEmptyState Renders an empty state for CONNECTION_FAILED 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for FOO STATE 1`] = `
<glemptystate-stub
compact="true"
description="An error occurred while loading the data. Please try again."
svgpath="/path/to/empty-group-illustration.svg"
title="An error has occurred"
/>
`;
exports[`GroupEmptyState Renders an empty state for FOO STATE 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for LOADING 1`] = `
<glemptystate-stub
compact="true"
description="Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available."
svgpath="/path/to/empty-group-illustration.svg"
title="Waiting for performance data"
/>
`;
exports[`GroupEmptyState Renders an empty state for LOADING 2`] = `undefined`;
exports[`GroupEmptyState Renders an empty state for NO_DATA 1`] = `
<glemptystate-stub
compact="true"
svgpath="/path/to/empty-group-illustration.svg"
title="No data to display"
/>
`;
exports[`GroupEmptyState Renders an empty state for NO_DATA 2`] = `"The data source is connected, but there is no data to display. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for TIMEOUT 1`] = `
<glemptystate-stub
compact="true"
svgpath="/path/to/empty-group-illustration.svg"
title="Connection timed out"
/>
`;
exports[`GroupEmptyState Renders an empty state for TIMEOUT 2`] = `"Charts can't be displayed as the request for data has timed out. <a href=\\"/path/to/docs\\">More information</a>"`;
exports[`GroupEmptyState Renders an empty state for UNKNOWN_ERROR 1`] = `
<glemptystate-stub
compact="true"
description="An error occurred while loading the data. Please try again."
svgpath="/path/to/empty-group-illustration.svg"
title="An error has occurred"
/>
`;
exports[`GroupEmptyState Renders an empty state for UNKNOWN_ERROR 2`] = `undefined`;

View file

@ -0,0 +1,34 @@
import { shallowMount } from '@vue/test-utils';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import { metricStates } from '~/monitoring/constants';
function createComponent(props) {
return shallowMount(GroupEmptyState, {
propsData: {
...props,
documentationPath: '/path/to/docs',
settingsPath: '/path/to/settings',
svgPath: '/path/to/empty-group-illustration.svg',
},
});
}
describe('GroupEmptyState', () => {
const supportedStates = [
metricStates.NO_DATA,
metricStates.TIMEOUT,
metricStates.CONNECTION_FAILED,
metricStates.BAD_QUERY,
metricStates.LOADING,
metricStates.UNKNOWN_ERROR,
'FOO STATE', // does not fail with unknown states
];
test.each(supportedStates)('Renders an empty state for %s', selectedState => {
const wrapper = createComponent({ selectedState });
expect(wrapper.element).toMatchSnapshot();
// slot is not rendered by the stub, test it separately
expect(wrapper.vm.currentState.slottedDescription).toMatchSnapshot();
});
});

View file

@ -529,7 +529,7 @@ describe('Monitoring store actions', () => {
},
},
{
type: types.RECEIVE_METRIC_RESULT_ERROR,
type: types.RECEIVE_METRIC_RESULT_FAILURE,
payload: {
metricId: metric.metric_id,
error,

View file

@ -1,7 +1,7 @@
import * as getters from '~/monitoring/stores/getters';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import { metricStates } from '~/monitoring/constants';
import {
metricsGroupsAPIResponse,
mockedEmptyResult,
@ -10,6 +10,124 @@ import {
} from '../mock_data';
describe('Monitoring store Getters', () => {
describe('getMetricStates', () => {
let setupState;
let state;
let getMetricStates;
beforeEach(() => {
setupState = (initState = {}) => {
state = initState;
getMetricStates = getters.getMetricStates(state);
};
});
it('has method-style access', () => {
setupState();
expect(getMetricStates).toEqual(expect.any(Function));
});
it('when dashboard has no panel groups, returns empty', () => {
setupState({
dashboard: {
panel_groups: [],
},
});
expect(getMetricStates()).toEqual([]);
});
describe('when the dashboard is set', () => {
let groups;
beforeEach(() => {
setupState({
dashboard: { panel_groups: [] },
});
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
groups = state.dashboard.panel_groups;
});
it('no loaded metric returns empty', () => {
expect(getMetricStates()).toEqual([]);
});
it('on an empty metric with no result, returns NO_DATA', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyResult);
expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
});
it('on a metric with a result, returns OK', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
expect(getMetricStates()).toEqual([metricStates.OK]);
});
it('on a metric with an error, returns an error', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
});
it('on multiple metrics with results, returns OK', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayloadCoresTotal);
expect(getMetricStates()).toEqual([metricStates.OK]);
// Filtered by groups
expect(getMetricStates(state.dashboard.panel_groups[0].key)).toEqual([]);
expect(getMetricStates(state.dashboard.panel_groups[1].key)).toEqual([metricStates.OK]);
});
it('on multiple metrics errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
});
// Entire dashboard fails
expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
});
it('on multiple metrics with errors', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsGroupsAPIResponse);
// An success in 1 group
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultPayload);
// An error in 2 groups
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[0].panels[0].metrics[0].metricId,
});
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
metricId: groups[1].panels[1].metrics[0].metricId,
});
expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
expect(getMetricStates(groups[1].key)).toEqual([
metricStates.OK,
metricStates.UNKNOWN_ERROR,
]);
});
});
});
describe('metricsWithData', () => {
let metricsWithData;
let setupState;

View file

@ -3,7 +3,7 @@ import httpStatusCodes from '~/lib/utils/http_status';
import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
import { metricsErrors } from '~/monitoring/constants';
import { metricStates } from '~/monitoring/constants';
import {
metricsGroupsAPIResponse,
deploymentData,
@ -120,7 +120,7 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: true,
result: null,
error: null,
state: metricStates.LOADING,
}),
);
});
@ -153,20 +153,20 @@ describe('Monitoring mutations', () => {
expect(getMetric()).toEqual(
expect.objectContaining({
loading: false,
error: null,
state: metricStates.OK,
}),
);
});
});
describe('RECEIVE_METRIC_RESULT_ERROR', () => {
describe('RECEIVE_METRIC_RESULT_FAILURE', () => {
beforeEach(() => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
it('maintains the loading state when a metric fails', () => {
expect(stateCopy.showEmptyState).toBe(true);
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: 'an error',
});
@ -175,7 +175,7 @@ describe('Monitoring mutations', () => {
});
it('stores a timeout error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: { message: 'BACKOFF_TIMEOUT' },
});
@ -184,13 +184,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.TIMEOUT,
state: metricStates.TIMEOUT,
}),
);
});
it('stores a connection failed error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: {
response: {
@ -202,13 +202,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.CONNECTION_FAILED,
state: metricStates.CONNECTION_FAILED,
}),
);
});
it('stores a bad data error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: {
response: {
@ -221,13 +221,13 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.BAD_DATA,
state: metricStates.BAD_QUERY,
}),
);
});
it('stores an unknown error in a metric', () => {
mutations[types.RECEIVE_METRIC_RESULT_ERROR](stateCopy, {
mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
metricId,
error: null, // no reason in response
});
@ -236,7 +236,7 @@ describe('Monitoring mutations', () => {
expect.objectContaining({
loading: false,
result: null,
error: metricsErrors.UNKNOWN_ERROR,
state: metricStates.UNKNOWN_ERROR,
}),
);
});

View file

@ -92,9 +92,46 @@ describe('Multi-file store actions', () => {
.catch(done.fail);
});
it('closes the temp file if it was open', done => {
it('closes the temp file and deletes it if it was open', done => {
f.tempFile = true;
testAction(
discardAllChanges,
undefined,
store.state,
[{ type: types.REMOVE_ALL_CHANGES_FILES }],
[
{ type: 'closeFile', payload: jasmine.objectContaining({ path: 'discardAll' }) },
{ type: 'deleteEntry', payload: 'discardAll' },
],
done,
);
});
it('renames the file to its original name and closes it if it was open', done => {
Object.assign(f, {
prevPath: 'parent/path/old_name',
prevName: 'old_name',
prevParentPath: 'parent/path',
});
testAction(
discardAllChanges,
undefined,
store.state,
[{ type: types.REMOVE_ALL_CHANGES_FILES }],
[
{ type: 'closeFile', payload: jasmine.objectContaining({ path: 'discardAll' }) },
{
type: 'renameEntry',
payload: { path: 'discardAll', name: 'old_name', parentPath: 'parent/path' },
},
],
done,
);
});
it('discards file changes on all other files', done => {
testAction(
discardAllChanges,
undefined,
@ -103,12 +140,7 @@ describe('Multi-file store actions', () => {
{ type: types.DISCARD_FILE_CHANGES, payload: 'discardAll' },
{ type: types.REMOVE_ALL_CHANGES_FILES },
],
[
{
type: 'closeFile',
payload: jasmine.objectContaining({ path: 'discardAll' }),
},
],
[],
done,
);
});

View file

@ -4,7 +4,8 @@ import { GlToast } from '@gitlab/ui';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
import Dashboard from '~/monitoring/components/dashboard.vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import { metricStates } from '~/monitoring/constants';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
@ -401,7 +402,7 @@ describe('Dashboard', () => {
});
beforeEach(done => {
createComponentWrapper({ hasMetrics: true }, { attachToDocument: true });
createComponentWrapper({ hasMetrics: true });
setupComponentStore(wrapper.vm);
wrapper.vm.$nextTick(done);
@ -411,16 +412,16 @@ describe('Dashboard', () => {
const emptyGroup = wrapper.findAll({ ref: 'empty-group' });
expect(emptyGroup).toHaveLength(1);
expect(emptyGroup.is(EmptyState)).toBe(true);
expect(emptyGroup.is(GroupEmptyState)).toBe(true);
});
it('group empty area displays a "noDataGroup"', () => {
it('group empty area displays a NO_DATA state', () => {
expect(
wrapper
.findAll({ ref: 'empty-group' })
.at(0)
.props('selectedState'),
).toEqual('noDataGroup');
).toEqual(metricStates.NO_DATA);
});
});

View file

@ -421,4 +421,21 @@ describe Blob do
end
end
end
describe 'policy' do
let(:project) { build(:project) }
subject { described_class.new(fake_blob(path: 'foo'), project) }
it 'works with policy' do
expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_truthy
end
context 'when project is nil' do
subject { described_class.new(fake_blob(path: 'foo')) }
it 'does not err' do
expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_falsey
end
end
end
end

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
describe SafeUrl do
describe '#safe_url' do
class TestClass
include SafeUrl
attr_reader :url
def initialize(url)
@url = url
end
end
let(:test_class) { TestClass.new(url) }
let(:url) { 'http://example.com' }
subject { test_class.safe_url }
it { is_expected.to eq(url) }
context 'when URL contains credentials' do
let(:url) { 'http://foo:bar@example.com' }
it { is_expected.to eq('http://*****:*****@example.com')}
context 'when username is whitelisted' do
subject { test_class.safe_url(usernames_whitelist: usernames_whitelist) }
let(:usernames_whitelist) { %w[foo] }
it 'does expect the whitelisted username not to be masked' do
is_expected.to eq('http://foo:*****@example.com')
end
end
end
context 'when URL is empty' do
let(:url) { nil }
it { is_expected.to be_nil }
end
context 'when URI raises an error' do
let(:url) { 123 }
it { is_expected.to be_nil }
end
end
end

View file

@ -29,6 +29,25 @@ describe WebHookLog do
end
end
describe '#save' do
let(:web_hook_log) { build(:web_hook_log, url: url) }
let(:url) { 'http://example.com' }
subject { web_hook_log.save! }
it { is_expected.to eq(true) }
context 'with basic auth credentials' do
let(:url) { 'http://test:123@example.com'}
it 'obfuscates the basic auth credentials' do
subject
expect(web_hook_log.url).to eq('http://*****:*****@example.com')
end
end
end
describe '#success?' do
let(:web_hook_log) { build(:web_hook_log, response_status: status) }

View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
describe ReadmeBlob do
include FakeBlobHelpers
describe 'policy' do
let(:project) { build(:project, :repository) }
subject { described_class.new(fake_blob(path: 'README.md'), project.repository) }
it 'works with policy' do
expect(Ability.allowed?(project.creator, :read_blob, subject)).to be_truthy
end
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
describe BlobPolicy do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :repository, project_level) }
let(:user) { create_user_from_membership(project, membership) }
let(:blob) { project.repository.blob_at(SeedRepo::FirstCommit::ID, 'README.md') }
subject(:policy) { described_class.new(user, blob) }
where(:project_level, :feature_access_level, :membership, :expected_count) do
permission_table_for_guest_feature_access_and_non_private_project_only
end
with_them do
it "grants permission" do
update_feature_access_level(project, feature_access_level)
if expected_count == 1
expect(policy).to be_allowed(:read_blob)
else
expect(policy).to be_disallowed(:read_blob)
end
end
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
describe WikiPagePolicy do
include_context 'ProjectPolicyTable context'
include ProjectHelpers
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project, :wiki_repo, project_level) }
let(:user) { create_user_from_membership(project, membership) }
let(:wiki_page) { create(:wiki_page, wiki: project.wiki) }
subject(:policy) { described_class.new(user, wiki_page) }
where(:project_level, :feature_access_level, :membership, :expected_count) do
permission_table_for_guest_feature_access
end
with_them do
it "grants permission" do
update_feature_access_level(project, feature_access_level)
if expected_count == 1
expect(policy).to be_allowed(:read_wiki_page)
else
expect(policy).to be_disallowed(:read_wiki_page)
end
end
end
end

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
describe ProjectHookPresenter do
let(:web_hook_log) { create(:web_hook_log) }
let(:project) { web_hook_log.web_hook.project }
let(:web_hook) { web_hook_log.web_hook }
describe '#logs_details_path' do
subject { web_hook.present.logs_details_path(web_hook_log) }
let(:expected_path) do
"/#{project.namespace.path}/#{project.name}/hooks/#{web_hook.id}/hook_logs/#{web_hook_log.id}"
end
it { is_expected.to eq(expected_path) }
end
describe '#logs_retry_path' do
subject { web_hook.present.logs_details_path(web_hook_log) }
let(:expected_path) do
"/#{project.namespace.path}/#{project.name}/hooks/#{web_hook.id}/hook_logs/#{web_hook_log.id}"
end
it { is_expected.to eq(expected_path) }
end
end

View file

@ -0,0 +1,30 @@
# frozen_string_literal: true
require 'spec_helper'
describe ServiceHookPresenter do
let(:web_hook_log) { create(:web_hook_log, web_hook: service_hook) }
let(:service_hook) { create(:service_hook, service: service) }
let(:service) { create(:drone_ci_service, project: project) }
let(:project) { create(:project) }
describe '#logs_details_path' do
subject { service_hook.present.logs_details_path(web_hook_log) }
let(:expected_path) do
"/#{project.namespace.path}/#{project.name}/-/services/#{service.to_param}/hook_logs/#{web_hook_log.id}"
end
it { is_expected.to eq(expected_path) }
end
describe '#logs_retry_path' do
subject { service_hook.present.logs_retry_path(web_hook_log) }
let(:expected_path) do
"/#{project.namespace.path}/#{project.name}/-/services/#{service.to_param}/hook_logs/#{web_hook_log.id}/retry"
end
it { is_expected.to eq(expected_path) }
end
end

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
describe WebHookLogPresenter do
include Gitlab::Routing.url_helpers
describe '#details_path' do
let(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
let(:project) { create(:project) }
subject { web_hook_log.present.details_path }
context 'project hook' do
let(:web_hook) { create(:project_hook, project: project) }
it { is_expected.to eq(project_hook_hook_log_path(project, web_hook, web_hook_log)) }
end
context 'service hook' do
let(:web_hook) { create(:service_hook, service: service) }
let(:service) { create(:drone_ci_service, project: project) }
it { is_expected.to eq(project_service_hook_log_path(project, service, web_hook_log)) }
end
end
describe '#retry_path' do
let(:web_hook_log) { create(:web_hook_log, web_hook: web_hook) }
let(:project) { create(:project) }
subject { web_hook_log.present.retry_path }
context 'project hook' do
let(:web_hook) { create(:project_hook, project: project) }
it { is_expected.to eq(retry_project_hook_hook_log_path(project, web_hook, web_hook_log)) }
end
context 'service hook' do
let(:web_hook) { create(:service_hook, service: service) }
let(:service) { create(:drone_ci_service, project: project) }
it { is_expected.to eq(retry_project_service_hook_log_path(project, service, web_hook_log)) }
end
end
end

View file

@ -0,0 +1,63 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'Mark snippet as spam' do
include GraphqlHelpers
let_it_be(:admin) { create(:admin) }
let_it_be(:other_user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet) }
let_it_be(:user_agent_detail) { create(:user_agent_detail, subject: snippet) }
let(:current_user) { snippet.author }
let(:mutation) do
variables = {
id: snippet.to_global_id.to_s
}
graphql_mutation(:mark_as_spam_snippet, variables)
end
def mutation_response
graphql_mutation_response(:mark_as_spam_snippet)
end
shared_examples 'does not mark the snippet as spam' do
it do
expect do
post_graphql_mutation(mutation, current_user: current_user)
end.not_to change { snippet.reload.user_agent_detail.submitted }
end
end
context 'when the user does not have permission' do
let(:current_user) { other_user }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
it_behaves_like 'does not mark the snippet as spam'
end
context 'when the user has permission' do
context 'when user can not mark snippet as spam' do
it_behaves_like 'does not mark the snippet as spam'
end
context 'when user can mark snippet as spam' do
let(:current_user) { admin }
before do
stub_application_setting(akismet_enabled: true)
end
it 'marks snippet as spam' do
expect_next_instance_of(SpamService) do |instance|
expect(instance).to receive(:mark_as_spam!)
end
post_graphql_mutation(mutation, current_user: current_user)
end
end
end
end

View file

@ -203,17 +203,6 @@ describe WebHookService do
expect(hook_log.internal_error_message).to be_nil
end
end
context 'should not log ServiceHooks' do
let(:service_hook) { create(:service_hook) }
let(:service_instance) { described_class.new(service_hook, data, 'service_hook') }
before do
stub_full_request(service_hook.url, method: :post).to_return(status: 200, body: 'Success')
end
it { expect { service_instance.execute }.not_to change(WebHookLog, :count) }
end
end
end

View file

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
describe 'projects/services/edit' do
let(:service) { create(:drone_ci_service, project: project) }
let(:project) { create(:project) }
before do
assign :project, project
assign :service, service
end
it do
render
expect(rendered).not_to have_text('Recent Deliveries')
end
context 'service using WebHooks' do
before do
assign(:web_hook_logs, [])
end
it do
render
expect(rendered).to have_text('Recent Deliveries')
end
end
end