@nsxbet/admin-sdk
v0.2.0
Published
SDK for building NSX Admin modules with integrated shell
Maintainers
Readme
@nsxbet/admin-sdk
SDK for building admin modules for the NSX Admin platform.
Installation
bun add @nsxbet/admin-sdk @nsxbet/admin-uiPeer dependencies (install these too):
bun add react react-dom react-router-dom i18next react-i18next
bun add -D @vitejs/plugin-react vite tailwindcss postcss autoprefixer typescriptQuick Start
This SDK enables you to build admin modules that integrate with the NSX Admin shell. Modules are loaded dynamically via React.lazy and share the shell's Router context.
Key concepts:
- Modules export a default React component from
spa.tsx - Modules share the shell's
BrowserRouter- useuseNavigate()fromreact-router-domdirectly - Use
@nsxbet/admin-uifor consistent UI components - Define module metadata in
admin.module.json
Architecture Overview
┌─────────────────────────────────────────────────────────────────┐
│ Shell (BrowserRouter) │
│ ├── TopBar, LeftNav, CommandPalette │
│ └── <Route path="/your-module/*"> │
│ └── React.lazy(() => import(baseUrl/spa.js)) │
│ └── Your App component (shares Router context) │
└─────────────────────────────────────────────────────────────────┘Two entry points pattern:
| File | Purpose | When used |
|------|---------|-----------|
| src/spa.tsx | Default export of App | Shell loads this via React.lazy |
| src/standalone.tsx | Full app with AdminShell wrapper | Local development (npm run dev) |
Complete Module Example
Below are all the files needed for a working module. Copy these and modify for your needs.
File: vite.config.ts
import { defineModuleConfig } from "@nsxbet/admin-sdk/vite";
import react from "@vitejs/plugin-react";
export default defineModuleConfig({
port: 3003,
plugins: [react()],
});File: admin.module.json
{
"id": "@admin/my-module",
"title": "My Module",
"description": "Description of what this module does",
"version": "1.0.0",
"category": "Tools",
"icon": "clipboard-list",
"routeBase": "/my-module",
"keywords": ["my", "module", "example"],
"commands": [
{
"id": "list",
"title": "List Items",
"route": "/my-module/list",
"icon": "file-text"
},
{
"id": "new",
"title": "New Item",
"route": "/my-module/new",
"icon": "plus"
}
],
"permissions": {
"view": ["admin.mymodule.view"],
"edit": ["admin.mymodule.edit"],
"delete": ["admin.mymodule.delete"]
},
"owners": {
"team": "Platform",
"supportChannel": "#platform-support"
}
}File: src/spa.tsx
/**
* Module entry point for shell mode.
* The module shares the shell's Router context.
*/
import { App } from "./App";
export default App;File: src/standalone.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { AdminShell, initI18n, i18n } from "@nsxbet/admin-sdk";
import type { AdminModuleManifest } from "@nsxbet/admin-sdk";
import { App } from "./App";
import manifest from "../admin.module.json";
import "./index.css";
// Import module translations (optional - for i18n support)
import enUS from "./i18n/locales/en-US.json";
import ptBR from "./i18n/locales/pt-BR.json";
// Initialize i18n BEFORE shell renders
initI18n();
// Register module translations with namespace matching your module
const NAMESPACE = "mymodule";
i18n.addResourceBundle("en-US", NAMESPACE, enUS, true, true);
i18n.addResourceBundle("pt-BR", NAMESPACE, ptBR, true, true);
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<AdminShell modules={[manifest as AdminModuleManifest]}>
<App />
</AdminShell>
</React.StrictMode>
);File: src/App.tsx
import { Routes, Route, Navigate } from "react-router-dom";
import { ItemList } from "./ItemList";
import { NewItem } from "./NewItem";
export function App() {
return (
<Routes>
{/* Redirect root to list */}
<Route path="/" element={<Navigate to="list" replace />} />
<Route path="list" element={<ItemList />} />
<Route path="new" element={<NewItem />} />
</Routes>
);
}File: src/ItemList.tsx
import { useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth, usePlatformAPI, useTelemetry } from "@nsxbet/admin-sdk";
import { Button, Card, CardContent, Badge } from "@nsxbet/admin-ui";
export function ItemList() {
const navigate = useNavigate();
const { hasPermission } = useAuth();
const { api } = usePlatformAPI();
const { track } = useTelemetry();
const canEdit = hasPermission("admin.mymodule.edit");
useEffect(() => {
// Set breadcrumbs
api?.nav.setBreadcrumbs([
{ label: "My Module" },
{ label: "List" }
]);
// Track page view
track("page.viewed", { page: "item_list" });
}, [api, track]);
return (
<div className="p-6 max-w-5xl mx-auto">
<div className="mb-6 flex items-center justify-between">
<h1 className="text-3xl font-bold">Items</h1>
{canEdit && (
<Button onClick={() => navigate("/my-module/new")}>
New Item
</Button>
)}
</div>
<Card>
<CardContent className="p-4">
<p className="text-muted-foreground">No items yet.</p>
</CardContent>
</Card>
</div>
);
}File: src/index.css
@import "@nsxbet/admin-ui/styles.css";
@tailwind base;
@tailwind components;
@tailwind utilities;File: tailwind.config.js
Use withAdminSdk which automatically includes the UI preset and SDK/UI content paths:
import { withAdminSdk } from "@nsxbet/admin-sdk/tailwind";
/** @type {import('tailwindcss').Config} */
export default withAdminSdk({
content: ["./index.html", "./src/**/*.{ts,tsx}"],
});File: package.json
{
"name": "@admin/my-module",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build && cp admin.module.json dist/",
"preview": "vite preview"
},
"dependencies": {
"@nsxbet/admin-sdk": "latest",
"@nsxbet/admin-ui": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.20.0",
"i18next": "^25.0.0",
"react-i18next": "^16.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"@vitejs/plugin-react": "^4.2.0",
"autoprefixer": "^10.4.16",
"postcss": "^8.4.32",
"tailwindcss": "^3.4.0",
"typescript": "^5.2.0",
"vite": "^5.0.0"
}
}File: tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src", "admin.module.json"]
}File: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Module - Standalone</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/standalone.tsx"></script>
</body>
</html>File: postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};Manifest Schema (admin.module.json)
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | ✅ | Unique identifier (e.g., @admin/tasks) |
| title | string | ✅ | Human-readable title |
| routeBase | string | ✅ | Base route path (must start with /) |
| description | string | | What the module does |
| version | string | | Semantic version |
| category | string | | Navigation grouping |
| icon | string | | Lucide icon name in kebab-case |
| keywords | string[] | | Search keywords |
| commands | Command[] | | Available actions |
| permissions | object | | Permission configuration |
| owners | object | | Team ownership info |
Command Schema
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| id | string | ✅ | Unique command identifier |
| title | string | ✅ | Command title |
| route | string | ✅ | Full route path |
| icon | string | | Lucide icon name |
| keywords | string[] | | Search keywords |
Icon Names
Use Lucide icon names in kebab-case:
| Category | Icons |
|----------|-------|
| Navigation | home, settings, menu, search, chevron-right |
| Actions | plus, edit, trash, copy, check |
| Content | file, file-text, folder, clipboard-list |
| Users | user, users |
| Status | alert-circle, info, ban, lock |
Vite Configuration
The SDK provides defineModuleConfig to simplify Vite setup:
import { defineModuleConfig } from "@nsxbet/admin-sdk/vite";
import react from "@vitejs/plugin-react";
export default defineModuleConfig({
port: 3003,
plugins: [react()], // REQUIRED: you must add the React plugin
});Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| port | number | required | Dev server port |
| entry | string | "./src/spa.tsx" | Entry file path |
| outDir | string | "dist" | Output directory |
| plugins | Plugin[] | [] | Additional Vite plugins |
| additionalExternals | string[] | [] | Extra externals |
| overrides | object | {} | Override any Vite config |
Shared Externals
These dependencies are provided by the shell (do not bundle them):
["react", "react-dom", "react-router-dom", "i18next", "react-i18next"]SDK Hooks
useAuth()
Access authentication and permissions.
import { useAuth } from "@nsxbet/admin-sdk";
function MyComponent() {
const { hasPermission, getUser, getAccessToken, logout } = useAuth();
if (!hasPermission("admin.mymodule.view")) {
return <div>Access denied</div>;
}
const user = getUser();
console.log(user.email, user.displayName);
}| Method | Returns | Description |
|--------|---------|-------------|
| hasPermission(perm) | boolean | Check if user has permission |
| getUser() | User | Get current user info |
| getAccessToken() | Promise<string> | Get JWT token |
| logout() | void | Log out user |
usePlatformAPI()
Access the shell's platform API.
import { usePlatformAPI } from "@nsxbet/admin-sdk";
function MyComponent() {
const { api, isShellMode } = usePlatformAPI();
useEffect(() => {
api?.nav.setBreadcrumbs([
{ label: "My Module" },
{ label: "Current Page" }
]);
}, [api]);
}useFetch()
Make authenticated API requests.
import { useFetch } from "@nsxbet/admin-sdk";
function MyComponent() {
const fetch = useFetch();
const loadData = async () => {
const response = await fetch("/api/data");
const data = await response.json();
};
}useTelemetry()
Track events and errors.
import { useTelemetry } from "@nsxbet/admin-sdk";
function MyComponent() {
const { track, trackError } = useTelemetry();
const handleClick = () => {
track("button.clicked", { buttonId: "save" });
};
const handleError = (error: Error) => {
trackError(error, { context: "save_operation" });
};
}useI18n()
Manage translations and locale.
import { useI18n } from "@nsxbet/admin-sdk";
function MyComponent() {
const { t, locale, setLocale } = useI18n();
return (
<div>
<h1>{t("common.title")}</h1>
<p>Current locale: {locale}</p>
</div>
);
}Navigation
Use useNavigate from react-router-dom directly:
import { useNavigate } from "react-router-dom";
function MyComponent() {
const navigate = useNavigate();
return (
<Button onClick={() => navigate("/my-module/new")}>
New Item
</Button>
);
}Custom Mock Users for Development
During standalone development, the SDK uses an in-memory auth client with default mock users. You can customize these users to match your module's specific permission requirements.
Using createMockUsersFromRoles()
The simplest way to create custom mock users is with the createMockUsersFromRoles() factory:
import {
AdminShell,
createInMemoryAuthClient,
createMockUsersFromRoles,
} from "@nsxbet/admin-sdk";
// Define roles for your module
const mockUsers = createMockUsersFromRoles({
admin: [
"admin.payments.view",
"admin.payments.edit",
"admin.payments.delete",
],
editor: ["admin.payments.view", "admin.payments.edit"],
viewer: ["admin.payments.view"],
noAccess: [],
});
// Create auth client with custom users
const authClient = createInMemoryAuthClient({ users: mockUsers });
// Use in standalone.tsx
ReactDOM.createRoot(document.getElementById("root")!).render(
<AdminShell authClient={authClient} modules={[manifest]}>
<App />
</AdminShell>
);This creates 4 standard user types with your custom roles:
- Admin User - Full access (all roles in
adminarray) - Editor User - View/edit access (roles in
editorarray) - Viewer User - View-only access (roles in
viewerarray) - No Access User - No permissions (roles in
noAccessarray)
Using Custom Users Directly
For more control, pass custom MockUser objects directly:
import { createInMemoryAuthClient, type MockUser } from "@nsxbet/admin-sdk";
const customUsers: MockUser[] = [
{
id: "super-admin",
email: "[email protected]",
displayName: "Super Admin",
roles: ["*"], // Wildcard: all permissions
},
{
id: "payments-admin",
email: "[email protected]",
displayName: "Payments Admin",
roles: ["admin.payments.view", "admin.payments.edit"],
},
];
const authClient = createInMemoryAuthClient({ users: customUsers });Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| users | MockUser[] | required | Mock users available for selection |
| storageKey | string | "@nsxbet/auth" | localStorage key for persistence |
DO NOT (Common Mistakes)
❌ DO NOT use MemoryRouter
// WRONG - don't create your own router
import { MemoryRouter, Routes, Route } from "react-router-dom";
function App() {
return (
<MemoryRouter> {/* ❌ WRONG */}
<Routes>
<Route path="/" element={<List />} />
</Routes>
</MemoryRouter>
);
}// CORRECT - use Routes directly, shell provides BrowserRouter
import { Routes, Route } from "react-router-dom";
function App() {
return (
<Routes>
<Route path="/" element={<List />} />
</Routes>
);
}❌ DO NOT create BrowserRouter in spa.tsx
// WRONG - shell already provides BrowserRouter
import { BrowserRouter } from "react-router-dom";
export default function App() {
return (
<BrowserRouter> {/* ❌ WRONG */}
<MyRoutes />
</BrowserRouter>
);
}// CORRECT - just export the component
import { App } from "./App";
export default App;❌ DO NOT forget the React plugin
// WRONG - missing React plugin
export default defineModuleConfig({
port: 3003,
// ❌ No plugins!
});// CORRECT - include React plugin
import react from "@vitejs/plugin-react";
export default defineModuleConfig({
port: 3003,
plugins: [react()], // ✅
});❌ DO NOT use external databases
// WRONG - no Supabase, Firebase, or external BaaS
import { createClient } from "@supabase/supabase-js"; // ❌
import { initializeApp } from "firebase/app"; // ❌// CORRECT - use authenticated fetch for internal APIs
import { useFetch } from "@nsxbet/admin-sdk";
const fetch = useFetch();
const data = await fetch("/api/internal-endpoint");❌ DO NOT import useNavigate from SDK
// WRONG - useNavigate is not exported from SDK
import { useNavigate } from "@nsxbet/admin-sdk"; // ❌// CORRECT - import from react-router-dom
import { useNavigate } from "react-router-dom"; // ✅Troubleshooting
"Failed to resolve module specifier 'react'"
Cause: Module is bundling React instead of using shell's version.
Solution: Ensure vite.config.ts uses defineModuleConfig or has external: ["react", "react-dom", "react-router-dom"].
"process is not defined"
Cause: Module code references Node.js globals.
Solution: defineModuleConfig handles this automatically. If using custom config, add:
define: {
"process.env": {},
"process.env.NODE_ENV": JSON.stringify("production"),
}Module not loading in shell
Checklist:
- ✅
spa.tsxexports default component - ✅
admin.module.jsonhas validid,title,routeBase - ✅ Build outputs to
dist/withspa-[hash].js - ✅ Module is registered in shell's catalog
Styles not working
Checklist:
- ✅
index.cssimports@nsxbet/admin-ui/styles.css - ✅
tailwind.config.jsuseswithAdminSdkfrom@nsxbet/admin-sdk/tailwind - ✅
postcss.config.jsexists with tailwindcss plugin
Running Your Module
# Development (standalone with shell UI)
bun run dev
# Open http://localhost:3003
# Build for production
bun run build
# Output: dist/assets/spa-[hash].js
# Preview built module
bun run previewTypes
import type {
AdminModuleManifest,
ModuleCommand,
PlatformAPI,
User,
Breadcrumb,
} from "@nsxbet/admin-sdk";License
UNLICENSED - Internal use only
