Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: add flag-gated membersPreview to /api/admin/projects#12107

Draft
dharmadeveloper108 wants to merge 4 commits into
mainUnleash/unleash:mainfrom
dx-4614-include-members-preview-backendUnleash/unleash:dx-4614-include-members-preview-backendCopy head branch name to clipboard
Draft

feat: add flag-gated membersPreview to /api/admin/projects#12107
dharmadeveloper108 wants to merge 4 commits into
mainUnleash/unleash:mainfrom
dx-4614-include-members-preview-backendUnleash/unleash:dx-4614-include-members-preview-backendCopy head branch name to clipboard

Conversation

@dharmadeveloper108
Copy link
Copy Markdown
Contributor

@dharmadeveloper108 dharmadeveloper108 commented May 25, 2026

As part of the project card redesign, we want to render avatars for 4 project members (see MUI component).
This adds a flag-gated membersPreview field to each project on /api/admin/projects, which represents the "last" 4 members of a project (sorted by ID).

  • Adds ProjectMembersReadModel with getMembersPreviewByProject() and a static membersUnion() for the "who counts as a project member" subquery.
  • getProjects in the project service merges membersPreview onto each project (gated by newProjectList)
  • getMembersCount in the new project members read model reuses membersUnion instead of duplicating the UNION inline.

@github-actions
Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@github-actions
Copy link
Copy Markdown
Contributor

API Changelog 7.6.5-beta.0 vs. 7.6.5-beta.0

No changes detected

this.db = db;
}

static membersUnion(db: Db) {
Copy link
Copy Markdown
Contributor Author

@dharmadeveloper108 dharmadeveloper108 May 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broke this out of project-read-model.ts for two reasons:

  1. The same exact operation (determine what users count as members of a project) is needed by getMembersCount there too.
  2. Conceptually it belongs better in a project members model than a project read model (better separation of concerns).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it actually do? If I understand correctly, it gives you all the members who are either direct members or members via a group? maybe there's a better name for the method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly, it's users who have a project scoped role (either individual or coming from a group they're a part of), excluding root roles (which are instance wide).
I agree that renaming this would definitely help, thank you 🙌

const rows = await this.db
.select('*')
.from(selectedMembers)
.where('rn', '<=', MEMBERS_PREVIEW_SIZE);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The project list card only renders 4 avatars (see figma in the ticket), and capping avoids transferring potentially hundreds of rows per project

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so is it only .. the 4 first members of the project? Or how does it work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"First" 4 in the sense that they have the "oldest" user IDs (in quotes because I'm not sure that's a super reliable way to determine whether they're actually the oldest users). So not the 4 oldest members, the 4 oldest users.
But it's completely arbitrary (it's a way to deterministically return the same users every time).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, interesting. Is that what we want? As a user, I'd probably expect to get the last 4 people who have done something in the project (e.g. the last 4 unique user ids from the event log) who are also members somehow. But if that's not something we're doing, this is gonna be cheaper.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to have an accurate representation of who were the last 4 users who did something in a project, then I don't think we can avoid looking into the events table. I can see that being considerably more expensive 😅 so I'd like to avoid that. Also, depending on the criteria we'd set, we could then have projects with people who were added as members but never did anything actively, so they wouldn't show up.
But we can at least reverse the sorting and get the 4 "newest" users (most recently registered)?

for (const project of projects) {
project.onboardingStatus = onboardingStatuses.get(project.id);
project.membersPreview =
membersPreviewByProject[project.id] ?? [];
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have owners: so I'm thinking of using those for the avatars as fallback if the membersPreview is empty (otherwise if both are missing, we're still rendering the "Updated/Created" so at least there is something in the card footer

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can members be empty? If there are no members and no owners, I guess. So ... that might be the case for the default project?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly. Not sure if that can be the case for a non-default enterprise project 🤔 maybe if you create a project and then remove your user access to it?

@gastonfournier gastonfournier moved this from New to In Progress in Issues and PRs May 26, 2026
Comment on lines +102 to +117
test('caps at 4 members per project', async () => {
const projectId = randomId();
await db.stores.projectStore.create({ id: projectId, name: projectId });

for (const user of [userA, userB, userC, userD, userE]) {
await db.stores.accessStore.addUserToRole(
user.id,
memberRoleId,
projectId,
);
}

const members = await readModel.getMembersPreviewByProject();

expect(members[projectId]).toHaveLength(4);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should there be a test about which 4 are kept? And how is that determined anyway? I might find out later, but the description made it sound like it'd be the 4 with the highest user ids?

this.db = db;
}

static membersUnion(db: Db) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it actually do? If I understand correctly, it gives you all the members who are either direct members or members via a group? maybe there's a better name for the method?

'users.email',
'users.image_url',
)
.rowNumber('rn', 'users.id', 'query.project')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not familiar with rowNumber, but the docs seem to say that it just adds an extra counter to the results that you can reference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the signature is rowNumber(alias, orderByClause, [partitionByClause]) so order by user ID (defaulted to ASC), and partition by project (restart counting after each project).

Comment on lines +55 to +57
const rows = await this.db
.select('*')
.from(selectedMembers)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this come out as a subquery? Or .. how does it work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes a nested query (but it's one big query under the hood):

select * from (select "query"."project", "users"."name", "users"."username", "users"."email", "users"."image_url", row_number() over (partition by "query"."project" order by "users"."id") as "rn" 
from (select "user_id", "project" from "role_user" left join "roles" on "role_user"."role_id" = "roles"."id" where (not "type" = 'root') union select "user_id", "project" from "group_role" left join "group_user" on "group_user"."group_id" = "group_role"."group_id") as "query" left join "users" on "users"."id" = "query"."user_id" where "users"."id" is not null) as "selected_members" where "rn" <= 4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, yep. It's probably fine. Can't remember how we deal with subqueries / nested queries in other places. I think there may be some perf implications, but I'm a little fuzzy on the details there. Maybe it's fine (and indeed, maybe this is the right way to do it) 🤷🏼

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to pull in a query expert for a quick look, I'm also not 1000% confident :D

const rows = await this.db
.select('*')
.from(selectedMembers)
.where('rn', '<=', MEMBERS_PREVIEW_SIZE);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so is it only .. the 4 first members of the project? Or how does it work?

Comment on lines +1 to +5
export type ProjectMember = {
name: string;
email?: string;
imageUrl?: string;
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we have this type or a similar one somewhere else? Feels like it's pretty close to IUser? Maybe they should be related? 🤷🏼

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're probably right actually 👀

for (const project of projects) {
project.onboardingStatus = onboardingStatuses.get(project.id);
project.membersPreview =
membersPreviewByProject[project.id] ?? [];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can members be empty? If there are no members and no owners, I guess. So ... that might be the case for the default project?

@dharmadeveloper108 dharmadeveloper108 force-pushed the dx-4614-include-members-preview-backend branch from 82c23a0 to 99eda54 Compare May 26, 2026 12:22
this.db = db;
}

static usersWithProjectRoles(db: Db) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Broke this out of project-read-model.ts for two reasons:

  1. The same exact operation (determine what users count as members of a project) is needed by getMembersCount there too.
  2. Conceptually it belongs better in a project members model than a project read model (better separation of concerns).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(put this comment back here as it disappeared after I renamed the method but I think it's useful to keep it)

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Jun 4, 2026

CLA assistant check
All committers have signed the CLA.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.