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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 15:09:42 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-03-24 15:09:42 +0300
commit729e3765d5feb762df1ccfbc228a8dd4662aa3f9 (patch)
treef326420fc64999c6bcc28816ed54f0972fb46459 /app
parent6f7881ee9dcec34141a8f34fc814b56b366d2b48 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue37
-rw-r--r--app/assets/javascripts/notes/stores/actions.js5
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js9
-rw-r--r--app/assets/javascripts/releases/components/evidence_block.vue102
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss17
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss20
-rw-r--r--app/controllers/projects/releases/evidences_controller.rb38
-rw-r--r--app/controllers/projects/releases_controller.rb14
-rw-r--r--app/finders/events_finder.rb9
-rw-r--r--app/graphql/resolvers/issues_resolver.rb11
-rw-r--r--app/graphql/types/group_type.rb6
-rw-r--r--app/helpers/nav_helper.rb4
-rw-r--r--app/models/event.rb9
-rw-r--r--app/models/event_collection.rb9
-rw-r--r--app/models/issue.rb2
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/release.rb20
-rw-r--r--app/models/releases/evidence.rb (renamed from app/models/evidence.rb)6
-rw-r--r--app/policies/release_policy.rb27
-rw-r--r--app/policies/releases/evidence_policy.rb34
-rw-r--r--app/presenters/release_presenter.rb5
-rw-r--r--app/presenters/releases/evidence_presenter.rb16
-rw-r--r--app/services/event_create_service.rb15
-rw-r--r--app/services/wiki_pages/base_service.rb52
-rw-r--r--app/services/wiki_pages/create_service.rb14
-rw-r--r--app/services/wiki_pages/destroy_service.rb14
-rw-r--r--app/services/wiki_pages/update_service.rb21
-rw-r--r--app/views/layouts/header/_default.html.haml2
-rw-r--r--app/workers/create_evidence_worker.rb2
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