Commit b41417b8 authored by Brandon Labuschagne's avatar Brandon Labuschagne Committed by Stan Hu

Group dashboard reveals number of projects and subgroups

parent 812a57a4
...@@ -40,24 +40,31 @@ export default { ...@@ -40,24 +40,31 @@ export default {
return this.item.type === ITEM_TYPE.GROUP; return this.item.type === ITEM_TYPE.GROUP;
}, },
}, },
methods: {
displayValue(value) {
return this.isGroup && value !== undefined;
},
},
}; };
</script> </script>
<template> <template>
<div class="stats gl-text-gray-500"> <div class="stats gl-text-gray-500">
<item-stats-value <item-stats-value
v-if="isGroup" v-if="displayValue(item.subgroupCount)"
:title="__('Subgroups')" :title="__('Subgroups')"
:value="item.subgroupCount" :value="item.subgroupCount"
css-class="number-subgroups gl-ml-5" css-class="number-subgroups gl-ml-5"
icon-name="folder-o" icon-name="folder-o"
data-testid="subgroups-count"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="displayValue(item.projectCount)"
:title="__('Projects')" :title="__('Projects')"
:value="item.projectCount" :value="item.projectCount"
css-class="number-projects gl-ml-5" css-class="number-projects gl-ml-5"
icon-name="bookmark" icon-name="bookmark"
data-testid="projects-count"
/> />
<item-stats-value <item-stats-value
v-if="isGroup" v-if="isGroup"
......
...@@ -111,8 +111,11 @@ class GroupPolicy < BasePolicy ...@@ -111,8 +111,11 @@ class GroupPolicy < BasePolicy
enable :read_issue_board enable :read_issue_board
enable :read_group_member enable :read_group_member
enable :read_custom_emoji enable :read_custom_emoji
enable :read_counts
end end
rule { ~public_group & ~has_access }.prevent :read_counts
rule { ~can?(:read_group) }.policy do rule { ~can?(:read_group) }.policy do
prevent :read_design_activity prevent :read_design_activity
end end
......
...@@ -37,9 +37,13 @@ class GroupChildEntity < Grape::Entity ...@@ -37,9 +37,13 @@ class GroupChildEntity < Grape::Entity
if: lambda { |_instance, _options| project? } if: lambda { |_instance, _options| project? }
# Group only attributes # Group only attributes
expose :children_count, :parent_id, :project_count, :subgroup_count, expose :children_count, :parent_id,
unless: lambda { |_instance, _options| project? } unless: lambda { |_instance, _options| project? }
expose :subgroup_count, if: lambda { |group| access_group_counts?(group) }
expose :project_count, if: lambda { |group| access_group_counts?(group) }
expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance| expose :leave_path, unless: lambda { |_instance, _options| project? } do |instance|
leave_group_members_path(instance) leave_group_members_path(instance)
end end
...@@ -52,10 +56,6 @@ class GroupChildEntity < Grape::Entity ...@@ -52,10 +56,6 @@ class GroupChildEntity < Grape::Entity
end end
end end
expose :number_projects_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
number_with_delimiter(instance.project_count)
end
expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance| expose :number_users_with_delimiter, unless: lambda { |_instance, _options| project? } do |instance|
number_with_delimiter(instance.member_count) number_with_delimiter(instance.member_count)
end end
...@@ -66,6 +66,10 @@ class GroupChildEntity < Grape::Entity ...@@ -66,6 +66,10 @@ class GroupChildEntity < Grape::Entity
private private
def access_group_counts?(group)
!project? && can?(request.current_user, :read_counts, group)
end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def membership def membership
return unless request.current_user return unless request.current_user
......
...@@ -40,10 +40,6 @@ class GroupEntity < Grape::Entity ...@@ -40,10 +40,6 @@ class GroupEntity < Grape::Entity
GroupsFinder.new(request.current_user, parent: group).execute.any? GroupsFinder.new(request.current_user, parent: group).execute.any?
end end
expose :number_projects_with_delimiter do |group|
number_with_delimiter(GroupProjectsFinder.new(group: group, current_user: request.current_user).execute.count)
end
expose :number_users_with_delimiter do |group| expose :number_users_with_delimiter do |group|
number_with_delimiter(group.users.count) number_with_delimiter(group.users.count)
end end
......
...@@ -227,8 +227,8 @@ RSpec.describe Groups::ChildrenController do ...@@ -227,8 +227,8 @@ RSpec.describe Groups::ChildrenController do
context 'when rendering hierarchies' do context 'when rendering hierarchies' do
# When loading hierarchies we load the all the ancestors for matched projects # When loading hierarchies we load the all the ancestors for matched projects
# in 1 separate query # in 2 separate queries
let(:extra_queries_for_hierarchies) { 1 } let(:extra_queries_for_hierarchies) { 2 }
def get_filtered_list def get_filtered_list
get :index, params: { group_id: group.to_param, filter: 'filter' }, format: :json get :index, params: { group_id: group.to_param, filter: 'filter' }, format: :json
......
import { shallowMount } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemStats from '~/groups/components/item_stats.vue'; import ItemStats from '~/groups/components/item_stats.vue';
import ItemStatsValue from '~/groups/components/item_stats_value.vue'; import ItemStatsValue from '~/groups/components/item_stats_value.vue';
...@@ -12,7 +12,7 @@ describe('ItemStats', () => { ...@@ -12,7 +12,7 @@ describe('ItemStats', () => {
}; };
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = shallowMount(ItemStats, { wrapper = shallowMountExtended(ItemStats, {
propsData: { ...defaultProps, ...props }, propsData: { ...defaultProps, ...props },
}); });
}; };
...@@ -46,5 +46,31 @@ describe('ItemStats', () => { ...@@ -46,5 +46,31 @@ describe('ItemStats', () => {
expect(findItemStatsValue().props('cssClass')).toBe('project-stars'); expect(findItemStatsValue().props('cssClass')).toBe('project-stars');
expect(wrapper.find('.last-updated').exists()).toBe(true); expect(wrapper.find('.last-updated').exists()).toBe(true);
}); });
describe('group specific rendering', () => {
describe.each`
provided | state | data
${true} | ${'displays'} | ${null}
${false} | ${'does not display'} | ${{ subgroupCount: undefined, projectCount: undefined }}
`('when provided = $provided', ({ provided, state, data }) => {
beforeEach(() => {
const item = {
...mockParentGroupItem,
...data,
type: ITEM_TYPE.GROUP,
};
createComponent({ item });
});
it.each`
entity | testId
${'subgroups'} | ${'subgroups-count'}
${'projects'} | ${'projects-count'}
`(`${state} $entity count`, ({ testId }) => {
expect(wrapper.findByTestId(testId).exists()).toBe(provided);
});
});
});
}); });
}); });
...@@ -11,6 +11,7 @@ RSpec.describe GroupPolicy do ...@@ -11,6 +11,7 @@ RSpec.describe GroupPolicy do
it do it do
expect_allowed(:read_group) expect_allowed(:read_group)
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_disallowed(:upload_file) expect_disallowed(:upload_file)
expect_disallowed(*reporter_permissions) expect_disallowed(*reporter_permissions)
...@@ -30,6 +31,7 @@ RSpec.describe GroupPolicy do ...@@ -30,6 +31,7 @@ RSpec.describe GroupPolicy do
end end
it { expect_disallowed(:read_group) } it { expect_disallowed(:read_group) }
it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) } it { expect_disallowed(*read_group_permissions) }
end end
...@@ -42,6 +44,7 @@ RSpec.describe GroupPolicy do ...@@ -42,6 +44,7 @@ RSpec.describe GroupPolicy do
end end
it { expect_disallowed(:read_group) } it { expect_disallowed(:read_group) }
it { expect_disallowed(:read_counts) }
it { expect_disallowed(*read_group_permissions) } it { expect_disallowed(*read_group_permissions) }
end end
...@@ -245,6 +248,7 @@ RSpec.describe GroupPolicy do ...@@ -245,6 +248,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { nil } let(:current_user) { nil }
it do it do
expect_disallowed(:read_counts)
expect_disallowed(*read_group_permissions) expect_disallowed(*read_group_permissions)
expect_disallowed(*guest_permissions) expect_disallowed(*guest_permissions)
expect_disallowed(*reporter_permissions) expect_disallowed(*reporter_permissions)
...@@ -258,6 +262,7 @@ RSpec.describe GroupPolicy do ...@@ -258,6 +262,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { guest } let(:current_user) { guest }
it do it do
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_disallowed(*reporter_permissions) expect_disallowed(*reporter_permissions)
...@@ -271,6 +276,7 @@ RSpec.describe GroupPolicy do ...@@ -271,6 +276,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { reporter } let(:current_user) { reporter }
it do it do
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions) expect_allowed(*reporter_permissions)
...@@ -284,6 +290,7 @@ RSpec.describe GroupPolicy do ...@@ -284,6 +290,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { developer } let(:current_user) { developer }
it do it do
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions) expect_allowed(*reporter_permissions)
...@@ -297,6 +304,7 @@ RSpec.describe GroupPolicy do ...@@ -297,6 +304,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { maintainer } let(:current_user) { maintainer }
it do it do
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions) expect_allowed(*reporter_permissions)
...@@ -310,6 +318,7 @@ RSpec.describe GroupPolicy do ...@@ -310,6 +318,7 @@ RSpec.describe GroupPolicy do
let(:current_user) { owner } let(:current_user) { owner }
it do it do
expect_allowed(:read_counts)
expect_allowed(*read_group_permissions) expect_allowed(*read_group_permissions)
expect_allowed(*guest_permissions) expect_allowed(*guest_permissions)
expect_allowed(*reporter_permissions) expect_allowed(*reporter_permissions)
......
...@@ -87,7 +87,7 @@ RSpec.describe GroupChildEntity do ...@@ -87,7 +87,7 @@ RSpec.describe GroupChildEntity do
expect(json[:children_count]).to eq(2) expect(json[:children_count]).to eq(2)
end end
%w[children_count leave_path parent_id number_projects_with_delimiter number_users_with_delimiter project_count subgroup_count].each do |attribute| %w[children_count leave_path parent_id number_users_with_delimiter project_count subgroup_count].each do |attribute|
it "includes #{attribute}" do it "includes #{attribute}" do
expect(json[attribute.to_sym]).to be_present expect(json[attribute.to_sym]).to be_present
end end
...@@ -114,6 +114,40 @@ RSpec.describe GroupChildEntity do ...@@ -114,6 +114,40 @@ RSpec.describe GroupChildEntity do
it_behaves_like 'group child json' it_behaves_like 'group child json'
end end
describe 'for a private group' do
let(:object) do
create(:group, :private)
end
describe 'user is member of the group' do
before do
object.add_owner(user)
end
it 'includes the counts' do
expect(json.keys).to include(*%i(project_count subgroup_count))
end
end
describe 'user is not a member of the group' do
it 'does not include the counts' do
expect(json.keys).not_to include(*%i(project_count subgroup_count))
end
end
describe 'user is only a member of a project in the group' do
let(:project) { create(:project, namespace: object) }
before do
project.add_guest(user)
end
it 'does not include the counts' do
expect(json.keys).not_to include(*%i(project_count subgroup_count))
end
end
end
describe 'for a project with external authorization enabled' do describe 'for a project with external authorization enabled' do
let(:object) do let(:object) do
create(:project, :with_avatar, create(:project, :with_avatar,
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment