Files
bknd/app/src/ui/client/BkndProvider.tsx

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;
}