diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 12:09:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-03-24 12:09:42 +0000 |
commit | 729e3765d5feb762df1ccfbc228a8dd4662aa3f9 (patch) | |
tree | f326420fc64999c6bcc28816ed54f0972fb46459 /app | |
parent | 6f7881ee9dcec34141a8f34fc814b56b366d2b48 (diff) | |
download | gitlab-ce-729e3765d5feb762df1ccfbc228a8dd4662aa3f9.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
32 files changed, 403 insertions, 130 deletions
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue index 577612de06a..c28ac94b3ed 100644 --- a/app/assets/javascripts/notes/components/discussion_counter.vue +++ b/app/assets/javascripts/notes/components/discussion_counter.vue @@ -1,5 +1,5 @@ <script> -import { mapGetters } from 'vuex'; +import { mapGetters, mapActions } from 'vuex'; import { GlTooltipDirective } from '@gitlab/ui'; import Icon from '~/vue_shared/components/icon.vue'; import discussionNavigation from '../mixins/discussion_navigation'; @@ -18,13 +18,11 @@ export default { 'getNoteableData', 'resolvableDiscussionsCount', 'unresolvedDiscussionsCount', + 'discussions', ]), isLoggedIn() { return this.getUserData.id; }, - hasNextButton() { - return this.isLoggedIn && !this.allResolved; - }, allResolved() { return this.unresolvedDiscussionsCount === 0; }, @@ -34,6 +32,21 @@ export default { resolvedDiscussionsCount() { return this.resolvableDiscussionsCount - this.unresolvedDiscussionsCount; }, + toggeableDiscussions() { + return this.discussions.filter(discussion => !discussion.individual_note); + }, + allExpanded() { + return this.toggeableDiscussions.every(discussion => discussion.expanded); + }, + }, + methods: { + ...mapActions(['setExpandDiscussions']), + handleExpandDiscussions() { + this.setExpandDiscussions({ + discussionIds: this.toggeableDiscussions.map(discussion => discussion.id), + expanded: !this.allExpanded, + }); + }, }, }; </script> @@ -44,8 +57,8 @@ export default { ref="discussionCounter" class="line-resolve-all-container full-width-mobile" > - <div class="full-width-mobile d-flex d-sm-block"> - <div :class="{ 'has-next-btn': hasNextButton }" class="line-resolve-all"> + <div class="full-width-mobile d-flex d-sm-flex"> + <div class="line-resolve-all"> <span :class="{ 'is-active': allResolved }" class="line-resolve-btn is-disabled" @@ -75,7 +88,7 @@ export default { <div v-if="isLoggedIn && !allResolved" class="btn-group btn-group-sm" role="group"> <button v-gl-tooltip - title="Jump to next unresolved thread" + :title="__('Jump to next unresolved thread')" class="btn btn-default discussion-next-btn" data-track-event="click_button" data-track-label="mr_next_unresolved_thread" @@ -85,6 +98,16 @@ export default { <icon name="comment-next" /> </button> </div> + <div v-if="isLoggedIn" class="btn-group btn-group-sm" role="group"> + <button + v-gl-tooltip + :title="__('Toggle all threads')" + class="btn btn-default toggle-all-discussions-btn" + @click="handleExpandDiscussions" + > + <icon :name="allExpanded ? 'angle-up' : 'angle-down'" /> + </button> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js index 2e6719bb4fb..accc37121d0 100644 --- a/app/assets/javascripts/notes/stores/actions.js +++ b/app/assets/javascripts/notes/stores/actions.js @@ -46,6 +46,10 @@ export const setNotesFetchedState = ({ commit }, state) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data); +export const setExpandDiscussions = ({ commit }, { discussionIds, expanded }) => { + commit(types.SET_EXPAND_DISCUSSIONS, { discussionIds, expanded }); +}; + export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFilter }) => { const config = filter !== undefined @@ -54,6 +58,7 @@ export const fetchDiscussions = ({ commit, dispatch }, { path, filter, persistFi return axios.get(path, config).then(({ data }) => { commit(types.SET_INITIAL_DISCUSSIONS, data); + dispatch('updateResolvableDiscussionsCounts'); }); }; diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js index 6554aee0d5b..0cc59f9150c 100644 --- a/app/assets/javascripts/notes/stores/mutation_types.js +++ b/app/assets/javascripts/notes/stores/mutation_types.js @@ -24,6 +24,7 @@ export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION'; export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION'; export const EXPAND_DISCUSSION = 'EXPAND_DISCUSSION'; export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION'; +export const SET_EXPAND_DISCUSSIONS = 'SET_EXPAND_DISCUSSIONS'; export const UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS = 'UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS'; export const SET_CURRENT_DISCUSSION_ID = 'SET_CURRENT_DISCUSSION_ID'; diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js index c23ef93c056..68bf8394508 100644 --- a/app/assets/javascripts/notes/stores/mutations.js +++ b/app/assets/javascripts/notes/stores/mutations.js @@ -190,6 +190,15 @@ export default { }); }, + [types.SET_EXPAND_DISCUSSIONS](state, { discussionIds, expanded }) { + if (discussionIds?.length) { + discussionIds.forEach(discussionId => { + const discussion = utils.findNoteObjectById(state.discussions, discussionId); + Object.assign(discussion, { expanded }); + }); + } + }, + [types.UPDATE_NOTE](state, note) { const noteObj = utils.findNoteObjectById(state.discussions, note.discussion_id); diff --git a/app/assets/javascripts/releases/components/evidence_block.vue b/app/assets/javascripts/releases/components/evidence_block.vue index 0c51fffc96c..59c1b3eb48e 100644 --- a/app/assets/javascripts/releases/components/evidence_block.vue +++ b/app/assets/javascripts/releases/components/evidence_block.vue @@ -1,8 +1,9 @@ <script> -import { GlLink, GlTooltipDirective } from '@gitlab/ui'; +import dateFormat from 'dateformat'; +import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui'; import { __, sprintf } from '~/locale'; import { truncateSha } from '~/lib/utils/text_utility'; -import Icon from '~/vue_shared/components/icon.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue'; @@ -12,7 +13,7 @@ export default { ClipboardButton, ExpandButton, GlLink, - Icon, + GlIcon, }, directives: { GlTooltip: GlTooltipDirective, @@ -24,17 +25,33 @@ export default { }, }, computed: { - evidenceTitle() { - return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName }); + evidences() { + return this.release.evidences; }, - evidenceUrl() { - return this.release.assets && this.release.assets.evidenceFilePath; + }, + methods: { + evidenceTitle(index) { + const [tag, evidence, filename] = this.release.evidences[index].filepath.split('/').slice(-3); + return sprintf(__('%{tag}-%{evidence}-%{filename}'), { tag, evidence, filename }); + }, + evidenceUrl(index) { + return this.release.evidences[index].filepath; + }, + sha(index) { + return this.release.evidences[index].sha; }, - shortSha() { - return truncateSha(this.sha); + shortSha(index) { + return truncateSha(this.release.evidences[index].sha); }, - sha() { - return this.release.evidenceSha; + collectedAt(index) { + return dateFormat(this.release.evidences[index].collectedAt, 'mmmm dS, yyyy, h:MM TT'); + }, + timeSummary(index) { + const { format } = getTimeago(); + const summary = sprintf(__(' Collected %{time}'), { + time: format(this.release.evidences[index].collectedAt), + }); + return summary; }, }, }; @@ -43,34 +60,45 @@ export default { <template> <div> <div class="card-text prepend-top-default"> - <b> - {{ __('Evidence collection') }} - </b> + <b>{{ __('Evidence collection') }}</b> </div> - <div class="d-flex align-items-baseline"> - <gl-link - v-gl-tooltip - class="monospace" - :title="__('Download evidence JSON')" - :download="evidenceTitle" - :href="evidenceUrl" - > - <icon name="review-list" class="align-top append-right-4" /><span>{{ evidenceTitle }}</span> - </gl-link> + <div v-for="(evidence, index) in evidences" :key="evidenceTitle(index)" class="mb-2"> + <div class="d-flex align-items-center"> + <gl-link + v-gl-tooltip + class="d-flex align-items-center monospace" + :title="__('Download evidence JSON')" + :download="evidenceTitle(index)" + :href="evidenceUrl(index)" + > + <gl-icon name="review-list" class="align-middle append-right-8" /> + <span>{{ evidenceTitle(index) }}</span> + </gl-link> + + <expand-button> + <template slot="short"> + <span class="js-short monospace">{{ shortSha(index) }}</span> + </template> + <template slot="expanded"> + <span class="js-expanded monospace gl-pl-1">{{ sha(index) }}</span> + </template> + </expand-button> + <clipboard-button + :title="__('Copy evidence SHA')" + :text="sha(index)" + css-class="btn-default btn-transparent btn-clipboard" + /> + </div> - <expand-button> - <template slot="short"> - <span class="js-short monospace">{{ shortSha }}</span> - </template> - <template slot="expanded"> - <span class="js-expanded monospace gl-pl-1">{{ sha }}</span> - </template> - </expand-button> - <clipboard-button - :title="__('Copy evidence SHA')" - :text="sha" - css-class="btn-default btn-transparent btn-clipboard" - /> + <div class="d-flex align-items-center text-muted"> + <gl-icon + v-gl-tooltip + name="clock" + class="align-middle append-right-8" + :title="collectedAt(index)" + /> + <span>{{ timeSummary(index) }}</span> + </div> </div> </div> </template> diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue index 61cd22dc161..515aa629476 100644 --- a/app/assets/javascripts/releases/components/release_block.vue +++ b/app/assets/javascripts/releases/components/release_block.vue @@ -44,7 +44,7 @@ export default { return this.release.assets || {}; }, hasEvidence() { - return Boolean(this.release.evidenceSha); + return Boolean(this.release.evidences && this.release.evidences.length); }, milestones() { return this.release.milestones || []; diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss index 418eafa153c..d0c3de59937 100644 --- a/app/assets/stylesheets/framework/gitlab_theme.scss +++ b/app/assets/stylesheets/framework/gitlab_theme.scss @@ -68,6 +68,23 @@ .header-user-avatar { border-color: $search-and-nav-links; } + + .header-user-notification-dot { + border: 2px solid $nav-svg-color; + } + } + + &:focus:hover, + &:focus { + &.header-user-dropdown-toggle .header-user-notification-dot { + border-color: $white-light; + } + } + + &:hover { + &.header-user-dropdown-toggle .header-user-notification-dot { + border-color: $nav-svg-color + 33; + } } &:hover, diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 5ae4f72de56..dd338a7134b 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -567,6 +567,14 @@ border: 1px solid $gray-normal; } +.header-user-notification-dot { + background-color: $orange-500; + height: 10px; + width: 10px; + right: 8px; + top: -8px; +} + .with-performance-bar .navbar-gitlab { top: $performance-bar-height; } diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index aaecbd6ff00..f2b8433a995 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -842,11 +842,11 @@ $note-form-margin-left: 72px; white-space: nowrap; } - .btn-group { - margin-left: -4px; + .discussion-next-btn { + border-radius: 0; } - .discussion-next-btn { + .toggle-all-discussions-btn { border-top-left-radius: 0; border-bottom-left-radius: 0; } @@ -859,7 +859,6 @@ $note-form-margin-left: 72px; } &.discussion-create-issue-btn { - margin-left: -4px; border-radius: 0; border-right: 0; @@ -873,6 +872,10 @@ $note-form-margin-left: 72px; } } } + + &.discussion-next-btn { + border-right: 0; + } } } @@ -884,12 +887,9 @@ $note-form-margin-left: 72px; border: 1px solid $border-color; border-radius: $border-radius-default; font-size: $gl-btn-small-font-size; - - &.has-next-btn { - border-top-right-radius: 0; - border-bottom-right-radius: 0; - border-right: 0; - } + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: 0; .line-resolve-btn { margin-right: 5px; diff --git a/app/controllers/projects/releases/evidences_controller.rb b/app/controllers/projects/releases/evidences_controller.rb new file mode 100644 index 00000000000..34e450d903f --- /dev/null +++ b/app/controllers/projects/releases/evidences_controller.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module Projects + module Releases + class EvidencesController < Projects::ApplicationController + before_action :require_non_empty_project + before_action :release + before_action :authorize_read_release_evidence! + + def show + respond_to do |format| + format.json do + render json: evidence.summary + end + end + end + + private + + def authorize_read_release_evidence! + access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true) + access_denied! unless can?(current_user, :read_release_evidence, evidence) + end + + def release + @release ||= project.releases.find_by_tag!(sanitized_tag_name) + end + + def evidence + release.evidences.find(params[:id]) + end + + def sanitized_tag_name + CGI.unescape(params[:tag]) + end + end + end +end diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 7d6b38dd243..fc60f42095c 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -11,7 +11,6 @@ class Projects::ReleasesController < Projects::ApplicationController push_frontend_feature_flag(:release_show_page, project, default_enabled: true) end before_action :authorize_update_release!, only: %i[edit update] - before_action :authorize_read_release_evidence!, only: [:evidence] def index respond_to do |format| @@ -22,14 +21,6 @@ class Projects::ReleasesController < Projects::ApplicationController end end - def evidence - respond_to do |format| - format.json do - render json: release.evidence_summary - end - end - end - def show return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true) @@ -64,11 +55,6 @@ class Projects::ReleasesController < Projects::ApplicationController access_denied! unless can?(current_user, :update_release, release) end - def authorize_read_release_evidence! - access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true) - access_denied! unless can?(current_user, :read_release_evidence, release) - end - def release @release ||= project.releases.find_by_tag!(sanitized_tag_name) end diff --git a/app/finders/events_finder.rb b/app/finders/events_finder.rb index 7755cbdf9e5..9c56451fd44 100644 --- a/app/finders/events_finder.rb +++ b/app/finders/events_finder.rb @@ -52,10 +52,17 @@ class EventsFinder if current_user && scope == 'all' EventCollection.new(current_user.authorized_projects).all_project_events else - source.events + # EventCollection is responsible for applying the feature flag + apply_feature_flags(source.events) end end + def apply_feature_flags(events) + return events if ::Feature.enabled?(:wiki_events) + + events.not_wiki_page + end + # rubocop: disable CodeReuse/ActiveRecord def by_current_user_access(events) events.merge(Project.public_or_visible_to_user(current_user)) diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb index ae77af32b5b..04da54a6bb6 100644 --- a/app/graphql/resolvers/issues_resolver.rb +++ b/app/graphql/resolvers/issues_resolver.rb @@ -56,12 +56,17 @@ module Resolvers # The project could have been loaded in batch by `BatchLoader`. # At this point we need the `id` of the project to query for issues, so # make sure it's loaded and not `nil` before continuing. - project = object.respond_to?(:sync) ? object.sync : object - return Issue.none if project.nil? + parent = object.respond_to?(:sync) ? object.sync : object + return Issue.none if parent.nil? + + if parent.is_a?(Group) + args[:group_id] = parent.id + else + args[:project_id] = parent.id + end # Will need to be be made group & namespace aware with # https://gitlab.com/gitlab-org/gitlab-foss/issues/54520 - args[:project_id] = project.id args[:iids] ||= [args[:iid]].compact args[:attempt_project_search_optimizations] = args[:search].present? diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb index bd9efef94f8..20b4c66ba95 100644 --- a/app/graphql/types/group_type.rb +++ b/app/graphql/types/group_type.rb @@ -43,6 +43,12 @@ module Types description: 'Parent group', resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, obj.parent_id).find } + field :issues, + Types::IssueType.connection_type, + null: true, + description: 'Issues of the group', + resolver: Resolvers::IssuesResolver + field :milestones, Types::MilestoneType.connection_type, null: true, description: 'Find milestones', resolver: Resolvers::MilestoneResolver diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb index 6013475acb1..7d48efcff01 100644 --- a/app/helpers/nav_helper.rb +++ b/app/helpers/nav_helper.rb @@ -65,6 +65,10 @@ module NavHelper %w(groups#issues labels#index milestones#index boards#index boards#show) end + def show_user_notification_dot? + experiment_enabled?(:ci_notification_dot) + end + private def get_header_links diff --git a/app/models/event.rb b/app/models/event.rb index c4ca5389fdf..447ab753421 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -36,6 +36,8 @@ class Event < ApplicationRecord expired: EXPIRED ).freeze + WIKI_ACTIONS = [CREATED, UPDATED, DESTROYED].freeze + TARGET_TYPES = HashWithIndifferentAccess.new( issue: Issue, milestone: Milestone, @@ -81,7 +83,10 @@ class Event < ApplicationRecord scope :recent, -> { reorder(id: :desc) } scope :code_push, -> { where(action: PUSHED) } scope :merged, -> { where(action: MERGED) } - scope :for_wiki_page, -> { where(target_type: WikiPage::Meta.name) } + scope :for_wiki_page, -> { where(target_type: 'WikiPage::Meta') } + + # Needed to implement feature flag: can be removed when feature flag is removed + scope :not_wiki_page, -> { where('target_type IS NULL or target_type <> ?', 'WikiPage::Meta') } scope :with_associations, -> do # We're using preload for "push_event_payload" as otherwise the association @@ -229,7 +234,7 @@ class Event < ApplicationRecord end def wiki_page? - target_type == WikiPage::Meta.name + target_type == 'WikiPage::Meta' end def milestone diff --git a/app/models/event_collection.rb b/app/models/event_collection.rb index 4768506b8fa..4c178e27b75 100644 --- a/app/models/event_collection.rb +++ b/app/models/event_collection.rb @@ -33,16 +33,23 @@ class EventCollection project_events end + relation = apply_feature_flags(relation) relation = paginate_events(relation) relation.with_associations.to_a end def all_project_events - Event.from_union([project_events]).recent + apply_feature_flags(Event.from_union([project_events]).recent) end private + def apply_feature_flags(events) + return events if ::Feature.enabled?(:wiki_events) + + events.not_wiki_page + end + def project_events relation_with_join_lateral('project_id', projects) end diff --git a/app/models/issue.rb b/app/models/issue.rb index 3d389013985..bdcebb4b942 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -78,8 +78,6 @@ class Issue < ApplicationRecord scope :counts_by_state, -> { reorder(nil).group(:state_id).count } - ignore_column :state, remove_with: '12.10', remove_after: '2020-03-22' - after_commit :expire_etag_cache, unless: :importing? after_save :ensure_metrics, unless: :importing? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index bb7afc49cd8..7934b0f8f59 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -261,8 +261,6 @@ class MergeRequest < ApplicationRecord includes(:metrics) end - ignore_column :state, remove_with: '12.10', remove_after: '2020-03-22' - after_save :keep_around_commit, unless: :importing? alias_attribute :project, :target_project diff --git a/app/models/release.rb b/app/models/release.rb index 45c2a56d764..403087a2cad 100644 --- a/app/models/release.rb +++ b/app/models/release.rb @@ -16,7 +16,7 @@ class Release < ApplicationRecord has_many :milestone_releases has_many :milestones, through: :milestone_releases - has_one :evidence + has_many :evidences, inverse_of: :release, class_name: 'Releases::Evidence' default_value_for :released_at, allows_nil: false do Time.zone.now @@ -28,7 +28,7 @@ class Release < ApplicationRecord validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } scope :sorted, -> { order(released_at: :desc) } - scope :preloaded, -> { includes(project: :namespace) } + scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) } scope :with_project_and_namespace, -> { includes(project: :namespace) } scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) } @@ -66,27 +66,27 @@ class Release < ApplicationRecord end def upcoming_release? - released_at.present? && released_at > Time.zone.now + released_at.present? && released_at.to_i > Time.zone.now.to_i end def historical_release? - released_at.present? && released_at < created_at + released_at.present? && released_at.to_i < created_at.to_i end def name self.read_attribute(:name) || tag end - def evidence_sha - evidence&.summary_sha + def milestone_titles + self.milestones.map {|m| m.title }.sort.join(", ") end - def evidence_summary - evidence&.summary || {} + def evidence_sha + evidences.first&.summary_sha end - def milestone_titles - self.milestones.map {|m| m.title }.sort.join(", ") + def evidence_summary + evidences.first&.summary || {} end private diff --git a/app/models/evidence.rb b/app/models/releases/evidence.rb index 55149ab0dfa..1aac7e33e41 100644 --- a/app/models/evidence.rb +++ b/app/models/releases/evidence.rb @@ -1,15 +1,17 @@ # frozen_string_literal: true -class Evidence < ApplicationRecord +class Releases::Evidence < ApplicationRecord include ShaAttribute + include Presentable - belongs_to :release + belongs_to :release, inverse_of: :evidences before_validation :generate_summary_and_sha default_scope { order(created_at: :asc) } sha_attribute :summary_sha + alias_attribute :collected_at, :created_at def milestones @milestones ||= release.milestones.includes(:issues) diff --git a/app/policies/release_policy.rb b/app/policies/release_policy.rb index 0fd1312c511..d7f9e5d7445 100644 --- a/app/policies/release_policy.rb +++ b/app/policies/release_policy.rb @@ -2,31 +2,4 @@ class ReleasePolicy < BasePolicy delegate { @subject.project } - - rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do - enable :read_release_evidence - end - - ## - # evidence.summary includes the following entities: - # - Release - # - git-tag (Repository) - # - Project - # - Milestones - # - Issues - condition(:allowed_to_read_evidence) do - can?(:read_release) && - can?(:download_code) && - can?(:read_project) && - can?(:read_milestone) && - can?(:read_issue) - end - - ## - # Currently, we don't support release evidence for the GitLab instances - # that enables external authorization services. - # See https://gitlab.com/gitlab-org/gitlab/issues/121930. - condition(:external_authorization_service_disabled) do - !Gitlab::ExternalAuthorization::Config.enabled? - end end diff --git a/app/policies/releases/evidence_policy.rb b/app/policies/releases/evidence_policy.rb new file mode 100644 index 00000000000..701913e6fe4 --- /dev/null +++ b/app/policies/releases/evidence_policy.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Releases + class EvidencePolicy < BasePolicy + delegate { @subject.release.project } + + rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do + enable :read_release_evidence + end + + ## + # evidence.summary includes the following entities: + # - Release + # - git-tag (Repository) + # - Project + # - Milestones + # - Issues + condition(:allowed_to_read_evidence) do + can?(:read_release) && + can?(:download_code) && + can?(:read_project) && + can?(:read_milestone) && + can?(:read_issue) + end + + ## + # Currently, we don't support release evidence for the GitLab instances + # that enables external authorization services. + # See https://gitlab.com/gitlab-org/gitlab/issues/121930. + condition(:external_authorization_service_disabled) do + !Gitlab::ExternalAuthorization::Config.enabled? + end + end +end diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index 8cf7446ce64..3db89df1cc8 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -44,9 +44,10 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated end def evidence_file_path - return unless release.evidence.present? + evidence = release.evidences.first + return unless evidence - evidence_project_release_url(project, release.to_param, format: :json) + project_evidence_url(project, release, evidence, format: :json) end private diff --git a/app/presenters/releases/evidence_presenter.rb b/app/presenters/releases/evidence_presenter.rb new file mode 100644 index 00000000000..a00cbacb7d8 --- /dev/null +++ b/app/presenters/releases/evidence_presenter.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Releases + class EvidencePresenter < Gitlab::View::Presenter::Delegated + include ActionView::Helpers::UrlHelper + + presents :evidence + + def filepath + release = evidence.release + project = release.project + + project_evidence_url(project, release, evidence, format: :json) + end + end +end diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb index 7460f0df535..0b044e1679a 100644 --- a/app/services/event_create_service.rb +++ b/app/services/event_create_service.rb @@ -8,6 +8,8 @@ # EventCreateService.new.new_issue(issue, current_user) # class EventCreateService + IllegalActionError = Class.new(StandardError) + def open_issue(issue, current_user) create_record_event(issue, current_user, Event::CREATED) end @@ -80,6 +82,19 @@ class EventCreateService create_push_event(BulkPushEventPayloadService, project, current_user, push_data) end + # Create a new wiki page event + # + # @param [WikiPage::Meta] wiki_page_meta The event target + # @param [User] current_user The event author + # @param [Integer] action One of the Event::WIKI_ACTIONS + def wiki_event(wiki_page_meta, current_user, action) + return unless Feature.enabled?(:wiki_events) + + raise IllegalActionError, action unless Event::WIKI_ACTIONS.include?(action) + + create_record_event(wiki_page_meta, current_user, action) + end + private def create_record_event(record, current_user, status) diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb index 82c15ffc9b9..2e774973ca5 100644 --- a/app/services/wiki_pages/base_service.rb +++ b/app/services/wiki_pages/base_service.rb @@ -1,19 +1,61 @@ # frozen_string_literal: true module WikiPages + # There are 3 notions of 'action' that inheriting classes must implement: + # + # - external_action: the action we report to external clients with webhooks + # - usage_counter_action: the action that we count in out internal counters + # - event_action: what we record as the value of `Event#action` class BaseService < ::BaseService private - def execute_hooks(page, action = 'create') - page_data = Gitlab::DataBuilder::WikiPage.build(page, current_user, action) + def execute_hooks(page) + page_data = payload(page) @project.execute_hooks(page_data, :wiki_page_hooks) @project.execute_services(page_data, :wiki_page_hooks) - increment_usage(action) + increment_usage + create_wiki_event(page) + end + + # Passed to web-hooks, and send to external consumers. + def external_action + raise NotImplementedError + end + + # Passed to the WikiPageCounter to count events. + # Must be one of WikiPageCounter::KNOWN_EVENTS + def usage_counter_action + raise NotImplementedError + end + + # Used to create `Event` records. + # Must be a valid value for `Event#action` + def event_action + raise NotImplementedError + end + + def payload(page) + Gitlab::DataBuilder::WikiPage.build(page, current_user, external_action) end # This method throws an error if the action is an unanticipated value. - def increment_usage(action) - Gitlab::UsageDataCounters::WikiPageCounter.count(action) + def increment_usage + Gitlab::UsageDataCounters::WikiPageCounter.count(usage_counter_action) + end + + def create_wiki_event(page) + return unless ::Feature.enabled?(:wiki_events) + + slug = slug_for_page(page) + + Event.transaction do + wiki_page_meta = WikiPage::Meta.find_or_create(slug, page) + EventCreateService.new.wiki_event(wiki_page_meta, current_user, event_action) + end + end + + def slug_for_page(page) + page.slug end end end diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb index 2e2e0fd9033..811f460e042 100644 --- a/app/services/wiki_pages/create_service.rb +++ b/app/services/wiki_pages/create_service.rb @@ -7,10 +7,22 @@ module WikiPages page = WikiPage.new(project_wiki) if page.create(@params) - execute_hooks(page, 'create') + execute_hooks(page) end page end + + def usage_counter_action + :create + end + + def external_action + 'create' + end + + def event_action + Event::CREATED + end end end diff --git a/app/services/wiki_pages/destroy_service.rb b/app/services/wiki_pages/destroy_service.rb index 3f9343339cd..eb162223723 100644 --- a/app/services/wiki_pages/destroy_service.rb +++ b/app/services/wiki_pages/destroy_service.rb @@ -4,10 +4,22 @@ module WikiPages class DestroyService < WikiPages::BaseService def execute(page) if page&.delete - execute_hooks(page, 'delete') + execute_hooks(page) end page end + + def usage_counter_action + :delete + end + + def external_action + 'delete' + end + + def event_action + Event::DESTROYED + end end end diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb index 2159dd91e9c..0a056f1ec33 100644 --- a/app/services/wiki_pages/update_service.rb +++ b/app/services/wiki_pages/update_service.rb @@ -3,11 +3,30 @@ module WikiPages class UpdateService < WikiPages::BaseService def execute(page) + # this class is not thread safe! + @old_slug = page.slug + if page.update(@params) - execute_hooks(page, 'update') + execute_hooks(page) end page end + + def usage_counter_action + :update + end + + def external_action + 'update' + end + + def event_action + Event::UPDATED + end + + def slug_for_page(page) + @old_slug.presence || super + end end end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index 5719fb24b89..202a4018050 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -68,6 +68,8 @@ %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" + - if show_user_notification_dot? + %span.header-user-notification-dot.rounded-circle.position-relative = sprite_icon('angle-down', css_class: 'caret-down') .dropdown-menu.dropdown-menu-right = render 'layouts/header/current_user_dropdown' diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb index c2faba84cfc..135e2ac38b4 100644 --- a/app/workers/create_evidence_worker.rb +++ b/app/workers/create_evidence_worker.rb @@ -10,6 +10,6 @@ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker release = Release.find_by_id(release_id) return unless release - Evidence.create!(release: release) + Releases::Evidence.create!(release: release) end end |