mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 12:37:20 +00:00
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:
201
docs/content/docs/(documentation)/extending/admin.mdx
Normal file
201
docs/content/docs/(documentation)/extending/admin.mdx
Normal 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)[];
|
||||
};
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"./extending/config",
|
||||
"./extending/events",
|
||||
"./extending/plugins",
|
||||
"./extending/admin",
|
||||
"---Integration---",
|
||||
"./integration/introduction",
|
||||
"./integration/(frameworks)/",
|
||||
|
||||
@@ -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 },
|
||||
]);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user