Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
badb9c1dea
commit
c2b98d3dbd
91 changed files with 1509 additions and 192 deletions
|
@ -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>
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
export default {
|
||||
computed: {
|
||||
isProjectPendingRemoval() {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
20
app/controllers/projects/service_hook_logs_controller.rb
Normal file
20
app/controllers/projects/service_hook_logs_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
39
app/graphql/mutations/snippets/mark_as_spam.rb
Normal file
39
app/graphql/mutations/snippets/mark_as_spam.rb
Normal 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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
19
app/models/concerns/blob_active_model.rb
Normal file
19
app/models/concerns/blob_active_model.rb
Normal 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
|
15
app/models/concerns/safe_url.rb
Normal file
15
app/models/concerns/safe_url.rb
Normal 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
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ProjectHook < WebHook
|
||||
include TriggerableHooks
|
||||
include Presentable
|
||||
|
||||
triggerable_hooks [
|
||||
:push_hooks,
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ServiceHook < WebHook
|
||||
include Presentable
|
||||
|
||||
belongs_to :service
|
||||
validates :service, presence: true
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ReadmeBlob < SimpleDelegator
|
||||
include BlobActiveModel
|
||||
|
||||
attr_reader :repository
|
||||
|
||||
def initialize(blob, repository)
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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.
|
||||
|
|
7
app/policies/blob_policy.rb
Normal file
7
app/policies/blob_policy.rb
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BlobPolicy < BasePolicy
|
||||
delegate { @subject.project }
|
||||
|
||||
rule { can?(:download_code) }.enable :read_blob
|
||||
end
|
7
app/policies/wiki_page_policy.rb
Normal file
7
app/policies/wiki_page_policy.rb
Normal 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
|
13
app/presenters/hooks/project_hook_presenter.rb
Normal file
13
app/presenters/hooks/project_hook_presenter.rb
Normal 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
|
13
app/presenters/hooks/service_hook_presenter.rb
Normal file
13
app/presenters/hooks/service_hook_presenter.rb
Normal 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
|
13
app/presenters/web_hook_log_presenter.rb
Normal file
13
app/presenters/web_hook_log_presenter.rb
Normal 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
|
|
@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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'
|
||||
|
|
3
app/views/admin/projects/_archived.html.haml
Normal file
3
app/views/admin/projects/_archived.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- if project.archived
|
||||
%span.badge.badge-warning
|
||||
= _('archived')
|
|
@ -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
|
||||
|
|
5
app/views/projects/_archived_notice.html.haml
Normal file
5
app/views/projects/_archived_notice.html.haml
Normal 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')
|
10
app/views/projects/_remove.html.haml
Normal file
10
app/views/projects/_remove.html.haml
Normal 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) }
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 }
|
||||
|
|
18
app/views/projects/settings/_archive.html.haml
Normal file
18
app/views/projects/settings/_archive.html.haml
Normal 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"
|
|
@ -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
|
||||
|
||||
|
|
3
app/views/shared/projects/_archived.html.haml
Normal file
3
app/views/shared/projects/_archived.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
- if project.archived
|
||||
%span.d-flex.badge.badge-warning
|
||||
= _('archived')
|
|
@ -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",
|
||||
|
|
5
changelogs/unreleased/27244-discard-all-changes.yml
Normal file
5
changelogs/unreleased/27244-discard-all-changes.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix "Discard all" for new and renamed files
|
||||
merge_request: 21854
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add specific error states to dashboard
|
||||
merge_request: 21618
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add mark as spam snippet mutation
|
||||
merge_request: 21912
|
||||
author:
|
||||
type: other
|
5
changelogs/unreleased/log_service_web_hooks.yml
Normal file
5
changelogs/unreleased/log_service_web_hooks.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Added WebHookLogs for ServiceHooks
|
||||
merge_request: 20976
|
||||
author:
|
||||
type: added
|
5
changelogs/unreleased/update_auto_deploy_image.yml
Normal file
5
changelogs/unreleased/update_auto_deploy_image.yml
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update auto-deploy-image to v0.8.3
|
||||
merge_request: 21696
|
||||
author:
|
||||
type: fixed
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -124,4 +124,4 @@
|
|||
- [design_management_new_version, 1]
|
||||
- [epics, 2]
|
||||
- [personal_access_tokens, 1]
|
||||
|
||||
- [adjourned_project_deletion, 1]
|
||||
|
|
|
@ -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)**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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: {})
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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 }
|
||||
|
|
|
@ -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`;
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
52
spec/models/concerns/safe_url_spec.rb
Normal file
52
spec/models/concerns/safe_url_spec.rb
Normal 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
|
|
@ -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) }
|
||||
|
||||
|
|
16
spec/models/readme_blob_spec.rb
Normal file
16
spec/models/readme_blob_spec.rb
Normal 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
|
31
spec/policies/blob_policy_spec.rb
Normal file
31
spec/policies/blob_policy_spec.rb
Normal 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
|
31
spec/policies/wiki_page_policy_spec.rb
Normal file
31
spec/policies/wiki_page_policy_spec.rb
Normal 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
|
29
spec/presenters/hooks/project_hook_presenter_spec.rb
Normal file
29
spec/presenters/hooks/project_hook_presenter_spec.rb
Normal 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
|
30
spec/presenters/hooks/service_hook_presenter_spec.rb
Normal file
30
spec/presenters/hooks/service_hook_presenter_spec.rb
Normal 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
|
47
spec/presenters/web_hook_log_presenter_spec.rb
Normal file
47
spec/presenters/web_hook_log_presenter_spec.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
31
spec/views/projects/services/edit.html.haml_spec.rb
Normal file
31
spec/views/projects/services/edit.html.haml_spec.rb
Normal 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
|
Loading…
Reference in a new issue