From 560379bd899311b070b1019a28a42fd5d25df3b3 Mon Sep 17 00:00:00 2001 From: dswbx Date: Thu, 25 Sep 2025 10:45:10 +0200 Subject: [PATCH] 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. --- app/src/ui/Admin.tsx | 56 +++-- app/src/ui/index.ts | 2 +- .../docs/(documentation)/extending/admin.mdx | 201 +++++++++++++++ .../docs/(documentation)/extending/config.mdx | 238 ++++++++++++++++-- docs/content/docs/(documentation)/meta.json | 1 + .../(documentation)/usage/introduction.mdx | 193 -------------- 6 files changed, 449 insertions(+), 242 deletions(-) create mode 100644 docs/content/docs/(documentation)/extending/admin.mdx diff --git a/app/src/ui/Admin.tsx b/app/src/ui/Admin.tsx index 0da2b90..17263bb 100644 --- a/app/src/ui/Admin.tsx +++ b/app/src/ui/Admin.tsx @@ -8,7 +8,33 @@ 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 BkndAdminConfig = { + /** + * Base path of the Admin UI + * @default `/` + */ + basepath?: string; + /** + * Path to return to when clicking the logo + * @default `/` + */ + logo_return_path?: string; + /** + * Theme of the Admin UI + * @default `system` + */ + theme?: AppTheme; + /** + * Entities configuration like headers, footers, actions, field renders, etc. + */ + entities?: BkndAdminEntitiesOptions; + /** + * App shell configuration like user menu actions. + */ + appShell?: BkndAdminAppShellOptions; +}; export type BkndAdminProps = { /** @@ -16,37 +42,13 @@ export type BkndAdminProps = { */ baseUrl?: string; /** - * Whether to wrap Admin in a + * Whether to wrap Admin in a `` */ withProvider?: boolean | ClientProviderProps; /** * Admin UI customization options */ - config?: { - /** - * Base path of the Admin UI - * @default `/` - */ - basepath?: string; - /** - * Path to return to when clicking the logo - * @default `/` - */ - logo_return_path?: string; - /** - * Theme of the Admin UI - * @default `system` - */ - theme?: AppTheme; - /** - * Entities configuration like headers, footers, actions, field renders, etc. - */ - entities?: BkndAdminEntitiesOptions; - /** - * App shell configuration like user menu actions. - */ - appShell?: BkndAdminAppShellOptions; - }; + config?: BkndAdminConfig; children?: ReactNode; }; diff --git a/app/src/ui/index.ts b/app/src/ui/index.ts index 5a3f15b..50c734f 100644 --- a/app/src/ui/index.ts +++ b/app/src/ui/index.ts @@ -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"; diff --git a/docs/content/docs/(documentation)/extending/admin.mdx b/docs/content/docs/(documentation)/extending/admin.mdx new file mode 100644 index 0000000..a2adede --- /dev/null +++ b/docs/content/docs/(documentation)/extending/admin.mdx @@ -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. + + + + +## 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 ( + 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) =>
test header
, + // override the rendering of the title field + fields: { + title: { + render: (context, entity, field, ctx) => { + return ( + ctx.handleChange(e.target.value)} + /> + ); + }, + }, + }, + }, + // system entities work too + users: { + header: () => { + return
System entity
; + }, + }, + }, + }} + > + {/* You may also add custom routes, these always have precedence over the Admin routes */} + +
custom
+
+
+ ); +} +``` + + +## `config` + + + +### `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; +}; + +export type BkndAdminEntityOptions = { + /** + * 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; + }; +}; + +export type BkndAdminEntityFieldOptions = { + /** + * 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)[]; +}; +``` \ No newline at end of file diff --git a/docs/content/docs/(documentation)/extending/config.mdx b/docs/content/docs/(documentation)/extending/config.mdx index 8f81579..30c63f8 100644 --- a/docs/content/docs/(documentation)/extending/config.mdx +++ b/docs/content/docs/(documentation)/extending/config.mdx @@ -23,26 +23,6 @@ export default { } satisfies BkndConfig; ``` -## Overview - -The `BkndConfig` extends the [`CreateAppConfig`](/usage/introduction#configuration-createappconfig) type with the following properties: - -{/* */} - - -```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; - // called after the app has been built - onBuilt?: (app: App) => Promise; - // passed as the first argument to the `App.build` method - buildConfig?: Parameters[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; +type ManagerOptions = { + basePath?: string; + trustFetched?: boolean; + onFirstBoot?: () => Promise; + seed?: (ctx: ModuleBuildContext) => Promise; +}; + +type BkndConfig = { + connection?: Connection | Config; + config?: InitialModuleConfigs; + options?: { + plugins?: AppPlugin[]; + manager?: ManagerOptions; + }; + app?: BkndConfig | ((args: Args) => MaybePromise>); + onBuilt?: (app: App) => Promise; + beforeBuild?: (app?: App) => Promise; + 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: "" }; + +// the same as above, but more explicit +import { sqlite } from "bknd/adapter/sqlite"; +const connection = sqlite({ url: "" }); + +// Node.js SQLite, default on Node.js +import { nodeSqlite } from "bknd/adapter/node"; +const connection = nodeSqlite({ url: "" }); + +// Bun SQLite, default on Bun +import { bunSqlite } from "bknd/adapter/bun"; +const connection = bunSqlite({ url: "" }); + +// LibSQL, default on Cloudflare +import { libsql } from "bknd"; +const connection = libsql({ 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` + + + 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. + + +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. diff --git a/docs/content/docs/(documentation)/meta.json b/docs/content/docs/(documentation)/meta.json index b2d5db7..442c37d 100644 --- a/docs/content/docs/(documentation)/meta.json +++ b/docs/content/docs/(documentation)/meta.json @@ -20,6 +20,7 @@ "./extending/config", "./extending/events", "./extending/plugins", + "./extending/admin", "---Integration---", "./integration/introduction", "./integration/(frameworks)/", diff --git a/docs/content/docs/(documentation)/usage/introduction.mdx b/docs/content/docs/(documentation)/usage/introduction.mdx index 472d0d1..ebac844 100644 --- a/docs/content/docs/(documentation)/usage/introduction.mdx +++ b/docs/content/docs/(documentation)/usage/introduction.mdx @@ -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; -type ManagerOptions = { - basePath?: string; - trustFetched?: boolean; - onFirstBoot?: () => Promise; - seed?: (ctx: ModuleBuildContext) => Promise; -}; - -type BkndConfig = { - connection?: Connection | Config; - config?: InitialModuleConfigs; - options?: { - plugins?: AppPlugin[]; - manager?: ManagerOptions; - }; - app?: BkndConfig | ((args: Args) => MaybePromise>); - onBuilt?: (app: App) => Promise; - beforeBuild?: (app?: App) => Promise; - 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: "" }; - -// the same as above, but more explicit -import { sqlite } from "bknd/adapter/sqlite"; -const connection = sqlite({ url: "" }); - -// Node.js SQLite, default on Node.js -import { nodeSqlite } from "bknd/adapter/node"; -const connection = nodeSqlite({ url: "" }); - -// Bun SQLite, default on Bun -import { bunSqlite } from "bknd/adapter/bun"; -const connection = bunSqlite({ url: "" }); - -// LibSQL, default on Cloudflare -import { libsql } from "bknd"; -const connection = libsql({ 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` - - - 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. - - -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 }, - ]); -}; -```