-
-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Add member count for groups in VaultDetail view #321
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -2,6 +2,7 @@ | |||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import jakarta.annotation.security.RolesAllowed; | ||||||||||||||||||||||||||||||||
import jakarta.inject.Inject; | ||||||||||||||||||||||||||||||||
import jakarta.ws.rs.core.Response; | ||||||||||||||||||||||||||||||||
import jakarta.ws.rs.GET; | ||||||||||||||||||||||||||||||||
import jakarta.ws.rs.Path; | ||||||||||||||||||||||||||||||||
import jakarta.ws.rs.PathParam; | ||||||||||||||||||||||||||||||||
|
@@ -13,6 +14,8 @@ | |||||||||||||||||||||||||||||||
import org.eclipse.microprofile.openapi.annotations.Operation; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
import java.util.List; | ||||||||||||||||||||||||||||||||
import java.util.HashMap; | ||||||||||||||||||||||||||||||||
import java.util.Map; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@Path("/groups") | ||||||||||||||||||||||||||||||||
public class GroupsResource { | ||||||||||||||||||||||||||||||||
|
@@ -28,7 +31,8 @@ public class GroupsResource { | |||||||||||||||||||||||||||||||
@Produces(MediaType.APPLICATION_JSON) | ||||||||||||||||||||||||||||||||
@Operation(summary = "list all groups") | ||||||||||||||||||||||||||||||||
public List<GroupDto> getAll() { | ||||||||||||||||||||||||||||||||
return groupRepo.findAll().stream().map(GroupDto::fromEntity).toList(); | ||||||||||||||||||||||||||||||||
List<Group> groups = groupRepo.findAll().list(); | ||||||||||||||||||||||||||||||||
return groups.stream().map(group -> GroupDto.fromEntity(group, groupRepo.countMembers(group.getId()))).toList(); | ||||||||||||||||||||||||||||||||
Comment on lines
+34
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize group listing to avoid N+1 query. The current implementation makes a separate query for each group's member count. Consider fetching all member counts in a single query. public List<GroupDto> getAll() {
- List<Group> groups = groupRepo.findAll().list();
- return groups.stream().map(group -> GroupDto.fromEntity(group, groupRepo.countMembers(group.getId()))).toList();
+ var groups = groupRepo.findAll().list();
+ var memberCounts = getEntityManager()
+ .createQuery("SELECT g.id, COUNT(m) FROM Group g LEFT JOIN g.members m GROUP BY g.id", Object[].class)
+ .getResultList()
+ .stream()
+ .collect(Collectors.toMap(row -> (String) row[0], row -> ((Long) row[1]).intValue()));
+ return groups.stream()
+ .map(group -> GroupDto.fromEntity(group, memberCounts.getOrDefault(group.getId(), 0)))
+ .toList();
} 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
@GET | ||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -46,7 +46,13 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div class="flex justify-between items-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div class="flex items-center whitespace-nowrap w-full" :title="member.name"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<img :src="member.pictureUrl" alt="" class="w-8 h-8 rounded-full" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<p class="w-full ml-4 text-sm font-medium text-gray-900 truncate">{{ member.name }}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div class="w-full flex justify-between items-center"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<p class="ml-4 text-sm font-medium text-gray-900 truncate">{{ member.name }}</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<span v-if="member.type === 'GROUP' && groupMemberCounts.get(member.id) !== undefined" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
class="text-xs italic text-gray-500 text-right"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{{ groupMemberCounts.get(member.id) }} {{ t('vaultDetails.sharedWith.members') }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</span> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<TrustDetails v-if="member.type === 'USER'" :trusted-user="member" :trusts="trusts" @trust-changed="refreshTrusts()"/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div v-if="member.role == 'OWNER'" class="ml-3 inline-flex items-center rounded-md bg-gray-50 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-500/10">{{ t('vaultDetails.sharedWith.badge.owner') }}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Menu v-if="member.id != me?.id" as="div" class="relative ml-2 inline-block shrink-0 text-left"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -298,10 +304,24 @@ async function fetchData() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function fetchOwnerData() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(await backend.vaults.getMembers(props.vaultId)).forEach(member => members.value.set(member.id, member)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const fetchedMembers = await backend.vaults.getMembers(props.vaultId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (const member of fetchedMembers) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (member.type === "GROUP") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const count = await backend.groups.getMemberCount(member.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
members.value.set(member.id, { ...member, memberCount: count }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
members.value.set(member.id, { ...member, memberCount: 0 }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
members.value.set(member.id, member); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+307
to
+319
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider using Promise.allSettled for parallel member count fetching. The current implementation fetches member counts sequentially, which could be optimized. - const fetchedMembers = await backend.vaults.getMembers(props.vaultId);
- for (const member of fetchedMembers) {
- if (member.type === "GROUP") {
- try {
- const count = await backend.groups.getMemberCount(member.id);
- members.value.set(member.id, { ...member, memberCount: count });
- } catch (error) {
- members.value.set(member.id, { ...member, memberCount: 0 });
- }
- } else {
- members.value.set(member.id, member);
- }
- }
+ const fetchedMembers = await backend.vaults.getMembers(props.vaultId);
+ const memberPromises = fetchedMembers.map(async member => {
+ if (member.type === "GROUP") {
+ const count = await backend.groups.getMemberCount(member.id)
+ .catch(() => 0);
+ return { ...member, memberCount: count };
+ }
+ return member;
+ });
+ const results = await Promise.allSettled(memberPromises);
+ results.forEach((result, index) => {
+ if (result.status === 'fulfilled') {
+ members.value.set(result.value.id, result.value);
+ }
+ }); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
await refreshTrusts(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
usersRequiringAccessGrant.value = await backend.vaults.getUsersRequiringAccessGrant(props.vaultId); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
vaultRecoveryRequired.value = false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const vaultKeyJwe = await backend.vaults.accessToken(props.vaultId, true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
vaultKeys.value = await loadVaultKeys(vaultKeyJwe); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -317,6 +337,16 @@ async function fetchOwnerData() { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const groupMemberCounts = computed(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const counts = new Map<string, number>(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
members.value.forEach((member) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (member.type === 'GROUP' && 'memberCount' in member) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
counts.set(member.id, member.memberCount); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return counts; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function loadVaultKeys(vaultKeyJwe: string): Promise<VaultKeys> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const userKeys = await userdata.decryptUserKeysWithBrowser(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return VaultKeys.decryptWithUserKey(vaultKeyJwe, userKeys.ecdhKeyPair.privateKey); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -465,10 +495,26 @@ function refreshVault(updatedVault: VaultDto) { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||
emit('vaultUpdated', updatedVault); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function searchAuthority(query: string): Promise<AuthorityDto[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return (await backend.authorities.search(query)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.filter(authority => !members.value.has(authority.id)) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
.sort((a, b) => a.name.localeCompare(b.name)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const searchResults = ref<Array<AuthorityDto & { memberCount?: number }>>([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function searchAuthority(query: string): Promise<(AuthorityDto & { memberCount?: number })[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const results = await backend.authorities.search(query); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const filtered = results.filter(authority => !members.value.has(authority.id)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const enhanced = await Promise.all( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
filtered.map(async authority => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (authority.type === "GROUP") { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const count = await backend.groups.getMemberCount(authority.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { ...authority, memberCount: count }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return { ...authority, memberCount: 0 }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return authority; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return enhanced.sort((a, b) => a.name.localeCompare(b.name)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
async function updateMemberRole(member: MemberDto, role: VaultRole) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid static injection for thread safety.
Using static injection for repositories can cause issues in multi-threaded environments and makes testing more difficult. Consider passing the repository as a parameter to the fromEntity method.
📝 Committable suggestion