summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-11-03 18:09:22 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-11-03 18:09:22 +0000
commit62baa95f25f1cc56b100d2b64b0a3906f47dcfe1 (patch)
tree0bee30bc13c3cb7444f1d89d2647719718a31d76 /app
parentff8eb438401fc82b883fc4ae69626f0035b69236 (diff)
downloadgitlab-ce-62baa95f25f1cc56b100d2b64b0a3906f47dcfe1.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue13
-rw-r--r--app/assets/javascripts/groups/components/app.vue58
-rw-r--r--app/assets/javascripts/groups/components/item_actions.vue29
-rw-r--r--app/assets/javascripts/groups/index.js3
-rw-r--r--app/assets/javascripts/monitoring/components/charts/column.vue10
-rw-r--r--app/assets/javascripts/monitoring/components/charts/stacked_column.vue21
-rw-r--r--app/assets/javascripts/pages/projects/graphs/charts/index.js14
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/app.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/members/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/members_table.vue36
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/members/utils.js29
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--app/controllers/projects/settings/repository_controller.rb7
-rw-r--r--app/graphql/mutations/alert_management/http_integration/destroy.rb24
-rw-r--r--app/graphql/mutations/alert_management/http_integration/http_integration_base.rb2
-rw-r--r--app/graphql/resolvers/users_resolver.rb11
-rw-r--r--app/graphql/types/merge_request_type.rb3
-rw-r--r--app/graphql/types/mutation_type.rb1
-rw-r--r--app/helpers/search_helper.rb24
-rw-r--r--app/mailers/emails/projects.rb9
-rw-r--r--app/services/alert_management/http_integrations/destroy_service.rb49
-rw-r--r--app/services/alert_management/process_prometheus_alert_service.rb15
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/services/projects/alerting/notify_service.rb4
-rw-r--r--app/services/projects/cleanup_service.rb20
-rw-r--r--app/services/projects/prometheus/alerts/notify_service.rb21
-rw-r--r--app/views/admin/application_settings/_signin.html.haml4
-rw-r--r--app/views/notify/prometheus_alert_fired_email.html.haml6
-rw-r--r--app/views/notify/prometheus_alert_fired_email.text.erb5
-rw-r--r--app/views/projects/merge_requests/_nav_btns.html.haml4
-rw-r--r--app/views/search/results/_empty.html.haml4
-rw-r--r--app/workers/repository_cleanup_worker.rb5
33 files changed, 321 insertions, 141 deletions
diff --git a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
index a475ff8fd25..2be9ebda87a 100644
--- a/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
+++ b/app/assets/javascripts/analytics/product_analytics/components/activity_chart.vue
@@ -17,10 +17,13 @@ export default {
},
},
computed: {
- seriesData() {
- return {
- full: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
- };
+ barSeriesData() {
+ return [
+ {
+ name: 'full',
+ data: this.formattedData.keys.map((val, idx) => [val, this.formattedData.values[idx]]),
+ },
+ ];
},
},
};
@@ -30,7 +33,7 @@ export default {
<div class="gl-xs-w-full">
<gl-column-chart
v-if="formattedData.keys"
- :data="seriesData"
+ :bars="barSeriesData"
:x-axis-title="__('Value')"
:y-axis-title="__('Number of events')"
:x-axis-type="'category'"
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 871f5c9a845..e057012a246 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -3,9 +3,8 @@
import $ from 'jquery';
import 'vendor/jquery.scrollTo';
-import { GlLoadingIcon } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
-import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
+import { GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -16,8 +15,8 @@ import groupsComponent from './groups.vue';
export default {
components: {
- DeprecatedModal,
groupsComponent,
+ GlModal,
GlLoadingIcon,
},
props: {
@@ -49,13 +48,30 @@ export default {
isLoading: true,
isSearchEmpty: false,
searchEmptyMessage: '',
- showModal: false,
- groupLeaveConfirmationMessage: '',
targetGroup: null,
targetParentGroup: null,
};
},
computed: {
+ primaryProps() {
+ return {
+ text: __('Leave group'),
+ attributes: [{ variant: 'warning' }, { category: 'primary' }],
+ };
+ },
+ cancelProps() {
+ return {
+ text: __('Cancel'),
+ };
+ },
+ groupLeaveConfirmationMessage() {
+ if (!this.targetGroup) {
+ return '';
+ }
+ return sprintf(s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'), {
+ fullName: this.targetGroup.fullName,
+ });
+ },
groups() {
return this.store.getGroups();
},
@@ -171,27 +187,17 @@ export default {
}
},
showLeaveGroupModal(group, parentGroup) {
- const { fullName } = group;
this.targetGroup = group;
this.targetParentGroup = parentGroup;
- this.showModal = true;
- this.groupLeaveConfirmationMessage = sprintf(
- s__('GroupsTree|Are you sure you want to leave the "%{fullName}" group?'),
- { fullName },
- );
- },
- hideLeaveGroupModal() {
- this.showModal = false;
},
leaveGroup() {
- this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service
.leaveGroup(this.targetGroup.leavePath)
.then(res => {
$.scrollTo(0);
this.store.removeGroup(this.targetGroup, this.targetParentGroup);
- Flash(res.data.notice, 'notice');
+ this.$toast.show(res.data.notice);
})
.catch(err => {
let message = COMMON_STR.FAILURE;
@@ -245,21 +251,21 @@ export default {
class="loading-animation prepend-top-20"
/>
<groups-component
- v-if="!isLoading"
+ v-else
:groups="groups"
:search-empty="isSearchEmpty"
:search-empty-message="searchEmptyMessage"
:page-info="pageInfo"
:action="action"
/>
- <deprecated-modal
- v-show="showModal"
- :primary-button-label="__('Leave')"
+ <gl-modal
+ modal-id="leave-group-modal"
:title="__('Are you sure?')"
- :text="groupLeaveConfirmationMessage"
- kind="warning"
- @cancel="hideLeaveGroupModal"
- @submit="leaveGroup"
- />
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="leaveGroup"
+ >
+ {{ groupLeaveConfirmationMessage }}
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/item_actions.vue b/app/assets/javascripts/groups/components/item_actions.vue
index 07c90de1e6e..ff52f5ef51c 100644
--- a/app/assets/javascripts/groups/components/item_actions.vue
+++ b/app/assets/javascripts/groups/components/item_actions.vue
@@ -1,14 +1,15 @@
<script>
-import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import eventHub from '../event_hub';
import { COMMON_STR } from '../constants';
export default {
components: {
- GlIcon,
+ GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
},
props: {
parentGroup: {
@@ -44,28 +45,28 @@ export default {
<template>
<div class="controls d-flex justify-content-end">
- <a
+ <gl-button
v-if="group.canLeave"
v-gl-tooltip.top
- :href="group.leavePath"
+ v-gl-modal.leave-group-modal
:title="leaveBtnTitle"
:aria-label="leaveBtnTitle"
data-testid="leave-group-btn"
- class="leave-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
- @click.prevent="onLeaveGroup"
- >
- <gl-icon name="leave" class="position-top-0" />
- </a>
- <a
+ size="small"
+ icon="leave"
+ class="leave-group gl-ml-3"
+ @click.stop="onLeaveGroup"
+ />
+ <gl-button
v-if="group.canEdit"
v-gl-tooltip.top
:href="group.editPath"
:title="editBtnTitle"
:aria-label="editBtnTitle"
data-testid="edit-group-btn"
- class="edit-group btn btn-xs no-expand gl-text-gray-500 gl-ml-5"
- >
- <gl-icon name="settings" class="position-top-0 align-middle" />
- </a>
+ size="small"
+ icon="pencil"
+ class="edit-group gl-ml-3"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index 928f1fe409f..0e2f2cf9d27 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '../vue_shared/translate';
import GroupFilterableList from './groups_filterable_list';
@@ -31,6 +32,8 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
Vue.component('group-folder', groupFolderComponent);
Vue.component('group-item', groupItemComponent);
+ Vue.use(GlToast);
+
// eslint-disable-next-line no-new
new Vue({
el,
diff --git a/app/assets/javascripts/monitoring/components/charts/column.vue b/app/assets/javascripts/monitoring/components/charts/column.vue
index d7d01def45e..511f77a441b 100644
--- a/app/assets/javascripts/monitoring/components/charts/column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/column.vue
@@ -35,18 +35,14 @@ export default {
};
},
computed: {
- chartData() {
- const queryData = this.graphData.metrics.reduce((acc, query) => {
+ barChartData() {
+ return this.graphData.metrics.reduce((acc, query) => {
const series = makeDataSeries(query.result || [], {
name: this.formatLegendLabel(query),
});
return acc.concat(series);
}, []);
-
- return {
- values: queryData[0].data,
- };
},
chartOptions() {
const xAxis = getTimeAxisOptions({ timezone: this.timezone });
@@ -109,7 +105,7 @@ export default {
<gl-column-chart
ref="columnChart"
v-bind="$attrs"
- :data="chartData"
+ :bars="barChartData"
:option="chartOptions"
:width="width"
:height="height"
diff --git a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
index 9bcd4419a14..66b4d0d86e6 100644
--- a/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
+++ b/app/assets/javascripts/monitoring/components/charts/stacked_column.vue
@@ -61,14 +61,16 @@ export default {
},
computed: {
chartData() {
- return this.graphData.metrics.map(({ result }) => {
- // This needs a fix. Not only metrics[0] should be shown.
- // See https://gitlab.com/gitlab-org/gitlab/-/issues/220492
- if (!result || result.length === 0) {
- return [];
- }
- return result[0].values.map(val => val[1]);
- });
+ return this.graphData.metrics
+ .map(({ label: name, result }) => {
+ // This needs a fix. Not only metrics[0] should be shown.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/220492
+ if (!result || result.length === 0) {
+ return [];
+ }
+ return { name, data: result[0].values.map(val => val[1]) };
+ })
+ .slice(0, 1);
},
xAxisTitle() {
return this.graphData.x_label !== undefined ? this.graphData.x_label : '';
@@ -136,7 +138,7 @@ export default {
<gl-stacked-column-chart
ref="chart"
v-bind="$attrs"
- :data="chartData"
+ :bars="chartData"
:option="chartOptions"
:x-axis-title="xAxisTitle"
:y-axis-title="yAxisTitle"
@@ -144,7 +146,6 @@ export default {
:group-by="groupBy"
:width="width"
:height="height"
- :series-names="seriesNames"
:legend-layout="legendLayout"
:legend-average-text="legendAverageText"
:legend-current-text="legendCurrentText"
diff --git a/app/assets/javascripts/pages/projects/graphs/charts/index.js b/app/assets/javascripts/pages/projects/graphs/charts/index.js
index 74abd1f67a5..6cf36463bda 100644
--- a/app/assets/javascripts/pages/projects/graphs/charts/index.js
+++ b/app/assets/javascripts/pages/projects/graphs/charts/index.js
@@ -5,6 +5,8 @@ import { __ } from '~/locale';
import CodeCoverage from '../components/code_coverage.vue';
import SeriesDataMixin from './series_data_mixin';
+const seriesDataToBarData = raw => Object.entries(raw).map(([name, data]) => ({ name, data }));
+
document.addEventListener('DOMContentLoaded', () => {
waitForCSSLoaded(() => {
const languagesContainer = document.getElementById('js-languages-chart');
@@ -41,13 +43,13 @@ document.addEventListener('DOMContentLoaded', () => {
},
computed: {
seriesData() {
- return { full: this.chartData.map(d => [d.label, d.value]) };
+ return [{ name: 'full', data: this.chartData.map(d => [d.label, d.value]) }];
},
},
render(h) {
return h(GlColumnChart, {
props: {
- data: this.seriesData,
+ bars: this.seriesData,
xAxisTitle: __('Used programming language'),
yAxisTitle: __('Percentage'),
xAxisType: 'category',
@@ -86,7 +88,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) {
return h(GlColumnChart, {
props: {
- data: this.seriesData,
+ bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Day of month'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
@@ -113,13 +115,13 @@ document.addEventListener('DOMContentLoaded', () => {
acc.push([key, weekDays[key]]);
return acc;
}, []);
- return { full: data };
+ return [{ name: 'full', data }];
},
},
render(h) {
return h(GlColumnChart, {
props: {
- data: this.seriesData,
+ bars: this.seriesData,
xAxisTitle: __('Weekday'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
@@ -143,7 +145,7 @@ document.addEventListener('DOMContentLoaded', () => {
render(h) {
return h(GlColumnChart, {
props: {
- data: this.seriesData,
+ bars: seriesDataToBarData(this.seriesData),
xAxisTitle: __('Hour (UTC)'),
yAxisTitle: __('No. of commits'),
xAxisType: 'category',
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/app.vue b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
index 0777dddfc19..c6e2b2e1140 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/app.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/app.vue
@@ -45,9 +45,12 @@ export default {
},
data() {
return {
- timesChartTransformedData: {
- full: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
- },
+ timesChartTransformedData: [
+ {
+ name: 'full',
+ data: this.mergeLabelsAndValues(this.timesChartData.labels, this.timesChartData.values),
+ },
+ ],
};
},
computed: {
@@ -128,7 +131,7 @@ export default {
<gl-column-chart
:height="$options.chartContainerHeight"
:option="$options.timesChartOptions"
- :data="timesChartTransformedData"
+ :bars="timesChartTransformedData"
:y-axis-title="__('Minutes')"
:x-axis-title="__('Commit')"
x-axis-type="category"
diff --git a/app/assets/javascripts/vue_shared/components/members/constants.js b/app/assets/javascripts/vue_shared/components/members/constants.js
index 6509779053e..5885420a122 100644
--- a/app/assets/javascripts/vue_shared/components/members/constants.js
+++ b/app/assets/javascripts/vue_shared/components/members/constants.js
@@ -51,6 +51,7 @@ export const FIELDS = [
key: 'actions',
thClass: 'col-actions',
tdClass: 'col-actions',
+ showFunction: 'showActionsField',
},
];
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
index 116be16b3cd..723e890ef92 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
+++ b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
@@ -2,6 +2,12 @@
import { mapState } from 'vuex';
import { GlTable, GlBadge } from '@gitlab/ui';
import MembersTableCell from 'ee_else_ce/vue_shared/components/members/table/members_table_cell.vue';
+import {
+ canOverride,
+ canRemove,
+ canResend,
+ canUpdate,
+} from 'ee_else_ce/vue_shared/components/members/utils';
import { FIELDS } from '../constants';
import initUserPopovers from '~/user_popovers';
import MemberAvatar from './member_avatar.vue';
@@ -33,14 +39,40 @@ export default {
),
},
computed: {
- ...mapState(['members', 'tableFields']),
+ ...mapState(['members', 'tableFields', 'currentUserId', 'sourceId']),
filteredFields() {
- return FIELDS.filter(field => this.tableFields.includes(field.key));
+ return FIELDS.filter(field => this.tableFields.includes(field.key) && this.showField(field));
+ },
+ userIsLoggedIn() {
+ return this.currentUserId !== null;
},
},
mounted() {
initUserPopovers(this.$el.querySelectorAll('.js-user-link'));
},
+ methods: {
+ showField(field) {
+ if (!Object.prototype.hasOwnProperty.call(field, 'showFunction')) {
+ return true;
+ }
+
+ return this[field.showFunction]();
+ },
+ showActionsField() {
+ if (!this.userIsLoggedIn) {
+ return false;
+ }
+
+ return this.members.some(member => {
+ return (
+ canRemove(member, this.sourceId) ||
+ canResend(member) ||
+ canUpdate(member, this.currentUserId, this.sourceId) ||
+ canOverride(member)
+ );
+ });
+ },
+ },
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
index 5602978bb6c..11e1aef9803 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
+++ b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
@@ -1,6 +1,7 @@
<script>
import { mapState } from 'vuex';
import { MEMBER_TYPES } from '../constants';
+import { isGroup, isDirectMember, isCurrentUser, canRemove, canResend, canUpdate } from '../utils';
export default {
name: 'MembersTableCell',
@@ -13,7 +14,7 @@ export default {
computed: {
...mapState(['sourceId', 'currentUserId']),
isGroup() {
- return Boolean(this.member.sharedWithGroup);
+ return isGroup(this.member);
},
isInvite() {
return Boolean(this.member.invite);
@@ -33,19 +34,19 @@ export default {
return MEMBER_TYPES.user;
},
isDirectMember() {
- return this.isGroup || this.member.source?.id === this.sourceId;
+ return isDirectMember(this.member, this.sourceId);
},
isCurrentUser() {
- return this.member.user?.id === this.currentUserId;
+ return isCurrentUser(this.member, this.currentUserId);
},
canRemove() {
- return this.isDirectMember && this.member.canRemove;
+ return canRemove(this.member, this.sourceId);
},
canResend() {
- return Boolean(this.member.invite?.canResend);
+ return canResend(this.member);
},
canUpdate() {
- return !this.isCurrentUser && this.isDirectMember && this.member.canUpdate;
+ return canUpdate(this.member, this.currentUserId, this.sourceId);
},
},
render() {
diff --git a/app/assets/javascripts/vue_shared/components/members/utils.js b/app/assets/javascripts/vue_shared/components/members/utils.js
index 782a0b7f96b..4229a62c0a7 100644
--- a/app/assets/javascripts/vue_shared/components/members/utils.js
+++ b/app/assets/javascripts/vue_shared/components/members/utils.js
@@ -17,3 +17,32 @@ export const generateBadges = (member, isCurrentUser) => [
variant: 'info',
},
];
+
+export const isGroup = member => {
+ return Boolean(member.sharedWithGroup);
+};
+
+export const isDirectMember = (member, sourceId) => {
+ return isGroup(member) || member.source?.id === sourceId;
+};
+
+export const isCurrentUser = (member, currentUserId) => {
+ return member.user?.id === currentUserId;
+};
+
+export const canRemove = (member, sourceId) => {
+ return isDirectMember(member, sourceId) && member.canRemove;
+};
+
+export const canResend = member => {
+ return Boolean(member.invite?.canResend);
+};
+
+export const canUpdate = (member, currentUserId, sourceId) => {
+ return (
+ !isCurrentUser(member, currentUserId) && isDirectMember(member, sourceId) && member.canUpdate
+ );
+};
+
+// Defined in `ee/app/assets/javascripts/vue_shared/components/members/utils.js`
+export const canOverride = () => false;
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 35d1f4ceaff..d42e97fb2d8 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -40,7 +40,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:highlight_current_diff_row, @project)
push_frontend_feature_flag(:default_merge_ref_for_diffs, @project)
push_frontend_feature_flag(:core_security_mr_widget, @project, default_enabled: true)
- push_frontend_feature_flag(:remove_resolve_note, @project)
+ push_frontend_feature_flag(:remove_resolve_note, @project, default_enabled: true)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)
@@ -318,7 +318,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
def export_csv
- return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project)
+ return render_404 unless Feature.enabled?(:export_merge_requests_as_csv, project, default_enabled: true)
IssuableExportCsvWorker.perform_async(:merge_request, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 0994bebb1d0..dd50ab1bc7a 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -18,14 +18,13 @@ module Projects
end
def cleanup
- cleanup_params = params.require(:project).permit(:bfg_object_map)
- result = Projects::UpdateService.new(project, current_user, cleanup_params).execute
+ bfg_object_map = params.require(:project).require(:bfg_object_map)
+ result = Projects::CleanupService.enqueue(project, current_user, bfg_object_map)
if result[:status] == :success
- RepositoryCleanupWorker.perform_async(project.id, current_user.id) # rubocop:disable CodeReuse/Worker
flash[:notice] = _('Repository cleanup has started. You will receive an email once the cleanup operation is complete.')
else
- flash[:alert] = _('Failed to upload object map file')
+ flash[:alert] = status.fetch(:message, _('Failed to upload object map file'))
end
redirect_to project_settings_repository_path(project)
diff --git a/app/graphql/mutations/alert_management/http_integration/destroy.rb b/app/graphql/mutations/alert_management/http_integration/destroy.rb
new file mode 100644
index 00000000000..0f478760aab
--- /dev/null
+++ b/app/graphql/mutations/alert_management/http_integration/destroy.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Mutations
+ module AlertManagement
+ module HttpIntegration
+ class Destroy < HttpIntegrationBase
+ graphql_name 'HttpIntegrationDestroy'
+
+ argument :id, Types::GlobalIDType[::AlertManagement::HttpIntegration],
+ required: true,
+ description: "The id of the integration to remove"
+
+ def resolve(id:)
+ integration = authorized_find!(id: id)
+
+ response ::AlertManagement::HttpIntegrations::DestroyService.new(
+ integration,
+ current_user
+ ).execute
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
index 300a797f138..d328eabf244 100644
--- a/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
+++ b/app/graphql/mutations/alert_management/http_integration/http_integration_base.rb
@@ -7,7 +7,7 @@ module Mutations
field :integration,
Types::AlertManagement::HttpIntegrationType,
null: true,
- description: "The updated HTTP integration"
+ description: "The HTTP integration"
authorize :admin_operations
diff --git a/app/graphql/resolvers/users_resolver.rb b/app/graphql/resolvers/users_resolver.rb
index 110a283b42e..b0c1baa742d 100644
--- a/app/graphql/resolvers/users_resolver.rb
+++ b/app/graphql/resolvers/users_resolver.rb
@@ -18,10 +18,14 @@ module Resolvers
required: false,
default_value: 'created_desc'
- def resolve(ids: nil, usernames: nil, sort: nil)
+ argument :search, GraphQL::STRING_TYPE,
+ required: false,
+ description: "Query to search users by name, username, or primary email."
+
+ def resolve(ids: nil, usernames: nil, sort: nil, search: nil)
authorize!
- ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort)).execute
+ ::UsersFinder.new(context[:current_user], finder_params(ids, usernames, sort, search)).execute
end
def ready?(**args)
@@ -42,11 +46,12 @@ module Resolvers
private
- def finder_params(ids, usernames, sort)
+ def finder_params(ids, usernames, sort, search)
params = {}
params[:sort] = sort if sort
params[:username] = usernames if usernames
params[:id] = parse_gids(ids) if ids
+ params[:search] = search if search
params
end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 372aeac055b..c35316fe374 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -118,8 +118,7 @@ module Types
resolver: Resolvers::MergeRequestPipelinesResolver
field :milestone, Types::MilestoneType, null: true,
- description: 'The milestone of the merge request',
- resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Milestone, obj.milestone_id).find }
+ description: 'The milestone of the merge request'
field :assignees, Types::UserType.connection_type, null: true, complexity: 5,
description: 'Assignees of the merge request'
field :author, Types::UserType, null: true,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index f63f8129b20..8a6a19d8e09 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -14,6 +14,7 @@ module Types
mount_mutation Mutations::AlertManagement::HttpIntegration::Create
mount_mutation Mutations::AlertManagement::HttpIntegration::Update
mount_mutation Mutations::AlertManagement::HttpIntegration::ResetToken
+ mount_mutation Mutations::AlertManagement::HttpIntegration::Destroy
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Create
mount_mutation Mutations::AlertManagement::PrometheusIntegration::Update
mount_mutation Mutations::AlertManagement::PrometheusIntegration::ResetToken
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 3f182e3ebca..f951c8e22cf 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -92,11 +92,27 @@ module SearchHelper
end
end
- def search_entries_empty_message(scope, term)
- (s_("SearchResults|We couldn't find any %{scope} matching %{term}") % {
+ def search_entries_empty_message(scope, term, group, project)
+ options = {
scope: search_entries_scope_label(scope, 0),
- term: "<code>#{h(term)}</code>"
- }).html_safe
+ term: "<code>#{h(term)}</code>".html_safe
+ }
+
+ # We check project first because we have 3 possible combinations here:
+ # - group && project
+ # - group
+ # - group: nil, project: nil
+ if project
+ html_escape(_("We couldn't find any %{scope} matching %{term} in project %{project}")) % options.merge(
+ project: link_to(project.full_name, project_path(project), target: '_blank', rel: 'noopener noreferrer').html_safe
+ )
+ elsif group
+ html_escape(_("We couldn't find any %{scope} matching %{term} in group %{group}")) % options.merge(
+ group: link_to(group.full_name, group_path(group), target: '_blank', rel: 'noopener noreferrer').html_safe
+ )
+ else
+ html_escape(_("We couldn't find any %{scope} matching %{term}")) % options
+ end
end
def repository_ref(project)
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 17ef8b41e79..a4b7b140169 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -56,12 +56,9 @@ module Emails
subject: @message.subject)
end
- def prometheus_alert_fired_email(project_id, user_id, alert_attributes)
- @project = ::Project.find(project_id)
- user = ::User.find(user_id)
-
- @alert = AlertManagement::Alert.new(alert_attributes.with_indifferent_access).present
- return unless @alert.parsed_payload.has_required_attributes?
+ def prometheus_alert_fired_email(project, user, alert)
+ @project = project
+ @alert = alert.present
subject_text = "Alert: #{@alert.email_title}"
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
diff --git a/app/services/alert_management/http_integrations/destroy_service.rb b/app/services/alert_management/http_integrations/destroy_service.rb
new file mode 100644
index 00000000000..208b2dc67e9
--- /dev/null
+++ b/app/services/alert_management/http_integrations/destroy_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module AlertManagement
+ module HttpIntegrations
+ class DestroyService
+ # @param integration [AlertManagement::HttpIntegration]
+ # @param current_user [User]
+ def initialize(integration, current_user)
+ @integration = integration
+ @current_user = current_user
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+ return error_multiple_integrations unless Feature.enabled?(:multiple_http_integrations, integration.project)
+
+ if integration.destroy
+ success
+ else
+ error(integration.errors.full_messages.to_sentence)
+ end
+ end
+
+ private
+
+ attr_reader :integration, :current_user
+
+ def allowed?
+ current_user&.can?(:admin_operations, integration)
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def success
+ ServiceResponse.success(payload: { integration: integration })
+ end
+
+ def error_no_permissions
+ error(_('You have insufficient permissions to remove this HTTP integration'))
+ end
+
+ def error_multiple_integrations
+ error(_('Removing integrations is not supported for this project'))
+ end
+ end
+ end
+end
diff --git a/app/services/alert_management/process_prometheus_alert_service.rb b/app/services/alert_management/process_prometheus_alert_service.rb
index 5c7698f724a..28ce5401a6c 100644
--- a/app/services/alert_management/process_prometheus_alert_service.rb
+++ b/app/services/alert_management/process_prometheus_alert_service.rb
@@ -9,6 +9,10 @@ module AlertManagement
return bad_request unless incoming_payload.has_required_attributes?
process_alert_management_alert
+ return bad_request unless alert.persisted?
+
+ process_incident_issues if process_issues?
+ send_alert_email if send_email?
ServiceResponse.success
end
@@ -30,8 +34,6 @@ module AlertManagement
else
create_alert_management_alert
end
-
- process_incident_issues if process_issues?
end
def reset_alert_management_alert_status
@@ -85,12 +87,17 @@ module AlertManagement
end
def process_incident_issues
- return unless alert.persisted?
- return if alert.issue
+ return if alert.issue || alert.resolved?
IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end
+ def send_alert_email
+ notification_service
+ .async
+ .prometheus_alerts_fired(project, [alert])
+ end
+
def logger
@logger ||= Gitlab::AppLogger
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 7853ad11c64..1f3f637b8ed 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -601,7 +601,7 @@ class NotificationService
return if project.emails_disabled?
owners_and_maintainers_without_invites(project).to_a.product(alerts).each do |recipient, alert|
- mailer.prometheus_alert_fired_email(project.id, recipient.user.id, alert).deliver_later
+ mailer.prometheus_alert_fired_email(project, recipient.user, alert).deliver_later
end
end
diff --git a/app/services/projects/alerting/notify_service.rb b/app/services/projects/alerting/notify_service.rb
index f5e60dc3cf0..b04a4962d25 100644
--- a/app/services/projects/alerting/notify_service.rb
+++ b/app/services/projects/alerting/notify_service.rb
@@ -73,7 +73,7 @@ module Projects
end
def process_incident_issues
- return if alert.issue
+ return if alert.issue || alert.resolved?
::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
end
@@ -81,7 +81,7 @@ module Projects
def send_alert_email
notification_service
.async
- .prometheus_alerts_fired(project, [alert.attributes])
+ .prometheus_alerts_fired(project, [alert])
end
def alert
diff --git a/app/services/projects/cleanup_service.rb b/app/services/projects/cleanup_service.rb
index 4ced9feff00..e1bdf789ae8 100644
--- a/app/services/projects/cleanup_service.rb
+++ b/app/services/projects/cleanup_service.rb
@@ -11,6 +11,24 @@ module Projects
include Gitlab::Utils::StrongMemoize
+ class << self
+ def enqueue(project, current_user, bfg_object_map)
+ Projects::UpdateService.new(project, current_user, bfg_object_map: bfg_object_map).execute.tap do |result|
+ next unless result[:status] == :success
+
+ project.set_repository_read_only!
+ RepositoryCleanupWorker.perform_async(project.id, current_user.id)
+ end
+ rescue Project::RepositoryReadOnlyError => err
+ { status: :error, message: (_('Failed to make repository read-only. %{reason}') % { reason: err.message }) }
+ end
+
+ def cleanup_after(project)
+ project.bfg_object_map.remove!
+ project.set_repository_writable!
+ end
+ end
+
# Attempt to clean up the project following the push. Warning: this is
# destructive!
#
@@ -29,7 +47,7 @@ module Projects
# time. Better to feel the pain immediately.
project.repository.expire_all_method_caches
- project.bfg_object_map.remove!
+ self.class.cleanup_after(project)
end
private
diff --git a/app/services/projects/prometheus/alerts/notify_service.rb b/app/services/projects/prometheus/alerts/notify_service.rb
index 1eb501de961..8ad4f59373d 100644
--- a/app/services/projects/prometheus/alerts/notify_service.rb
+++ b/app/services/projects/prometheus/alerts/notify_service.rb
@@ -23,7 +23,6 @@ module Projects
return unauthorized unless valid_alert_manager_token?(token)
process_prometheus_alerts
- send_alert_email if send_email?
ServiceResponse.success
end
@@ -120,14 +119,6 @@ module Projects
ActiveSupport::SecurityUtils.secure_compare(expected, actual)
end
- def send_alert_email
- return unless firings.any?
-
- notification_service
- .async
- .prometheus_alerts_fired(project, alerts_attributes)
- end
-
def process_prometheus_alerts
alerts.each do |alert|
AlertManagement::ProcessPrometheusAlertService
@@ -136,18 +127,6 @@ module Projects
end
end
- def alerts_attributes
- firings.map do |payload|
- alert_params = Gitlab::AlertManagement::Payload.parse(
- project,
- payload,
- monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
- ).alert_params
-
- AlertManagement::Alert.new(alert_params).attributes
- end
- end
-
def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 4a8616beff6..66fd0087c3e 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -50,11 +50,11 @@
= f.text_field :home_page_url, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'home_help_block'
%span.form-text.text-muted#home_help_block We will redirect non-logged in users to this page
.form-group
- = f.label :after_sign_out_path, class: 'label-bold'
+ = f.label :after_sign_out_path, _('After sign-out path'), class: 'label-bold'
= f.text_field :after_sign_out_path, class: 'form-control', placeholder: 'http://company.example.com', :'aria-describedby' => 'after_sign_out_path_help_block'
%span.form-text.text-muted#after_sign_out_path_help_block We will redirect users to this page after they sign out
.form-group
- = f.label :sign_in_text, class: 'label-bold'
+ = f.label :sign_in_text, _('Sign-in text'), class: 'label-bold'
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.form-text.text-muted Markdown enabled
= f.submit 'Save changes', class: "gl-button btn btn-success"
diff --git a/app/views/notify/prometheus_alert_fired_email.html.haml b/app/views/notify/prometheus_alert_fired_email.html.haml
index 75ba66b44f9..cdc97d583df 100644
--- a/app/views/notify/prometheus_alert_fired_email.html.haml
+++ b/app/views/notify/prometheus_alert_fired_email.html.haml
@@ -1,5 +1,9 @@
+- body = @alert.resolved? ? _('An alert has been resolved in %{project_path}.') : _('An alert has been triggered in %{project_path}.')
+
+%p
+ = body % { project_path: @alert.project.full_path }
%p
- = _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path }
+ = link_to(_('View alert details.'), @alert.details_url)
- if description = @alert.description
%p
diff --git a/app/views/notify/prometheus_alert_fired_email.text.erb b/app/views/notify/prometheus_alert_fired_email.text.erb
index 8853f2a317b..b23cd8b6ccc 100644
--- a/app/views/notify/prometheus_alert_fired_email.text.erb
+++ b/app/views/notify/prometheus_alert_fired_email.text.erb
@@ -1,4 +1,7 @@
-<%= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path } %>.
+<% body = @alert.resolved? ? _('An alert has been resolved in %{project_path}.') : _('An alert has been triggered in %{project_path}.') %>
+
+<%= body % { project_path: @alert.project.full_path } %>
+<%= _('View alert details at') %> <%= @alert.details_url %>
<% if description = @alert.description %>
<%= _('Description:') %> <%= description %>
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index fb055c62647..9d367caa390 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -1,4 +1,4 @@
-- if Feature.enabled?(:export_merge_requests_as_csv, @project)
+- if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
.btn-group
= render 'shared/issuable/csv_export/button', issuable_type: 'merge-requests'
@@ -8,5 +8,5 @@
= link_to new_merge_request_path, class: "gl-button btn btn-success", title: "New merge request" do
New merge request
- - if Feature.enabled?(:export_merge_requests_as_csv, @project)
+ - if Feature.enabled?(:export_merge_requests_as_csv, @project, default_enabled: true)
= render 'shared/issuable/csv_export/modal', issuable_type: 'merge_requests'
diff --git a/app/views/search/results/_empty.html.haml b/app/views/search/results/_empty.html.haml
index 3cd1c901f8e..0462c29f5c1 100644
--- a/app/views/search/results/_empty.html.haml
+++ b/app/views/search/results/_empty.html.haml
@@ -1,5 +1,5 @@
-.search_box
+.search_box.gl-my-8
.search_glyph
%h4
= sprite_icon('search', size: 24, css_class: 'gl-vertical-align-text-bottom')
- = search_entries_empty_message(@scope, @search_term)
+ = search_entries_empty_message(@scope, @search_term, @group, @project)
diff --git a/app/workers/repository_cleanup_worker.rb b/app/workers/repository_cleanup_worker.rb
index 33b7223dd95..03c9add6afb 100644
--- a/app/workers/repository_cleanup_worker.rb
+++ b/app/workers/repository_cleanup_worker.rb
@@ -27,8 +27,9 @@ class RepositoryCleanupWorker # rubocop:disable Scalability/IdempotentWorker
project = Project.find(project_id)
user = User.find(user_id)
- # Ensure the file is removed
- project.bfg_object_map.remove!
+ # Ensure the file is removed and the repository is made read-write again
+ Projects::CleanupService.cleanup_after(project)
+
notification_service.repository_cleanup_failure(project, user, error)
end