removed admin config from server, theme is now client side, fixed module manager migrations

This commit is contained in:
dswbx
2025-03-11 17:41:33 +01:00
parent de89e26ffb
commit 96781f5d3d
33 changed files with 1572 additions and 319 deletions

View File

@@ -30,7 +30,7 @@ export class DebugLogger {
const now = performance.now();
const time = this.last === 0 ? 0 : Number.parseInt(String(now - this.last));
const indents = " ".repeat(this._context.length);
const indents = " ".repeat(Math.max(this._context.length - 1, 0));
const context =
this._context.length > 0 ? `[${this._context[this._context.length - 1]}]` : "";
console.log(indents, context, time, ...args);

View File

@@ -19,8 +19,6 @@ export class AppData extends Module<typeof dataConfigSchema> {
indices: _indices = {},
} = this.config;
this.ctx.logger.context("AppData").log("building with entities", Object.keys(_entities));
const entities = transformObject(_entities, (entityConfig, name) => {
return constructEntity(name, entityConfig);
});
@@ -60,7 +58,6 @@ export class AppData extends Module<typeof dataConfigSchema> {
);
this.ctx.guard.registerPermissions(Object.values(DataPermissions));
this.ctx.logger.clear();
this.setBuilt();
}

View File

@@ -115,7 +115,7 @@ const configJsonSchema = Type.Union([
}),
),
]);
const __bknd = entity(TABLE_NAME, {
export const __bknd = entity(TABLE_NAME, {
version: number().required(),
type: enumm({ enum: ["config", "diff", "backup"] }).required(),
json: jsonSchema({ schema: configJsonSchema }).required(),
@@ -170,6 +170,8 @@ export class ModuleManager {
}
}
this.logger.log("booted with", this._booted_with);
this.createModules(initial);
}
@@ -271,7 +273,7 @@ export class ModuleManager {
};
}
private async fetch(): Promise<ConfigTable> {
private async fetch(): Promise<ConfigTable | undefined> {
this.logger.context("fetch").log("fetching");
const startTime = performance.now();
@@ -285,7 +287,7 @@ export class ModuleManager {
if (!result) {
this.logger.log("error fetching").clear();
throw BkndError.with("no config");
return undefined;
}
this.logger
@@ -305,6 +307,7 @@ export class ModuleManager {
try {
const state = await this.fetch();
if (!state) throw new BkndError("save: no config found");
this.logger.log("fetched version", state.version);
if (state.version !== version) {
@@ -321,11 +324,11 @@ export class ModuleManager {
json: configs,
});
} else {
this.logger.log("version matches");
this.logger.log("version matches", state.version);
// clean configs because of Diff() function
const diffs = diff(state.json, clone(configs));
this.logger.log("checking diff", diffs);
this.logger.log("checking diff", [diffs.length]);
if (diff.length > 0) {
// store diff
@@ -380,78 +383,6 @@ export class ModuleManager {
return this;
}
private async migrate() {
const state = {
success: false,
migrated: false,
version: {
before: this.version(),
after: this.version(),
},
};
this.logger.context("migrate").log("migrating?", this.version(), CURRENT_VERSION);
if (this.version() < CURRENT_VERSION) {
state.version.before = this.version();
this.logger.log("there are migrations, verify version");
// sync __bknd table
await this.syncConfigTable();
// modules must be built before migration
this.logger.log("building modules");
await this.buildModules({ graceful: true });
this.logger.log("modules built");
try {
const state = await this.fetch();
if (state.version !== this.version()) {
// @todo: potentially drop provided config and use database version
throw new Error(
`Given version (${this.version()}) and fetched version (${state.version}) do not match.`,
);
}
} catch (e: any) {
throw new Error(`Version is ${this.version()}, fetch failed: ${e.message}`);
}
this.logger.log("now migrating");
let version = this.version();
let configs: any = this.configs();
//console.log("migrating with", version, configs);
if (Object.keys(configs).length === 0) {
throw new Error("No config to migrate");
}
const [_version, _configs] = await migrate(version, configs, {
db: this.db,
});
version = _version;
configs = _configs;
this._version = version;
state.version.after = version;
state.migrated = true;
this.ctx().flags.sync_required = true;
this.logger.log("setting configs");
this.createModules(configs);
await this.buildModules();
this.logger.log("migrated to", version);
$console.log("Migrated config from", state.version.before, "to", state.version.after);
await this.save();
} else {
this.logger.log("no migrations needed");
}
state.success = true;
this.logger.clear();
return state;
}
private setConfigs(configs: ModuleConfigs): void {
this.logger.log("setting configs");
objectEach(configs, (config, key) => {
@@ -469,66 +400,66 @@ export class ModuleManager {
async build(opts?: { fetch?: boolean }) {
this.logger.context("build").log("version", this.version());
this.logger.log("booted with", this._booted_with);
// if no config provided, try fetch from db
if (this.version() === 0 || opts?.fetch === true) {
if (this.version() === 0) {
this.logger.context("no version").log("version is 0");
} else {
this.logger.context("force fetch").log("force fetch");
if (opts?.fetch) {
this.logger.log("force fetch");
}
try {
const result = await this.fetch();
const result = await this.fetch();
// if no version, and nothing found, go with initial
if (!result) {
this.logger.log("nothing in database, go initial");
await this.setupInitial();
} else {
this.logger.log("db has", result.version);
// set version and config from fetched
this._version = result.version;
if (this.version() !== CURRENT_VERSION) {
await this.syncConfigTable();
}
if (this.options?.trustFetched === true) {
this.logger.log("trusting fetched config (mark)");
mark(result.json);
}
this.setConfigs(result.json);
} catch (e: any) {
this.logger.clear(); // fetch couldn't clear
// if version doesn't match, migrate before building
if (this.version() !== CURRENT_VERSION) {
this.logger.log("now migrating");
this.logger.context("error handler").log("fetch failed", e.message);
await this.syncConfigTable();
// we can safely build modules, since config version is up to date
// it's up to date because we use default configs (no fetch result)
this._version = CURRENT_VERSION;
await this.syncConfigTable();
const state = await this.buildModules();
if (!state.saved) {
await this.save();
const version_before = this.version();
const [_version, _configs] = await migrate(version_before, result.json, {
db: this.db,
});
this._version = _version;
this.ctx().flags.sync_required = true;
this.logger.log("migrated to", _version);
$console.log("Migrated config from", version_before, "to", this.version());
this.createModules(_configs);
await this.buildModules();
} else {
this.logger.log("version is current", this.version());
this.createModules(result.json);
await this.buildModules();
}
// run initial setup
await this.setupInitial();
this.logger.clear();
return this;
}
this.logger.clear();
}
// migrate to latest if needed
this.logger.log("check migrate");
const migration = await this.migrate();
if (migration.success && migration.migrated) {
this.logger.log("skipping build after migration");
} else {
this.logger.log("trigger build modules");
if (this.version() !== CURRENT_VERSION) {
throw new Error(
`Given version (${this.version()}) and current version (${CURRENT_VERSION}) do not match.`,
);
}
this.logger.log("current version is up to date", this.version());
await this.buildModules();
}
this.logger.log("done");
this.logger.clear();
return this;
}
@@ -589,6 +520,14 @@ export class ModuleManager {
}
protected async setupInitial() {
this.logger.context("initial").log("start");
this._version = CURRENT_VERSION;
await this.syncConfigTable();
const state = await this.buildModules();
if (!state.saved) {
await this.save();
}
const ctx = {
...this.ctx(),
// disable events for initial setup
@@ -601,6 +540,7 @@ export class ModuleManager {
// run first boot event
await this.options?.onFirstBoot?.();
this.logger.clear();
}
mutateConfigSafe<Module extends keyof Modules>(

View File

@@ -1,6 +1,7 @@
import { _jsonp, transformObject } from "core/utils";
import { type Kysely, sql } from "kysely";
import { set } from "lodash-es";
import type { InitialModuleConfigs } from "modules/ModuleManager";
export type MigrationContext = {
db: Kysely<any>;
@@ -91,6 +92,17 @@ export const migrations: Migration[] = [
};
},
},
{
// remove admin config
version: 9,
up: async (config) => {
const { admin, ...server } = config.server;
return {
...config,
server,
};
},
},
];
export const CURRENT_VERSION = migrations[migrations.length - 1]?.version ?? 0;

View File

@@ -8,7 +8,6 @@ import { Fragment } from "hono/jsx";
import { css, Style } from "hono/css";
import { Controller } from "modules/Controller";
import * as SystemPermissions from "modules/permissions";
import type { AppTheme } from "modules/server/AppServer";
const htmlBkndContextReplace = "<!-- BKND_CONTEXT -->";
@@ -74,7 +73,6 @@ export class AdminController extends Controller {
const obj = {
user: c.get("auth")?.user,
logout_route: this.withBasePath(authRoutes.logout),
color_scheme: configs.server.admin.color_scheme,
};
const html = await this.getHtml(obj);
if (!html) {

View File

@@ -4,24 +4,9 @@ import { cors } from "hono/cors";
import { Module } from "modules/Module";
const serverMethods = ["GET", "POST", "PATCH", "PUT", "DELETE"];
const appThemes = ["dark", "light", "system"] as const;
export type AppTheme = (typeof appThemes)[number];
export const serverConfigSchema = Type.Object(
{
admin: Type.Object(
{
basepath: Type.Optional(Type.String({ default: "", pattern: "^(/.+)?$" })),
color_scheme: Type.Optional(StringEnum(["dark", "light", "system"])),
logo_return_path: Type.Optional(
Type.String({
default: "/",
description: "Path to return to after *clicking* the logo",
}),
),
},
{ default: {}, additionalProperties: false },
),
cors: Type.Object(
{
origin: Type.String({ default: "*" }),
@@ -43,12 +28,6 @@ export const serverConfigSchema = Type.Object(
export type AppServerConfig = Static<typeof serverConfigSchema>;
/*declare global {
interface Request {
cf: IncomingRequestCfProperties;
}
}*/
export class AppServer extends Module<typeof serverConfigSchema> {
//private admin_html?: string;

View File

@@ -1,8 +1,7 @@
import { MantineProvider } from "@mantine/core";
import { Notifications } from "@mantine/notifications";
import type { ModuleConfigs } from "modules";
import React from "react";
import { BkndProvider } from "ui/client/bknd";
import { BkndProvider, type BkndAdminOptions } from "ui/client/bknd";
import { useTheme } from "ui/client/use-theme";
import { Logo } from "ui/components/display/Logo";
import * as AppShell from "ui/layouts/AppShell/AppShell";
@@ -14,7 +13,7 @@ import { Routes } from "./routes";
export type BkndAdminProps = {
baseUrl?: string;
withProvider?: boolean | ClientProviderProps;
config?: ModuleConfigs["server"]["admin"];
config?: BkndAdminOptions;
};
export default function Admin({
@@ -23,7 +22,7 @@ export default function Admin({
config,
}: BkndAdminProps) {
const Component = (
<BkndProvider adminOverride={config} fallback={<Skeleton theme={config?.color_scheme} />}>
<BkndProvider options={config} fallback={<Skeleton />}>
<AdminInternal />
</BkndProvider>
);

View File

@@ -4,7 +4,13 @@ import { createContext, startTransition, useContext, useEffect, useRef, useState
import { useApi } from "ui/client";
import { type TSchemaActions, getSchemaActions } from "./schema/actions";
import { AppReduced } from "./utils/AppReduced";
import type { AppTheme } from "ui/client/use-theme";
export type BkndAdminOptions = {
logo_return_path?: string;
basepath?: string;
theme?: AppTheme;
};
type BkndContext = {
version: number;
schema: ModuleSchemas;
@@ -14,7 +20,7 @@ type BkndContext = {
requireSecrets: () => Promise<void>;
actions: ReturnType<typeof getSchemaActions>;
app: AppReduced;
adminOverride?: ModuleConfigs["server"]["admin"];
options: BkndAdminOptions;
fallback: boolean;
};
@@ -29,13 +35,15 @@ enum Fetching {
export function BkndProvider({
includeSecrets = false,
adminOverride,
options,
children,
fallback = null,
}: { includeSecrets?: boolean; children: any; fallback?: React.ReactNode } & Pick<
BkndContext,
"adminOverride"
>) {
}: {
includeSecrets?: boolean;
children: any;
fallback?: React.ReactNode;
options?: BkndAdminOptions;
}) {
const [withSecrets, setWithSecrets] = useState<boolean>(includeSecrets);
const [schema, setSchema] =
useState<Pick<BkndContext, "version" | "schema" | "config" | "permissions" | "fallback">>();
@@ -93,13 +101,6 @@ export function BkndProvider({
fallback: true,
} as any);
if (adminOverride) {
newSchema.config.server.admin = {
...newSchema.config.server.admin,
...adminOverride,
};
}
startTransition(() => {
document.startViewTransition(() => {
setSchema(newSchema);
@@ -122,13 +123,13 @@ export function BkndProvider({
}, []);
if (!fetched || !schema) return fallback;
const app = new AppReduced(schema?.config as any);
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, adminOverride, hasSecrets }}
value={{ ...schema, actions, requireSecrets, app, options: app.options, hasSecrets }}
key={local_version}
>
{/*{error && (
@@ -153,3 +154,12 @@ export function useBknd({ withSecrets }: { withSecrets?: boolean } = {}): BkndCo
return ctx;
}
export function useBkndOptions(): BkndAdminOptions {
const ctx = useContext(BkndContext);
return (
ctx.options ?? {
basepath: "/",
}
);
}

View File

@@ -1,6 +1,5 @@
import { Api, type ApiOptions, type TApiUser } from "Api";
import { isDebug } from "core";
import type { AppTheme } from "modules/server/AppServer";
import { createContext, useContext } from "react";
const ClientContext = createContext<{ baseUrl: string; api: Api }>({
@@ -62,7 +61,6 @@ export const useBaseUrl = () => {
type BkndWindowContext = {
user?: TApiUser;
logout_route: string;
color_scheme?: AppTheme;
};
export function useBkndWindowContext(): BkndWindowContext {
if (typeof window !== "undefined" && window.__BKND__) {

View File

@@ -1 +1 @@
export { BkndProvider, useBknd } from "./BkndProvider";
export { BkndProvider, type BkndAdminOptions, useBknd } from "./BkndProvider";

View File

@@ -29,17 +29,3 @@ export function useBkndSystem() {
actions,
};
}
export function useBkndSystemTheme() {
const $sys = useBkndSystem();
return {
theme: $sys.theme,
set: $sys.actions.theme.set,
toggle: async () => {
document.startViewTransition(async () => {
await $sys.actions.theme.toggle();
});
},
};
}

View File

@@ -1,29 +1,49 @@
import type { AppTheme } from "modules/server/AppServer";
import { useBkndWindowContext } from "ui/client/ClientProvider";
import { useBknd } from "ui/client/bknd";
import { create } from "zustand";
import { combine, persist } from "zustand/middleware";
const themes = ["dark", "light", "system"] as const;
export type AppTheme = (typeof themes)[number];
const themeStore = create(
persist(
combine({ theme: null as AppTheme | null }, (set) => ({
setTheme: (theme: AppTheme | any) => {
if (themes.includes(theme)) {
document.startViewTransition(() => {
set({ theme });
});
}
},
})),
{
name: "bknd-admin-theme",
},
),
);
export function useTheme(fallback: AppTheme = "system") {
const b = useBknd();
const winCtx = useBkndWindowContext();
const theme_state = themeStore((state) => state.theme);
const theme_set = themeStore((state) => state.setTheme);
// 1. override
// 2. config
// 3. winCtx
// 4. fallback
// 5. default
const override = b?.adminOverride?.color_scheme;
const config = b?.config.server.admin.color_scheme;
const win = winCtx.color_scheme;
// 2. local storage
// 3. fallback
// 4. default
const override = b?.options?.theme;
const prefersDark =
typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches;
const theme = override ?? config ?? win ?? fallback;
const theme = override ?? theme_state ?? fallback;
return {
theme: (theme === "system" ? (prefersDark ? "dark" : "light") : theme) as AppTheme,
value: theme,
themes,
setTheme: theme_set,
state: theme_state,
prefersDark,
override,
config,
win,
};
}

View File

@@ -2,6 +2,7 @@ import type { App } from "App";
import { type Entity, type EntityRelation, constructEntity, constructRelation } from "data";
import { RelationAccessor } from "data/relations/RelationAccessor";
import { Flow, TaskMap } from "flows";
import type { BkndAdminOptions } from "ui/client/BkndProvider";
export type AppType = ReturnType<App["toJSON"]>;
@@ -15,7 +16,10 @@ export class AppReduced {
private _relations: EntityRelation[] = [];
private _flows: Flow[] = [];
constructor(protected appJson: AppType) {
constructor(
protected appJson: AppType,
protected _options: BkndAdminOptions = {},
) {
//console.log("received appjson", appJson);
this._entities = Object.entries(this.appJson.data.entities ?? {}).map(([name, entity]) => {
@@ -62,18 +66,21 @@ export class AppReduced {
return this.appJson;
}
getAdminConfig() {
return this.appJson.server.admin;
get options() {
return {
basepath: "",
logo_return_path: "/",
...this._options,
};
}
getSettingsPath(path: string[] = []): string {
const { basepath } = this.getAdminConfig();
const base = `~/${basepath}/settings`.replace(/\/+/g, "/");
const base = `~/${this.options.basepath}/settings`.replace(/\/+/g, "/");
return [base, ...path].join("/");
}
getAbsolutePath(path?: string): string {
const { basepath } = this.getAdminConfig();
const { basepath } = this.options;
return (path ? `~/${basepath}/${path}` : `~/${basepath}`).replace(/\/+/g, "/");
}

View File

@@ -1,28 +0,0 @@
import { useState } from "react";
export type AppTheme = "light" | "dark" | string;
export function useSetTheme(initialTheme: AppTheme = "light") {
const [theme, _setTheme] = useState(initialTheme);
const $html = document.querySelector("#bknd-admin")!;
function setTheme(newTheme: AppTheme) {
$html?.classList.remove("dark", "light");
$html?.classList.add(newTheme);
_setTheme(newTheme);
// @todo: just a quick switcher config update test
fetch("/api/system/config/patch/server/admin", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ color_scheme: newTheme }),
})
.then((res) => res.json())
.then((data) => {
console.log("theme updated", data);
});
}
return { theme, setTheme };
}

View File

@@ -1,19 +1,17 @@
import {
Background,
BackgroundVariant,
MarkerType,
MiniMap,
type MiniMapProps,
ReactFlow,
type ReactFlowProps,
ReactFlowProvider,
addEdge,
useEdgesState,
useNodesState,
useReactFlow,
} from "@xyflow/react";
import { type ReactNode, useCallback, useEffect, useState } from "react";
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
import { useTheme } from "ui/client/use-theme";
type CanvasProps = ReactFlowProps & {
externalProvider?: boolean;
@@ -38,7 +36,7 @@ export function Canvas({
const [nodes, setNodes, onNodesChange] = useNodesState(_nodes ?? []);
const [edges, setEdges, onEdgesChange] = useEdgesState(_edges ?? []);
const { screenToFlowPosition } = useReactFlow();
const { theme } = useBkndSystemTheme();
const { theme } = useTheme();
const [isCommandPressed, setIsCommandPressed] = useState(false);
const [isSpacePressed, setIsSpacePressed] = useState(false);

View File

@@ -7,10 +7,10 @@ import type { ComponentPropsWithoutRef } from "react";
import { Button } from "ui/components/buttons/Button";
import { Group, Input, Label } from "ui/components/form/Formy/components";
import { SocialLink } from "./SocialLink";
import type { ValueError } from "@sinclair/typebox/value";
import { type TSchema, Value } from "core/utils";
import type { Validator } from "json-schema-form-react";
import { useTheme } from "ui/client/use-theme";
class TypeboxValidator implements Validator<ValueError> {
async validate(schema: TSchema, data: any) {
@@ -46,6 +46,7 @@ export function AuthForm({
buttonLabel = action === "login" ? "Sign in" : "Sign up",
...props
}: LoginFormProps) {
const { theme } = useTheme();
const basepath = auth?.basepath ?? "/api/auth";
const password = {
action: `${basepath}/password/${action}`,

View File

@@ -12,7 +12,6 @@ import {
} from "react-icons/tb";
import { useAuth, useBkndWindowContext } from "ui/client";
import { useBknd } from "ui/client/bknd";
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
import { useTheme } from "ui/client/use-theme";
import { Button } from "ui/components/buttons/Button";
import { IconButton } from "ui/components/buttons/IconButton";
@@ -24,6 +23,7 @@ import { useAppShell } from "ui/layouts/AppShell/use-appshell";
import { useNavigate } from "ui/lib/routes";
import { useLocation } from "wouter";
import { NavLink } from "./AppShell";
import { autoFormatString } from "core/utils";
export function HeaderNavigation() {
const [location, navigate] = useLocation();
@@ -114,7 +114,7 @@ function SidebarToggler() {
export function Header({ hasSidebar = true }) {
const { app } = useBknd();
const { theme } = useTheme();
const { logo_return_path = "/" } = app.getAdminConfig();
const { logo_return_path = "/" } = app.options;
return (
<header
@@ -142,7 +142,7 @@ export function Header({ hasSidebar = true }) {
}
function UserMenu() {
const { adminOverride, config } = useBknd();
const { config, options } = useBknd();
const auth = useAuth();
const [navigate] = useNavigate();
const { logout_route } = useBkndWindowContext();
@@ -173,7 +173,7 @@ function UserMenu() {
}
}
if (!adminOverride) {
if (!options.theme) {
items.push(() => <UserMenuThemeToggler />);
}
@@ -193,17 +193,15 @@ function UserMenu() {
}
function UserMenuThemeToggler() {
const { theme, toggle } = useBkndSystemTheme();
const { value, themes, setTheme } = useTheme();
return (
<div className="flex flex-col items-center mt-1 pt-1 border-t border-primary/5">
<SegmentedControl
withItemsBorders={false}
className="w-full"
data={[
{ value: "light", label: "Light" },
{ value: "dark", label: "Dark" },
]}
value={theme}
onChange={toggle}
data={themes.map((t) => ({ value: t, label: autoFormatString(t) }))}
value={value}
onChange={setTheme}
size="xs"
/>
</div>

View File

@@ -49,8 +49,7 @@ export function withQuery(url: string, query: object) {
export function withAbsolute(url: string) {
const { app } = useBknd();
const basepath = app.getAdminConfig().basepath;
return `~/${basepath}/${url}`.replace(/\/+/g, "/");
return app.getAbsolutePath(url);
}
export function useRouteNavigate() {
@@ -65,7 +64,7 @@ export function useNavigate() {
const [location, navigate] = useLocation();
const router = useRouter();
const { app } = useBknd();
const basepath = app.getAdminConfig().basepath;
const basepath = app.options.basepath;
return [
(
url: string,
@@ -121,7 +120,6 @@ export function useGoBack(
},
) {
const { app } = useBknd();
const basepath = app.getAdminConfig().basepath;
const [navigate] = useNavigate();
const referrer = document.referrer;
const history_length = window.history.length;
@@ -142,9 +140,7 @@ export function useGoBack(
} else {
//console.log("used fallback");
if (typeof fallback === "string") {
const _fallback = options?.absolute
? `~/${basepath}${fallback}`.replace(/\/+/g, "/")
: fallback;
const _fallback = options?.absolute ? app.getAbsolutePath(fallback) : fallback;
//console.log("fallback", _fallback);
if (options?.native) {

View File

@@ -23,6 +23,7 @@
}
.dark,
.dark .bknd-admin /* currently used for elements, drop after making headless */,
#bknd-admin.dark,
.bknd-admin.dark {
--color-primary: rgb(250 250 250); /* zinc-50 */

View File

@@ -1,11 +1,11 @@
import { MarkerType, type Node, Position, ReactFlowProvider } from "@xyflow/react";
import type { AppDataConfig, TAppDataEntity } from "data/data-schema";
import { useBknd } from "ui/client/BkndProvider";
import { useBkndSystemTheme } from "ui/client/schema/system/use-bknd-system";
import { Canvas } from "ui/components/canvas/Canvas";
import { layoutWithDagre } from "ui/components/canvas/layouts";
import { Panels } from "ui/components/canvas/panels";
import { EntityTableNode } from "./EntityTableNode";
import { useTheme } from "ui/client/use-theme";
function entitiesToNodes(entities: AppDataConfig["entities"]): Node<TAppDataEntity>[] {
return Object.entries(entities ?? {}).map(([name, entity]) => {
@@ -69,7 +69,7 @@ export function DataSchemaCanvas() {
const {
config: { data },
} = useBknd();
const { theme } = useBkndSystemTheme();
const { theme } = useTheme();
const nodes = entitiesToNodes(data.entities);
const edges = relationsToEdges(data.relations).map((e) => ({
...e,

View File

@@ -20,12 +20,12 @@ import { Dropdown } from "../../components/overlay/Dropdown";
import { useFlow } from "../../container/use-flows";
import * as AppShell from "../../layouts/AppShell/AppShell";
import { SectionHeader } from "../../layouts/AppShell/AppShell";
import { useTheme } from "ui/client/use-theme";
export function FlowEdit({ params }) {
const { app } = useBknd();
const { color_scheme: theme } = app.getAdminConfig();
const { basepath } = app.getAdminConfig();
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const { theme } = useTheme();
const prefix = app.getAbsolutePath("settings");
const [location, navigate] = useLocation();
const [execution, setExecution] = useState<Execution>();
const [selectedNodes, setSelectedNodes] = useState<Node[]>([]);

View File

@@ -17,12 +17,11 @@ const TestRoutes = lazy(() => import("./test"));
export function Routes() {
const { app } = useBknd();
const { theme } = useTheme();
const { basepath } = app.getAdminConfig();
return (
<div id="bknd-admin" className={theme + " antialiased"}>
<FlashMessage />
<Router base={basepath}>
<Router base={app.options.basepath}>
<Switch>
<Route path="/auth/login" component={AuthLogin} />
<Route path="/" nest>

View File

@@ -15,7 +15,7 @@ import { ServerSettings } from "./routes/server.settings";
import { IconButton } from "ui/components/buttons/IconButton";
function SettingsSidebar() {
const { version, schema, actions } = useBknd();
const { version, schema, actions, app } = useBknd();
useBrowserTitle(["Settings"]);
async function handleRefresh() {
@@ -151,11 +151,10 @@ const FallbackRoutes = ({
...settingProps
}: SettingProps<any> & { module: string }) => {
const { app } = useBknd();
const basepath = app.getAdminConfig();
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const prefix = app.getAbsolutePath("settings");
return (
<Route path={`/${module}`} nest>
<Route path={module} nest>
<Switch>
<Route
path="/"

View File

@@ -44,8 +44,7 @@ const uiSchema = {
export const AuthSettings = ({ schema: _unsafe_copy, config }) => {
const _s = useBknd();
const _schema = cloneDeep(_unsafe_copy);
const { basepath } = _s.app.getAdminConfig();
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const prefix = _s.app.getAbsolutePath("settings");
try {
const user_entity = config.entity_name ?? "users";

View File

@@ -68,8 +68,7 @@ export const DataSettings = ({
config,
}: { schema: ModuleSchemas["data"]; config: ModuleConfigs["data"] }) => {
const { app } = useBknd();
const basepath = app.getAdminConfig().basepath;
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const prefix = app.getAbsolutePath("settings");
const entities = Object.keys(config.entities ?? {});
function fillEntities(schema: any, key: string = "entity") {

View File

@@ -31,8 +31,7 @@ const uiSchema = {
export const FlowsSettings = ({ schema, config }) => {
const { app } = useBknd();
const { basepath } = app.getAdminConfig();
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const prefix = app.getAbsolutePath("settings");
function fillTasks(schema: any, flow: any, key: string) {
const tasks = Object.keys(flow.tasks ?? {});

View File

@@ -17,15 +17,11 @@ const uiSchema = {
};
export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
const { app, adminOverride } = useBknd();
const { basepath } = app.getAdminConfig();
const { app } = useBknd();
const _schema = cloneDeep(_unsafe_copy);
const prefix = `~/${basepath}/settings`.replace(/\/+/g, "/");
const prefix = app.getAbsolutePath("settings");
const schema = _schema;
if (adminOverride) {
schema.properties.admin.readOnly = true;
}
return (
<Route path="/server" nest>
@@ -33,14 +29,6 @@ export const ServerSettings = ({ schema: _unsafe_copy, config }) => {
path="/"
component={() => (
<Setting
options={{
showAlert: () => {
if (adminOverride) {
return "The admin settings are read-only as they are overriden. Remaining server configuration can be edited.";
}
return;
},
}}
schema={schema}
uiSchema={uiSchema}
config={config}