Merge pull request 'UI: Init api playground' (#2471) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -163,6 +163,28 @@ def get_modules(base_path: str) -> dict[str, str]:
|
|||||||
return modules
|
return modules
|
||||||
|
|
||||||
|
|
||||||
|
@API.register
|
||||||
|
def get_module_interface(base_path: str, module_name: str) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Check if a module exists and returns the interface schema
|
||||||
|
Returns an error if the module does not exist or has an incorrect interface
|
||||||
|
"""
|
||||||
|
cmd = nix_eval([f"{base_path}#clanInternals.moduleSchemas.{module_name}", "--json"])
|
||||||
|
try:
|
||||||
|
proc = run_no_stdout(cmd)
|
||||||
|
res = proc.stdout.strip()
|
||||||
|
except ClanCmdError as e:
|
||||||
|
msg = "clanInternals might not have moduleSchemas attributes"
|
||||||
|
raise ClanError(
|
||||||
|
msg,
|
||||||
|
location=f"list_modules {base_path}",
|
||||||
|
description="Evaluation failed on clanInternals.moduleSchemas attribute",
|
||||||
|
) from e
|
||||||
|
modules_schema: dict[str, Any] = json.loads(res)
|
||||||
|
|
||||||
|
return modules_schema
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_modules(base_path: str) -> dict[str, ModuleInfo]:
|
def list_modules(base_path: str) -> dict[str, ModuleInfo]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
107
pkgs/webview-ui/app/src/api_test.tsx
Normal file
107
pkgs/webview-ui/app/src/api_test.tsx
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
FieldValues,
|
||||||
|
getValues,
|
||||||
|
SubmitHandler,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { TextInput } from "./components/TextInput";
|
||||||
|
import { Button } from "./components/button";
|
||||||
|
import { callApi } from "./api";
|
||||||
|
import { API } from "@/api/API";
|
||||||
|
import { createSignal, Match, Switch } from "solid-js";
|
||||||
|
import { Typography } from "./components/Typography";
|
||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
|
||||||
|
interface APITesterForm extends FieldValues {
|
||||||
|
endpoint: string;
|
||||||
|
payload: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiTester = () => {
|
||||||
|
const [persistedTestData, setPersistedTestData] = makePersisted(
|
||||||
|
createSignal<APITesterForm>(),
|
||||||
|
{
|
||||||
|
name: "_test_data",
|
||||||
|
storage: localStorage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [formStore, { Form, Field }] = createForm<APITesterForm>({
|
||||||
|
initialValues: persistedTestData(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = createQuery(() => ({
|
||||||
|
// eslint-disable-next-line @tanstack/query/exhaustive-deps
|
||||||
|
queryKey: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
const values = getValues(formStore);
|
||||||
|
return await callApi(
|
||||||
|
values.endpoint as keyof API,
|
||||||
|
JSON.parse(values.payload || ""),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
staleTime: Infinity,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<APITesterForm> = (values) => {
|
||||||
|
console.log(values);
|
||||||
|
setPersistedTestData(values);
|
||||||
|
query.refetch();
|
||||||
|
|
||||||
|
const v = getValues(formStore);
|
||||||
|
console.log(v);
|
||||||
|
// const result = callApi(
|
||||||
|
// values.endpoint as keyof API,
|
||||||
|
// JSON.parse(values.payload)
|
||||||
|
// );
|
||||||
|
// setResult(result);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class="p-2">
|
||||||
|
<h1>API Tester</h1>
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Field name="endpoint">
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextInput
|
||||||
|
label={"endpoint"}
|
||||||
|
value={field.value || ""}
|
||||||
|
inputProps={fieldProps}
|
||||||
|
formStore={formStore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="payload">
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextInput
|
||||||
|
label={"payload"}
|
||||||
|
value={field.value || ""}
|
||||||
|
inputProps={fieldProps}
|
||||||
|
formStore={formStore}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Button class="m-2" disabled={query.isFetching}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
<div>
|
||||||
|
<Typography hierarchy="title" size="default">
|
||||||
|
Result
|
||||||
|
</Typography>
|
||||||
|
<Switch>
|
||||||
|
<Match when={query.isFetching}>
|
||||||
|
<span>loading ...</span>
|
||||||
|
</Match>
|
||||||
|
<Match when={query.isFetched}>
|
||||||
|
<pre>
|
||||||
|
<code>{JSON.stringify(query.data, null, 2)}</code>
|
||||||
|
</pre>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -69,7 +69,12 @@ export const Sidebar = (props: RouteSectionProps) => {
|
|||||||
<div class="sidebar__body">
|
<div class="sidebar__body">
|
||||||
<For each={routes.filter((r) => !r.hidden && r.path != "/clans")}>
|
<For each={routes.filter((r) => !r.hidden && r.path != "/clans")}>
|
||||||
{(route: AppRoute) => (
|
{(route: AppRoute) => (
|
||||||
<Show when={route.children}>
|
<Show
|
||||||
|
when={route.children}
|
||||||
|
fallback={
|
||||||
|
<SidebarListItem href={route.path} title={route.label} />
|
||||||
|
}
|
||||||
|
>
|
||||||
{(children) => (
|
{(children) => (
|
||||||
<SidebarSection title={route.label}>
|
<SidebarSection title={route.label}>
|
||||||
<ul>
|
<ul>
|
||||||
|
|||||||
@@ -67,17 +67,25 @@ const weightMap: Record<Weight, string> = {
|
|||||||
|
|
||||||
interface TypographyProps<H extends Hierarchy> {
|
interface TypographyProps<H extends Hierarchy> {
|
||||||
hierarchy: H;
|
hierarchy: H;
|
||||||
weight: Weight;
|
weight?: Weight;
|
||||||
color: Color;
|
color?: Color;
|
||||||
inverted: boolean;
|
inverted?: boolean;
|
||||||
size: AllowedSizes<H>;
|
size: AllowedSizes<H>;
|
||||||
tag: Tag;
|
tag?: Tag;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
classes?: string;
|
classes?: string;
|
||||||
}
|
}
|
||||||
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
||||||
const { size, color, inverted, hierarchy, weight, tag, children, classes } =
|
const {
|
||||||
props;
|
size,
|
||||||
|
color = "primary",
|
||||||
|
inverted,
|
||||||
|
hierarchy,
|
||||||
|
weight = "normal",
|
||||||
|
tag,
|
||||||
|
children,
|
||||||
|
classes,
|
||||||
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type JSX } from "solid-js";
|
import { splitProps, type JSX } from "solid-js";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
|
|
||||||
type Variants = "dark" | "light";
|
type Variants = "dark" | "light";
|
||||||
@@ -42,32 +42,34 @@ interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
startIcon?: JSX.Element;
|
startIcon?: JSX.Element;
|
||||||
endIcon?: JSX.Element;
|
endIcon?: JSX.Element;
|
||||||
|
class?: string;
|
||||||
}
|
}
|
||||||
export const Button = (props: ButtonProps) => {
|
export const Button = (props: ButtonProps) => {
|
||||||
const {
|
const [local, other] = splitProps(props, [
|
||||||
children,
|
"children",
|
||||||
variant = "dark",
|
"variant",
|
||||||
size = "default",
|
"size",
|
||||||
startIcon,
|
"startIcon",
|
||||||
endIcon,
|
"endIcon",
|
||||||
...buttonProps
|
"class",
|
||||||
} = props;
|
]);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
class={cx(
|
class={cx(
|
||||||
|
local.class,
|
||||||
// Layout
|
// Layout
|
||||||
"inline-flex items-center flex-shrink gap-2 justify-center",
|
"inline-flex items-center flex-shrink gap-2 justify-center",
|
||||||
// Styles
|
// Styles
|
||||||
"border border-solid",
|
"border border-solid",
|
||||||
sizePaddings[size],
|
sizePaddings[local.size || "default"],
|
||||||
// Colors
|
// Colors
|
||||||
variantColors[variant],
|
variantColors[local.variant || "dark"],
|
||||||
)}
|
)}
|
||||||
{...buttonProps}
|
{...other}
|
||||||
>
|
>
|
||||||
<span class="h-4">{startIcon}</span>
|
<span class="h-4">{local.startIcon}</span>
|
||||||
<span>{children}</span>
|
<span>{local.children}</span>
|
||||||
<span class="h-4">{endIcon}</span>
|
<span class="h-4">{local.endIcon}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Toaster } from "solid-toast";
|
|||||||
import { ModuleList } from "./routes/modules/list";
|
import { ModuleList } from "./routes/modules/list";
|
||||||
import { ModuleDetails } from "./routes/modules/details";
|
import { ModuleDetails } from "./routes/modules/details";
|
||||||
import { ModuleDetails as AddModule } from "./routes/modules/add";
|
import { ModuleDetails as AddModule } from "./routes/modules/add";
|
||||||
|
import { ApiTester } from "./api_test";
|
||||||
|
|
||||||
export const client = new QueryClient();
|
export const client = new QueryClient();
|
||||||
|
|
||||||
@@ -138,6 +139,13 @@ export const routes: AppRoute[] = [
|
|||||||
hidden: true,
|
hidden: true,
|
||||||
component: () => <Welcome />,
|
component: () => <Welcome />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/api_testing",
|
||||||
|
label: "api_testing",
|
||||||
|
icon: "bolt",
|
||||||
|
hidden: false,
|
||||||
|
component: () => <ApiTester />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
render(
|
render(
|
||||||
|
|||||||
Reference in New Issue
Block a user