Skip to content

Commit 37a682f

Browse files
kyle-ssgclaude
andauthored
feat: Use new segment members api (#7864)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent f5b6273 commit 37a682f

8 files changed

Lines changed: 308 additions & 106 deletions

File tree

frontend/common/services/useIdentity.ts

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Res } from 'common/types/responses'
22
import { Req } from 'common/types/requests'
33
import { service } from 'common/service'
44
import transformCorePaging from 'common/transformCorePaging'
5+
import transformCursorPaging from 'common/transformCursorPaging'
56
import Utils from 'common/utils/utils'
67

78
const getIdentityEndpoint = (environmentId: string, isEdge: boolean) => {
@@ -87,34 +88,10 @@ export const identityService = service
8788
}
8889
},
8990
transformResponse(baseQueryReturnValue: Res['identities'], meta, req) {
90-
const {
91-
isEdge,
92-
page = 1,
93-
page_size = 10,
94-
pageType,
95-
pages: _pages,
96-
} = req
97-
if (isEdge) {
98-
// For edge, we create our own paging
99-
let pages = _pages ? _pages.concat([]) : []
100-
const next_evaluated_key = baseQueryReturnValue.last_evaluated_key
101-
if (pageType === 'NEXT') {
102-
pages.push(next_evaluated_key)
103-
} else if (pageType === 'PREVIOUS') {
104-
pages.unshift()
105-
} else {
106-
pages = []
107-
}
108-
91+
if (req.isEdge) {
92+
// For edge, identities are cursor-paginated.
10993
return {
110-
...baseQueryReturnValue,
111-
next:
112-
baseQueryReturnValue.results.length < page_size
113-
? undefined
114-
: '1',
115-
pages,
116-
//
117-
previous: pages.length ? '1' : undefined,
94+
...transformCursorPaging(req, baseQueryReturnValue),
11895
results: baseQueryReturnValue.results?.map((v) => {
11996
if (v.id) {
12097
return v
@@ -123,7 +100,7 @@ export const identityService = service
123100
...v,
124101
id: v.identity_uuid,
125102
}
126-
}), //
103+
}),
127104
}
128105
}
129106
return transformCorePaging(req, baseQueryReturnValue)
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Res } from 'common/types/responses'
2+
import { Req } from 'common/types/requests'
3+
import { service } from 'common/service'
4+
import Utils from 'common/utils/utils'
5+
import transformCursorPaging from 'common/transformCursorPaging'
6+
7+
export const segmentMembersService = service
8+
.enhanceEndpoints({ addTagTypes: ['SegmentMembers'] })
9+
.injectEndpoints({
10+
endpoints: (builder) => ({
11+
getSegmentMembers: builder.query<
12+
Res['segmentMembers'],
13+
Req['getSegmentMembers']
14+
>({
15+
providesTags: (_res, _err, arg) => [
16+
{ id: arg.id, type: 'SegmentMembers' },
17+
],
18+
query: ({ environment, id, page_size = 10, pages, projectId, q }) => {
19+
// The cursor for the current page is the last entry on the stack the
20+
// component has stepped through; the first page sends no cursor.
21+
const cursor = pages?.[pages.length - 1]
22+
return {
23+
url: `projects/${projectId}/segments/${id}/members/?${Utils.toParam(
24+
{ cursor, environment, limit: page_size, q },
25+
)}`,
26+
}
27+
},
28+
transformResponse: (
29+
res: Res['segmentMembers'],
30+
_meta,
31+
req: Req['getSegmentMembers'],
32+
) => transformCursorPaging(req, res),
33+
}),
34+
// END OF ENDPOINTS
35+
}),
36+
})
37+
38+
export async function getSegmentMembers(
39+
store: any,
40+
data: Req['getSegmentMembers'],
41+
options?: Parameters<
42+
typeof segmentMembersService.endpoints.getSegmentMembers.initiate
43+
>[1],
44+
) {
45+
return store.dispatch(
46+
segmentMembersService.endpoints.getSegmentMembers.initiate(data, options),
47+
)
48+
}
49+
// END OF FUNCTION_EXPORTS
50+
51+
export const {
52+
useGetSegmentMembersQuery,
53+
// END OF EXPORTS
54+
} = segmentMembersService
55+
56+
/* Usage examples:
57+
const { data, isLoading } = useGetSegmentMembersQuery({ id: 2, projectId: 1, environment: 3 }, {}) //get hook
58+
segmentMembersService.endpoints.getSegmentMembers.select({ id: 2, projectId: 1, environment: 3 })(store.getState()) //access data from any function
59+
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { PagedResponse } from './types/responses'
2+
3+
export type CursorPagedRequest = {
4+
page_size?: number
5+
// Stack of cursors the calling component has stepped through to reach the
6+
// current page. The last entry is the cursor for the current page; empty or
7+
// undefined means the first page.
8+
pages?: (string | undefined)[]
9+
}
10+
11+
// Normalises a cursor/keyset-paginated response into the `next`/`previous`
12+
// sentinels that <Paging> understands. The cursor stack (`pages`) is owned by
13+
// the calling component and threaded through the request; this transform only
14+
// derives prev/next availability:
15+
// - `next`: a full page implies there may be more rows.
16+
// - `previous`: a non-empty cursor stack means we are past the first page.
17+
export default function transformCursorPaging<T, R extends PagedResponse<T>>(
18+
req: CursorPagedRequest,
19+
res: R,
20+
): R {
21+
const pageSize = req.page_size ?? 10
22+
return {
23+
...res,
24+
next: res.results.length < pageSize ? undefined : '1',
25+
previous: req.pages?.length ? '1' : undefined,
26+
}
27+
}

frontend/common/types/requests.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,12 @@ export type Req = {
263263
tag: Omit<Tag, 'id' | 'project' | 'type' | 'is_system_tag' | 'is_permanent'>
264264
}
265265
getSegment: { projectId: number; id: number }
266+
getSegmentMembers: PagedRequest<{
267+
projectId: number
268+
id: number
269+
environment: number
270+
pages?: (string | undefined)[]
271+
}>
266272
updateAccount: Account
267273
deleteAccount: {
268274
current_password: string

frontend/common/types/responses.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ export type SegmentMembership = {
165165
count: number
166166
last_synced_at: string
167167
}
168+
export type SegmentMember = {
169+
identifier: string
170+
identity_key: string
171+
traits: Record<string, FlagsmithValue> | null
172+
}
173+
export type SegmentMembersResponse = PagedResponse<SegmentMember> & {
174+
// Pass as `cursor` to fetch the next page; null when there are no more rows.
175+
next_cursor: string | null
176+
}
168177
export type Segment = {
169178
id: number
170179
rules: SegmentRule[]
@@ -1274,6 +1283,7 @@ export type WarehouseConnection = {
12741283
export type Res = {
12751284
segments: PagedResponse<Segment>
12761285
segment: Segment
1286+
segmentMembers: SegmentMembersResponse
12771287
auditLogs: PagedResponse<AuditLogItem>
12781288
organisationLicence: {}
12791289
organisation: Organisation

frontend/web/components/modals/CreateSegment.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ type CreateSegmentType = {
7575
onComplete?: (segment: Segment) => void
7676
readOnly?: boolean
7777
segment?: Segment
78+
membersEnabled: boolean
7879
}
7980
type CreateSegmentError = {
8081
status: number
@@ -103,6 +104,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
103104
identities,
104105
identitiesLoading,
105106
identity,
107+
membersEnabled,
106108
onCancel,
107109
onComplete,
108110
page,
@@ -610,6 +612,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
610612
<div className='my-4'>
611613
<CreateSegmentUsersTabContent
612614
projectId={projectId}
615+
segmentId={segment.id}
613616
environmentId={environmentId}
614617
setEnvironmentId={setEnvironmentId}
615618
identitiesLoading={identitiesLoading}
@@ -620,6 +623,7 @@ const CreateSegment: FC<CreateSegmentType> = ({
620623
searchInput={searchInput}
621624
setSearchInput={setSearchInput}
622625
memberships={segment.membership_counts}
626+
membersEnabled={membersEnabled}
623627
/>
624628
</div>
625629
</TabItem>
@@ -753,6 +757,12 @@ const LoadingCreateSegment: FC<LoadingCreateSegmentType> = (props) => {
753757

754758
const isEdge = Utils.getIsEdge()
755759

760+
// When membership inspection is enabled and the project uses edge, the
761+
// Identities tab uses the dedicated segment members endpoint, so the legacy
762+
// identities list (and its request) is not needed.
763+
const membersEnabled =
764+
Utils.getFlagsmithHasFeature('segment_membership_inspection') && isEdge
765+
756766
const { data: identities, isLoading: identitiesLoading } =
757767
useGetIdentitiesQuery(
758768
{
@@ -765,7 +775,7 @@ const LoadingCreateSegment: FC<LoadingCreateSegmentType> = (props) => {
765775
q: search,
766776
},
767777
{
768-
skip: !environmentId,
778+
skip: !environmentId || membersEnabled,
769779
},
770780
)
771781

@@ -785,6 +795,7 @@ const LoadingCreateSegment: FC<LoadingCreateSegmentType> = (props) => {
785795
page={page}
786796
environmentId={environmentId}
787797
setEnvironmentId={setEnvironmentId}
798+
membersEnabled={membersEnabled}
788799
/>
789800
)
790801
}

0 commit comments

Comments
 (0)