mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-15 20:17:22 +00:00
195 lines
5.1 KiB
TypeScript
195 lines
5.1 KiB
TypeScript
import type { ModuleConfigs, ModuleSchemas } from "modules";
|
|
import { getDefaultConfig, getDefaultSchema } from "modules/ModuleManager";
|
|
import {
|
|
createContext,
|
|
startTransition,
|
|
useContext,
|
|
useEffect,
|
|
useRef,
|
|
useState,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { useApi } from "bknd/client";
|
|
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
|
|
import { AppReduced } from "./utils/AppReduced";
|
|
import { Message } from "ui/components/display/Message";
|
|
import { useNavigate } from "ui/lib/routes";
|
|
import type { BkndAdminProps } from "ui/Admin";
|
|
import type { TPermission } from "auth/authorize/Permission";
|
|
|
|
export type BkndContext = {
|
|
version: number;
|
|
readonly: boolean;
|
|
schema: ModuleSchemas;
|
|
config: ModuleConfigs;
|
|
permissions: TPermission[];
|
|
hasSecrets: boolean;
|
|
requireSecrets: () => Promise<void>;
|
|
actions: ReturnType<typeof getSchemaActions>;
|
|
app: AppReduced;
|
|
options: BkndAdminProps["config"];
|
|
fallback: boolean;
|
|
};
|
|
|
|
const BkndContext = createContext<BkndContext>(undefined!);
|
|
export type { TSchemaActions };
|
|
|
|
enum Fetching {
|
|
None = 0,
|
|
Schema = 1,
|
|
Secrets = 2,
|
|
}
|
|
|
|
export function BkndProvider({
|
|
includeSecrets = false,
|
|
options,
|
|
children,
|
|
fallback = null,
|
|
}: {
|
|
includeSecrets?: boolean;
|
|
children: any;
|
|
fallback?: React.ReactNode;
|
|
options?: BkndAdminProps["config"];
|
|
}) {
|
|
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
|
|
const [schema, setSchema] =
|
|
useState<
|
|
Pick<
|
|
BkndContext,
|
|
"version" | "schema" | "config" | "permissions" | "fallback" | "readonly"
|
|
>
|
|
>();
|
|
const [fetched, setFetched] = useState(false);
|
|
const [error, setError] = useState<boolean>();
|
|
const errorShown = useRef<boolean>(false);
|
|
const fetching = useRef<Fetching>(Fetching.None);
|
|
const [local_version, set_local_version] = useState(0);
|
|
const api = useApi();
|
|
|
|
async function reloadSchema() {
|
|
await fetchSchema(includeSecrets, {
|
|
force: true,
|
|
fresh: true,
|
|
});
|
|
}
|
|
|
|
async function fetchSchema(
|
|
_includeSecrets: boolean = false,
|
|
opts?: {
|
|
force?: boolean;
|
|
fresh?: boolean;
|
|
},
|
|
) {
|
|
const requesting = withSecrets ? Fetching.Secrets : Fetching.Schema;
|
|
if (fetching.current === requesting) return;
|
|
|
|
if (withSecrets && opts?.force !== true) return;
|
|
fetching.current = requesting;
|
|
|
|
const res = await api.system.readSchema({
|
|
config: true,
|
|
secrets: _includeSecrets,
|
|
fresh: opts?.fresh,
|
|
});
|
|
|
|
if (!res.ok) {
|
|
if (errorShown.current) return;
|
|
errorShown.current = true;
|
|
|
|
setError(true);
|
|
// if already has schema, don't overwrite
|
|
if (fetched && schema?.schema) return;
|
|
} else if (error) {
|
|
setError(false);
|
|
}
|
|
|
|
const newSchema = res.ok
|
|
? res.body
|
|
: ({
|
|
version: 0,
|
|
mode: "db",
|
|
schema: getDefaultSchema(),
|
|
config: getDefaultConfig(),
|
|
permissions: [],
|
|
fallback: true,
|
|
} as any);
|
|
|
|
startTransition(() => {
|
|
const commit = () => {
|
|
setSchema(newSchema);
|
|
setWithSecrets(_includeSecrets);
|
|
setFetched(true);
|
|
set_local_version((v) => v + 1);
|
|
fetching.current = Fetching.None;
|
|
};
|
|
|
|
// disable view transitions for now
|
|
// because it causes browser crash on heavy pages (e.g. schema)
|
|
commit();
|
|
/* if ("startViewTransition" in document) {
|
|
document.startViewTransition(commit);
|
|
} else {
|
|
commit();
|
|
} */
|
|
});
|
|
}
|
|
|
|
async function requireSecrets() {
|
|
if (withSecrets) return;
|
|
await fetchSchema(true);
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (schema?.schema) return;
|
|
fetchSchema(includeSecrets);
|
|
}, []);
|
|
|
|
if (!fetched || !schema) return fallback;
|
|
const app = new AppReduced(schema?.config as any, options);
|
|
const actions = getSchemaActions({ api, setSchema, reloadSchema });
|
|
const hasSecrets = withSecrets && !error;
|
|
|
|
return (
|
|
<BkndContext.Provider
|
|
value={{ ...schema, actions, requireSecrets, app, options: app.options, hasSecrets }}
|
|
key={local_version}
|
|
>
|
|
{error ? <AccessDenied /> : children}
|
|
</BkndContext.Provider>
|
|
);
|
|
}
|
|
|
|
function AccessDenied() {
|
|
const [navigate] = useNavigate();
|
|
return (
|
|
<Message.MissingPermission
|
|
what="the Admin UI"
|
|
primary={{
|
|
children: "Login",
|
|
onClick: () => navigate("/auth/login"),
|
|
}}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndContext {
|
|
const ctx = useContext(BkndContext);
|
|
if (withSecrets) ctx.requireSecrets();
|
|
|
|
return ctx;
|
|
}
|
|
|
|
export function useBkndOptions(): BkndAdminProps["config"] {
|
|
const ctx = useContext(BkndContext);
|
|
return (
|
|
ctx.options ?? {
|
|
basepath: "/",
|
|
}
|
|
);
|
|
}
|
|
|
|
export function SchemaEditable({ children }: { children: ReactNode }) {
|
|
const { readonly } = useBknd();
|
|
return !readonly ? children : null;
|
|
}
|