Skip to content
代码片段 群组 项目
提交 b9471e57 编辑于 作者: Kushal Pandya's avatar Kushal Pandya
浏览文件

Merge branch 'jlouw-compliance-dashboard-as-vue-app' into 'master'

Refactor Compliance Dashboard into Vue app

Closes #213094

See merge request gitlab-org/gitlab!28361
No related branches found
No related tags found
无相关合并请求
显示
473 个添加66 个删除
...@@ -27,3 +27,5 @@ def represent(merge_request, opts = {}, entity = nil) ...@@ -27,3 +27,5 @@ def represent(merge_request, opts = {}, entity = nil)
super(merge_request, opts, entity) super(merge_request, opts, entity)
end end
end end
MergeRequestSerializer.prepend_if_ee('EE::MergeRequestSerializer')
import Vue from 'vue';
import ComplianceDashboard from './components/dashboard.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
const el = document.getElementById('js-compliance-dashboard');
const { mergeRequests, emptyStateSvgPath, isLastPage } = el.dataset;
return new Vue({
el,
render: createElement =>
createElement(ComplianceDashboard, {
props: {
mergeRequests: JSON.parse(mergeRequests),
isLastPage: parseBoolean(isLastPage),
emptyStateSvgPath,
},
}),
});
};
<script>
import { sprintf, __ } from '~/locale';
import { GlAvatarLink, GlAvatar, GlTooltipDirective } from '@gitlab/ui';
import { PRESENTABLE_APPROVERS_LIMIT } from '../constants';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
GlAvatarLink,
GlAvatar,
},
props: {
approvers: {
type: Array,
required: true,
},
},
computed: {
hasApprovers() {
return this.approvers.length > 0;
},
approversToPresent() {
return this.approvers.slice(0, PRESENTABLE_APPROVERS_LIMIT);
},
amountOfApproversOverLimit() {
return this.approvers.length - PRESENTABLE_APPROVERS_LIMIT;
},
isApproversOverLimit() {
return this.amountOfApproversOverLimit > 0;
},
approversOverLimitString() {
return sprintf(__('+%{approvers} more approvers'), {
approvers: this.amountOfApproversOverLimit,
});
},
},
strings: {
approvedBy: __('Approved by: '),
noApprovers: __('No approvers'),
},
};
</script>
<template>
<li class="issuable-status d-flex approvers align-items-center">
<span class="gl-text-gray-700">
<template v-if="hasApprovers">
{{ $options.strings.approvedBy }}
</template>
<template v-else>
{{ $options.strings.noApprovers }}
</template>
</span>
<gl-avatar-link
v-for="approver in approversToPresent"
:key="approver.id"
:title="approver.name"
:href="approver.web_url"
:data-user-id="approver.id"
:data-name="approver.name"
class="d-flex align-items-center ml-2 author-link js-user-link "
>
<gl-avatar
:src="approver.avatar_url"
:entity-id="approver.id"
:entity-name="approver.name"
:size="16"
class="mr-1"
/>
<span>{{ approver.name }}</span>
</gl-avatar-link>
<span
v-if="isApproversOverLimit"
v-gl-tooltip.top="approversOverLimitString"
class="avatar-counter ml-2"
>+ {{ amountOfApproversOverLimit }}</span
>
</li>
</template>
<script>
import { __ } from '~/locale';
import MergeRequest from './merge_request.vue';
import EmptyState from './empty_state.vue';
import Pagination from './pagination.vue';
export default {
name: 'ComplianceDashboard',
components: {
MergeRequest,
EmptyState,
Pagination,
},
props: {
emptyStateSvgPath: {
type: String,
required: true,
},
mergeRequests: {
type: Array,
required: true,
},
isLastPage: {
type: Boolean,
required: true,
},
},
computed: {
hasMergeRequests() {
return this.mergeRequests.length > 0;
},
},
strings: {
heading: __('Compliance Dashboard'),
subheading: __('Here you will find recent merge request activity'),
},
};
</script>
<template>
<div v-if="hasMergeRequests" class="compliance-dashboard">
<header class="my-3">
<h4>{{ $options.strings.heading }}</h4>
<p>{{ $options.strings.subheading }}</p>
</header>
<ul class="content-list issuable-list issues-list">
<merge-request v-for="mr in mergeRequests" :key="mr.id" :merge-request="mr" />
</ul>
<pagination class="my-3" :is-last-page="isLastPage" />
</div>
<empty-state v-else :image-path="emptyStateSvgPath" />
</template>
<script>
import { __ } from '~/locale';
export default {
props: {
imagePath: {
type: String,
required: true,
},
},
strings: {
heading: __(
"Merge requests are a place to propose changes you've made to a project and discuss those changes with others",
),
subheading: __('Interested parties can even contribute by pushing commits if they want to.'),
alt: __('Merge Requests'),
},
};
</script>
<template>
<div class="row empty-state merge-requests">
<div class="col-12">
<div class="svg-content">
<img :src="imagePath" :alt="$options.strings.alt" />
</div>
</div>
<div class="col-12">
<div class="text-content">
<h4>{{ $options.strings.heading }}</h4>
<p>{{ $options.strings.subheading }}</p>
</div>
</div>
</div>
</template>
<script>
import { sprintf, s__ } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import Approvers from './approvers.vue';
export default {
directives: {
GlTooltip: GlTooltipDirective,
},
components: {
CiIcon,
Approvers,
},
mixins: [timeagoMixin],
props: {
mergeRequest: {
type: Object,
required: true,
},
},
computed: {
hasCiPipeline() {
return Boolean(this.mergeRequest.pipeline_status);
},
pipelineCiStatus() {
const details = this.mergeRequest.pipeline_status;
return { ...details, group: details.group || details.label };
},
pipelineTitle() {
const { tooltip } = this.mergeRequest.pipeline_status;
return sprintf(s__('PipelineStatusTooltip|Pipeline: %{ci_status}'), {
ci_status: tooltip,
});
},
timeAgoString() {
return sprintf(s__('merged %{time_ago}'), {
time_ago: this.timeFormatted(this.mergeRequest.merged_at),
});
},
timeTooltip() {
return this.tooltipTitle(this.mergeRequest.merged_at);
},
},
};
</script>
<template>
<li class="merge-request">
<div class="issuable-info-container">
<div class="issuable-main-info">
<div class="title">
<a :href="mergeRequest.path">
{{ mergeRequest.title }}
</a>
</div>
<span class="gl-text-gray-700">
{{ mergeRequest.issuable_reference }}
</span>
</div>
<div class="issuable-meta">
<ul class="controls">
<li v-if="hasCiPipeline" class="mr-2">
<a :href="pipelineCiStatus.details_path">
<ci-icon
v-gl-tooltip.left="pipelineTitle"
class="d-flex"
:status="pipelineCiStatus"
/>
</a>
</li>
<approvers :approvers="mergeRequest.approved_by_users" />
</ul>
<span class="gl-text-gray-700">
<time v-gl-tooltip.bottom="timeTooltip">{{ timeAgoString }}</time>
</span>
</div>
</div>
</li>
</template>
<script>
import { GlPagination } from '@gitlab/ui';
import { getParameterValues, setUrlParams } from '~/lib/utils/url_utility';
export default {
components: {
GlPagination,
},
props: {
isLastPage: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
page: parseInt(getParameterValues('page')[0], 10) || 1,
};
},
computed: {
isOnlyPage() {
return this.isLastPage && this.page === 1;
},
prevPage() {
return this.page > 1 ? this.page - 1 : null;
},
nextPage() {
return !this.isLastPage ? this.page + 1 : null;
},
},
methods: {
generateLink(page) {
return setUrlParams({ page });
},
},
};
</script>
<template>
<gl-pagination
v-if="!isOnlyPage"
v-model="page"
:prev-page="prevPage"
:next-page="nextPage"
:link-gen="generateLink"
align="center"
class="w-100"
/>
</template>
export const PRESENTABLE_APPROVERS_LIMIT = 2;
export default {};
import initComplianceDashboard from 'ee/compliance_dashboard/compliance_dashboard_bundle';
document.addEventListener('DOMContentLoaded', initComplianceDashboard);
...@@ -3,13 +3,4 @@ ...@@ -3,13 +3,4 @@
border-top: 1px solid $border-color; border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color; border-bottom: 1px solid $border-color;
} }
.author-link {
align-items: center;
margin-right: 10px;
&:last-child {
margin-right: 0;
}
}
} }
...@@ -7,13 +7,23 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont ...@@ -7,13 +7,23 @@ class Groups::Security::ComplianceDashboardsController < Groups::ApplicationCont
before_action :authorize_compliance_dashboard! before_action :authorize_compliance_dashboard!
def show def show
@merge_requests = MergeRequestsComplianceFinder.new(current_user, { group_id: @group.id }) @last_page = paginated_merge_requests.last_page?
.execute @merge_requests = serialize(paginated_merge_requests)
@merge_requests = Kaminari.paginate_array(@merge_requests).page(params[:page])
end end
private private
def paginated_merge_requests
@paginated_merge_requests ||= begin
merge_requests = MergeRequestsComplianceFinder.new(current_user, { group_id: @group.id }).execute
Kaminari.paginate_array(merge_requests).page(params[:page])
end
end
def serialize(merge_requests)
MergeRequestSerializer.new(current_user: current_user).represent(merge_requests, serializer: 'compliance_dashboard')
end
def authorize_compliance_dashboard! def authorize_compliance_dashboard!
render_404 unless group_level_compliance_dashboard_available?(group) render_404 unless group_level_compliance_dashboard_available?(group)
end end
......
# frozen_string_literal: true
module EE
module MergeRequestSerializer
extend ::Gitlab::Utils::Override
override :represent
def represent(merge_request, opts = {}, entity = nil)
entity ||=
case opts[:serializer]
when 'compliance_dashboard'
MergeRequestComplianceEntity
end
super(merge_request, opts, entity)
end
end
end
# frozen_string_literal: true
class MergeRequestComplianceEntity < Grape::Entity
include RequestAwareEntity
expose :id
expose :title
expose :merged_at
expose :milestone
expose :path do |merge_request|
merge_request_path(merge_request)
end
expose :issuable_reference do |merge_request|
merge_request.to_reference(merge_request.project.group)
end
expose :approved_by_users, using: API::Entities::UserBasic
expose :pipeline_status, if: -> (*) { can_read_pipeline? }, with: DetailedStatusEntity
private
alias_method :merge_request, :object
def can_read_pipeline?
can?(request.current_user, :read_pipeline, merge_request.head_pipeline)
end
def pipeline_status
merge_request.head_pipeline.detailed_status(request.current_user)
end
end
- presentable_approvers_limit = 2
- approvers_over_presentable_limit = merge_request.approved_by_users.size - presentable_approvers_limit
- project = merge_request.project
%li.issuable-status
%span.gl-text-gray-700
= _('Approved by: ')
- merge_request.approved_by_users.take(presentable_approvers_limit).each do |approver| # rubocop: disable CodeReuse/ActiveRecord
= link_to_member(project, approver, name: true, title: "Approved by :name")
- if approvers_over_presentable_limit.positive?
%span{ class: 'avatar-counter has-tooltip', data: { container: 'body', placement: 'bottom', 'line-type' => 'old', qa_selector: 'avatar_counter' }, title: _("+%{approvers} more approvers") % { approvers: approvers_over_presentable_limit } }
= "+ #{approvers_over_presentable_limit}"
.row.empty-state.merge-requests
.col-12
.svg-content
= image_tag 'illustrations/merge_requests.svg'
.col-12
.text-content
%h4
= _("Merge requests are a place to propose changes you've made to a project and discuss those changes with others")
%p
= _("Interested parties can even contribute by pushing commits if they want to.")
%li.merge-request{ id: dom_id(merge_request), data: { id: merge_request.id } }
.issuable-info-container
.issuable-main-info
.title
= link_to merge_request.title, merge_request_path(merge_request)
%span.gl-text-gray-700
= issuable_reference(merge_request)
.issuable-meta
%ul.controls
= render 'shared/merge_request_pipeline_status', merge_request: merge_request
- if merge_request.approved_by_users.any?
= render 'approvers', project: merge_request.project, merge_request: merge_request
- else
%li.issuable-status
%span.gl-text-gray-700
= _('No approvers')
%span.gl-text-gray-700
= _('merged %{time_ago}').html_safe % { time_ago: time_ago_with_tooltip(merge_request.merged_at, placement: 'bottom', html_class: 'merge_request_updated_ago') }
- if @merge_requests.present?
.compliance-dashboard
%header.my-3
%h4= _("Compliance Dashboard")
%p= _("Here you will find recent merge request activity")
%ul.content-list.issuable-list.issues-list
= render partial: 'merge_request', collection: @merge_requests
= paginate_without_count @merge_requests
- else
= render 'empty_state'
- breadcrumb_title _("Compliance Dashboard") - breadcrumb_title _("Compliance Dashboard")
- page_title _("Compliance Dashboard") - page_title _("Compliance Dashboard")
= render "merge_requests" #js-compliance-dashboard{ data: { merge_requests: @merge_requests.to_json,
is_last_page: @last_page.to_json,
empty_state_svg_path: image_path('illustrations/merge_requests.svg') } }
# frozen_string_literal: true
require 'spec_helper'
describe 'Compliance Dashboard', :js do
let_it_be(:current_user) { create(:user) }
let_it_be(:user) { current_user }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, :public, namespace: group) }
before do
stub_licensed_features(group_level_compliance_dashboard: true)
group.add_owner(user)
sign_in(user)
visit group_security_compliance_dashboard_path(group)
end
context 'when there are no merge requests' do
it 'shows an empty state' do
expect(page).to have_selector('.empty-state')
end
end
context 'when there are merge requests' do
let_it_be(:merge_request) { create(:merge_request, source_project: project, state: :merged) }
before_all do
create(:event, :merged, project: project, target: merge_request, author: user, created_at: 10.minutes.ago)
end
it 'shows merge requests with details' do
expect(page).to have_link(merge_request.title)
expect(page).to have_content('merged 10 minutes ago')
expect(page).to have_content('No approvers')
end
end
end
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`MergeRequest component when there are approvers matches snapshot 1`] = `
<li
class="issuable-status d-flex approvers align-items-center"
>
<span
class="gl-text-gray-700"
>
Approved by:
</span>
<gl-link-stub
class="gl-avatar-link d-flex align-items-center ml-2 author-link js-user-link "
data-name="User 0"
data-user-id="0"
href="http://localhost:3000/user-0"
title="User 0"
>
<gl-avatar-stub
alt="avatar"
class="mr-1"
entityid="0"
entityname="User 0"
shape="circle"
size="16"
src="https://0"
/>
<span>
User 0
</span>
</gl-link-stub>
<!---->
</li>
`;
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册