Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
5825c22
fix pagination and add filter to Organization.members
jdolle Nov 18, 2025
475e7fe
Merge branch 'main' into member-pagination-backend
jdolle Nov 18, 2025
68af504
Merge remote-tracking branch 'origin/member-pagination-backend' into …
jonathanawesome Nov 21, 2025
546b550
update route
jonathanawesome Nov 21, 2025
abbd0e4
remove empty default search param string
jonathanawesome Nov 21, 2025
e1a5546
update query
jonathanawesome Nov 21, 2025
64578a0
strip sorting from UI, remove unused refetch prop, init basic input f…
jonathanawesome Nov 21, 2025
8e820f2
init filter ui
jonathanawesome Nov 22, 2025
5a12547
update hook to clear single value param if length === 0
jonathanawesome Nov 22, 2025
a2e9037
initial pagination work
jonathanawesome Nov 22, 2025
dea8fc1
fix wonky edge case when clicking prev page button
jonathanawesome Nov 22, 2025
a2d73a4
update api to allow for substring search
jonathanawesome Nov 22, 2025
69f6939
update page number viz to show search string and remove cursor-pointe…
jonathanawesome Nov 22, 2025
c28e2a5
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 23, 2025
cf07f1a
lock file
jonathanawesome Nov 23, 2025
c7a399f
fix previous change to useSearchParamsFilter
jonathanawesome Nov 23, 2025
2628b1b
clean up debounced input
jonathanawesome Nov 23, 2025
555e5f1
use undefined rather than string on navigate
jonathanawesome Nov 23, 2025
91975f6
remove useMemo usage
jonathanawesome Nov 23, 2025
7d69bc4
Update packages/services/api/src/modules/organization/providers/organ…
jdolle Nov 24, 2025
041045e
Update packages/services/api/src/modules/organization/resolvers/Organ…
jdolle Nov 24, 2025
650eeef
Use pg_trgm to index by user name and email; adjust lookup condition
jdolle Nov 24, 2025
f9c17da
add null state for no members found
jonathanawesome Nov 24, 2025
7df7e4b
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 24, 2025
b6b8419
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 24, 2025
c6c914e
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 25, 2025
5e91797
be more precise with memo import
jonathanawesome Nov 25, 2025
e45c665
lock file
jonathanawesome Nov 25, 2025
5efef97
remove Link (causing ref warnings) and replace with simple button
jonathanawesome Nov 25, 2025
e1f56a6
small refactor to remove confusing double selection of members
jonathanawesome Nov 25, 2025
34a0dc9
relocate all pagination code into the actual view file
jonathanawesome Nov 25, 2025
3f201d6
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 25, 2025
e1d5da1
relocate the check for hasCollectedOperations to inside the necessary…
jonathanawesome Nov 25, 2025
a2c0ae1
Revert "relocate the check for hasCollectedOperations to inside the n…
jonathanawesome Nov 25, 2025
1e83941
Merge branch 'main' into console-1382-member-pagination-and-search
jonathanawesome Nov 26, 2025
7a6af7b
Merge branch 'main' into console-1382-member-pagination-and-search
jdolle Nov 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { type MigrationExecutor } from '../pg-migrator';

