diff --git a/devfront/src/features/audit/AuditLogsPage.tsx b/devfront/src/features/audit/AuditLogsPage.tsx index 8b0469fe..dda76cad 100644 --- a/devfront/src/features/audit/AuditLogsPage.tsx +++ b/devfront/src/features/audit/AuditLogsPage.tsx @@ -10,6 +10,7 @@ import { PageHeader } from "../../../../common/core/components/page"; import { SearchFilterBar } from "../../../../common/ui/search-filter-bar"; import { ForbiddenMessage } from "../../components/common/ForbiddenMessage"; import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard"; +import { useDeveloperAccessGate } from "../developer-access/developerAccessGate"; import { Badge } from "../../components/ui/badge"; import { Button } from "../../components/ui/button"; import { @@ -20,7 +21,6 @@ import { CardTitle, } from "../../components/ui/card"; import { Input } from "../../components/ui/input"; -import { fetchDeveloperRequestStatus } from "../../lib/devApi"; import type { DevAuditLog } from "../../lib/devApi"; import { fetchDevAuditLogs } from "../../lib/devApi"; import { t } from "../../lib/i18n"; @@ -92,22 +92,17 @@ function AuditLogsPage() { enabled: hasAccessToken, }); const profileRole = me?.role?.trim() || role; - const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ - queryKey: ["developer-request", tenantId], - queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: hasAccessToken && profileRole === "user", + const { + hasDeveloperAccess, + isDeveloperRequestPending, + canRequestDeveloperAccess, + isLoadingDeveloperAccessGate, + } = useDeveloperAccessGate({ + hasAccessToken, + profileRole, + tenantId, + isLoadingIdentity: isLoadingMe, }); - const hasDeveloperAccess = - profileRole === "super_admin" || - profileRole === "tenant_admin" || - profileRole === "rp_admin" || - requestStatus?.status === "approved"; - const isDeveloperRequestPending = requestStatus?.status === "pending"; - const canRequestDeveloperAccess = - profileRole === "user" && - !isLoadingRequestStatus && - !hasDeveloperAccess && - !isDeveloperRequestPending; const query = useInfiniteQuery({ queryKey: [ @@ -138,10 +133,7 @@ function AuditLogsPage() { downloadCsv(csv, `dev-audit-logs-${stamp}.csv`); }; - if ( - profileRole === "user" && - (isLoadingMe || isLoadingRequestStatus) - ) { + if (isLoadingDeveloperAccessGate) { return (
{t("ui.common.loading", "Loading...")} diff --git a/devfront/src/features/developer-access/developerAccessGate.test.ts b/devfront/src/features/developer-access/developerAccessGate.test.ts new file mode 100644 index 00000000..02acae89 --- /dev/null +++ b/devfront/src/features/developer-access/developerAccessGate.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from "vitest"; +import { + resolveDeveloperAccessGate, + shouldFetchDeveloperRequestStatus, + shouldShowDeveloperAccessLoading, +} from "./developerAccessGate"; + +describe("developer access gate", () => { + it("fetches request status only for user roles", () => { + expect(shouldFetchDeveloperRequestStatus("user")).toBe(true); + expect(shouldFetchDeveloperRequestStatus("tenant_admin")).toBe(false); + expect(shouldFetchDeveloperRequestStatus("rp_admin")).toBe(false); + }); + + it("resolves access and request states from the request status", () => { + expect(resolveDeveloperAccessGate("super_admin", "pending")).toEqual({ + hasDeveloperAccess: true, + isDeveloperRequestPending: true, + canRequestDeveloperAccess: false, + }); + + expect(resolveDeveloperAccessGate("user", "approved")).toEqual({ + hasDeveloperAccess: true, + isDeveloperRequestPending: false, + canRequestDeveloperAccess: false, + }); + + expect(resolveDeveloperAccessGate("user", "pending")).toEqual({ + hasDeveloperAccess: false, + isDeveloperRequestPending: true, + canRequestDeveloperAccess: false, + }); + + expect(resolveDeveloperAccessGate("user", "none")).toEqual({ + hasDeveloperAccess: false, + isDeveloperRequestPending: false, + canRequestDeveloperAccess: true, + }); + }); + + it("shows the loading gate only for user requests", () => { + expect(shouldShowDeveloperAccessLoading("user", true, false)).toBe(true); + expect(shouldShowDeveloperAccessLoading("user", false, true)).toBe(true); + expect(shouldShowDeveloperAccessLoading("tenant_admin", true, true)).toBe( + false, + ); + }); +}); diff --git a/devfront/src/features/developer-access/developerAccessGate.ts b/devfront/src/features/developer-access/developerAccessGate.ts new file mode 100644 index 00000000..1dbc206a --- /dev/null +++ b/devfront/src/features/developer-access/developerAccessGate.ts @@ -0,0 +1,88 @@ +import { useQuery } from "@tanstack/react-query"; +import { + fetchDeveloperRequestStatus, + type DeveloperRequestStatus, +} from "../../lib/devApi"; + +export type DeveloperAccessGateState = { + hasDeveloperAccess: boolean; + isDeveloperRequestPending: boolean; + canRequestDeveloperAccess: boolean; + isLoadingDeveloperAccessGate: boolean; +}; + +function isPrivilegedDeveloperRole(profileRole: string) { + return ( + profileRole === "super_admin" || + profileRole === "tenant_admin" || + profileRole === "rp_admin" + ); +} + +export function resolveDeveloperAccessGate( + profileRole: string, + requestStatus?: DeveloperRequestStatus, +): Omit { + const hasDeveloperAccess = + isPrivilegedDeveloperRole(profileRole) || requestStatus === "approved"; + const isDeveloperRequestPending = requestStatus === "pending"; + const canRequestDeveloperAccess = + profileRole === "user" && + !hasDeveloperAccess && + !isDeveloperRequestPending; + + return { + hasDeveloperAccess, + isDeveloperRequestPending, + canRequestDeveloperAccess, + }; +} + +export function shouldFetchDeveloperRequestStatus(profileRole: string) { + return profileRole === "user"; +} + +export function shouldShowDeveloperAccessLoading( + profileRole: string, + isLoadingIdentity: boolean, + isLoadingRequestStatus: boolean, +) { + return ( + profileRole === "user" && (isLoadingIdentity || isLoadingRequestStatus) + ); +} + +export function useDeveloperAccessGate({ + hasAccessToken, + profileRole, + tenantId, + isLoadingIdentity = false, +}: { + hasAccessToken: boolean; + profileRole: string; + tenantId?: string; + isLoadingIdentity?: boolean; +}) { + const shouldFetchRequestStatus = shouldFetchDeveloperRequestStatus( + profileRole, + ); + const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ + queryKey: ["developer-request", tenantId], + queryFn: () => fetchDeveloperRequestStatus(tenantId), + enabled: hasAccessToken && shouldFetchRequestStatus, + }); + + const resolvedGate = resolveDeveloperAccessGate( + profileRole, + requestStatus?.status, + ); + + return { + ...resolvedGate, + isLoadingDeveloperAccessGate: shouldShowDeveloperAccessLoading( + profileRole, + isLoadingIdentity, + isLoadingRequestStatus, + ), + } satisfies DeveloperAccessGateState; +} diff --git a/devfront/src/features/overview/GlobalOverviewPage.tsx b/devfront/src/features/overview/GlobalOverviewPage.tsx index 3f2330ee..a3741a3f 100644 --- a/devfront/src/features/overview/GlobalOverviewPage.tsx +++ b/devfront/src/features/overview/GlobalOverviewPage.tsx @@ -17,6 +17,7 @@ import { OverviewSelectionChips, } from "../../../../common/core/components/overview"; import { DeveloperAccessRequestCard } from "../../components/common/DeveloperAccessRequestCard"; +import { useDeveloperAccessGate } from "../developer-access/developerAccessGate"; import { type ClientSummary, fetchClients, @@ -491,11 +492,6 @@ function GlobalOverviewPage() { const [period, setPeriod] = useState("day"); const [selectedClientIds, setSelectedClientIds] = useState([]); const usageDays = period === "day" ? 14 : period === "week" ? 84 : 90; - const { data: requestStatus, isLoading: isLoadingRequestStatus } = useQuery({ - queryKey: ["developer-request", tenantId], - queryFn: () => fetchDeveloperRequestStatus(tenantId), - enabled: hasAccessToken && profileRole === "user", - }); const statsQuery = useQuery({ queryKey: ["dev-dashboard-stats"], queryFn: fetchDevStats, @@ -517,17 +513,17 @@ function GlobalOverviewPage() { }); const clients = clientsQuery.data?.items ?? []; - const hasDeveloperAccess = - profileRole === "super_admin" || - profileRole === "tenant_admin" || - profileRole === "rp_admin" || - requestStatus?.status === "approved"; - const isDeveloperRequestPending = requestStatus?.status === "pending"; - const canRequestDeveloperAccess = - profileRole === "user" && - !isLoadingRequestStatus && - !hasDeveloperAccess && - !isDeveloperRequestPending; + const { + hasDeveloperAccess, + isDeveloperRequestPending, + canRequestDeveloperAccess, + isLoadingDeveloperAccessGate, + } = useDeveloperAccessGate({ + hasAccessToken, + profileRole, + tenantId, + isLoadingIdentity: isLoadingMe, + }); const distribution = useMemo( () => buildClientDistribution(clients), [clients], @@ -616,8 +612,7 @@ function GlobalOverviewPage() { }; if ( - profileRole === "user" && - (isLoadingMe || isLoadingRequestStatus) + isLoadingDeveloperAccessGate ) { return (