docs: added basic Admin UI configuration documentation

Added a new `BkndAdminConfig` type to streamline Admin UI configuration options, consolidating properties for base path, logo return path, theme, entities, and app shell settings. Updated `BkndAdminProps` to utilize this new configuration type. Additionally, introduced a new documentation section for extending the Admin UI, detailing customization options and providing examples for advanced usage.
This commit is contained in:
dswbx
2025-09-25 10:45:10 +02:00
parent daafee2c06
commit 560379bd89
6 changed files with 449 additions and 242 deletions

View File

@@ -8,21 +8,9 @@ import * as AppShell from "ui/layouts/AppShell/AppShell";
import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client";
import { createMantineTheme } from "./lib/mantine/theme";
import { Routes } from "./routes";
import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "ui/options";
import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "./options";
export type BkndAdminProps = {
/**
* Base URL of the API, only needed if you are not using the `withProvider` prop
*/
baseUrl?: string;
/**
* Whether to wrap Admin in a <ClientProvider />
*/
withProvider?: boolean | ClientProviderProps;
/**
* Admin UI customization options
*/
config?: {
export type BkndAdminConfig = {
/**
* Base path of the Admin UI
* @default `/`
@@ -47,6 +35,20 @@ export type BkndAdminProps = {
*/
appShell?: BkndAdminAppShellOptions;
};
export type BkndAdminProps = {
/**
* Base URL of the API, only needed if you are not using the `withProvider` prop
*/
baseUrl?: string;
/**
* Whether to wrap Admin in a `<ClientProvider />`
*/
withProvider?: boolean | ClientProviderProps;
/**
* Admin UI customization options
*/
config?: BkndAdminConfig;
children?: ReactNode;
};

View File

@@ -1,4 +1,4 @@
export { default as Admin, type BkndAdminProps } from "./Admin";
export { default as Admin, type BkndAdminProps, type BkndAdminConfig } from "./Admin";
export * from "./components/form/json-schema-form";
export { JsonViewer } from "./components/code/JsonViewer";
export type * from "./options";

View File

@@ -0,0 +1,201 @@
---
title: Admin UI
tags: ["documentation"]
---
import { TypeTable } from "fumadocs-ui/components/type-table";
bknd features an integrated Admin UI that can be used to:
- fully manage your backend visually when run in [`db` mode](/usage/introduction/#ui-only-mode)
- manage your database contents
- manage your media contents
In case you're using bknd with a [React framework](integration/introduction/#start-with-a-framework) and render the Admin as React component, you can go further and customize the Admin UI to your liking.
<AutoTypeTable path="../app/src/ui/Admin.tsx" name="BkndAdminProps" />
## Advanced Example
The following example shows how to customize the Admin UI for each entity.
- adds a custom action item to the user menu (top right)
- adds a custom action item to the entity list
- adds a custom action item to the entity create/update form
- overrides the rendering of the title field
- renders a custom header for the entity
- renders a custom footer for the entity
- adds a custom route
```tsx
import { Admin } from "bknd/ui";
import { Route } from "wouter";
export function App() {
return (
<Admin
withProvider
config={{
appShell: {
// add a custom user menu item (top right)
userMenu: [
{
label: "Custom",
onClick: () => alert("custom"),
},
],
},
entities: {
// use any entity that is registered
tests: {
actions: (context, entity, data) => ({
primary: [
// this action is only rendered in the update context
context === "update" && {
children: "another",
onClick: () => alert("another"),
},
],
context: [
// this action is always rendered in the dropdown
{
label: "Custom",
onClick: () =>
alert(
"custom: " +
JSON.stringify({ context, entity: entity.name, data }),
),
},
],
}),
// render a header for the entity
header: (context, entity, data) => <div>test header</div>,
// override the rendering of the title field
fields: {
title: {
render: (context, entity, field, ctx) => {
return (
<input
type="text"
value={ctx.value}
onChange={(e) => ctx.handleChange(e.target.value)}
/>
);
},
},
},
},
// system entities work too
users: {
header: () => {
return <div>System entity</div>;
},
},
},
}}
>
{/* You may also add custom routes, these always have precedence over the Admin routes */}
<Route path="/data/custom">
<div>custom</div>
</Route>
</Admin>
);
}
```
## `config`
<AutoTypeTable path="../app/src/ui/Admin.tsx" name="BkndAdminConfig" />
### `entities`
With the `entities` option, you can customize the Admin UI for each entity. You can override the header, footer, add additional actions, and override each field rendering.
```ts
export type BkndAdminEntityContext = "list" | "create" | "update";
export type BkndAdminEntitiesOptions = {
[E in keyof DB]?: BkndAdminEntityOptions<E>;
};
export type BkndAdminEntityOptions<E extends keyof DB | string> = {
/**
* Header to be rendered depending on the context
*/
header?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => ReactNode | void | undefined;
/**
* Footer to be rendered depending on the context
*/
footer?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => ReactNode | void | undefined;
/**
* Actions to be rendered depending on the context
*/
actions?: (
context: BkndAdminEntityContext,
entity: Entity,
data?: DB[E],
) => {
/**
* Primary actions are always visible
*/
primary?: (ButtonProps | undefined | null | false)[];
/**
* Context actions are rendered in a dropdown
*/
context?: DropdownProps["items"];
};
/**
* Field UI overrides
*/
fields?: {
[F in keyof DB[E]]?: BkndAdminEntityFieldOptions<E>;
};
};
export type BkndAdminEntityFieldOptions<E extends keyof DB | string> = {
/**
* Override the rendering of a certain field
*/
render?: (
context: BkndAdminEntityContext,
entity: Entity,
field: Field,
ctx: {
data?: DB[E];
value?: DB[E][keyof DB[E]];
handleChange: (value: any) => void;
},
) => ReactNode | void | undefined;
};
```
### `appShell`
```ts
export type DropdownItem =
| (() => ReactNode)
| {
label: string | ReactElement;
icon?: any;
onClick?: () => void;
destructive?: boolean;
disabled?: boolean;
title?: string;
[key: string]: any;
};
export type BkndAdminAppShellOptions = {
userMenu?: (DropdownItem | undefined | boolean)[];
};
```

View File

@@ -23,26 +23,6 @@ export default {
} satisfies BkndConfig;
```
## Overview
The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties:
{/* <AutoTypeTable path="../app/src/adapter/index.ts" name="BkndConfig" /> */}
```typescript
export type BkndConfig = CreateAppConfig & {
// return the app configuration as object or from a function
app?: CreateAppConfig | ((args: Args) => CreateAppConfig);
// called before the app is built
beforeBuild?: (app: App) => Promise<void>;
// called after the app has been built
onBuilt?: (app: App) => Promise<void>;
// passed as the first argument to the `App.build` method
buildConfig?: Parameters<App["build"]>[0];
};
```
The supported configuration file extensions are `js`, `ts`, `mjs`, `cjs` and `json`. Throughout the documentation, we'll use `ts` for the file extension.
## Example
@@ -74,7 +54,177 @@ export default {
} satisfies BkndConfig;
```
### `app` (CreateAppConfig)
## Configuration (`BkndConfig`)
The `BkndConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext, Connection, MaybePromise } from "bknd";
import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void;
type ManagerOptions = {
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
};
type BkndConfig<Args = any> = {
connection?: Connection | Config;
config?: InitialModuleConfigs;
options?: {
plugins?: AppPlugin[];
manager?: ManagerOptions;
};
app?: BkndConfig<Args> | ((args: Args) => MaybePromise<BkndConfig<Args>>);
onBuilt?: (app: App) => Promise<void>;
beforeBuild?: (app?: App) => Promise<void>;
buildConfig?: {
sync?: boolean;
}
};
```
### `connection`
The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
```ts
// uses the default SQLite connection depending on the runtime
const connection = { url: "<url>" };
// the same as above, but more explicit
import { sqlite } from "bknd/adapter/sqlite";
const connection = sqlite({ url: "<url>" });
// Node.js SQLite, default on Node.js
import { nodeSqlite } from "bknd/adapter/node";
const connection = nodeSqlite({ url: "<url>" });
// Bun SQLite, default on Bun
import { bunSqlite } from "bknd/adapter/bun";
const connection = bunSqlite({ url: "<url>" });
// LibSQL, default on Cloudflare
import { libsql } from "bknd";
const connection = libsql({ url: "<url>" });
```
See a full list of available connections in the [Database](/usage/database) section. Alternatively, you can pass an instance of a `Connection` class directly, see [Custom Connection](/usage/database#custom-connection) as a reference.
If the connection object is omitted, the app will try to use an in-memory database.
### `config`
As configuration, you can either pass a partial configuration object or a complete one
with a version number. The version number is used to automatically migrate the configuration up
to the latest version upon boot ([`db` mode](/usage/introduction#ui-only-mode) only). The default configuration looks like this:
```json
{
"server": {
"cors": {
"origin": "*",
"allow_methods": [
"GET",
"POST",
"PATCH",
"PUT",
"DELETE"
],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
],
"allow_credentials": true
},
"mcp": {
"enabled": false,
"path": "/api/system/mcp",
"logLevel": "emergency"
}
},
"data": {
"basepath": "/api/data",
"default_primary_format": "integer",
"entities": {},
"relations": {},
"indices": {}
},
"auth": {
"enabled": false,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "",
"alg": "HS256",
"expires": 0,
"issuer": "",
"fields": [
"id",
"email",
"role"
]
},
"cookie": {
"path": "/",
"sameSite": "strict",
"secure": true,
"httpOnly": true,
"expires": 604800,
"partitioned": false,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"type": "password",
"enabled": true,
"config": {
"hashing": "sha256"
}
}
},
"guard": {
"enabled": false
},
"roles": {}
},
"media": {
"enabled": false,
"basepath": "/api/media",
"entity_name": "media",
"storage": {}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
```
You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
```sh
npx bknd config --default --pretty
```
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
```sh
npx bknd schema
```
To create an initial data structure, you can use helpers [described here](/usage/database#data-structure).
### `app`
The `app` property is a function that returns a `CreateAppConfig` object. It allows accessing the adapter specific environment variables. This is especially useful when using the [Cloudflare Workers](/integration/cloudflare) runtime, where the environment variables are only available inside the request handler.
@@ -129,6 +279,52 @@ export default {
};
```
### `options.plugins`
The `plugins` property is an array of functions that are called after the app has been built,
but before its event is emitted. This is useful for adding custom routes or other functionality.
A simple plugin that adds a custom route looks like this:
```ts
import type { AppPlugin } from "bknd";
export const myPlugin: AppPlugin = (app) => {
app.server.get("/hello", (c) => c.json({ hello: "world" }));
};
```
Since each plugin has full access to the `app` instance, it can add routes, modify the database
structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
make sure to only run trusted ones.
Read more about plugins in the [Plugins](/extending/plugins) section.
### `options.seed`
<Callout type="info">
The seed function will only be executed on app's first boot in `"db"` mode. If a configuration already exists in the database, or in `"code"` mode, it will not be executed.
</Callout>
The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
```ts
type ModuleBuildContext = {
connection: Connection;
server: Hono;
em: EntityManager;
emgr: EventManager;
guard: Guard;
};
const seed = async (ctx: ModuleBuildContext) => {
// seed the database
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
};
```
## Framework & Runtime configuration
Depending on which framework or runtime you're using to run bknd, the configuration object will extend the `BkndConfig` type with additional properties.

View File

@@ -20,6 +20,7 @@
"./extending/config",
"./extending/events",
"./extending/plugins",
"./extending/admin",
"---Integration---",
"./integration/introduction",
"./integration/(frameworks)/",

View File

@@ -184,197 +184,4 @@ To keep your config, secrets and types in sync, you can either use the CLI or th
## Configuration (`BkndConfig`)
The `BkndConfig` type is the main configuration object for the `createApp` function. It has
the following properties:
```typescript
import type { App, InitialModuleConfigs, ModuleBuildContext, Connection, MaybePromise } from "bknd";
import type { Config } from "@libsql/client";
type AppPlugin = (app: App) => Promise<void> | void;
type ManagerOptions = {
basePath?: string;
trustFetched?: boolean;
onFirstBoot?: () => Promise<void>;
seed?: (ctx: ModuleBuildContext) => Promise<void>;
};
type BkndConfig<Args = any> = {
connection?: Connection | Config;
config?: InitialModuleConfigs;
options?: {
plugins?: AppPlugin[];
manager?: ManagerOptions;
};
app?: BkndConfig<Args> | ((args: Args) => MaybePromise<BkndConfig<Args>>);
onBuilt?: (app: App) => Promise<void>;
beforeBuild?: (app?: App) => Promise<void>;
buildConfig?: {
sync?: boolean;
}
};
```
### `connection`
The `connection` property is the main connection object to the database. It can be either an object with libsql config or the actual `Connection` class.
```ts
// uses the default SQLite connection depending on the runtime
const connection = { url: "<url>" };
// the same as above, but more explicit
import { sqlite } from "bknd/adapter/sqlite";
const connection = sqlite({ url: "<url>" });
// Node.js SQLite, default on Node.js
import { nodeSqlite } from "bknd/adapter/node";
const connection = nodeSqlite({ url: "<url>" });
// Bun SQLite, default on Bun
import { bunSqlite } from "bknd/adapter/bun";
const connection = bunSqlite({ url: "<url>" });
// LibSQL, default on Cloudflare
import { libsql } from "bknd";
const connection = libsql({ url: "<url>" });
```
See a full list of available connections in the [Database](/usage/database) section. Alternatively, you can pass an instance of a `Connection` class directly, see [Custom Connection](/usage/database#custom-connection) as a reference.
If the connection object is omitted, the app will try to use an in-memory database.
### `config`
As [initial configuration](/usage/database#initial-structure), you can either pass a partial configuration object or a complete one
with a version number. The version number is used to automatically migrate the configuration up
to the latest version upon boot. The default configuration looks like this:
```json
{
"server": {
"admin": {
"basepath": "",
"color_scheme": "light",
"logo_return_path": "/"
},
"cors": {
"origin": "*",
"allow_methods": ["GET", "POST", "PATCH", "PUT", "DELETE"],
"allow_headers": [
"Content-Type",
"Content-Length",
"Authorization",
"Accept"
]
}
},
"data": {
"basepath": "/api/data",
"entities": {},
"relations": {},
"indices": {}
},
"auth": {
"enabled": false,
"basepath": "/api/auth",
"entity_name": "users",
"allow_register": true,
"jwt": {
"secret": "",
"alg": "HS256",
"fields": ["id", "email", "role"]
},
"cookie": {
"path": "/",
"sameSite": "lax",
"secure": true,
"httpOnly": true,
"expires": 604800,
"renew": true,
"pathSuccess": "/",
"pathLoggedOut": "/"
},
"strategies": {
"password": {
"type": "password",
"config": {
"hashing": "sha256"
}
}
},
"roles": {}
},
"media": {
"enabled": false,
"basepath": "/api/media",
"entity_name": "media",
"storage": {}
},
"flows": {
"basepath": "/api/flows",
"flows": {}
}
}
```
You can use the [CLI](/usage/cli/#getting-the-configuration-config) to get the default configuration:
```sh
npx bknd config --default --pretty
```
To validate your configuration against a JSON schema, you can also dump the schema using the CLI:
```sh
npx bknd schema
```
To create an initial data structure, you can use helpers [described here](/usage/database#initial-structure).
### `options.plugins`
The `plugins` property is an array of functions that are called after the app has been built,
but before its event is emitted. This is useful for adding custom routes or other functionality.
A simple plugin that adds a custom route looks like this:
```ts
import type { AppPlugin } from "bknd";
export const myPlugin: AppPlugin = (app) => {
app.server.get("/hello", (c) => c.json({ hello: "world" }));
};
```
Since each plugin has full access to the `app` instance, it can add routes, modify the database
structure, add custom middlewares, respond to or add events, etc. Plugins are very powerful, so
make sure to only run trusted ones.
### `options.seed`
<Callout type="info">
The seed function will only be executed on app's first boot in `"db"` mode. If a configuration already exists in the database, or in `"code"` mode, it will not be executed.
</Callout>
The `seed` property is a function that is called when the app is booted for the first time. It is used to seed the database with initial data. The function is passed a `ModuleBuildContext` object:
```ts
type ModuleBuildContext = {
connection: Connection;
server: Hono;
em: EntityManager;
emgr: EventManager;
guard: Guard;
};
const seed = async (ctx: ModuleBuildContext) => {
// seed the database
await ctx.em.mutator("todos").insertMany([
{ title: "Learn bknd", done: true },
{ title: "Build something cool", done: false },
]);
};
```