Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 18:08:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-12-17 18:08:15 +0300
commitc2b98d3dbd47ab92c79c702276fe9130d9a28036 (patch)
treebf4071f551fdc12c22b23b2bb66483064e7b9ea9
parentbadb9c1deacbea601b02f88811b7e123589d9251 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue7
-rw-r--r--app/assets/javascripts/groups/mixins/is_project_pending_removal.js7
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions.js12
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue50
-rw-r--r--app/assets/javascripts/monitoring/components/empty_state.vue5
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue105
-rw-r--r--app/assets/javascripts/monitoring/constants.js18
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js30
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js32
-rw-r--r--app/controllers/projects/hook_logs_controller.rb10
-rw-r--r--app/controllers/projects/service_hook_logs_controller.rb20
-rw-r--r--app/controllers/projects/services_controller.rb7
-rw-r--r--app/graphql/mutations/snippets/mark_as_spam.rb39
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/models/blob.rb1
-rw-r--r--app/models/concerns/blob_active_model.rb19
-rw-r--r--app/models/concerns/safe_url.rb15
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/service_hook.rb2
-rw-r--r--app/models/hooks/web_hook_log.rb11
-rw-r--r--app/models/readme_blob.rb2
-rw-r--r--app/models/remote_mirror.rb9
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/policies/blob_policy.rb7
-rw-r--r--app/policies/wiki_page_policy.rb7
-rw-r--r--app/presenters/hooks/project_hook_presenter.rb13
-rw-r--r--app/presenters/hooks/service_hook_presenter.rb13
-rw-r--r--app/presenters/web_hook_log_presenter.rb13
-rw-r--r--app/serializers/group_child_entity.rb2
-rw-r--r--app/services/web_hook_service.rb3
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml2
-rw-r--r--app/views/admin/projects/_archived.html.haml3
-rw-r--r--app/views/admin/projects/_projects.html.haml3
-rw-r--r--app/views/projects/_archived_notice.html.haml5
-rw-r--r--app/views/projects/_remove.html.haml10
-rw-r--r--app/views/projects/edit.html.haml27
-rw-r--r--app/views/projects/hook_logs/_index.html.haml2
-rw-r--r--app/views/projects/hook_logs/show.html.haml3
-rw-r--r--app/views/projects/services/edit.html.haml4
-rw-r--r--app/views/projects/settings/_archive.html.haml18
-rw-r--r--app/views/projects/show.html.haml7
-rw-r--r--app/views/shared/projects/_archived.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml3
-rw-r--r--changelogs/unreleased/27244-discard-all-changes.yml5
-rw-r--r--changelogs/unreleased/34121-add-error-states-to-getters.yml5
-rw-r--r--changelogs/unreleased/fj-add-mark-as-spam-snippet-mutation.yml5
-rw-r--r--changelogs/unreleased/log_service_web_hooks.yml5
-rw-r--r--changelogs/unreleased/update_auto_deploy_image.yml5
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/routes/project.rb6
-rw-r--r--config/sidekiq_queues.yml2
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql36
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json129
-rw-r--r--doc/api/graphql/reference/index.md8
-rw-r--r--doc/api/projects.md19
-rw-r--r--doc/api/settings.md7
-rw-r--r--doc/user/admin_area/settings/visibility_and_access_controls.md11
-rw-r--r--lib/api/projects.rb14
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml2
-rw-r--r--lib/gitlab/metrics/transaction.rb1
-rw-r--r--locale/gitlab.pot81
-rw-r--r--qa/qa/page/project/settings/advanced.rb3
-rwxr-xr-xscripts/ee-specific-lines-check42
-rwxr-xr-xscripts/frontend/check_no_partial_karma_jest.sh8
-rw-r--r--spec/controllers/projects/service_hook_logs_controller_spec.rb41
-rw-r--r--spec/factories/services.rb7
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap (renamed from spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap)0
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap79
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js (renamed from spec/frontend/monitoring/dashboard_state_spec.js)0
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js34
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js2
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js120
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js26
-rw-r--r--spec/javascripts/ide/stores/actions_spec.js42
-rw-r--r--spec/javascripts/monitoring/components/dashboard_spec.js11
-rw-r--r--spec/models/blob_spec.rb17
-rw-r--r--spec/models/concerns/safe_url_spec.rb52
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb19
-rw-r--r--spec/models/readme_blob_spec.rb16
-rw-r--r--spec/policies/blob_policy_spec.rb31
-rw-r--r--spec/policies/wiki_page_policy_spec.rb31
-rw-r--r--spec/presenters/hooks/project_hook_presenter_spec.rb29
-rw-r--r--spec/presenters/hooks/service_hook_presenter_spec.rb30
-rw-r--r--spec/presenters/web_hook_log_presenter_spec.rb47
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb63
-rw-r--r--spec/services/web_hook_service_spec.rb11
-rw-r--r--spec/views/projects/services/edit.html.haml_spec.rb31
91 files changed, 1507 insertions, 190 deletions
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 734a9a89c72..675552e6c2b 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -1,5 +1,6 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
+import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
ITEM_TYPE,
@@ -8,13 +9,16 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../constants';
import itemStatsValue from './item_stats_value.vue';
+import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending_removal';
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
+ GlBadge,
},
+ mixins: [isProjectPendingRemoval],
props: {
item: {
type: Object,
@@ -70,6 +74,9 @@ export default {
css-class="project-stars"
icon-name="star"
/>
+ <div v-if="isProjectPendingRemoval">
+ <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
+ </div>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
</div>
diff --git a/app/assets/javascripts/groups/mixins/is_project_pending_removal.js b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
new file mode 100644
index 00000000000..e44e5780199
--- /dev/null
+++ b/app/assets/javascripts/groups/mixins/is_project_pending_removal.js
@@ -0,0 +1,7 @@
+export default {
+ computed: {
+ isProjectPendingRemoval() {
+ return false;
+ },
+ },
+};
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 16f95d5a0cc..214ac5e3db5 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -93,6 +93,7 @@ export default class GroupsStore {
memberCount: rawGroupItem.number_users_with_delimiter,
starCount: rawGroupItem.star_count,
updatedAt: rawGroupItem.updated_at,
+ pendingRemoval: rawGroupItem.marked_for_deletion_at,
};
}
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 2e7bf9a7d5a..dd69e2d6f1f 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -17,10 +17,18 @@ export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DAT
export const discardAllChanges = ({ state, commit, dispatch }) => {
state.changedFiles.forEach(file => {
- commit(types.DISCARD_FILE_CHANGES, file.path);
+ if (file.tempFile || file.prevPath) dispatch('closeFile', file);
if (file.tempFile) {
- dispatch('closeFile', file);
+ dispatch('deleteEntry', file.path);
+ } else if (file.prevPath) {
+ dispatch('renameEntry', {
+ path: file.path,
+ name: file.prevName,
+ parentPath: file.prevParentPath,
+ });
+ } else {
+ commit(types.DISCARD_FILE_CHANGES, file.path);
}
});
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 2a9321f6733..c1ca5449ba3 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -20,8 +20,10 @@ import invalidUrl from '~/lib/utils/invalid_url';
import DateTimePicker from './date_time_picker/date_time_picker.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
+import GroupEmptyState from './group_empty_state.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import { getTimeDiff, isValidDate, getAddMetricTrackingOptions } from '../utils';
+import { metricStates } from '../constants';
export default {
components: {
@@ -29,6 +31,7 @@ export default {
PanelType,
GraphGroup,
EmptyState,
+ GroupEmptyState,
Icon,
GlButton,
GlDropdown,
@@ -184,7 +187,7 @@ export default {
'allDashboards',
'additionalPanelTypesEnabled',
]),
- ...mapGetters('monitoringDashboard', ['metricsWithData']),
+ ...mapGetters('monitoringDashboard', ['getMetricStates']),
firstDashboard() {
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
@@ -284,12 +287,35 @@ export default {
submitCustomMetricsForm() {
this.$refs.customMetricsForm.submit();
},
- groupHasData(group) {
- return this.metricsWithData(group.key).length > 0;
- },
onDateTimePickerApply(timeWindowUrlParams) {
return redirectTo(mergeUrlParams(timeWindowUrlParams, window.location.href));
},
+ /**
+ * Return a single empty state for a group.
+ *
+ * If all states are the same a single state is returned to be displayed
+ * Except if the state is OK, in which case the group is displayed.
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {String} state code from `metricStates`
+ */
+ groupSingleEmptyState(groupKey) {
+ const states = this.getMetricStates(groupKey);
+ if (states.length === 1 && states[0] !== metricStates.OK) {
+ return states[0];
+ }
+ return null;
+ },
+ /**
+ * A group should be not collapsed if any metric is loaded (OK)
+ *
+ * @param {String} groupKey - Identifier for group
+ * @returns {Boolean} If the group should be collapsed
+ */
+ collapseGroup(groupKey) {
+ // Collapse group if no data is available
+ return !this.getMetricStates(groupKey).includes(metricStates.OK);
+ },
getAddMetricTrackingOptions,
},
addMetric: {
@@ -446,9 +472,9 @@ export default {
:key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
- :collapse-group="!groupHasData(groupData)"
+ :collapse-group="collapseGroup(groupData.key)"
>
- <div v-if="groupHasData(groupData)">
+ <div v-if="!groupSingleEmptyState(groupData.key)">
<vue-draggable
:value="groupData.panels"
group="metrics-dashboard"
@@ -487,18 +513,12 @@ export default {
</vue-draggable>
</div>
<div v-else class="py-5 col col-sm-10 col-md-8 col-lg-7 col-xl-6">
- <empty-state
+ <group-empty-state
ref="empty-group"
- selected-state="noDataGroup"
:documentation-path="documentationPath"
:settings-path="settingsPath"
- :clusters-path="clustersPath"
- :empty-getting-started-svg-path="emptyGettingStartedSvgPath"
- :empty-loading-svg-path="emptyLoadingSvgPath"
- :empty-no-data-svg-path="emptyNoDataSvgPath"
- :empty-no-data-small-svg-path="emptyNoDataSmallSvgPath"
- :empty-unable-to-connect-svg-path="emptyUnableToConnectSvgPath"
- :compact="true"
+ :selected-state="groupSingleEmptyState(groupData.key)"
+ :svg-path="emptyNoDataSmallSvgPath"
/>
</div>
</graph-group>
diff --git a/app/assets/javascripts/monitoring/components/empty_state.vue b/app/assets/javascripts/monitoring/components/empty_state.vue
index 728910dd633..d3157b731b2 100644
--- a/app/assets/javascripts/monitoring/components/empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/empty_state.vue
@@ -84,11 +84,6 @@ export default {
secondaryButtonText: '',
secondaryButtonPath: '',
},
- noDataGroup: {
- svgUrl: this.emptyNoDataSmallSvgPath,
- title: __('No data to display'),
- description: __('The data source is connected, but there is no data to display.'),
- },
unableToConnect: {
svgUrl: this.emptyUnableToConnectSvgPath,
title: __('Unable to connect to Prometheus server'),
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
new file mode 100644
index 00000000000..dee4e5998ee
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -0,0 +1,105 @@
+<script>
+import { __, sprintf } from '~/locale';
+import { GlEmptyState } from '@gitlab/ui';
+import { metricStates } from '../constants';
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ documentationPath: {
+ type: String,
+ required: true,
+ },
+ settingsPath: {
+ type: String,
+ required: true,
+ },
+ selectedState: {
+ type: String,
+ required: true,
+ },
+ svgPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ const documentationLink = `<a href="${this.documentationPath}">${__('More information')}</a>`;
+ return {
+ states: {
+ [metricStates.NO_DATA]: {
+ title: __('No data to display'),
+ slottedDescription: sprintf(
+ __(
+ 'The data source is connected, but there is no data to display. %{documentationLink}',
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.TIMEOUT]: {
+ title: __('Connection timed out'),
+ slottedDescription: sprintf(
+ __(
+ "Charts can't be displayed as the request for data has timed out. %{documentationLink}",
+ ),
+ { documentationLink },
+ false,
+ ),
+ },
+ [metricStates.CONNECTION_FAILED]: {
+ title: __('Connection failed'),
+ description: __(`We couldn't reach the Prometheus server.
+ Either the server no longer exists or the configuration details need updating.`),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.BAD_QUERY]: {
+ title: __('Query cannot be processed'),
+ slottedDescription: sprintf(
+ __(
+ `The Prometheus server responded with "bad request".
+ Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}`,
+ ),
+ { documentationLink },
+ false,
+ ),
+ buttonText: __('Verify configuration'),
+ buttonPath: this.settingsPath,
+ },
+ [metricStates.LOADING]: {
+ title: __('Waiting for performance data'),
+ description: __(`Creating graphs uses the data from the Prometheus server.
+ If this takes a long time, ensure that data is available.`),
+ },
+ [metricStates.UNKNOWN_ERROR]: {
+ title: __('An error has occurred'),
+ description: __('An error occurred while loading the data. Please try again.'),
+ },
+ },
+ };
+ },
+ computed: {
+ currentState() {
+ return this.states[this.selectedState] || this.states[metricStates.UNKNOWN_ERROR];
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="currentState.title"
+ :primary-button-text="currentState.buttonText"
+ :primary-button-link="currentState.buttonPath"
+ :description="currentState.description"
+ :svg-path="svgPath"
+ :compact="true"
+ >
+ <template v-if="currentState.slottedDescription" #description>
+ <div v-html="currentState.slottedDescription"></div>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index e613351e524..398b45b9012 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -3,9 +3,19 @@ import { __ } from '~/locale';
export const PROMETHEUS_TIMEOUT = 120000; // TWO_MINUTES
/**
- * Errors in Prometheus Queries (PromQL) for metrics
+ * States and error states in Prometheus Queries (PromQL) for metrics
*/
-export const metricsErrors = {
+export const metricStates = {
+ /**
+ * Metric data is available
+ */
+ OK: 'OK',
+
+ /**
+ * Metric data is being fetched
+ */
+ LOADING: 'LOADING',
+
/**
* Connection timed out to prometheus server
* the timeout is set to PROMETHEUS_TIMEOUT
@@ -24,12 +34,12 @@ export const metricsErrors = {
CONNECTION_FAILED: 'CONNECTION_FAILED',
/**
- * The prometheus server was reach but it cannot process
+ * The prometheus server was reached but it cannot process
* the query. This can happen for several reasons:
* - PromQL syntax is incorrect
* - An operator is not supported
*/
- BAD_DATA: 'BAD_DATA',
+ BAD_QUERY: 'BAD_QUERY',
/**
* No specific reason found for error
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index a655191b2b4..1cb82ce0083 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -132,7 +132,7 @@ export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metric_id, result });
})
.catch(error => {
- commit(types.RECEIVE_METRIC_RESULT_ERROR, { metricId: metric.metric_id, error });
+ commit(types.RECEIVE_METRIC_RESULT_FAILURE, { metricId: metric.metric_id, error });
// Continue to throw error so the dashboard can notify using createFlash
throw error;
});
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 3eddd52705d..a13157c6f87 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -2,6 +2,36 @@ const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
/**
+ * Get all state for metric in the dashboard or a group. The
+ * states are not repeated so the dashboard or group can show
+ * a global state.
+ *
+ * @param {Object} state
+ * @returns {Function} A function that returns an array of
+ * states in all the metric in the dashboard or group.
+ */
+export const getMetricStates = state => groupKey => {
+ let groups = state.dashboard.panel_groups;
+ if (groupKey) {
+ groups = groups.filter(group => group.key === groupKey);
+ }
+
+ const metricStates = groups.reduce((acc, group) => {
+ group.panels.forEach(panel => {
+ panel.metrics.forEach(metric => {
+ if (metric.state) {
+ acc.push(metric.state);
+ }
+ });
+ });
+ return acc;
+ }, []);
+
+ // Deduplicate and sort array
+ return Array.from(new Set(metricStates)).sort();
+};
+
+/**
* Getter to obtain the list of metric ids that have data
*
* Useful to understand which parts of the dashboard should
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index e4e467f3d68..74068e1d846 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -12,7 +12,7 @@ export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAIL
export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
-export const RECEIVE_METRIC_RESULT_ERROR = 'RECEIVE_METRIC_RESULT_ERROR';
+export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index f04c12c2ac8..16a34a6c026 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -3,7 +3,7 @@ import { slugify } from '~/lib/utils/text_utility';
import * as types from './mutation_types';
import { normalizeMetric, normalizeQueryResult } from './utils';
import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
-import { metricsErrors } from '../constants';
+import { metricStates } from '../constants';
import httpStatusCodes from '~/lib/utils/http_status';
const normalizePanelMetrics = (metrics, defaultLabel) =>
@@ -41,39 +41,39 @@ const findMetricInDashboard = (metricId, dashboard) => {
* @param {Object} metric - Metric object as defined in the dashboard
* @param {Object} state - New state
* @param {Array|null} state.result - Array of results
- * @param {String} state.error - Error code from metricsErrors
+ * @param {String} state.error - Error code from metricStates
* @param {Boolean} state.loading - True if the metric is loading
*/
-const setMetricState = (metric, { result = null, error = null, loading = false }) => {
+const setMetricState = (metric, { result = null, loading = false, state = null }) => {
Vue.set(metric, 'result', result);
- Vue.set(metric, 'error', error);
Vue.set(metric, 'loading', loading);
+ Vue.set(metric, 'state', state);
};
/**
- * Maps a backened error state to a `metricsErrors` constant
+ * Maps a backened error state to a `metricStates` constant
* @param {Object} error - Error from backend response
*/
-const getMetricError = error => {
+const emptyStateFromError = error => {
if (!error) {
- return metricsErrors.UNKNOWN_ERROR;
+ return metricStates.UNKNOWN_ERROR;
}
// Special error responses
if (error.message === BACKOFF_TIMEOUT) {
- return metricsErrors.TIMEOUT;
+ return metricStates.TIMEOUT;
}
// Axios error responses
const { response } = error;
if (response && response.status === httpStatusCodes.SERVICE_UNAVAILABLE) {
- return metricsErrors.CONNECTION_FAILED;
+ return metricStates.CONNECTION_FAILED;
} else if (response && response.status === httpStatusCodes.BAD_REQUEST) {
// Note: "error.response.data.error" may contain Prometheus error information
- return metricsErrors.BAD_DATA;
+ return metricStates.BAD_QUERY;
}
- return metricsErrors.UNKNOWN_ERROR;
+ return metricStates.UNKNOWN_ERROR;
};
export default {
@@ -132,9 +132,9 @@ export default {
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
-
setMetricState(metric, {
loading: true,
+ state: metricStates.LOADING,
});
},
[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
@@ -146,24 +146,24 @@ export default {
const metric = findMetricInDashboard(metricId, state.dashboard);
if (!result || result.length === 0) {
- // If no data is return we still consider it an error and set it to undefined
setMetricState(metric, {
- error: metricsErrors.NO_DATA,
+ state: metricStates.NO_DATA,
});
} else {
const normalizedResults = result.map(normalizeQueryResult);
setMetricState(metric, {
result: Object.freeze(normalizedResults),
+ state: metricStates.OK,
});
}
},
- [types.RECEIVE_METRIC_RESULT_ERROR](state, { metricId, error }) {
+ [types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
if (!metricId) {
return;
}
const metric = findMetricInDashboard(metricId, state.dashboard);
setMetricState(metric, {
- error: getMetricError(error),
+ state: emptyStateFromError(error),
});
},
diff --git a/app/controllers/projects/hook_logs_controller.rb b/app/controllers/projects/hook_logs_controller.rb
index a7afc3d77a5..ed7e7b68acb 100644
--- a/app/controllers/projects/hook_logs_controller.rb
+++ b/app/controllers/projects/hook_logs_controller.rb
@@ -16,15 +16,17 @@ class Projects::HookLogsController < Projects::ApplicationController
end
def retry
- result = hook.execute(hook_log.request_data, hook_log.trigger)
-
- set_hook_execution_notice(result)
-
+ execute_hook
redirect_to edit_project_hook_path(@project, @hook)
end
private
+ def execute_hook
+ result = hook.execute(hook_log.request_data, hook_log.trigger)
+ set_hook_execution_notice(result)
+ end
+
def hook
@hook ||= @project.hooks.find(params[:hook_id])
end
diff --git a/app/controllers/projects/service_hook_logs_controller.rb b/app/controllers/projects/service_hook_logs_controller.rb
new file mode 100644
index 00000000000..5c814ea139f
--- /dev/null
+++ b/app/controllers/projects/service_hook_logs_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Projects::ServiceHookLogsController < Projects::HookLogsController
+ before_action :service, only: [:show, :retry]
+
+ def retry
+ execute_hook
+ redirect_to edit_project_service_path(@project, @service)
+ end
+
+ private
+
+ def hook
+ @hook ||= service.service_hook
+ end
+
+ def service
+ @service ||= @project.find_or_initialize_service(params[:service_id])
+ end
+end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index c9f680a4696..daaca9e1268 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -7,6 +7,7 @@ class Projects::ServicesController < Projects::ApplicationController
before_action :authorize_admin_project!
before_action :ensure_service_enabled
before_action :service
+ before_action :web_hook_logs, only: [:edit, :update]
respond_to :html
@@ -77,6 +78,12 @@ class Projects::ServicesController < Projects::ApplicationController
@service ||= @project.find_or_initialize_service(params[:id])
end
+ def web_hook_logs
+ return unless @service.service_hook.present?
+
+ @web_hook_logs ||= @service.service_hook.web_hook_logs.recent.page(params[:page])
+ end
+
def ensure_service_enabled
render_404 unless service
end
diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb
new file mode 100644
index 00000000000..260a9753f76
--- /dev/null
+++ b/app/graphql/mutations/snippets/mark_as_spam.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Snippets
+ class MarkAsSpam < Base
+ graphql_name 'MarkAsSpamSnippet'
+
+ argument :id,
+ GraphQL::ID_TYPE,
+ required: true,
+ description: 'The global id of the snippet to update'
+
+ def resolve(id:)
+ snippet = authorized_find!(id: id)
+
+ result = mark_as_spam(snippet)
+ errors = result ? [] : ['Error with Akismet. Please check the logs for more info.']
+
+ {
+ errors: errors
+ }
+ end
+
+ private
+
+ def mark_as_spam(snippet)
+ SpamService.new(snippet).mark_as_spam!
+ end
+
+ def authorized_resource?(snippet)
+ super && snippet.submittable_as_spam_by?(context[:current_user])
+ end
+
+ def ability_name
+ "admin"
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 998dfdc7815..0a9c0143945 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -28,6 +28,7 @@ module Types
mount_mutation Mutations::Snippets::Destroy
mount_mutation Mutations::Snippets::Update
mount_mutation Mutations::Snippets::Create
+ mount_mutation Mutations::Snippets::MarkAsSpam
end
end
diff --git a/app/models/blob.rb b/app/models/blob.rb
index c0f26ee64f8..0a425f2b961 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -4,6 +4,7 @@
class Blob < SimpleDelegator
include Presentable
include BlobLanguageFromGitAttributes
+ include BlobActiveModel
CACHE_TIME = 60 # Cache raw blobs referred to by a (mutable) ref for 1 minute
CACHE_TIME_IMMUTABLE = 3600 # Cache blobs referred to by an immutable reference for 1 hour
diff --git a/app/models/concerns/blob_active_model.rb b/app/models/concerns/blob_active_model.rb
new file mode 100644
index 00000000000..89157e90e34
--- /dev/null
+++ b/app/models/concerns/blob_active_model.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+# To be included in blob classes which are to be
+# treated as ActiveModel.
+#
+# The blob class must respond_to `project`
+module BlobActiveModel
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def declarative_policy_class
+ 'BlobPolicy'
+ end
+ end
+
+ def to_ability_name
+ 'blob'
+ end
+end
diff --git a/app/models/concerns/safe_url.rb b/app/models/concerns/safe_url.rb
new file mode 100644
index 00000000000..febca7d241f
--- /dev/null
+++ b/app/models/concerns/safe_url.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module SafeUrl
+ extend ActiveSupport::Concern
+
+ def safe_url(usernames_whitelist: [])
+ return if url.nil?
+
+ uri = URI.parse(url)
+ uri.password = '*****' if uri.password
+ uri.user = '*****' if uri.user && !usernames_whitelist.include?(uri.user)
+ uri.to_s
+ rescue URI::Error
+ end
+end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 9ae697b9e59..a5f68831f34 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@
class ProjectHook < WebHook
include TriggerableHooks
+ include Presentable
triggerable_hooks [
:push_hooks,
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 8f305dd7c22..4caa45a13d4 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ServiceHook < WebHook
+ include Presentable
+
belongs_to :service
validates :service, presence: true
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index cfb1f3ec63b..df0e7b30f84 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -1,6 +1,9 @@
# frozen_string_literal: true
class WebHookLog < ApplicationRecord
+ include SafeUrl
+ include Presentable
+
belongs_to :web_hook
serialize :request_headers, Hash # rubocop:disable Cop/ActiveRecordSerialize
@@ -9,6 +12,8 @@ class WebHookLog < ApplicationRecord
validates :web_hook, presence: true
+ before_save :obfuscate_basic_auth
+
def self.recent
where('created_at >= ?', 2.days.ago.beginning_of_day)
.order(created_at: :desc)
@@ -17,4 +22,10 @@ class WebHookLog < ApplicationRecord
def success?
response_status =~ /^2/
end
+
+ private
+
+ def obfuscate_basic_auth
+ self.url = safe_url
+ end
end
diff --git a/app/models/readme_blob.rb b/app/models/readme_blob.rb
index 7b49fa632f6..695b4e3ffe3 100644
--- a/app/models/readme_blob.rb
+++ b/app/models/readme_blob.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ReadmeBlob < SimpleDelegator
+ include BlobActiveModel
+
attr_reader :repository
def initialize(blob, repository)
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index c165a1a9b0d..1e5c93cd913 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -3,6 +3,7 @@
class RemoteMirror < ApplicationRecord
include AfterCommitQueue
include MirrorAuthentication
+ include SafeUrl
MAX_FIRST_RUNTIME = 3.hours
MAX_INCREMENTAL_RUNTIME = 1.hour
@@ -194,13 +195,7 @@ class RemoteMirror < ApplicationRecord
end
def safe_url
- return if url.nil?
-
- result = URI.parse(url)
- result.password = '*****' if result.password
- result.user = '*****' if result.user && result.user != 'git' # tokens or other data may be saved as user
- result.to_s
- rescue URI::Error
+ super(usernames_whitelist: %w[git])
end
def ensure_remote!
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index f9c562364cb..c6867e48cbf 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -274,6 +274,10 @@ class WikiPage
@attributes.merge!(attrs)
end
+ def to_ability_name
+ 'wiki_page'
+ end
+
private
# Process and format the title based on the user input.
diff --git a/app/policies/blob_policy.rb b/app/policies/blob_policy.rb
new file mode 100644
index 00000000000..639b9dfeea7
--- /dev/null
+++ b/app/policies/blob_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class BlobPolicy < BasePolicy
+ delegate { @subject.project }
+
+ rule { can?(:download_code) }.enable :read_blob
+end
diff --git a/app/policies/wiki_page_policy.rb b/app/policies/wiki_page_policy.rb
new file mode 100644
index 00000000000..468632c9085
--- /dev/null
+++ b/app/policies/wiki_page_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class WikiPagePolicy < BasePolicy
+ delegate { @subject.wiki.project }
+
+ rule { can?(:read_wiki) }.enable :read_wiki_page
+end
diff --git a/app/presenters/hooks/project_hook_presenter.rb b/app/presenters/hooks/project_hook_presenter.rb
new file mode 100644
index 00000000000..a65c7221b5a
--- /dev/null
+++ b/app/presenters/hooks/project_hook_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
+ presents :project_hook
+
+ def logs_details_path(log)
+ project_hook_hook_log_path(project, self, log)
+ end
+
+ def logs_retry_path(log)
+ retry_project_hook_hook_log_path(project, self, log)
+ end
+end
diff --git a/app/presenters/hooks/service_hook_presenter.rb b/app/presenters/hooks/service_hook_presenter.rb
new file mode 100644
index 00000000000..bc20d5b1a3b
--- /dev/null
+++ b/app/presenters/hooks/service_hook_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
+ presents :service_hook
+
+ def logs_details_path(log)
+ project_service_hook_log_path(service.project, service, log)
+ end
+
+ def logs_retry_path(log)
+ retry_project_service_hook_log_path(service.project, service, log)
+ end
+end
diff --git a/app/presenters/web_hook_log_presenter.rb b/app/presenters/web_hook_log_presenter.rb
new file mode 100644
index 00000000000..fca03ddb5d7
--- /dev/null
+++ b/app/presenters/web_hook_log_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
+ presents :web_hook_log
+
+ def details_path
+ web_hook.present.logs_details_path(self)
+ end
+
+ def retry_path
+ web_hook.present.logs_retry_path(self)
+ end
+end
diff --git a/app/serializers/group_child_entity.rb b/app/serializers/group_child_entity.rb
index 20d7032c970..a7fe4d3f9b9 100644
--- a/app/serializers/group_child_entity.rb
+++ b/app/serializers/group_child_entity.rb
@@ -99,3 +99,5 @@ class GroupChildEntity < Grape::Entity
end
end
end
+
+GroupChildEntity.prepend_if_ee('EE::GroupChildEntity')
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 8c294218708..87edac36e33 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -92,9 +92,6 @@ class WebHookService
end
def log_execution(trigger:, url:, request_data:, response:, execution_duration:, error_message: nil)
- # logging for ServiceHook's is not available
- return if hook.is_a?(ServiceHook)
-
WebHookLog.create(
web_hook: hook,
trigger: trigger,
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index be5f1f4f9a8..ae90ffd9efc 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -9,6 +9,7 @@
= f.label s_('ProjectCreationLevel|Default project creation protection'), class: 'label-bold'
= f.select :default_project_creation, options_for_select(Gitlab::Access.project_creation_options, @application_setting.default_project_creation), {}, class: 'form-control'
= render_if_exists 'admin/application_settings/default_project_deletion_protection_setting', form: f
+ = render_if_exists 'admin/application_settings/default_project_deletion_adjourned_period_setting', form: f
.form-group.visibility-level-setting
= f.label :default_project_visibility, class: 'label-bold'
= render('shared/visibility_radios', model_method: :default_project_visibility, form: f, selected_level: @application_setting.default_project_visibility, form_model: Project.new)
@@ -53,6 +54,7 @@
= select(:application_setting, :enabled_git_access_protocol, [['Both SSH and HTTP(S)', nil], ['Only SSH', 'ssh'], ['Only HTTP(S)', 'http']], {}, class: 'form-control')
%span.form-text.text-muted#clone-protocol-help
= _('Allow only the selected protocols to be used for Git access.')
+
.form-group
= f.label :custom_http_clone_url_root, _('Custom Git clone URL for HTTP(S)'), class: 'label-bold'
= f.text_field :custom_http_clone_url_root, class: 'form-control', placeholder: 'https://git.example.com', :'aria-describedby' => 'custom_http_clone_url_root_help_block'
diff --git a/app/views/admin/projects/_archived.html.haml b/app/views/admin/projects/_archived.html.haml
new file mode 100644
index 00000000000..8b4d5806c47
--- /dev/null
+++ b/app/views/admin/projects/_archived.html.haml
@@ -0,0 +1,3 @@
+- if project.archived
+ %span.badge.badge-warning
+ = _('archived')
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index 2f7ad35eb3e..f842ab2d009 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -14,8 +14,7 @@
.stats
%span.badge.badge-pill
= storage_counter(project.statistics&.storage_size)
- - if project.archived
- %span.badge.badge-warning archived
+ = render_if_exists 'admin/projects/archived', project: project
.title
= link_to(admin_project_path(project)) do
.dash-project-avatar
diff --git a/app/views/projects/_archived_notice.html.haml b/app/views/projects/_archived_notice.html.haml
new file mode 100644
index 00000000000..522693ae24a
--- /dev/null
+++ b/app/views/projects/_archived_notice.html.haml
@@ -0,0 +1,5 @@
+- if project.archived?
+ .text-warning.center.prepend-top-20
+ %p
+ = icon("exclamation-triangle fw")
+ = _('Archived project! Repository and other project resources are read only')
diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml
new file mode 100644
index 00000000000..6c84fbfeeb3
--- /dev/null
+++ b/app/views/projects/_remove.html.haml
@@ -0,0 +1,10 @@
+- return unless can?(current_user, :remove_project, project)
+
+.sub-section
+ %h4.danger-title= _('Remove project')
+ %p
+ %strong= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
+ = form_tag(project_path(project), method: :delete) do
+ %p
+ %strong= _('Removed projects cannot be restored!')
+ = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(project) }
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 7ad52673137..1c18487f688 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -73,23 +73,7 @@
= render 'export', project: @project
- - if can? current_user, :archive_project, @project
- .sub-section
- %h4.warning-title
- - if @project.archived?
- = _('Unarchive project')
- - else
- = _('Archive project')
- - if @project.archived?
- %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments and other entities can be created. <strong>Once active this project shows up in the search and on the dashboard.</strong>").html_safe
- = link_to _('Unarchive project'), unarchive_project_path(@project),
- data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
- method: :post, class: "btn btn-success"
- - else
- %p= _("Archiving the project will make it entirely read-only. It is hidden from the dashboard and doesn't show up in searches. <strong>The repository cannot be committed to, and no issues, comments or other entities can be created.</strong>").html_safe
- = link_to _('Archive project'), archive_project_path(@project),
- data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
- method: :post, class: "btn btn-warning"
+ = render_if_exists 'projects/settings/archive'
.sub-section.rename-repository
%h4.warning-title= _('Change path')
= render 'projects/errors'
@@ -135,14 +119,7 @@
%strong= _('Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.')
= button_to _('Remove fork relationship'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
- - if can?(current_user, :remove_project, @project)
- .sub-section
- %h4.danger-title= _('Remove project')
- %p= _('Removing the project will delete its repository and all related resources including issues, merge requests etc.')
- = form_tag(project_path(@project), method: :delete) do
- %p
- %strong= _('Removed projects cannot be restored!')
- = button_to _('Remove project'), '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_project_message(@project) }
+ = render 'remove', project: @project
.save-project-loader.hide
.center
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index 3e54c3ca9f8..ada986dd969 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -28,7 +28,7 @@
%td.light
= time_ago_with_tooltip(hook_log.created_at)
%td
- = link_to 'View details', project_hook_hook_log_path(project, hook, hook_log)
+ = link_to 'View details', hook_log.present.details_path
= paginate hook_logs, theme: 'gitlab'
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index bd8ca5e7d70..a8796cd7b1c 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -3,7 +3,6 @@
%h4.prepend-top-0
Request details
.col-lg-9
-
- = link_to 'Resend Request', retry_project_hook_hook_log_path(@project, @hook, @hook_log), method: :post, class: "btn btn-default float-right prepend-left-10"
+ = link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn btn-default float-right prepend-left-10"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/services/edit.html.haml b/app/views/projects/services/edit.html.haml
index 1e7903535c6..e3e8a312431 100644
--- a/app/views/projects/services/edit.html.haml
+++ b/app/views/projects/services/edit.html.haml
@@ -1,8 +1,10 @@
- breadcrumb_title @service.title
- page_title @service.title, s_("ProjectService|Services")
- add_to_breadcrumbs(s_("ProjectService|Settings"), edit_project_path(@project))
-- add_to_breadcrumbs(s_("ProjectService|Integrations"), namespace_project_settings_integrations_path)
+- add_to_breadcrumbs(s_("ProjectService|Integrations"), project_settings_integrations_path(@project))
= render 'deprecated_message' if @service.deprecation_message
= render 'form'
+- if @web_hook_logs
+ = render partial: 'projects/hook_logs/index', locals: { hook: @service.service_hook, hook_logs: @web_hook_logs, project: @project }
diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml
new file mode 100644
index 00000000000..3307c3775ec
--- /dev/null
+++ b/app/views/projects/settings/_archive.html.haml
@@ -0,0 +1,18 @@
+- return unless can?(current_user, :archive_project, @project)
+
+.sub-section
+ %h4.warning-title
+ - if @project.archived?
+ = _('Unarchive project')
+ - else
+ = _('Archive project')
+ - if @project.archived?
+ %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = link_to _('Unarchive project'), unarchive_project_path(@project),
+ data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' },
+ method: :post, class: "btn btn-success"
+ - else
+ %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe }
+ = link_to _('Archive project'), archive_project_path(@project),
+ data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' },
+ method: :post, class: "btn btn-warning"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index c5653c3dd5a..8f13806e8cd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -18,11 +18,8 @@
- if can?(current_user, :download_code, @project) && @project.repository_languages.present?
= repository_languages_bar(@project.repository_languages)
- - if @project.archived?
- .text-warning.center.prepend-top-20
- %p
- = icon("exclamation-triangle fw")
- #{ _('Archived project! Repository and other project resources are read-only') }
+ = render "archived_notice", project: @project
+ = render_if_exists "projects/marked_for_deletion_notice", project: @project
- view_path = @project.default_view
diff --git a/app/views/shared/projects/_archived.html.haml b/app/views/shared/projects/_archived.html.haml
new file mode 100644
index 00000000000..fad93d14390
--- /dev/null
+++ b/app/views/shared/projects/_archived.html.haml
@@ -0,0 +1,3 @@
+- if project.archived
+ %span.d-flex.badge.badge-warning
+ = _('archived')
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 5b9af0267cc..45e95685677 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -67,8 +67,7 @@
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
- - if project.archived
- %span.d-flex.icon-wrapper.badge.badge-warning archived
+ = render_if_exists 'shared/projects/archived', project: project
- if stars
= link_to project_starrers_path(project),
class: "d-flex align-items-center icon-wrapper stars has-tooltip",
diff --git a/changelogs/unreleased/27244-discard-all-changes.yml b/changelogs/unreleased/27244-discard-all-changes.yml
new file mode 100644
index 00000000000..70750208667
--- /dev/null
+++ b/changelogs/unreleased/27244-discard-all-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Fix "Discard all" for new and renamed files
+merge_request: 21854
+author:
+type: fixed
diff --git a/changelogs/unreleased/34121-add-error-states-to-getters.yml b/changelogs/unreleased/34121-add-error-states-to-getters.yml
new file mode 100644
index 00000000000..9e9bd93d509
--- /dev/null
+++ b/changelogs/unreleased/34121-add-error-states-to-getters.yml
@@ -0,0 +1,5 @@
+---
+title: Add specific error states to dashboard
+merge_request: 21618
+author:
+type: added
diff --git a/changelogs/unreleased/fj-add-mark-as-spam-snippet-mutation.yml b/changelogs/unreleased/fj-add-mark-as-spam-snippet-mutation.yml
new file mode 100644
index 00000000000..b5f492e6628
--- /dev/null
+++ b/changelogs/unreleased/fj-add-mark-as-spam-snippet-mutation.yml
@@ -0,0 +1,5 @@
+---
+title: Add mark as spam snippet mutation
+merge_request: 21912
+author:
+type: other
diff --git a/changelogs/unreleased/log_service_web_hooks.yml b/changelogs/unreleased/log_service_web_hooks.yml
new file mode 100644
index 00000000000..b0a5772da22
--- /dev/null
+++ b/changelogs/unreleased/log_service_web_hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Added WebHookLogs for ServiceHooks
+merge_request: 20976
+author:
+type: added
diff --git a/changelogs/unreleased/update_auto_deploy_image.yml b/changelogs/unreleased/update_auto_deploy_image.yml
new file mode 100644
index 00000000000..382369e47bb
--- /dev/null
+++ b/changelogs/unreleased/update_auto_deploy_image.yml
@@ -0,0 +1,5 @@
+---
+title: Update auto-deploy-image to v0.8.3
+merge_request: 21696
+author:
+type: fixed
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8e4aa5701b4..691e4339bf0 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -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'
diff --git a/config/routes/project.rb b/config/routes/project.rb
index ea406d17bef..d2abc73f7cc 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -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
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 063a3cc2b5b..68ad819d48b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -124,4 +124,4 @@
- [design_management_new_version, 1]
- [epics, 2]
- [personal_access_tokens, 1]
-
+ - [adjourned_project_deletion, 1]
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index 80fa30da357..57048059476 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -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)**
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 484841fd712..4673356cf9d 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -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
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index f3437a26f42..398ae52c130 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -16122,6 +16122,33 @@
"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,
"args": [
@@ -19664,6 +19691,108 @@
},
{
"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",
"description": "Autogenerated return type of DesignManagementUpload",
"fields": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1371daa6453..9fb39322f5c 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b49fac8d2c9..209d41d62cd 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -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.
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 185cce6353e..fa0efcaa5f0 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -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. |
diff --git a/doc/user/admin_area/settings/visibility_and_access_controls.md b/doc/user/admin_area/settings/visibility_and_access_controls.md
index 95e4c45e56c..74398128593 100644
--- a/doc/user/admin_area/settings/visibility_and_access_controls.md
+++ b/doc/user/admin_area/settings/visibility_and_access_controls.md
@@ -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:
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a1fce9e8b20..d1f99ea49ce 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -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'
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index cb45c12c2b0..d20d04425f6 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -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
diff --git a/lib/gitlab/metrics/transaction.rb b/lib/gitlab/metrics/transaction.rb
index 115368c8bc6..552eae639e6 100644
--- a/lib/gitlab/metrics/transaction.rb
+++ b/lib/gitlab/metrics/transaction.rb
@@ -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: {})
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 9fe8e8e670b..6301a72f48a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb
index 3a5067a9541..c95c47fa560 100644
--- a/qa/qa/page/project/settings/advanced.rb
+++ b/qa/qa/page/project/settings/advanced.rb
@@ -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
diff --git a/scripts/ee-specific-lines-check b/scripts/ee-specific-lines-check
deleted file mode 100755
index 4114575168c..00000000000
--- a/scripts/ee-specific-lines-check
+++ /dev/null
@@ -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)
diff --git a/scripts/frontend/check_no_partial_karma_jest.sh b/scripts/frontend/check_no_partial_karma_jest.sh
index 0d0c897bb18..c5fffa5900b 100755
--- a/scripts/frontend/check_no_partial_karma_jest.sh
+++ b/scripts/frontend/check_no_partial_karma_jest.sh
@@ -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
diff --git a/spec/controllers/projects/service_hook_logs_controller_spec.rb b/spec/controllers/projects/service_hook_logs_controller_spec.rb
new file mode 100644
index 00000000000..ca57b0579a8
--- /dev/null
+++ b/spec/controllers/projects/service_hook_logs_controller_spec.rb
@@ -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
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index f9c77dbf87f..b6bb30d1f93 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -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 }
diff --git a/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
index 5f24bab600c..5f24bab600c 100644
--- a/spec/frontend/monitoring/__snapshots__/dashboard_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
new file mode 100644
index 00000000000..7f37a83d291
--- /dev/null
+++ b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
@@ -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`;
diff --git a/spec/frontend/monitoring/dashboard_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
index e985e5fb443..e985e5fb443 100644
--- a/spec/frontend/monitoring/dashboard_state_spec.js
+++ b/spec/frontend/monitoring/components/empty_state_spec.js
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
new file mode 100644
index 00000000000..e8ef8192067
--- /dev/null
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -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();
+ });
+});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 92d469270c9..f38bd4384e2 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -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,
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index 3b6f33ed8b1..9e325fe3cf9 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -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;
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 8da172ec634..60107a03674 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -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,
}),
);
});
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index 708c5ea75e0..0ee114cb70d 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -92,26 +92,58 @@ 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: types.DISCARD_FILE_CHANGES, payload: 'discardAll' },
- { 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: '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,
+ store.state,
+ [
+ { type: types.DISCARD_FILE_CHANGES, payload: 'discardAll' },
+ { type: types.REMOVE_ALL_CHANGES_FILES },
+ ],
+ [],
+ done,
+ );
+ });
});
describe('closeAllFiles', () => {
diff --git a/spec/javascripts/monitoring/components/dashboard_spec.js b/spec/javascripts/monitoring/components/dashboard_spec.js
index 37a811f153f..b29bac21820 100644
--- a/spec/javascripts/monitoring/components/dashboard_spec.js
+++ b/spec/javascripts/monitoring/components/dashboard_spec.js
@@ -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);
});
});
diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb
index 9e55fbcce20..2c141cae98d 100644
--- a/spec/models/blob_spec.rb
+++ b/spec/models/blob_spec.rb
@@ -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
diff --git a/spec/models/concerns/safe_url_spec.rb b/spec/models/concerns/safe_url_spec.rb
new file mode 100644
index 00000000000..3244410181e
--- /dev/null
+++ b/spec/models/concerns/safe_url_spec.rb
@@ -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
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 85934b81086..22aad2fab0a 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -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) }
diff --git a/spec/models/readme_blob_spec.rb b/spec/models/readme_blob_spec.rb
new file mode 100644
index 00000000000..f07713bd908
--- /dev/null
+++ b/spec/models/readme_blob_spec.rb
@@ -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
diff --git a/spec/policies/blob_policy_spec.rb b/spec/policies/blob_policy_spec.rb
new file mode 100644
index 00000000000..20c8a55f437
--- /dev/null
+++ b/spec/policies/blob_policy_spec.rb
@@ -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
diff --git a/spec/policies/wiki_page_policy_spec.rb b/spec/policies/wiki_page_policy_spec.rb
new file mode 100644
index 00000000000..e550ccf6d65
--- /dev/null
+++ b/spec/policies/wiki_page_policy_spec.rb
@@ -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
diff --git a/spec/presenters/hooks/project_hook_presenter_spec.rb b/spec/presenters/hooks/project_hook_presenter_spec.rb
new file mode 100644
index 00000000000..773e8ccf51e
--- /dev/null
+++ b/spec/presenters/hooks/project_hook_presenter_spec.rb
@@ -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
diff --git a/spec/presenters/hooks/service_hook_presenter_spec.rb b/spec/presenters/hooks/service_hook_presenter_spec.rb
new file mode 100644
index 00000000000..bea57768e3e
--- /dev/null
+++ b/spec/presenters/hooks/service_hook_presenter_spec.rb
@@ -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
diff --git a/spec/presenters/web_hook_log_presenter_spec.rb b/spec/presenters/web_hook_log_presenter_spec.rb
new file mode 100644
index 00000000000..8812a0ba594
--- /dev/null
+++ b/spec/presenters/web_hook_log_presenter_spec.rb
@@ -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
diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
new file mode 100644
index 00000000000..0e8fe4987b9
--- /dev/null
+++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb
@@ -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
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 2a4368868d5..d8f13bc2e61 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -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
diff --git a/spec/views/projects/services/edit.html.haml_spec.rb b/spec/views/projects/services/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..12e1cda2c00
--- /dev/null
+++ b/spec/views/projects/services/edit.html.haml_spec.rb
@@ -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