Commit 729e3765 authored by 🤖 GitLab Bot 🤖's avatar 🤖 GitLab Bot 🤖

Add latest changes from gitlab-org/[email protected]

parent 6f7881ee
Pipeline #129202891 passed with stages
in 99 minutes and 24 seconds
......@@ -202,7 +202,6 @@ GitlabSecurity/PublicSend:
Gitlab/DuplicateSpecLocation:
Exclude:
- ee/spec/controllers/groups_controller_spec.rb
- ee/spec/controllers/projects/jobs_controller_spec.rb
- ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb
......@@ -215,7 +214,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/services/merge_requests/refresh_service_spec.rb
- ee/spec/services/merge_requests/update_service_spec.rb
- ee/spec/services/system_hooks_service_spec.rb
- ee/spec/controllers/ee/groups_controller_spec.rb
- ee/spec/controllers/ee/projects/jobs_controller_spec.rb
- ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/lib/ee/gitlab/gl_repository_spec.rb
......
<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>
......@@ -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');
});
};
......
......@@ -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';
......
......@@ -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);
......
<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>
......@@ -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 || [];
......
......@@ -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,
......
......@@ -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;
}
......
......@@ -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;
......
# 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
......@@ -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
......
......@@ -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))
......
......@@ -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?
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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
......
......@@ -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?
......
......@@ -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
......
......@@ -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
......
# 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)
......
......@@ -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
##