Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion web/apps/admin/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { Flex } from "@raystack/apsara";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";
import "@raystack/apsara/normalize.css";
import "@raystack/apsara/style.css";
import "./App.css";
import IAMSidebar from "./components/Sidebar";
import LoadingState from "./components/states/Loading";

function App() {
return (
<div style={{ display: "flex", height: "100vh", overflow: "hidden" }}>
<IAMSidebar />
<Flex style={{ flexGrow: "1", overflow: "auto" }}>
<Outlet />
{/* Boundary for the lazily-loaded route pages. Sits inside the layout
so the sidebar stays mounted while a route chunk loads. */}
<Suspense fallback={<LoadingState />}>
<Outlet />
</Suspense>
</Flex>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/admins/AdminsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { AdminsView, useAdminPaths } from "@raystack/frontier/admin";
import { useNavigate } from "react-router-dom";
import AdminsIcon from "~/assets/icons/admins.svg?react";

export function AdminsPage() {
export default function AdminsPage() {
const navigate = useNavigate();
const paths = useAdminPaths();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/audit-logs/AuditLogsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { RQLExportRequest, RQLRequest } from "@raystack/proton/frontier";

const adminClient = clients.admin({ useBinary: true });

export function AuditLogsPage() {
export default function AuditLogsPage() {
const navigate = useNavigate();
const onExportCsv = useCallback(async (query: RQLRequest) => {
await exportCsvFromStream(
Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/invoices/InvoicesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { InvoicesView } from "@raystack/frontier/admin";

export function InvoicesPage() {
export default function InvoicesPage() {
return <InvoicesView />;
}
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/organizations/details/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ async function loadCountries(): Promise<string[]> {
return (data.default as { name: string }[]).map((c) => c.name);
}

export function OrganizationDetailsPage() {
export default function OrganizationDetailsPage() {
const { organizationId } = useParams<{ organizationId: string }>();
const location = useLocation();
const navigate = useNavigate();
Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/organizations/list/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ async function loadCountries(): Promise<string[]> {
return (data.default as { name: string }[]).map((c) => c.name);
}

export function OrganizationListPage() {
export default function OrganizationListPage() {
const navigate = useNavigate();
const { config } = useContext(AppContext);
const paths = useAdminPaths();
Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/plans/PlansPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PlansView } from "@raystack/frontier/admin";
import { useNavigate, useParams } from "react-router-dom";
import PlansIcon from "~/assets/icons/plans.svg?react";

export function PlansPage() {
export default function PlansPage() {
const { planId } = useParams();
const navigate = useNavigate();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/preferences/PreferencesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
import { PreferencesView } from "@raystack/frontier/admin";
import PreferencesIcon from "~/assets/icons/preferences.svg?react";

export function PreferencesPage() {
export default function PreferencesPage() {
const { name } = useParams();
const navigate = useNavigate();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/products/ProductPricesPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ProductPricesView } from "@raystack/frontier/admin";
import { useParams, useNavigate } from "react-router-dom";

export function ProductPricesPage() {
export default function ProductPricesPage() {
const { productId } = useParams();
const navigate = useNavigate();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/products/ProductsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ProductsView } from "@raystack/frontier/admin";
import { useParams, useNavigate } from "react-router-dom";
import ProductsIcon from "~/assets/icons/products.svg?react";

export function ProductsPage() {
export default function ProductsPage() {
const { productId } = useParams();
const navigate = useNavigate();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/roles/RolesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { RolesView } from "@raystack/frontier/admin";
import { useParams, useNavigate } from "react-router-dom";
import RolesIcon from "~/assets/icons/roles.svg?react";

export function RolesPage() {
export default function RolesPage() {
const { roleId } = useParams();
const navigate = useNavigate();

Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/users/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { exportCsvFromStream } from "~/utils/helper";

const adminClient = clients.admin({ useBinary: true });

export function UsersPage() {
export default function UsersPage() {
const { userId } = useParams();
const navigate = useNavigate();
const location = useLocation();
Expand Down
2 changes: 1 addition & 1 deletion web/apps/admin/src/pages/webhooks/WebhooksPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { WebhooksView } from "@raystack/frontier/admin";
import { AppContext } from "~/contexts/App";
import WebhooksIcon from "~/assets/icons/webhooks.svg?react";

export function WebhooksPage() {
export default function WebhooksPage() {
const { config } = useContext(AppContext);
const { webhookId } = useParams();
const navigate = useNavigate();
Expand Down
90 changes: 61 additions & 29 deletions web/apps/admin/src/routes.tsx
Original file line number Diff line number Diff line change
@@ -1,43 +1,75 @@
import * as R from "ramda";
import { memo, useContext } from "react";
import { lazy, memo, useContext } from "react";
import { Navigate, Route, Routes } from "react-router-dom";

import LoadingState from "./components/states/Loading";
import UnauthorizedState from "./components/states/Unauthorized";

// Eager: the app shell and the unauthenticated flow. Keeping these in the
// initial bundle means the login screen paints instantly.
import App from "./App";
import { PlansPage } from "./pages/plans/PlansPage";
import Login from "./containers/login";
import MagicLink from "./containers/magiclink";

import { PreferencesPage } from "./pages/preferences/PreferencesPage";
import { ProductsPage } from "./pages/products/ProductsPage";
import { ProductPricesPage } from "./pages/products/ProductPricesPage";

import { RolesPage } from "./pages/roles/RolesPage";

import { AppContext } from "./contexts/App";
import { AdminsPage } from "./pages/admins/AdminsPage";
import { WebhooksPage } from "./pages/webhooks/WebhooksPage";
import AuthLayout from "./layout/auth";

import { OrganizationListPage } from "./pages/organizations/list";
import { OrganizationDetailsPage } from "./pages/organizations/details";
import {
OrganizationSecurity,
OrganizationMembersView,
OrganizationProjectsView,
OrganizationInvoicesView,
OrganizationTokensView,
OrganizationApisView,
OrganizationPatView,
useAdminPaths,
} from "@raystack/frontier/admin";

import { UsersPage } from "./pages/users/UsersPage";

import { InvoicesPage } from "./pages/invoices/InvoicesPage";
import { AuditLogsPage } from "./pages/audit-logs/AuditLogsPage";
import { AppContext } from "./contexts/App";
// useAdminPaths is a hook called during render, so it must stay a static import.
import { useAdminPaths } from "@raystack/frontier/admin";

// Lazily-loaded route pages — each becomes its own async chunk, so the heavy
// admin code stays out of the initial/unauthenticated bundle. The pages are
// default exports, so React.lazy imports them directly (per the React docs).
const PlansPage = lazy(() => import("./pages/plans/PlansPage"));
const PreferencesPage = lazy(() => import("./pages/preferences/PreferencesPage"));
const ProductsPage = lazy(() => import("./pages/products/ProductsPage"));
const ProductPricesPage = lazy(() => import("./pages/products/ProductPricesPage"));
const RolesPage = lazy(() => import("./pages/roles/RolesPage"));
const AdminsPage = lazy(() => import("./pages/admins/AdminsPage"));
const WebhooksPage = lazy(() => import("./pages/webhooks/WebhooksPage"));
const OrganizationListPage = lazy(() => import("./pages/organizations/list"));
const OrganizationDetailsPage = lazy(() => import("./pages/organizations/details"));
const UsersPage = lazy(() => import("./pages/users/UsersPage"));
const InvoicesPage = lazy(() => import("./pages/invoices/InvoicesPage"));
const AuditLogsPage = lazy(() => import("./pages/audit-logs/AuditLogsPage"));

// Organization detail child views come from the SDK barrel. Each lazy() shares
// the same `@raystack/frontier/admin` chunk (the module promise is cached), so
// the admin SDK loads once on the first org route and stays cached after.
const OrganizationSecurity = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationSecurity,
})),
);
const OrganizationMembersView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationMembersView,
})),
);
const OrganizationProjectsView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationProjectsView,
})),
);
const OrganizationInvoicesView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationInvoicesView,
})),
);
const OrganizationTokensView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationTokensView,
})),
);
const OrganizationApisView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationApisView,
})),
);
const OrganizationPatView = lazy(() =>
import("@raystack/frontier/admin").then((m) => ({
default: m.OrganizationPatView,
})),
);
Comment on lines +38 to +72
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect the org details page for a nested Suspense around its Outlet.
fd -t f 'index.tsx' web/apps/admin/src/pages/organizations/details --exec rg -nP 'Suspense|Outlet|fallback' {}

Repository: raystack/frontier

Length of output: 163


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Find App.tsx top-level Suspense around routed Outlet =="
if [ -f web/apps/admin/src/App.tsx ]; then
  rg -n --context 3 "Suspense|Outlet|LoadingState" web/apps/admin/src/App.tsx || true
else
  echo "App.tsx not found at web/apps/admin/src/App.tsx"
fi

echo
echo "== Inspect organizations/details page wrapper (web/apps/admin/src/pages/organizations/details) =="
fd -t f 'index.tsx' web/apps/admin/src/pages/organizations/details --exec rg -n --context 3 "Suspense|Outlet|fallback|OrganizationDetails" {} || true

echo
echo "== Inspect OrganizationDetailsView implementation (web/sdk/admin/views/organizations/details) =="
fd -t f 'index.tsx' web/sdk/admin/views/organizations/details --exec rg -n --context 3 "Suspense|children|fallback|Outlet" {} || true

echo
echo "== Locate where {children} is rendered in sdk details view =="
fd -t f 'index.tsx' web/sdk/admin/views/organizations/details --exec rg -n --context 2 "\\{children\\}" {} || true

Repository: raystack/frontier

Length of output: 4099


Add a nested Suspense boundary around the org-detail tab Outlet/children.

web/apps/admin/src/pages/organizations/details/index.tsx passes <Outlet /> into OrganizationDetailsView as children, and web/sdk/admin/views/organizations/details/index.tsx renders {children} directly with no local <Suspense fallback>. When a lazy org-detail tab suspends, it bubbles to the top-level <Suspense fallback={<LoadingState />}> in web/apps/admin/src/App.tsx, which replaces the whole org-details layout (header/tabs) instead of keeping it mounted. Add a <Suspense> boundary around the org-detail outlet/content in the org-details page/view.


export default memo(function AppRoutes() {
const { isAdmin, isLoading, user } = useContext(AppContext);
Expand Down
Loading