Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0633432
fix(admin): stop server tables issuing a duplicate request on mount
Shreyag02 Aug 10, 2026
85174f4
fix(admin): drop the empty default sort on project members
Shreyag02 Aug 10, 2026
35ec779
fix(admin): keep the org detail tab mounted while billing loads
Shreyag02 Aug 10, 2026
968f603
fix(admin): stop fast scrolling firing redundant page requests
Shreyag02 Aug 10, 2026
c8db582
fix(admin): scope the members invalidation to its organization
Shreyag02 Aug 10, 2026
75bc15c
fix(admin): give queries a default staleTime
Shreyag02 Aug 10, 2026
c101560
fix(admin): reuse the resolved org instead of refetching it by id
Shreyag02 Aug 10, 2026
c0a73c5
fix(admin): fetch the org member map only where it is used
Shreyag02 Aug 10, 2026
7a9491e
fix(admin): fetch invite dialog options only when it opens
Shreyag02 Aug 11, 2026
e39a065
fix(admin): guard the last three load-more handlers
Shreyag02 Aug 11, 2026
cec34fa
docs(admin): tighten the comments added in this branch
Shreyag02 Aug 11, 2026
31015f5
Merge branch 'main' into fix/admin-duplicate-api-calls
Shreyag02 Aug 11, 2026
b5d4a93
fix(admin): latch load-more against scroll bursts
Shreyag02 Aug 17, 2026
b86af87
fix(admin): cover both billing legs in isBillingAccountLoading
Shreyag02 Aug 17, 2026
b2509ec
fix(admin): seed the resolved org only into an empty cache key
Shreyag02 Aug 17, 2026
5ff0902
docs(admin): keep only the load-bearing comments
Shreyag02 Aug 17, 2026
b0e8108
fix(admin): disable Add tokens when there is no billing account
Shreyag02 Aug 18, 2026
79c7f9e
fix(admin): log member map failures again
Shreyag02 Aug 18, 2026
2cfe257
Merge branch 'main' into fix/admin-duplicate-api-calls
Shreyag02 Aug 18, 2026
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
4 changes: 3 additions & 1 deletion web/apps/admin/src/contexts/ConnectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import type { ReactNode } from "react";
import { TransportProvider } from "@connectrpc/connect-query";
import { jsonTransport as transport } from "~/connect/transport";

