mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-16 04:27:21 +00:00
Merge pull request #265 from bknd-io/feat/admin-ui-customizations
feat: add admin options for entities and app shell
This commit is contained in:
@@ -11,10 +11,12 @@ export interface AppEntity<IdType = number | string> {
|
|||||||
|
|
||||||
export interface DB {
|
export interface DB {
|
||||||
// make sure to make unknown as "any"
|
// make sure to make unknown as "any"
|
||||||
[key: string]: {
|
/* [key: string]: {
|
||||||
id: PrimaryFieldType;
|
id: PrimaryFieldType;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
}; */
|
||||||
|
// @todo: that's not good, but required for admin options
|
||||||
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ export class DataApi extends ModuleApi<DataApiOptions> {
|
|||||||
) {
|
) {
|
||||||
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
type Data = E extends keyof DB ? Selectable<DB[E]> : EntityData;
|
||||||
type T = RepositoryResultJSON<Data>;
|
type T = RepositoryResultJSON<Data>;
|
||||||
|
|
||||||
|
// @todo: if none found, still returns meta...
|
||||||
|
|
||||||
return this.readMany(entity, {
|
return this.readMany(entity, {
|
||||||
...query,
|
...query,
|
||||||
limit: 1,
|
limit: 1,
|
||||||
|
|||||||
@@ -1,24 +1,60 @@
|
|||||||
import { MantineProvider } from "@mantine/core";
|
import { MantineProvider } from "@mantine/core";
|
||||||
import { Notifications } from "@mantine/notifications";
|
import { Notifications } from "@mantine/notifications";
|
||||||
import React, { type ReactNode } from "react";
|
import React, { type ReactNode } from "react";
|
||||||
import { BkndProvider, type BkndAdminOptions } from "ui/client/bknd";
|
import { BkndProvider } from "ui/client/bknd";
|
||||||
import { useTheme } from "ui/client/use-theme";
|
import { useTheme, type AppTheme } from "ui/client/use-theme";
|
||||||
import { Logo } from "ui/components/display/Logo";
|
import { Logo } from "ui/components/display/Logo";
|
||||||
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
import * as AppShell from "ui/layouts/AppShell/AppShell";
|
||||||
import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client";
|
import { ClientProvider, useBkndWindowContext, type ClientProviderProps } from "./client";
|
||||||
import { createMantineTheme } from "./lib/mantine/theme";
|
import { createMantineTheme } from "./lib/mantine/theme";
|
||||||
import { Routes } from "./routes";
|
import { Routes } from "./routes";
|
||||||
|
import type { BkndAdminAppShellOptions, BkndAdminEntitiesOptions } from "ui/options";
|
||||||
|
|
||||||
export type BkndAdminProps = {
|
export type BkndAdminProps = {
|
||||||
|
/**
|
||||||
|
* Base URL of the API, only needed if you are not using the `withProvider` prop
|
||||||
|
*/
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
/**
|
||||||
|
* Whether to wrap Admin in a <ClientProvider />
|
||||||
|
*/
|
||||||
withProvider?: boolean | ClientProviderProps;
|
withProvider?: boolean | ClientProviderProps;
|
||||||
config?: BkndAdminOptions;
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
children?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Admin({
|
export default function Admin({
|
||||||
baseUrl: baseUrlOverride,
|
baseUrl: baseUrlOverride,
|
||||||
withProvider = false,
|
withProvider = false,
|
||||||
config: _config = {},
|
config: _config = {},
|
||||||
|
children,
|
||||||
}: BkndAdminProps) {
|
}: BkndAdminProps) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const Provider = ({ children }: any) =>
|
const Provider = ({ children }: any) =>
|
||||||
@@ -47,7 +83,9 @@ export default function Admin({
|
|||||||
<Provider>
|
<Provider>
|
||||||
<MantineProvider {...createMantineTheme(theme as any)}>
|
<MantineProvider {...createMantineTheme(theme as any)}>
|
||||||
<Notifications position="top-right" />
|
<Notifications position="top-right" />
|
||||||
<Routes BkndWrapper={BkndWrapper} basePath={config?.basepath} />
|
<Routes BkndWrapper={BkndWrapper} basePath={config?.basepath}>
|
||||||
|
{children}
|
||||||
|
</Routes>
|
||||||
</MantineProvider>
|
</MantineProvider>
|
||||||
</Provider>
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,16 +12,11 @@ import {
|
|||||||
import { useApi } from "ui/client";
|
import { useApi } from "ui/client";
|
||||||
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
||||||
import { AppReduced } from "./utils/AppReduced";
|
import { AppReduced } from "./utils/AppReduced";
|
||||||
import type { AppTheme } from "ui/client/use-theme";
|
|
||||||
import { Message } from "ui/components/display/Message";
|
import { Message } from "ui/components/display/Message";
|
||||||
import { useNavigate } from "ui/lib/routes";
|
import { useNavigate } from "ui/lib/routes";
|
||||||
|
import type { BkndAdminProps } from "ui/Admin";
|
||||||
|
|
||||||
export type BkndAdminOptions = {
|
export type BkndContext = {
|
||||||
basepath?: string;
|
|
||||||
logo_return_path?: string;
|
|
||||||
theme?: AppTheme;
|
|
||||||
};
|
|
||||||
type BkndContext = {
|
|
||||||
version: number;
|
version: number;
|
||||||
readonly: boolean;
|
readonly: boolean;
|
||||||
schema: ModuleSchemas;
|
schema: ModuleSchemas;
|
||||||
@@ -31,7 +26,7 @@ type BkndContext = {
|
|||||||
requireSecrets: () => Promise<void>;
|
requireSecrets: () => Promise<void>;
|
||||||
actions: ReturnType<typeof getSchemaActions>;
|
actions: ReturnType<typeof getSchemaActions>;
|
||||||
app: AppReduced;
|
app: AppReduced;
|
||||||
options: BkndAdminOptions;
|
options: BkndAdminProps["config"];
|
||||||
fallback: boolean;
|
fallback: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,7 +48,7 @@ export function BkndProvider({
|
|||||||
includeSecrets?: boolean;
|
includeSecrets?: boolean;
|
||||||
children: any;
|
children: any;
|
||||||
fallback?: React.ReactNode;
|
fallback?: React.ReactNode;
|
||||||
options?: BkndAdminOptions;
|
options?: BkndAdminProps["config"];
|
||||||
}) {
|
}) {
|
||||||
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
||||||
const [schema, setSchema] =
|
const [schema, setSchema] =
|
||||||
@@ -180,7 +175,7 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBkndOptions(): BkndAdminOptions {
|
export function useBkndOptions(): BkndAdminProps["config"] {
|
||||||
const ctx = useContext(BkndContext);
|
const ctx = useContext(BkndContext);
|
||||||
return (
|
return (
|
||||||
ctx.options ?? {
|
ctx.options ?? {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
export { BkndProvider, type BkndAdminOptions, useBknd, SchemaEditable } from "./BkndProvider";
|
export { BkndProvider, type BkndContext, useBknd, SchemaEditable } from "./BkndProvider";
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { EntityRelation } from "data/relations";
|
|||||||
import { constructEntity, constructRelation } from "data/schema/constructor";
|
import { constructEntity, constructRelation } from "data/schema/constructor";
|
||||||
import { RelationAccessor } from "data/relations/RelationAccessor";
|
import { RelationAccessor } from "data/relations/RelationAccessor";
|
||||||
import { Flow, TaskMap } from "flows";
|
import { Flow, TaskMap } from "flows";
|
||||||
import type { BkndAdminOptions } from "ui/client/BkndProvider";
|
import type { BkndAdminProps } from "ui/Admin";
|
||||||
|
|
||||||
export type AppType = ReturnType<App["toJSON"]>;
|
export type AppType = ReturnType<App["toJSON"]>;
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ export class AppReduced {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected appJson: AppType,
|
protected appJson: AppType,
|
||||||
protected _options: BkndAdminOptions = {},
|
protected _options: BkndAdminProps["config"] = {},
|
||||||
) {
|
) {
|
||||||
//console.log("received appjson", appJson);
|
//console.log("received appjson", appJson);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export { default as Admin, type BkndAdminProps } from "./Admin";
|
export { default as Admin, type BkndAdminProps } from "./Admin";
|
||||||
export * from "./components/form/json-schema-form";
|
export * from "./components/form/json-schema-form";
|
||||||
export { JsonViewer } from "./components/code/JsonViewer";
|
export { JsonViewer } from "./components/code/JsonViewer";
|
||||||
|
export type * from "./options";
|
||||||
|
|
||||||
// bknd admin ui
|
// bknd admin ui
|
||||||
export { Button } from "./components/buttons/Button";
|
export { Button } from "./components/buttons/Button";
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import { useTheme } from "ui/client/use-theme";
|
|||||||
import { Button } from "ui/components/buttons/Button";
|
import { Button } from "ui/components/buttons/Button";
|
||||||
import { IconButton } from "ui/components/buttons/IconButton";
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
import { Logo } from "ui/components/display/Logo";
|
import { Logo } from "ui/components/display/Logo";
|
||||||
import { Dropdown, type DropdownItem } from "ui/components/overlay/Dropdown";
|
import { Dropdown, type DropdownProps } from "ui/components/overlay/Dropdown";
|
||||||
import { Link } from "ui/components/wouter/Link";
|
import { Link } from "ui/components/wouter/Link";
|
||||||
import { useEvent } from "ui/hooks/use-event";
|
import { useEvent } from "ui/hooks/use-event";
|
||||||
import { useNavigate } from "ui/lib/routes";
|
import { useNavigate } from "ui/lib/routes";
|
||||||
@@ -26,6 +26,7 @@ import { autoFormatString } from "core/utils";
|
|||||||
import { appShellStore } from "ui/store";
|
import { appShellStore } from "ui/store";
|
||||||
import { getVersion } from "core/env";
|
import { getVersion } from "core/env";
|
||||||
import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon";
|
import { McpIcon } from "ui/routes/tools/mcp/components/mcp-icon";
|
||||||
|
import { useAppShellAdminOptions } from "ui/options";
|
||||||
|
|
||||||
export function HeaderNavigation() {
|
export function HeaderNavigation() {
|
||||||
const [location, navigate] = useLocation();
|
const [location, navigate] = useLocation();
|
||||||
@@ -144,7 +145,9 @@ export function Header({ hasSidebar = true }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function UserMenu() {
|
function UserMenu() {
|
||||||
const { config, options } = useBknd();
|
const { config } = useBknd();
|
||||||
|
const uiOptions = useAppShellAdminOptions();
|
||||||
|
|
||||||
const auth = useAuth();
|
const auth = useAuth();
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
const { logout_route } = useBkndWindowContext();
|
const { logout_route } = useBkndWindowContext();
|
||||||
@@ -159,7 +162,8 @@ function UserMenu() {
|
|||||||
navigate("/auth/login");
|
navigate("/auth/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
const items: DropdownItem[] = [
|
const items: DropdownProps["items"] = [
|
||||||
|
...(uiOptions.userMenu ?? []),
|
||||||
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
{ label: "Settings", onClick: () => navigate("/settings"), icon: IconSettings },
|
||||||
{
|
{
|
||||||
label: "OpenAPI",
|
label: "OpenAPI",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { Alert } from "ui/components/display/Alert";
|
|||||||
import { bkndModals } from "ui/modals";
|
import { bkndModals } from "ui/modals";
|
||||||
import type { EnumField, JsonField, JsonSchemaField } from "data/fields";
|
import type { EnumField, JsonField, JsonSchemaField } from "data/fields";
|
||||||
import type { RelationField } from "data/relations";
|
import type { RelationField } from "data/relations";
|
||||||
|
import { useEntityAdminOptions } from "ui/options";
|
||||||
|
|
||||||
// simplify react form types 🤦
|
// simplify react form types 🤦
|
||||||
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
|
export type FormApi = ReactFormExtendedApi<any, any, any, any, any, any, any, any, any, any>;
|
||||||
@@ -44,6 +45,7 @@ export function EntityForm({
|
|||||||
action,
|
action,
|
||||||
}: EntityFormProps) {
|
}: EntityFormProps) {
|
||||||
const fields = entity.getFillableFields(action, true);
|
const fields = entity.getFillableFields(action, true);
|
||||||
|
const options = useEntityAdminOptions(entity, action);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit}>
|
<form onSubmit={handleSubmit}>
|
||||||
@@ -107,16 +109,29 @@ export function EntityForm({
|
|||||||
>
|
>
|
||||||
<Form.Field
|
<Form.Field
|
||||||
name={field.name}
|
name={field.name}
|
||||||
children={(props) => (
|
children={(props) => {
|
||||||
<EntityFormField
|
const fieldOptions = options.field(field.name);
|
||||||
field={field}
|
if (fieldOptions?.render) {
|
||||||
fieldApi={props}
|
const custom = fieldOptions.render(action, entity, field, {
|
||||||
disabled={fieldsDisabled}
|
handleChange: props.handleChange,
|
||||||
tabIndex={key + 1}
|
value: props.state.value,
|
||||||
action={action}
|
data,
|
||||||
data={data}
|
});
|
||||||
/>
|
if (custom) {
|
||||||
)}
|
return custom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<EntityFormField
|
||||||
|
field={field}
|
||||||
|
fieldApi={props}
|
||||||
|
disabled={fieldsDisabled}
|
||||||
|
tabIndex={key + 1}
|
||||||
|
action={action}
|
||||||
|
data={data}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
|||||||
12
app/src/ui/options/app-shell.ts
Normal file
12
app/src/ui/options/app-shell.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
|
import type { DropdownProps } from "ui/components/overlay/Dropdown";
|
||||||
|
|
||||||
|
export type BkndAdminAppShellOptions = {
|
||||||
|
userMenu?: DropdownProps["items"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useAppShellAdminOptions() {
|
||||||
|
const { options } = useBknd();
|
||||||
|
const userMenu = options?.appShell?.userMenu ?? [];
|
||||||
|
return { userMenu };
|
||||||
|
}
|
||||||
85
app/src/ui/options/entities.ts
Normal file
85
app/src/ui/options/entities.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import type { DB, Field } from "bknd";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { Entity } from "data/entities";
|
||||||
|
import { useBknd } from "ui/client/BkndProvider";
|
||||||
|
import type { DropdownProps } from "ui/components/overlay/Dropdown";
|
||||||
|
import type { ButtonProps } from "ui/components/buttons/Button";
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useEntityAdminOptions(entity: Entity, context: BkndAdminEntityContext, data?: any) {
|
||||||
|
const b = useBknd();
|
||||||
|
const opts = b.options?.entities?.[entity.name];
|
||||||
|
const footer = opts?.footer?.(context, entity, data) ?? null;
|
||||||
|
const header = opts?.header?.(context, entity, data) ?? null;
|
||||||
|
const actions = opts?.actions?.(context, entity, data);
|
||||||
|
|
||||||
|
return {
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
field: (name: string) => opts?.fields?.[name],
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
2
app/src/ui/options/index.ts
Normal file
2
app/src/ui/options/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./entities";
|
||||||
|
export * from "./app-shell";
|
||||||
@@ -18,6 +18,7 @@ import { EntityForm } from "ui/modules/data/components/EntityForm";
|
|||||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useEntityAdminOptions } from "ui/options";
|
||||||
|
|
||||||
export function DataEntityUpdate({ params }) {
|
export function DataEntityUpdate({ params }) {
|
||||||
return <DataEntityUpdateImpl params={params} key={params.entity} />;
|
return <DataEntityUpdateImpl params={params} key={params.entity} />;
|
||||||
@@ -53,6 +54,7 @@ function DataEntityUpdateImpl({ params }) {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const options = useEntityAdminOptions(entity, "update", $q.data);
|
||||||
const backHref = routes.data.entity.list(entity.name);
|
const backHref = routes.data.entity.list(entity.name);
|
||||||
const goBack = () => _goBack({ fallback: backHref });
|
const goBack = () => _goBack({ fallback: backHref });
|
||||||
|
|
||||||
@@ -125,6 +127,7 @@ function DataEntityUpdateImpl({ params }) {
|
|||||||
<Dropdown
|
<Dropdown
|
||||||
position="bottom-end"
|
position="bottom-end"
|
||||||
items={[
|
items={[
|
||||||
|
...(options.actions?.context ?? []),
|
||||||
{
|
{
|
||||||
label: "Inspect",
|
label: "Inspect",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -156,6 +159,10 @@ function DataEntityUpdateImpl({ params }) {
|
|||||||
>
|
>
|
||||||
<IconButton Icon={TbDots} />
|
<IconButton Icon={TbDots} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{options.actions?.primary?.map(
|
||||||
|
(button, key) =>
|
||||||
|
button && <Button variant="primary" {...button} type="button" key={key} />,
|
||||||
|
)}
|
||||||
<Form.Subscribe
|
<Form.Subscribe
|
||||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||||
children={([canSubmit, isSubmitting]) => (
|
children={([canSubmit, isSubmitting]) => (
|
||||||
@@ -185,6 +192,7 @@ function DataEntityUpdateImpl({ params }) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AppShell.Scrollable>
|
<AppShell.Scrollable>
|
||||||
|
{options.header}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
|
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
|
||||||
<b className="mr-2">Update failed: </b> {error}
|
<b className="mr-2">Update failed: </b> {error}
|
||||||
@@ -200,6 +208,8 @@ function DataEntityUpdateImpl({ params }) {
|
|||||||
action="update"
|
action="update"
|
||||||
className="flex flex-grow flex-col gap-3 p-3"
|
className="flex flex-grow flex-col gap-3 p-3"
|
||||||
/>
|
/>
|
||||||
|
{options.footer}
|
||||||
|
|
||||||
{targetRelations.length > 0 ? (
|
{targetRelations.length > 0 ? (
|
||||||
<EntityDetailRelations
|
<EntityDetailRelations
|
||||||
id={entityId}
|
id={entityId}
|
||||||
@@ -247,7 +257,8 @@ function EntityDetailRelations({
|
|||||||
return {
|
return {
|
||||||
as: "button",
|
as: "button",
|
||||||
type: "button",
|
type: "button",
|
||||||
label: ucFirst(other.entity.label),
|
//label: ucFirst(other.entity.label),
|
||||||
|
label: ucFirst(other.reference),
|
||||||
onClick: () => handleClick(relation),
|
onClick: () => handleClick(relation),
|
||||||
active: selected?.other(entity).reference === other.reference,
|
active: selected?.other(entity).reference === other.reference,
|
||||||
badge: relation.type(),
|
badge: relation.type(),
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import { EntityForm } from "ui/modules/data/components/EntityForm";
|
|||||||
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
import { useEntityForm } from "ui/modules/data/hooks/useEntityForm";
|
||||||
import { s } from "bknd/utils";
|
import { s } from "bknd/utils";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
|
import { useEntityAdminOptions } from "ui/options";
|
||||||
|
import { Dropdown } from "ui/components/overlay/Dropdown";
|
||||||
|
import { TbDots } from "react-icons/tb";
|
||||||
|
import { IconButton } from "ui/components/buttons/IconButton";
|
||||||
|
|
||||||
export function DataEntityCreate({ params }) {
|
export function DataEntityCreate({ params }) {
|
||||||
const { $data } = useBkndData();
|
const { $data } = useBkndData();
|
||||||
@@ -23,6 +27,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
} else if (entity.type === "system") {
|
} else if (entity.type === "system") {
|
||||||
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
|
return <Message.NotAllowed description={`Entity "${params.entity}" cannot be created.`} />;
|
||||||
}
|
}
|
||||||
|
const options = useEntityAdminOptions(entity, "create");
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
useBrowserTitle(["Data", entity.label, "Create"]);
|
useBrowserTitle(["Data", entity.label, "Create"]);
|
||||||
@@ -71,7 +76,16 @@ export function DataEntityCreate({ params }) {
|
|||||||
<AppShell.SectionHeader
|
<AppShell.SectionHeader
|
||||||
right={
|
right={
|
||||||
<>
|
<>
|
||||||
|
{options.actions?.context && (
|
||||||
|
<Dropdown position="bottom-end" items={options.actions.context}>
|
||||||
|
<IconButton Icon={TbDots} />
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
<Button onClick={goBack}>Cancel</Button>
|
<Button onClick={goBack}>Cancel</Button>
|
||||||
|
{options.actions?.primary?.map(
|
||||||
|
(button, key) =>
|
||||||
|
button && <Button {...button} type="button" key={key} variant="primary" />,
|
||||||
|
)}
|
||||||
<Form.Subscribe
|
<Form.Subscribe
|
||||||
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
selector={(state) => [state.canSubmit, state.isSubmitting]}
|
||||||
children={([canSubmit, isSubmitting]) => (
|
children={([canSubmit, isSubmitting]) => (
|
||||||
@@ -96,6 +110,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
/>
|
/>
|
||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable key={entity.name}>
|
<AppShell.Scrollable key={entity.name}>
|
||||||
|
{options.header}
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
|
<div className="flex flex-row dark:bg-red-950 bg-red-100 p-4">
|
||||||
<b className="mr-2">Create failed: </b> {error}
|
<b className="mr-2">Create failed: </b> {error}
|
||||||
@@ -110,6 +125,7 @@ export function DataEntityCreate({ params }) {
|
|||||||
action="create"
|
action="create"
|
||||||
className="flex flex-grow flex-col gap-3 p-3"
|
className="flex flex-grow flex-col gap-3 p-3"
|
||||||
/>
|
/>
|
||||||
|
{options.footer}
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useCreateUserModal } from "ui/modules/auth/hooks/use-create-user-modal"
|
|||||||
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
import { EntityTable2 } from "ui/modules/data/components/EntityTable2";
|
||||||
import { s } from "bknd/utils";
|
import { s } from "bknd/utils";
|
||||||
import { pick } from "core/utils/objects";
|
import { pick } from "core/utils/objects";
|
||||||
|
import { useEntityAdminOptions } from "ui/options";
|
||||||
|
|
||||||
const searchSchema = s.partialObject({
|
const searchSchema = s.partialObject({
|
||||||
...pick(repoQuery.properties, ["select", "where", "sort"]),
|
...pick(repoQuery.properties, ["select", "where", "sort"]),
|
||||||
@@ -36,6 +37,7 @@ function DataEntityListImpl({ params }) {
|
|||||||
if (!entity) {
|
if (!entity) {
|
||||||
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
return <Message.NotFound description={`Entity "${params.entity}" doesn't exist.`} />;
|
||||||
}
|
}
|
||||||
|
const options = useEntityAdminOptions(entity, "list");
|
||||||
|
|
||||||
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
useBrowserTitle(["Data", entity?.label ?? params.entity]);
|
||||||
const [navigate] = useNavigate();
|
const [navigate] = useNavigate();
|
||||||
@@ -100,6 +102,7 @@ function DataEntityListImpl({ params }) {
|
|||||||
<>
|
<>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[
|
items={[
|
||||||
|
...(options.actions?.context ?? []),
|
||||||
{
|
{
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
onClick: () => navigate(routes.data.schema.entity(entity.name)),
|
onClick: () => navigate(routes.data.schema.entity(entity.name)),
|
||||||
@@ -120,6 +123,10 @@ function DataEntityListImpl({ params }) {
|
|||||||
>
|
>
|
||||||
<IconButton Icon={TbDots} />
|
<IconButton Icon={TbDots} />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
{options.actions?.primary?.map(
|
||||||
|
(button, key) =>
|
||||||
|
button && <Button variant="primary" {...button} type="button" key={key} />,
|
||||||
|
)}
|
||||||
<EntityCreateButton entity={entity} />
|
<EntityCreateButton entity={entity} />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -127,6 +134,7 @@ function DataEntityListImpl({ params }) {
|
|||||||
<AppShell.SectionHeaderTitle>{entity.label}</AppShell.SectionHeaderTitle>
|
<AppShell.SectionHeaderTitle>{entity.label}</AppShell.SectionHeaderTitle>
|
||||||
</AppShell.SectionHeader>
|
</AppShell.SectionHeader>
|
||||||
<AppShell.Scrollable key={entity.name}>
|
<AppShell.Scrollable key={entity.name}>
|
||||||
|
{options.header}
|
||||||
<div className="flex flex-col flex-grow p-3 gap-3">
|
<div className="flex flex-col flex-grow p-3 gap-3">
|
||||||
{/*<div className="w-64">
|
{/*<div className="w-64">
|
||||||
<SearchInput placeholder={`Filter ${entity.label}`} />
|
<SearchInput placeholder={`Filter ${entity.label}`} />
|
||||||
@@ -134,7 +142,7 @@ function DataEntityListImpl({ params }) {
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
data-updating={isUpdating ? 1 : undefined}
|
data-updating={isUpdating ? 1 : undefined}
|
||||||
className="data-[updating]:opacity-50 transition-opacity pb-10"
|
className="data-[updating]:opacity-50 transition-opacity"
|
||||||
>
|
>
|
||||||
<EntityTable2
|
<EntityTable2
|
||||||
data={data ?? null}
|
data={data ?? null}
|
||||||
@@ -152,6 +160,7 @@ function DataEntityListImpl({ params }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{options.footer}
|
||||||
</AppShell.Scrollable>
|
</AppShell.Scrollable>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ const TestRoutes = lazy(() => import("./test"));
|
|||||||
export function Routes({
|
export function Routes({
|
||||||
BkndWrapper,
|
BkndWrapper,
|
||||||
basePath = "",
|
basePath = "",
|
||||||
}: { BkndWrapper: ComponentType<{ children: ReactNode }>; basePath?: string }) {
|
children,
|
||||||
|
}: {
|
||||||
|
BkndWrapper: ComponentType<{ children: ReactNode }>;
|
||||||
|
basePath?: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
}) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const ctx = useBkndWindowContext();
|
const ctx = useBkndWindowContext();
|
||||||
const actualBasePath = basePath || ctx.admin_basepath;
|
const actualBasePath = basePath || ctx.admin_basepath;
|
||||||
@@ -44,6 +49,8 @@ export function Routes({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
<Route path="/" component={RootEmpty} />
|
<Route path="/" component={RootEmpty} />
|
||||||
<Route path="/data" nest>
|
<Route path="/data" nest>
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
498
docs/bknd_ui_in_docs.patch
Normal file
498
docs/bknd_ui_in_docs.patch
Normal file
@@ -0,0 +1,498 @@
|
|||||||
|
Subject: [PATCH] bknd ui in docs
|
||||||
|
---
|
||||||
|
Index: docs/package.json
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/package.json b/docs/package.json
|
||||||
|
--- a/docs/package.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/package.json (date 1758275461936)
|
||||||
|
@@ -32,7 +32,8 @@
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
- "twoslash": "^0.3.2"
|
||||||
|
+ "twoslash": "^0.3.2",
|
||||||
|
+ "bknd": "file:../app"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.11",
|
||||||
|
Index: docs/package-lock.json
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/package-lock.json b/docs/package-lock.json
|
||||||
|
--- a/docs/package-lock.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/package-lock.json (date 1758275464962)
|
||||||
|
@@ -10,6 +10,7 @@
|
||||||
|
"dependencies": {
|
||||||
|
"@iconify/react": "^6.0.0",
|
||||||
|
"@orama/orama": "^3.1.10",
|
||||||
|
+ "bknd": "file:../app",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"fumadocs-core": "^15.6.1",
|
||||||
|
@@ -44,6 +45,110 @@
|
||||||
|
"wrangler": "^4.25.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
+ "../app": {
|
||||||
|
+ "name": "bknd",
|
||||||
|
+ "version": "0.18.0-rc.3",
|
||||||
|
+ "license": "FSL-1.1-MIT",
|
||||||
|
+ "dependencies": {
|
||||||
|
+ "@cfworker/json-schema": "^4.1.1",
|
||||||
|
+ "@codemirror/lang-html": "^6.4.9",
|
||||||
|
+ "@codemirror/lang-json": "^6.0.1",
|
||||||
|
+ "@hello-pangea/dnd": "^18.0.1",
|
||||||
|
+ "@hono/swagger-ui": "^0.5.1",
|
||||||
|
+ "@mantine/core": "^7.17.1",
|
||||||
|
+ "@mantine/hooks": "^7.17.1",
|
||||||
|
+ "@tanstack/react-form": "^1.0.5",
|
||||||
|
+ "@uiw/react-codemirror": "^4.23.10",
|
||||||
|
+ "@xyflow/react": "^12.4.4",
|
||||||
|
+ "aws4fetch": "^1.0.20",
|
||||||
|
+ "bcryptjs": "^3.0.2",
|
||||||
|
+ "dayjs": "^1.11.13",
|
||||||
|
+ "fast-xml-parser": "^5.0.8",
|
||||||
|
+ "hono": "4.8.3",
|
||||||
|
+ "json-schema-library": "10.0.0-rc7",
|
||||||
|
+ "json-schema-to-ts": "^3.1.1",
|
||||||
|
+ "jsonv-ts": "0.8.2",
|
||||||
|
+ "kysely": "0.27.6",
|
||||||
|
+ "lodash-es": "^4.17.21",
|
||||||
|
+ "oauth4webapi": "^2.11.1",
|
||||||
|
+ "object-path-immutable": "^4.1.2",
|
||||||
|
+ "picocolors": "^1.1.1",
|
||||||
|
+ "radix-ui": "^1.1.3",
|
||||||
|
+ "swr": "^2.3.3"
|
||||||
|
+ },
|
||||||
|
+ "bin": {
|
||||||
|
+ "bknd": "dist/cli/index.js"
|
||||||
|
+ },
|
||||||
|
+ "devDependencies": {
|
||||||
|
+ "@aws-sdk/client-s3": "^3.758.0",
|
||||||
|
+ "@bluwy/giget-core": "^0.1.2",
|
||||||
|
+ "@clack/prompts": "^0.11.0",
|
||||||
|
+ "@cloudflare/vitest-pool-workers": "^0.8.38",
|
||||||
|
+ "@cloudflare/workers-types": "^4.20250606.0",
|
||||||
|
+ "@dagrejs/dagre": "^1.1.4",
|
||||||
|
+ "@hono/vite-dev-server": "^0.19.1",
|
||||||
|
+ "@hookform/resolvers": "^4.1.3",
|
||||||
|
+ "@libsql/client": "^0.15.9",
|
||||||
|
+ "@mantine/modals": "^7.17.1",
|
||||||
|
+ "@mantine/notifications": "^7.17.1",
|
||||||
|
+ "@playwright/test": "^1.51.1",
|
||||||
|
+ "@rjsf/core": "5.22.2",
|
||||||
|
+ "@standard-schema/spec": "^1.0.0",
|
||||||
|
+ "@tabler/icons-react": "3.18.0",
|
||||||
|
+ "@tailwindcss/postcss": "^4.0.12",
|
||||||
|
+ "@tailwindcss/vite": "^4.0.12",
|
||||||
|
+ "@testing-library/jest-dom": "^6.6.3",
|
||||||
|
+ "@testing-library/react": "^16.2.0",
|
||||||
|
+ "@types/node": "^22.13.10",
|
||||||
|
+ "@types/react": "^19.0.10",
|
||||||
|
+ "@types/react-dom": "^19.0.4",
|
||||||
|
+ "@vitejs/plugin-react": "^4.3.4",
|
||||||
|
+ "@vitest/coverage-v8": "^3.0.9",
|
||||||
|
+ "autoprefixer": "^10.4.21",
|
||||||
|
+ "clsx": "^2.1.1",
|
||||||
|
+ "dotenv": "^16.4.7",
|
||||||
|
+ "jotai": "^2.12.2",
|
||||||
|
+ "jsdom": "^26.0.0",
|
||||||
|
+ "kysely-d1": "^0.3.0",
|
||||||
|
+ "kysely-generic-sqlite": "^1.2.1",
|
||||||
|
+ "libsql-stateless-easy": "^1.8.0",
|
||||||
|
+ "open": "^10.1.0",
|
||||||
|
+ "openapi-types": "^12.1.3",
|
||||||
|
+ "postcss": "^8.5.3",
|
||||||
|
+ "postcss-preset-mantine": "^1.17.0",
|
||||||
|
+ "postcss-simple-vars": "^7.0.1",
|
||||||
|
+ "posthog-js-lite": "^3.4.2",
|
||||||
|
+ "react": "^19.0.0",
|
||||||
|
+ "react-dom": "^19.0.0",
|
||||||
|
+ "react-hook-form": "^7.54.2",
|
||||||
|
+ "react-icons": "5.2.1",
|
||||||
|
+ "react-json-view-lite": "^2.4.1",
|
||||||
|
+ "sql-formatter": "^15.4.11",
|
||||||
|
+ "tailwind-merge": "^3.0.2",
|
||||||
|
+ "tailwindcss": "^4.0.12",
|
||||||
|
+ "tailwindcss-animate": "^1.0.7",
|
||||||
|
+ "tsc-alias": "^1.8.11",
|
||||||
|
+ "tsup": "^8.4.0",
|
||||||
|
+ "tsx": "^4.19.3",
|
||||||
|
+ "uuid": "^11.1.0",
|
||||||
|
+ "vite": "^6.3.5",
|
||||||
|
+ "vite-plugin-circular-dependency": "^0.5.0",
|
||||||
|
+ "vite-tsconfig-paths": "^5.1.4",
|
||||||
|
+ "vitest": "^3.0.9",
|
||||||
|
+ "wouter": "^3.6.0",
|
||||||
|
+ "wrangler": "^4.37.1"
|
||||||
|
+ },
|
||||||
|
+ "engines": {
|
||||||
|
+ "node": ">=22.13"
|
||||||
|
+ },
|
||||||
|
+ "optionalDependencies": {
|
||||||
|
+ "@hono/node-server": "^1.14.3"
|
||||||
|
+ },
|
||||||
|
+ "peerDependencies": {
|
||||||
|
+ "react": ">=19",
|
||||||
|
+ "react-dom": ">=19"
|
||||||
|
+ }
|
||||||
|
+ },
|
||||||
|
"node_modules/@alloc/quick-lru": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
|
||||||
|
@@ -4363,6 +4468,10 @@
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
+ "node_modules/bknd": {
|
||||||
|
+ "resolved": "../app",
|
||||||
|
+ "link": true
|
||||||
|
+ },
|
||||||
|
"node_modules/blake3-wasm": {
|
||||||
|
"version": "2.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
|
||||||
|
Index: docs/content/docs/(documentation)/meta.json
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/content/docs/(documentation)/meta.json b/docs/content/docs/(documentation)/meta.json
|
||||||
|
--- a/docs/content/docs/(documentation)/meta.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/content/docs/(documentation)/meta.json (date 1758275212528)
|
||||||
|
@@ -20,6 +20,7 @@
|
||||||
|
"./extending/config",
|
||||||
|
"./extending/events",
|
||||||
|
"./extending/plugins",
|
||||||
|
+ "./extending/admin",
|
||||||
|
"---Integration---",
|
||||||
|
"./integration/introduction",
|
||||||
|
"./integration/(frameworks)/",
|
||||||
|
Index: docs/content/docs/(documentation)/extending/admin.mdx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/content/docs/(documentation)/extending/admin.mdx b/docs/content/docs/(documentation)/extending/admin.mdx
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null (date 1758281513770)
|
||||||
|
+++ b/docs/content/docs/(documentation)/extending/admin.mdx (date 1758281513770)
|
||||||
|
@@ -0,0 +1,56 @@
|
||||||
|
+---
|
||||||
|
+title: Admin UI
|
||||||
|
+tags: ["documentation"]
|
||||||
|
+---
|
||||||
|
+import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||||
|
+import { Button } from "bknd-ui"
|
||||||
|
+import { BkndUi } from "@/app/_components/BkndUi";
|
||||||
|
+
|
||||||
|
+Describe how to extend the Admin UI.
|
||||||
|
+
|
||||||
|
+<BkndUi code={`import { Button } from "bknd/ui"
|
||||||
|
+
|
||||||
|
+function ButtonDemo() {
|
||||||
|
+ return <div className="flex flex-row gap-4">
|
||||||
|
+ <Button variant="primary">
|
||||||
|
+ Primary
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="outline">
|
||||||
|
+ Outline
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="ghost">
|
||||||
|
+ Ghost
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="red">
|
||||||
|
+ Red
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="subtlered">
|
||||||
|
+ Subtlered
|
||||||
|
+ </Button>
|
||||||
|
+ </div>
|
||||||
|
+}`}>
|
||||||
|
+ <div className="flex flex-row gap-4">
|
||||||
|
+ <Button variant="primary">
|
||||||
|
+ Primary
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="outline">
|
||||||
|
+ Outline
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="ghost">
|
||||||
|
+ Ghost
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="red">
|
||||||
|
+ Red
|
||||||
|
+ </Button>
|
||||||
|
+ <Button variant="subtlered">
|
||||||
|
+ Subtlered
|
||||||
|
+ </Button>
|
||||||
|
+ </div>
|
||||||
|
+</BkndUi>
|
||||||
|
+
|
||||||
|
+<AutoTypeTable path="../app/src/ui/Admin.tsx" name="BkndAdminProps" />
|
||||||
|
+
|
||||||
|
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminEntityOptions" />
|
||||||
|
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminEntityFieldOptions" />
|
||||||
|
+
|
||||||
|
+<AutoTypeTable path="../app/src/ui/options/index.ts" name="BkndAdminAppShellOptions" />
|
||||||
|
\ No newline at end of file
|
||||||
|
Index: docs/tsconfig.json
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/tsconfig.json b/docs/tsconfig.json
|
||||||
|
--- a/docs/tsconfig.json (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/tsconfig.json (date 1758275691058)
|
||||||
|
@@ -18,6 +18,7 @@
|
||||||
|
"paths": {
|
||||||
|
"@/.source": ["./.source/index.ts"],
|
||||||
|
"@/bknd/*": ["../app/src/"],
|
||||||
|
+ "bknd-ui": ["../app/dist/ui/index.js"],
|
||||||
|
"@/*": ["./*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
Index: docs/app/_components/BkndUi.tsx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/app/_components/BkndUi.tsx b/docs/app/_components/BkndUi.tsx
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null (date 1758280846757)
|
||||||
|
+++ b/docs/app/_components/BkndUi.tsx (date 1758280846757)
|
||||||
|
@@ -0,0 +1,36 @@
|
||||||
|
+import { readFileSync } from "node:fs";
|
||||||
|
+import { join } from "node:path";
|
||||||
|
+import { BkndUiClient } from "./BkndUiClient";
|
||||||
|
+
|
||||||
|
+// Server component that reads CSS from disk
|
||||||
|
+export function BkndUi({ children, code }: { children: React.ReactNode; code?: string }) {
|
||||||
|
+ let bkndCss = "";
|
||||||
|
+
|
||||||
|
+ try {
|
||||||
|
+ // Try to read the CSS file from the app directory
|
||||||
|
+ const cssPath = join(process.cwd(), "node_modules", "bknd", "dist", "ui", "main.css");
|
||||||
|
+ bkndCss = readFileSync(cssPath, "utf-8");
|
||||||
|
+ } catch (error) {
|
||||||
|
+ console.warn("Could not read bknd CSS file:", error);
|
||||||
|
+ // Fallback CSS
|
||||||
|
+ bkndCss = `
|
||||||
|
+ .bknd-admin {
|
||||||
|
+ --color-primary: #18181b;
|
||||||
|
+ --color-background: #fafafa;
|
||||||
|
+ --color-muted: #e4e4e7;
|
||||||
|
+ color: var(--color-primary);
|
||||||
|
+ background: var(--color-background);
|
||||||
|
+ padding: 16px;
|
||||||
|
+ border: 1px solid var(--color-muted);
|
||||||
|
+ border-radius: 8px;
|
||||||
|
+ font-family: system-ui, sans-serif;
|
||||||
|
+ }
|
||||||
|
+ `;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return (
|
||||||
|
+ <BkndUiClient bkndCss={bkndCss} code={code}>
|
||||||
|
+ {children}
|
||||||
|
+ </BkndUiClient>
|
||||||
|
+ );
|
||||||
|
+}
|
||||||
|
Index: docs/next.config.mjs
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/next.config.mjs b/docs/next.config.mjs
|
||||||
|
--- a/docs/next.config.mjs (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/next.config.mjs (date 1758281330764)
|
||||||
|
@@ -19,6 +19,9 @@
|
||||||
|
webpack(config) {
|
||||||
|
config.resolve.alias["@/bknd"] = path.resolve(__dirname, "../app/src");
|
||||||
|
config.resolve.alias["@"] = path.resolve(__dirname);
|
||||||
|
+
|
||||||
|
+ // @todo: this doesn't work with turbo
|
||||||
|
+ config.resolve.alias["bknd-ui"] = path.resolve(__dirname, "../app/dist/ui/index.js");
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
eslint: {
|
||||||
|
Index: docs/app/_components/BkndUiClient.tsx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/app/_components/BkndUiClient.tsx b/docs/app/_components/BkndUiClient.tsx
|
||||||
|
new file mode 100644
|
||||||
|
--- /dev/null (date 1758281176041)
|
||||||
|
+++ b/docs/app/_components/BkndUiClient.tsx (date 1758281176041)
|
||||||
|
@@ -0,0 +1,62 @@
|
||||||
|
+"use client";
|
||||||
|
+
|
||||||
|
+import { useEffect, useRef, useState } from "react";
|
||||||
|
+import { createPortal } from "react-dom";
|
||||||
|
+import { Card } from "fumadocs-ui/components/card";
|
||||||
|
+import { DynamicCodeBlock } from "fumadocs-ui/components/dynamic-codeblock";
|
||||||
|
+import { useTheme } from "next-themes";
|
||||||
|
+
|
||||||
|
+interface BkndUiClientProps {
|
||||||
|
+ bkndCss: string;
|
||||||
|
+ children: React.ReactNode;
|
||||||
|
+ code?: string;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
+export function BkndUiClient({ bkndCss, children, code }: BkndUiClientProps) {
|
||||||
|
+ const { theme } = useTheme();
|
||||||
|
+ const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
+ const [shadowRoot, setShadowRoot] = useState<ShadowRoot | null>(null);
|
||||||
|
+ const [mountPoint, setMountPoint] = useState<HTMLDivElement | null>(null);
|
||||||
|
+
|
||||||
|
+ useEffect(() => {
|
||||||
|
+ if (!containerRef.current || shadowRoot) return;
|
||||||
|
+
|
||||||
|
+ // Create shadow root
|
||||||
|
+ const shadow = containerRef.current.attachShadow({ mode: "open" });
|
||||||
|
+
|
||||||
|
+ // Create style element with the CSS passed from server
|
||||||
|
+ const style = document.createElement("style");
|
||||||
|
+ style.textContent = `
|
||||||
|
+ :host {
|
||||||
|
+ display: block;
|
||||||
|
+ isolation: isolate;
|
||||||
|
+ }
|
||||||
|
+ ${bkndCss}
|
||||||
|
+ `;
|
||||||
|
+
|
||||||
|
+ shadow.appendChild(style);
|
||||||
|
+
|
||||||
|
+ // Create mount point for React content
|
||||||
|
+ const mount = document.createElement("div");
|
||||||
|
+ mount.className = `bknd-admin ${theme}`;
|
||||||
|
+ shadow.appendChild(mount);
|
||||||
|
+
|
||||||
|
+ setShadowRoot(shadow);
|
||||||
|
+ setMountPoint(mount);
|
||||||
|
+ }, [shadowRoot, bkndCss]);
|
||||||
|
+
|
||||||
|
+ return (
|
||||||
|
+ <>
|
||||||
|
+ {/* @ts-ignore */}
|
||||||
|
+ <Card className="p-0">
|
||||||
|
+ <div className="flex flex-col justify-center items-center p-4">
|
||||||
|
+ <div ref={containerRef} className="bknd-ui-shadow-host">
|
||||||
|
+ <div>Loading...</div>
|
||||||
|
+ {shadowRoot && mountPoint && createPortal(children, mountPoint)}
|
||||||
|
+ </div>
|
||||||
|
+ </div>
|
||||||
|
+ {code && <DynamicCodeBlock lang="tsx" code={code} />}
|
||||||
|
+ </Card>
|
||||||
|
+ </>
|
||||||
|
+ );
|
||||||
|
+}
|
||||||
|
Index: docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
|
||||||
|
IDEA additional info:
|
||||||
|
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
|
||||||
|
<+>UTF-8
|
||||||
|
===================================================================
|
||||||
|
diff --git a/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx b/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx
|
||||||
|
--- a/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx (revision 26d1f2b583b5299f9d6d8ba6ec5a7b74b3297e06)
|
||||||
|
+++ b/docs/content/docs/(documentation)/usage/mcp/tools-resources.mdx (date 1758280630236)
|
||||||
|
@@ -1,5 +1,5 @@
|
||||||
|
---
|
||||||
|
-title: "MCP"
|
||||||
|
+title: "Tools & Resources"
|
||||||
|
description: "Tools & Resources of the built-in full featured MCP server."
|
||||||
|
tags: ["documentation"]
|
||||||
|
---
|
||||||
|
@@ -24,7 +24,7 @@
|
||||||
|
|
||||||
|
Create a new user
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"role":{"type":"string","enum":[]}},"required":["email","password"]}} key={"auth_user_create"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","properties":{"email":{"type":"string","format":"email"},"password":{"type":"string","minLength":8},"role":{"type":"string"}},"required":["email","password"]}} key={"auth_user_create"} />
|
||||||
|
|
||||||
|
### `auth_user_password_change`
|
||||||
|
|
||||||
|
@@ -48,61 +48,61 @@
|
||||||
|
|
||||||
|
Delete many
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_delete_many"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_delete_many"} />
|
||||||
|
|
||||||
|
### `data_entity_delete_one`
|
||||||
|
|
||||||
|
Delete one
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"}}}} key={"data_entity_delete_one"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"}}}} key={"data_entity_delete_one"} />
|
||||||
|
|
||||||
|
### `data_entity_fn_count`
|
||||||
|
|
||||||
|
Count entities
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_count"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_count"} />
|
||||||
|
|
||||||
|
### `data_entity_fn_exists`
|
||||||
|
|
||||||
|
Check if entity exists
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_exists"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_fn_exists"} />
|
||||||
|
|
||||||
|
### `data_entity_info`
|
||||||
|
|
||||||
|
Retrieve entity info
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"}}}} key={"data_entity_info"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"}}}} key={"data_entity_info"} />
|
||||||
|
|
||||||
|
### `data_entity_insert`
|
||||||
|
|
||||||
|
Insert one or many
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"json":{"anyOf":[{"type":"object","properties":{}},{"type":"array","items":{"type":"object","properties":{}}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_insert"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"json":{"anyOf":[{"type":"object","properties":{}},{"type":"array","items":{"type":"object","properties":{}}}],"$synthetic":true,"$target":"json"}}}} key={"data_entity_insert"} />
|
||||||
|
|
||||||
|
### `data_entity_read_many`
|
||||||
|
|
||||||
|
Query entities
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"limit":{"type":"number","default":10,"$target":"json"},"offset":{"type":"number","default":0,"$target":"json"},"sort":{"type":"string","default":"id","$target":"json"},"where":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"},"select":{"type":"array","$target":"json","items":{"type":"string"}},"join":{"type":"array","$target":"json","items":{"type":"string"}},"with":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_read_many"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"limit":{"type":"number","default":10,"$target":"json"},"offset":{"type":"number","default":0,"$target":"json"},"sort":{"type":"string","default":"id","$target":"json"},"where":{"examples":[{"attribute":{"$eq":1}}],"default":{},"anyOf":[{"type":"string"},{"type":"object","properties":{}}],"$synthetic":true,"$target":"json"},"select":{"type":"array","$target":"json","items":{"type":"string"}},"join":{"type":"array","$target":"json","items":{"type":"string"}},"with":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_read_many"} />
|
||||||
|
|
||||||
|
### `data_entity_read_one`
|
||||||
|
|
||||||
|
Read one
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"offset":{"type":"number","default":0,"$target":"query"},"sort":{"type":"string","default":"id","$target":"query"},"select":{"type":"array","$target":"query","items":{"type":"string"}}}}} key={"data_entity_read_one"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"offset":{"type":"number","default":0,"$target":"query"},"sort":{"type":"string","default":"id","$target":"query"},"select":{"type":"array","$target":"query","items":{"type":"string"}}}}} key={"data_entity_read_one"} />
|
||||||
|
|
||||||
|
### `data_entity_update_many`
|
||||||
|
|
||||||
|
Update many
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","update","where"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"update":{"type":"object","$target":"json","properties":{}},"where":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_update_many"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","update","where"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"update":{"type":"object","$target":"json","properties":{}},"where":{"type":"object","$target":"json","properties":{}}}}} key={"data_entity_update_many"} />
|
||||||
|
|
||||||
|
### `data_entity_update_one`
|
||||||
|
|
||||||
|
Update one
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id","json"],"properties":{"entity":{"type":"string","enum":["users","media"],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_update_one"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","required":["entity","id","json"],"properties":{"entity":{"anyOf":[{"type":"string","enum":["users","media"]},{"type":"string"}],"$target":"param"},"id":{"anyOf":[{"type":"number","title":"Integer"},{"type":"string","title":"UUID"}],"$target":"param"},"json":{"type":"object","$synthetic":true,"$target":"json","properties":{}}}}} key={"data_entity_update_one"} />
|
||||||
|
|
||||||
|
### `data_sync`
|
||||||
|
|
||||||
|
@@ -306,7 +306,7 @@
|
||||||
|
|
||||||
|
Update Server configuration
|
||||||
|
|
||||||
|
-<JsonSchemaTypeTable schema={{"type":"object","additionalProperties":false,"properties":{"full":{"type":"boolean","default":false},"return_config":{"type":"boolean","description":"If the new configuration should be returned","default":false},"value":{"type":"object","additionalProperties":false,"properties":{"cors":{"type":"object","additionalProperties":false,"properties":{"origin":{"type":"string","default":"*"},"allow_methods":{"type":"array","default":["GET","POST","PATCH","PUT","DELETE"],"uniqueItems":true,"items":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"]}},"allow_headers":{"type":"array","default":["Content-Type","Content-Length","Authorization","Accept"],"items":{"type":"string"}},"allow_credentials":{"type":"boolean","default":true}}},"mcp":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean","default":false}}}}}},"required":["value"]}} key={"config_server_update"} />
|
||||||
|
+<JsonSchemaTypeTable schema={{"type":"object","additionalProperties":false,"properties":{"full":{"type":"boolean","default":false},"return_config":{"type":"boolean","description":"If the new configuration should be returned","default":false},"value":{"type":"object","additionalProperties":false,"properties":{"cors":{"type":"object","additionalProperties":false,"properties":{"origin":{"type":"string","default":"*"},"allow_methods":{"type":"array","default":["GET","POST","PATCH","PUT","DELETE"],"uniqueItems":true,"items":{"type":"string","enum":["GET","POST","PATCH","PUT","DELETE"]}},"allow_headers":{"type":"array","default":["Content-Type","Content-Length","Authorization","Accept"],"items":{"type":"string"}},"allow_credentials":{"type":"boolean","default":true}}},"mcp":{"type":"object","additionalProperties":false,"properties":{"enabled":{"type":"boolean","default":false},"path":{"type":"string","default":"/api/system/mcp"}}}}}},"required":["value"]}} key={"config_server_update"} />
|
||||||
|
|
||||||
|
|
||||||
|
## Resources
|
||||||
Reference in New Issue
Block a user