Merge pull request 'UI: Init api playground' (#2471) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-11-22 13:43:03 +00:00
6 changed files with 174 additions and 22 deletions

View File

@@ -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]:
""" """

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

View File

@@ -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>

View File

@@ -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

View File

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

View File

@@ -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(