// Create a QueryClient instance
/* Otherwise every mount refetches. Mutations invalidate their own keys;
* the search tables opt out with staleTime: 0. */
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
refetchOnWindowFocus: false,
staleTime: 30 * 1000,
},
},
});
Expand Down
38 changes: 33 additions & 5 deletions web/apps/admin/src/pages/organizations/details/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { OrganizationDetailsView, useAdminPaths } from '@raystack/frontier/admin';
import { useCallback, useContext, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate, useParams, Outlet, Navigate } from 'react-router-dom';
import { useQuery } from '@connectrpc/connect-query';
import { FrontierServiceQueries } from '@raystack/proton/frontier';
import { createConnectQueryKey, useQuery, useTransport } from '@connectrpc/connect-query';
import { useQueryClient } from '@tanstack/react-query';
import { create } from '@bufbuild/protobuf';
import {
FrontierServiceQueries,
GetOrganizationResponseSchema,
} from '@raystack/proton/frontier';
import { AppContext } from '~/contexts/App';
import { clients } from '~/connect/clients';
import { exportCsvFromStream } from '~/utils/helper';
Expand Down Expand Up @@ -33,6 +38,8 @@ export default function OrganizationDetailsPage() {
const paths = useAdminPaths();
const { config } = useContext(AppContext);
const [countries, setCountries] = useState<string[]>([]);
const queryClient = useQueryClient();
const transport = useTransport();

const incomingOrgId = (location.state as { orgId?: string } | null)?.orgId;

Expand All @@ -53,8 +60,9 @@ export default function OrganizationDetailsPage() {

/*
* Cold-load resolve (only when state carries no id):
* - getOrganization takes an id OR a slug and returns disabled orgs too,
* so a single call covers every URL form (server GetRaw branches on UUID)
* - getOrganization takes an id OR a slug, so a single call covers every URL
* form (server GetRaw branches on UUID)
* - disabled orgs resolve for superusers only; the console is superuser-only
* - a UUID param is already the id, but we still resolve to read the slug +
* state for the canonical-URL rewrite below
*/
Expand All @@ -77,6 +85,26 @@ export default function OrganizationDetailsPage() {
const orgId = stateOrgId || (paramIsId ? urlParam : org?.id);
const notFound = needsResolve && isSuccess && !org?.id;

/* The slug resolve caches under the slug, so seed the id key the view uses.
* In render, not an effect: the view mounts this commit. Empty keys only —
* this copy can be stale, and edits invalidate the id key, not the slug. */
const primedOrgId = useRef<string | undefined>(undefined);
if (org?.id && org.id !== urlParam && primedOrgId.current !== org.id) {
primedOrgId.current = org.id;
const orgKey = createConnectQueryKey({
schema: FrontierServiceQueries.getOrganization,
transport,
input: { id: org.id },
cardinality: 'finite',
});
if (queryClient.getQueryData(orgKey) === undefined) {
queryClient.setQueryData(
orgKey,
create(GetOrganizationResponseSchema, { organization: org }),
);
}
}

/*
* Old UUID bookmark → canonical slug URL:
* - one live URL per org; replace keeps the back-button sane
Expand Down
24 changes: 24 additions & 0 deletions web/sdk/admin/hooks/useOrgMembersMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useQuery } from "@connectrpc/connect-query";
import { FrontierServiceQueries, type User } from "@raystack/proton/frontier";
import type { ListOrganizationUsersResponse } from "@raystack/proton/frontier";

// Stable identity so react-query memoizes the select.
const toMembersMap = (data?: ListOrganizationUsersResponse) =>
(data?.users || []).reduce(
(acc, user) => {
acc[user.id || ""] = user;
return acc;
},
{} as Record<string, User>,
);

/** Org members keyed by id. Deduped across callers; empty orgId disables. */
export const useOrgMembersMap = (orgId?: string) =>
useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: orgId || "" },
{
enabled: !!orgId,
select: toMembersMap,
},
);
11 changes: 9 additions & 2 deletions web/sdk/admin/views/audit-logs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
Flex,
} from "@raystack/apsara";
import { useDebouncedState } from "@raystack/apsara/hooks";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import Navbar from "./navbar";
import styles from "./audit-logs.module.css";
import { getColumns } from "./columns";
Expand Down Expand Up @@ -48,6 +48,8 @@
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
// Must match DataTable's mount emit, or it refetches.
sort: [DEFAULT_SORT],
};
const TRANSFORM_OPTIONS = {
fieldNameMapping: {
Expand All @@ -65,7 +67,7 @@
/** App name displayed in the page title. */
appName?: string;
/** Callback to export audit logs as CSV with the current query filters applied. */
onExportCsv?: (query: RQLRequest) => Promise<void>;

Check warning on line 70 in web/sdk/admin/views/audit-logs/index.tsx

View workflow job for this annotation

GitHub Actions / JS SDK Lint

'query' is defined but never used
/** Navigate to a link in an audit entry (e.g. org/user page). `state` carries the org id. */
onNavigate?: (path: string, state?: { orgId?: string }) => void;
};
Expand Down Expand Up @@ -140,12 +142,17 @@
[queryClient],
);

// isFetchingNextPage lags a render; the ref doesn't.
const isLoadingMoreRef = useRef(false);
const handleLoadMore = async () => {
if (!hasNextPage || isFetchingNextPage || isLoadingMoreRef.current) return;
isLoadingMoreRef.current = true;
try {
if (!hasNextPage) return;
await fetchNextPage();
} catch (error) {
console.error("Error loading more audit logs:", error);
} finally {
isLoadingMoreRef.current = false;
}
};

Expand Down
11 changes: 9 additions & 2 deletions web/sdk/admin/views/invoices/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
EmptyState,
Flex,
} from "@raystack/apsara";
import { useState } from "react";
import { useRef, useState } from "react";
import { PageTitle } from "../../components/PageTitle";
import { InvoicesNavabar } from "./navbar";
import styles from "./invoices.module.css";
Expand Down Expand Up @@ -40,6 +40,8 @@ const DEFAULT_SORT: DataTableSort = { name: "createdAt", order: "desc" };
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
// Must match DataTable's mount emit, or it refetches.
sort: [DEFAULT_SORT],
};

export type InvoicesViewProps = {
Expand Down Expand Up @@ -89,12 +91,17 @@ export default function InvoicesView({ appName }: InvoicesViewProps = {}) {
});
};

// isFetchingNextPage lags a render; the ref doesn't.
const isLoadingMoreRef = useRef(false);
const handleLoadMore = async () => {
if (!hasNextPage || isFetchingNextPage || isLoadingMoreRef.current) return;
isLoadingMoreRef.current = true;
try {
if (!hasNextPage) return;
await fetchNextPage();
} catch (error) {
console.error("Error loading more invoices:", error);
} finally {
isLoadingMoreRef.current = false;
}
};

Expand Down
11 changes: 9 additions & 2 deletions web/sdk/admin/views/organizations/details/apis/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
CodeIcon,
ExclamationTriangleIcon,
} from "@radix-ui/react-icons";
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
import { OrganizationContext } from "../contexts/organization-context";
import { PageTitle } from "~/admin/components/PageTitle";
import { getColumns } from "./columns";
Expand Down Expand Up @@ -69,6 +69,8 @@ const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
// Must match DataTable's mount emit, or it refetches.
sort: [DEFAULT_SORT],
};
const TRANSFORM_OPTIONS = {
fieldNameMapping: {
Expand Down Expand Up @@ -148,12 +150,17 @@ export function OrganizationApisView() {
setTableQuery(newQuery);
};

// isFetchingNextPage lags a render; the ref doesn't.
const isLoadingMoreRef = useRef(false);
const handleLoadMore = async () => {
if (!hasNextPage || isFetchingNextPage || isLoadingMoreRef.current) return;
isLoadingMoreRef.current = true;
try {
if (!hasNextPage) return;
await fetchNextPage();
} catch (error) {
console.error("Error loading more service users:", error);
} finally {
isLoadingMoreRef.current = false;
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {
OrganizationSchema,
type Role,
type BillingAccount,
type User,
type OrganizationKyc,
type BillingAccountDetails,
} from "@raystack/proton/frontier";
Expand All @@ -29,8 +28,6 @@ interface OrganizationContextType {
tokenBalance: string;
isTokenBalanceLoading: boolean;
fetchTokenBalance: () => void;
orgMembersMap: Record<string, User>;
isOrgMembersMapLoading: boolean;
updateKYCDetails: (kycDetails: OrganizationKyc | undefined) => void;
kycDetails?: OrganizationKyc;
isKYCLoading: boolean;
Expand All @@ -55,8 +52,6 @@ const defaultOrganiztionContextValue = {
query: "",
onChange: () => {},
},
orgMembersMap: {},
isOrgMembersMapLoading: false,
updateKYCDetails: () => {},
kycDetails: undefined,
isKYCLoading: false,
Expand Down
1 change: 1 addition & 0 deletions web/sdk/admin/views/organizations/details/edit/billing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function EditBillingPanel({ open = false, onClose }: EditBillingPanelProp
);

const onSubmit = async (data: BillingDetailsForm) => {
if (!organizationId || !billingId) return;

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align billing controls with missing-identifier guards.

Both handlers silently no-op when required identifiers are unavailable. Keep the corresponding controls disabled, or show an explicit unavailable-account state.

  • web/sdk/admin/views/organizations/details/edit/billing.tsx#L129-L129: include missing organizationId and billingId in the Save disabled condition.
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L88-L88: retain the guard for organisationId and billingAccountId.
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L161-L161: include missing identifiers in the Add disabled condition.
📍 Affects 2 files
  • web/sdk/admin/views/organizations/details/edit/billing.tsx#L129-L129 (this comment)
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L88-L88
  • web/sdk/admin/views/organizations/details/layout/add-tokens-dialog.tsx#L161-L161

try {
// For prepaid, set values to 0; for postpaid, use form values
const creditMinValue = data.tokenPaymentType === "prepaid" ? 0n : BigInt(data.creditMin);
Expand Down
52 changes: 13 additions & 39 deletions web/sdk/admin/views/organizations/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
GetBillingBalanceRequestSchema,
GetOrganizationKycResponseSchema,
type Organization,
type User,
} from "@raystack/proton/frontier";

export type OrganizationDetailsViewProps = {
Expand Down Expand Up @@ -118,41 +117,19 @@ export const OrganizationDetailsView = ({
{ enabled: !!organizationId },
);

// Fetch organization members
const {
data: orgMembersMap = {},
isLoading: isOrgMembersMapLoading,
error: orgMembersError,
data: firstBillingAccountId = "",
isLoading: isBillingAccountsLoading,
error: billingAccountsError,
} = useQuery(
FrontierServiceQueries.listOrganizationUsers,
{ id: organizationId || "" },
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => {
const users = data?.users || [];
return users.reduce(
(acc, user) => {
const id = user.id || "";
acc[id] = user;
return acc;
},
{} as Record<string, User>,
);
},
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
);

// Fetch billing accounts list
const { data: firstBillingAccountId = "", error: billingAccountsError } =
useQuery(
FrontierServiceQueries.listBillingAccounts,
{ orgId: organizationId || "" },
{
enabled: !!organizationId,
select: (data) => data?.billingAccounts?.[0]?.id || "",
},
);

// Fetch billing account details
const {
data: billingAccountData,
Expand All @@ -177,6 +154,9 @@ export const OrganizationDetailsView = ({
const billingAccount = billingAccountData?.billingAccount;
const billingAccountDetails = billingAccountData?.billingAccountDetails;

// getBillingAccount is disabled until the list yields an id.
const isBillingLoading = isBillingAccountsLoading || isBillingAccountLoading;

// Fetch billing balance
const {
data: tokenBalance = "0",
Expand All @@ -202,9 +182,6 @@ export const OrganizationDetailsView = ({
if (kycError) {
console.error("Failed to fetch KYC details:", kycError);
}
if (orgMembersError) {
console.error("Failed to fetch organization members:", orgMembersError);
}
if (billingAccountsError) {
console.error("Failed to fetch billing accounts:", billingAccountsError);
}
Expand All @@ -220,14 +197,13 @@ export const OrganizationDetailsView = ({
}, [
organizationError,
kycError,
orgMembersError,
billingAccountsError,
billingAccountError,
tokenBalanceError,
]);

const isLoading =
isOrganizationLoading || isRolesLoading || isBillingAccountLoading;
// Billing waits on an id, so including it here remounted the tab mid-load.
const isLoading = isOrganizationLoading || isRolesLoading;
return (
<OrganizationContext.Provider
value={{
Expand All @@ -236,13 +212,11 @@ export const OrganizationDetailsView = ({
roles,
billingAccount,
billingAccountDetails,
isBillingAccountLoading,
isBillingAccountLoading: isBillingLoading,
fetchBillingAccountDetails,
tokenBalance,
isTokenBalanceLoading,
isTokenBalanceLoading: isBillingAccountsLoading || isTokenBalanceLoading,
fetchTokenBalance,
orgMembersMap,
isOrgMembersMapLoading,
updateKYCDetails,
kycDetails,
isKYCLoading,
Expand Down
12 changes: 10 additions & 2 deletions web/sdk/admin/views/organizations/details/invoices/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { DataTableQuery, DataTableSort } from "@raystack/apsara";
import styles from "./invoices.module.css";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { BanknotesIcon } from "~/admin/assets/icons/BanknotesIcon";
import { useContext, useEffect, useMemo, useState } from "react";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { OrganizationContext } from "../contexts/organization-context";
import { PageTitle } from "~/admin/components/PageTitle";
import { getColumns } from "./columns";
Expand All @@ -22,6 +22,8 @@ const DEFAULT_SORT: DataTableSort = { name: 'createdAt', order: 'desc' };
const INITIAL_QUERY: DataTableQuery = {
offset: 0,
limit: DEFAULT_PAGE_SIZE,
// Must match DataTable's mount emit, or it refetches.
sort: [DEFAULT_SORT],
};
const TRANSFORM_OPTIONS = {
fieldNameMapping: {
Expand Down Expand Up @@ -172,9 +174,15 @@ export function OrganizationInvoicesView() {
setTableQuery(newQuery);
};

// isFetchingNextPage lags a render; the ref doesn't.
const isLoadingMoreRef = useRef(false);
const fetchMore = async () => {
if (hasNextPage && !isFetchingNextPage && !isError) {
if (!hasNextPage || isFetchingNextPage || isError || isLoadingMoreRef.current) return;
isLoadingMoreRef.current = true;
try {
await fetchNextPage();
} finally {
isLoadingMoreRef.current = false;
}
};

Expand Down
Loading
Loading