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
63 changes: 63 additions & 0 deletions docs/docs/sdk/web/admin/utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,69 @@ const query = transformDataTableQueryToRQLRequest(tableQuery, {

---

## `useTerminology`

Hook that returns customizable entity labels based on the terminology config provided via `AdminConfigProvider`. Allows deployments to rename entities (e.g. "Organization" to "Workspace") across the UI without code changes.

The terminology is configured at runtime via the `/configs` endpoint:

```json
{
"terminology": {
"organization": { "singular": "Workspace", "plural": "Workspaces" },
"user": { "singular": "Person", "plural": "People" }
}
}
```

### Usage

```tsx
import { useTerminology } from "@raystack/frontier/admin";

const MyView = () => {
const t = useTerminology();

return (
<div>
<h1>{t.organization({ plural: true, case: "capital" })}</h1>
<p>Create a new {t.organization({ case: "lower" })}</p>
</div>
);
};
```

### Options

Each entity function accepts an optional options object:

| Option | Type | Description |
| --- | --- | --- |
| `plural` | `boolean` | Use plural form. Defaults to `false`. |
| `case` | `"lower" \| "upper" \| "capital"` | Text casing. Returns the configured value as-is when omitted. |

### Available entities

`t.organization()`, `t.project()`, `t.team()`, `t.member()`, `t.user()`, `t.appName()`

---

## `useAdminPaths`

Hook that returns URL-safe slugs derived from terminology config, for use in dynamic routing.

```tsx
import { useAdminPaths } from "@raystack/frontier/admin";

const paths = useAdminPaths();
// Default: { organizations: "organizations", users: "users", projects: "projects", ... }
// With custom terminology: { organizations: "workspaces", users: "people", ... }

navigate(`/${paths.organizations}/${id}`);
```

---

## `ConnectRPCPaginatedResponse`

Type for paginated API responses.
Expand Down
32 changes: 32 additions & 0 deletions web/apps/admin/configs.dev.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"title": "Frontier Admin",
"app_url": "localhost:5173",
"token_product_id": "token",
"organization_types": [],
"webhooks": {
"enable_delete": false
},
"terminology": {
"organization": {
"singular": "Organization",
"plural": "Organizations"
},
"project": {
"singular": "Project",
"plural": "Projects"
},
"team": {
"singular": "Team",
"plural": "Teams"
},
"member": {
"singular": "Member",
"plural": "Members"
},
"user": {
"singular": "User",
"plural": "Users"
},
"appName": "Frontier Admin"
}
}
167 changes: 87 additions & 80 deletions web/apps/admin/src/components/Sidebar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import CpuChipIcon from "~/assets/icons/cpu-chip.svg?react";
import { AppContext } from "~/contexts/App";
import { MoonIcon, SunIcon } from "@radix-ui/react-icons";
import { Link, useLocation } from "react-router-dom";
import { useTerminology, useAdminPaths } from "@raystack/frontier/admin";

export type NavigationItemsTypes = {
to?: string;
Expand All @@ -36,90 +37,96 @@ export type NavigationItemsTypes = {

const BRAND_NAME = "Frontier";

const navigationItems: NavigationItemsTypes[] = [
{
name: "Organizations",
to: `/organizations`,
icon: <OrganizationIcon />,
},
{
name: "Users",
to: `/users`,
icon: <UserIcon />,
},
{
name: "Audit Logs",
to: `/audit-logs`,
icon: <CpuChipIcon />,
},
{
name: "Invoices",
to: `/invoices`,
icon: <InvoicesIcon />,
},
{
name: "Authorization",
subItems: [
{
name: "Roles",
to: `/roles`,
icon: <RolesIcon />,
},
],
},
{
name: "Billing",
subItems: [
{
name: "Products",
to: `/products`,
icon: <ProductsIcon />,
},
{
name: "Plans",
to: `/plans`,
icon: <PlansIcon />,
},
],
},
{
name: "Features",
subItems: [
{
name: "Webhooks",
to: `/webhooks`,
icon: <WebhooksIcon />,
},
],
},
{
name: "Settings",
subItems: [
{
name: "Preferences",
to: `/preferences`,
icon: <PreferencesIcon />,
},
{
name: "Admins",
to: `/super-admins`,
icon: <AdminsIcon />,
},
],
},
// {
// name: "Projects",
// to: `/projects`,
// },
const useNavigationItems = (): NavigationItemsTypes[] => {
const t = useTerminology();
const paths = useAdminPaths();

// {
// name: "Groups",
// to: `/groups`,
// },
];
return [
{
name: t.organization({ plural: true, case: "capital" }),
to: `/${paths.organizations}`,
icon: <OrganizationIcon />,
},
{
name: t.user({ plural: true, case: "capital" }),
to: `/${paths.users}`,
icon: <UserIcon />,
},
{
name: "Audit Logs",
to: `/audit-logs`,
icon: <CpuChipIcon />,
},
{
name: "Invoices",
to: `/invoices`,
icon: <InvoicesIcon />,
},
{
name: "Authorization",
subItems: [
{
name: "Roles",
to: `/roles`,
icon: <RolesIcon />,
},
],
},
{
name: "Billing",
subItems: [
{
name: "Products",
to: `/products`,
icon: <ProductsIcon />,
},
{
name: "Plans",
to: `/plans`,
icon: <PlansIcon />,
},
],
},
{
name: "Features",
subItems: [
{
name: "Webhooks",
to: `/webhooks`,
icon: <WebhooksIcon />,
},
],
},
{
name: "Settings",
subItems: [
{
name: "Preferences",
to: `/preferences`,
icon: <PreferencesIcon />,
},
{
name: "Admins",
to: `/super-admins`,
icon: <AdminsIcon />,
},
],
},
// {
// name: t.project({ plural: true, case: "capital" }),
// to: `/projects`,
// },

// {
// name: t.team({ plural: true, case: "capital" }),
// to: `/groups`,
// },
];
};

export default function IAMSidebar() {
const location = useLocation();
const navigationItems = useNavigationItems();

const isActive = (navlink?: string) => {
const firstPathPart = location.pathname.split("/")[1];
Expand Down
5 changes: 4 additions & 1 deletion web/apps/admin/src/contexts/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
type User,
} from "@raystack/proton/frontier";
import { Config, defaultConfig } from "~/utils/constants";
import { AdminConfigProvider } from "@raystack/frontier/admin";

interface AppContextValue {
isAdmin: boolean;
Expand Down Expand Up @@ -62,7 +63,9 @@ export const AppContextProvider: React.FC<PropsWithChildren> = function ({
config,
user,
}}>
{children}
<AdminConfigProvider config={config}>
{children}
</AdminConfigProvider>
</AppContext.Provider>
);
};
5 changes: 3 additions & 2 deletions web/apps/admin/src/pages/admins/AdminsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { AdminsView } from "@raystack/frontier/admin";
import { AdminsView, useAdminPaths } from "@raystack/frontier/admin";
import { useNavigate } from "react-router-dom";

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

return (
<AdminsView
onNavigateToOrg={(orgId: string) => navigate(`/organizations/${orgId}`)}
onNavigateToOrg={(orgId: string) => navigate(`/${paths.organizations}/${orgId}`)}
/>
);
}
6 changes: 4 additions & 2 deletions web/apps/admin/src/pages/organizations/list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
OrganizationListView,
useAdminPaths,
} from "@raystack/frontier/admin";
import { useCallback, useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
Expand All @@ -17,15 +18,16 @@ async function loadCountries(): Promise<string[]> {
export function OrganizationListPage() {
const navigate = useNavigate();
const { config } = useContext(AppContext);
const paths = useAdminPaths();
const [countries, setCountries] = useState<string[]>([]);

useEffect(() => {
loadCountries().then(setCountries);
}, []);

const onNavigateToOrg = useCallback(
(id: string) => navigate(`/organizations/${id}`),
[navigate],
(id: string) => navigate(`/${paths.organizations}/${id}`),
[navigate, paths.organizations],
);

const onExportCsv = useCallback(async () => {
Expand Down
9 changes: 5 additions & 4 deletions web/apps/admin/src/pages/users/UsersPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UsersView } from "@raystack/frontier/admin";
import { UsersView, useAdminPaths } from "@raystack/frontier/admin";
import { useCallback } from "react";
import { useParams, useNavigate, useLocation } from "react-router-dom";
import { clients } from "~/connect/clients";
Expand All @@ -10,22 +10,23 @@ export function UsersPage() {
const { userId } = useParams();
const navigate = useNavigate();
const location = useLocation();
const paths = useAdminPaths();

const onExportUsers = useCallback(async () => {
await exportCsvFromStream(adminClient.exportUsers, {}, "users.csv");
}, []);

const onNavigateToUser = useCallback(
(id: string) => {
navigate(`/users/${id}/security`);
navigate(`/${paths.users}/${id}/security`);
},
[navigate],
[navigate, paths.users],
);

return (
<UsersView
selectedUserId={userId}
onCloseDetail={() => navigate("/users")}
onCloseDetail={() => navigate(`/${paths.users}`)}
onExportUsers={onExportUsers}
onNavigateToUser={onNavigateToUser}
currentPath={location.pathname}
Expand Down
Loading
Loading