Skip to content

Commit a4d8987

Browse files
authored
Read every page of a member's organizations (#156)
* Ask for a skill's trigger, and a name that can be a directory The name is the skill's directory on the agent's filesystem, so the form states the rule instead of surfacing a server error, and the description is required: it is what the agent reads before it opens the body. * Read every page of a member's organizations Which organizations the console shows is decided from the caller's memberships, and it asked for one page of them. A member of more than two hundred saw the rest disappear -- and because the console falls back to the first organization alphabetically whenever the persisted choice is not among the ones it can see, opening one of the missing organizations by address quietly landed on a different one instead.
1 parent e2c2927 commit a4d8987

2 files changed

Lines changed: 68 additions & 12 deletions

File tree

src/__tests__/organization-context.test.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,4 +336,51 @@ describe('OrganizationContext', () => {
336336
expect(screen.getByTestId('pending-ids').textContent).toBe('pending-1,pending-2');
337337
});
338338
});
339+
340+
// A member of more than one page's worth had the rest silently disappear --
341+
// including, for anyone near the boundary, the organization they had just
342+
// made and asked to open.
343+
it('reads every page of memberships, not just the first', async () => {
344+
const onPageTwo = create(MembershipSchema, {
345+
id: 'membership-late',
346+
organizationId: 'org-late',
347+
identityId: 'identity-1',
348+
role: MembershipRole.OWNER,
349+
status: MembershipStatus.ACTIVE,
350+
});
351+
listMyMemberships.mockImplementation((request: { status?: MembershipStatus; pageToken?: string }) => {
352+
if (request?.status === MembershipStatus.PENDING) {
353+
return Promise.resolve({ memberships: [] });
354+
}
355+
if (!request?.pageToken) {
356+
return Promise.resolve({
357+
memberships: [
358+
create(MembershipSchema, {
359+
id: 'membership-early',
360+
organizationId: 'org-early',
361+
identityId: 'identity-1',
362+
role: MembershipRole.OWNER,
363+
status: MembershipStatus.ACTIVE,
364+
}),
365+
],
366+
nextPageToken: 'page-2',
367+
});
368+
}
369+
return Promise.resolve({ memberships: [onPageTwo] });
370+
});
371+
mockOrganizationLookup([
372+
{ id: 'org-early', name: 'Org Early' },
373+
{ id: 'org-late', name: 'Org Late' },
374+
]);
375+
window.localStorage.setItem(
376+
'console.contextMode',
377+
JSON.stringify({ mode: 'organization', organizationId: 'org-late' }),
378+
);
379+
380+
renderWithProviders();
381+
382+
await waitFor(() => {
383+
expect(screen.getByTestId('selected').textContent).toBe('org-late');
384+
});
385+
});
339386
});

src/context/OrganizationContext.tsx

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,25 @@ function mapOrganizations(
101101
});
102102
}
103103

104+
// Every page of them, not the first. Which organizations the console will show
105+
// is decided from this list, and a member of more than one page's worth had the
106+
// rest silently disappear -- including, for anyone near the boundary, the
107+
// organization they had just made and asked to open.
108+
async function listAllMyMemberships(status: MembershipStatus): Promise<{ memberships: Membership[] }> {
109+
const memberships: Membership[] = [];
110+
let pageToken = '';
111+
do {
112+
const response = await organizationsClient.listMyMemberships({
113+
status,
114+
pageSize: MAX_PAGE_SIZE,
115+
pageToken,
116+
});
117+
memberships.push(...response.memberships);
118+
pageToken = response.nextPageToken;
119+
} while (pageToken);
120+
return { memberships };
121+
}
122+
104123
export function OrganizationProvider({ children }: { children: ReactNode }) {
105124
const { identityId, isClusterAdmin, status: userStatus } = useUserContext();
106125
const storedContextRef = useRef<StoredContextMode | null>(readStoredContextMode());
@@ -109,25 +128,15 @@ export function OrganizationProvider({ children }: { children: ReactNode }) {
109128
// Memberships include role/status data used to filter org visibility.
110129
const membershipsQuery = useQuery({
111130
queryKey: ['organizations', 'memberships'],
112-
queryFn: () =>
113-
organizationsClient.listMyMemberships({
114-
status: MembershipStatus.ACTIVE,
115-
pageSize: MAX_PAGE_SIZE,
116-
pageToken: '',
117-
}),
131+
queryFn: () => listAllMyMemberships(MembershipStatus.ACTIVE),
118132
enabled: userStatus === 'ready' && Boolean(identityId),
119133
staleTime: 60 * 1000,
120134
refetchOnWindowFocus: false,
121135
});
122136

123137
const pendingMembershipsQuery = useQuery({
124138
queryKey: ['organizations', 'pendingMemberships'],
125-
queryFn: () =>
126-
organizationsClient.listMyMemberships({
127-
status: MembershipStatus.PENDING,
128-
pageSize: MAX_PAGE_SIZE,
129-
pageToken: '',
130-
}),
139+
queryFn: () => listAllMyMemberships(MembershipStatus.PENDING),
131140
enabled: userStatus === 'ready' && Boolean(identityId),
132141
staleTime: 60 * 1000,
133142
refetchOnWindowFocus: false,

0 commit comments

Comments
 (0)