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 (