export default {
name: '2025.11.25T00-00-00.members-search.ts',
run: ({ sql }) => sql`
CREATE EXTENSION IF NOT EXISTS pg_trgm;
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this require additional configuration to our azure database? Please make sure to test this migration on the dev environment.

Copy link
Contributor

Choose a reason for hiding this comment

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

😢

SQL Error [0A000]: ERROR: extension "pg_trgm" is not allow-listed for users in Azure Database for PostgreSQL

@jdolle we need to make sure we can introduce this extension without downtime

Copy link
Collaborator

Choose a reason for hiding this comment

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

Added to allow list per https://learn.microsoft.com/en-us/azure/postgresql/extensions/how-to-allow-extensions?tabs=allow-extensions-portal

There was no downtime when enabling this and there are no special considerations for the pg_trgm extension that would cause downtime, since it's not instantly changing behavior at install -- it's enabling more ways to index for searching.

For adding the index -- I'm not concerned due to the size of the table. The migration may take a few seconds in the background. And this will slightly increase create/update times for these records but again due to the size of the table, and also because of how infrequently these records are created/updated, this should not be an issue.

CREATE INDEX CONCURRENTLY IF NOT EXISTS "users_search_by_email_and_display_name" on users using gin(LOWER(email|| ' ' || display_name) gin_trgm_ops);
`,
} satisfies MigrationExecutor;
15 changes: 13 additions & 2 deletions packages/services/api/src/modules/organization/module.graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -868,6 +868,14 @@ export default gql`
appDeployments: [String!]
}
input MembersFilter {
"""
Part of a user's email or username that is used to filter the list of
members.
"""
searchTerm: String
}
type Organization {
"""
Unique UUID of the organization
Expand All @@ -881,8 +889,11 @@ export default gql`
name: String! @deprecated(reason: "Use the 'slug' field instead.")
owner: Member! @tag(name: "public")
me: Member!
members(first: Int @tag(name: "public"), after: String @tag(name: "public")): MemberConnection!
@tag(name: "public")
members(
first: Int @tag(name: "public")
after: String @tag(name: "public")
filters: MembersFilter
Copy link
Contributor

Choose a reason for hiding this comment

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

filters
MembersFilter

Does singular make more sense here?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's named after Query.members like how we name inputs

Copy link
Contributor

Choose a reason for hiding this comment

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

So

  • MembersFilter and Query.members(filter:)
  • or MembersFilters and Query.members(filters:)?

Copy link
Member Author

Choose a reason for hiding this comment

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

I see XFilter and XFilterInput for input name and both filter: and filters in the codebase.

Copy link
Contributor

Choose a reason for hiding this comment

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

For inputs we also kind of have the convention to append Input to the name.

If in doubt on how to name things, I would always recommend to look for similar fields that are tagged with @tag(name: "public"), as everything that is part of the public API schema, in theory, is consistent and follows how we want the GraphQL schema to be exposed.

Having that said I am also guilty of my own feedback 😆
image

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is part of the private GraphQL API for now, I am okay with handling it later though. Just make sure to create a follow up task

): MemberConnection! @tag(name: "public")
invitations(
first: Int @tag(name: "public")
after: String @tag(name: "public")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1221,7 +1221,7 @@ export class OrganizationManager {

async getPaginatedOrganizationMembersForOrganization(
organization: Organization,
args: { first: number | null; after: string | null },
args: { first: number | null; after: string | null; searchTerm: string | null },
) {
await this.session.assertPerformAction({
action: 'member:describe',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,31 @@ export class OrganizationMembers {

async getPaginatedOrganizationMembersForOrganization(
organization: Organization,
args: { first: number | null; after: string | null },
args: { first: number | null; after: string | null; searchTerm: string | null },
) {
this.logger.debug(
'Find paginated organization members for organization. (organizationId=%s)',
organization.id,
);

const first = args.first;
const first = args.first ? Math.min(args.first, 50) : 50;
const cursor = args.after ? decodeCreatedAtAndUUIDIdBasedCursor(args.after) : null;
const searchTerm = args.searchTerm?.trim().toLowerCase() ?? '';
const searching = searchTerm.length > 0;

const query = sql`
SELECT
${organizationMemberFields(sql`"om"`)}
FROM
"organization_member" AS "om"
${
searching
? sql`
JOIN "users" as "u"
ON "om"."user_id" = "u"."id"
`
: sql``
}
WHERE
"om"."organization_id" = ${organization.id}
${
Expand All @@ -178,11 +188,12 @@ export class OrganizationMembers {
`
: sql``
}
${searching ? sql`AND (LOWER("u"."display_name" || ' ' || "u"."email") LIKE ${'%' + searchTerm + '%'})` : sql``}
ORDER BY
"om"."organization_id" DESC
, "om"."created_at" DESC
, "om"."user_id" DESC
Comment on lines 193 to 195
Copy link
Contributor

Choose a reason for hiding this comment

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

How does this affect usage of existing indices?

Copy link
Collaborator

Choose a reason for hiding this comment

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

We have an index already that handles this:

"organization_member_pagination_idx" btree (organization_id DESC, user_id DESC, created_at DESC)

But this is important for the pagination -- so that the last element in the list has a cursor with the oldest timestamp

Copy link
Contributor

Choose a reason for hiding this comment

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

Now I get it, so in case of, we would need to update the index as well. 😢 Can be a follow-up PR, though!

 "om"."organization_id" DESC
        , "om"."created_at" DESC
        , "om"."user_id" DESC

, "om"."user_id" DESC
${first ? sql`LIMIT ${first + 1}` : sql``}
LIMIT ${first + 1}
`;

const result = await this.pool.any<unknown>(query);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export const Organization: Pick<
.getPaginatedOrganizationMembersForOrganization(organization, {
first: args.first ?? null,
after: args.after ?? null,
searchTerm: args.filters?.searchTerm ?? null,
});
},
invitations: async (organization, args, { injector }) => {
Expand Down
Loading
Loading