diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-03 18:09:22 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-11-03 18:09:22 +0000 |
commit | 62baa95f25f1cc56b100d2b64b0a3906f47dcfe1 (patch) | |
tree | 0bee30bc13c3cb7444f1d89d2647719718a31d76 /app | |
parent | ff8eb438401fc82b883fc4ae69626f0035b69236 (diff) | |
download | gitlab-ce-62baa95f25f1cc56b100d2b64b0a3906f47dcfe1.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |