UI/refactor: unify clan details and edit
This commit is contained in:
21
pkgs/webview-ui/app/src/hooks/index.ts
Normal file
21
pkgs/webview-ui/app/src/hooks/index.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { callApi } from "../api";
|
||||||
|
import { setActiveURI, setClanList } from "../App";
|
||||||
|
|
||||||
|
export const registerClan = async () => {
|
||||||
|
try {
|
||||||
|
const loc = await callApi("open_file", {
|
||||||
|
file_request: { mode: "select_folder" },
|
||||||
|
});
|
||||||
|
if (loc.status === "success" && loc.data) {
|
||||||
|
const data = loc.data[0];
|
||||||
|
setClanList((s) => {
|
||||||
|
const res = new Set([...s, data]);
|
||||||
|
return Array.from(res);
|
||||||
|
});
|
||||||
|
setActiveURI(data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -7,15 +7,12 @@ import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
|||||||
import { MachineDetails } from "./routes/machines/[name]/view";
|
import { MachineDetails } from "./routes/machines/[name]/view";
|
||||||
import { Layout } from "./layout/layout";
|
import { Layout } from "./layout/layout";
|
||||||
import { MachineListView } from "./routes/machines/view";
|
import { MachineListView } from "./routes/machines/view";
|
||||||
import { CreateClan } from "./routes/clan/view";
|
import { ClanList, CreateClan, ClanDetails } from "./routes/clans";
|
||||||
import { Settings } from "./routes/settings";
|
|
||||||
import { EditClanForm } from "./routes/clan/editClan";
|
|
||||||
import { Flash } from "./routes/flash/view";
|
import { Flash } from "./routes/flash/view";
|
||||||
import { CreateMachine } from "./routes/machines/create";
|
import { CreateMachine } from "./routes/machines/create";
|
||||||
import { HostList } from "./routes/hosts/view";
|
import { HostList } from "./routes/hosts/view";
|
||||||
import { Welcome } from "./routes/welcome";
|
import { Welcome } from "./routes/welcome";
|
||||||
import { Toaster } from "solid-toast";
|
import { Toaster } from "solid-toast";
|
||||||
import { Details } from "./routes/clan/details";
|
|
||||||
|
|
||||||
const client = new QueryClient();
|
const client = new QueryClient();
|
||||||
|
|
||||||
@@ -74,7 +71,7 @@ export const routes: AppRoute[] = [
|
|||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
label: "Overview",
|
label: "Overview",
|
||||||
component: () => <Settings />,
|
component: () => <ClanList />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/create",
|
path: "/create",
|
||||||
@@ -85,7 +82,7 @@ export const routes: AppRoute[] = [
|
|||||||
path: "/:id",
|
path: "/:id",
|
||||||
label: "Details",
|
label: "Details",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
component: () => <Details />,
|
component: () => <ClanDetails />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,11 +64,18 @@ export const Header = (props: HeaderProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
<Show when={activeURI()}>
|
||||||
<button class="link" onClick={() => navigate("/clan")}>
|
{(d) => (
|
||||||
<span class="material-icons">settings</span>
|
<span class="tooltip tooltip-bottom" data-tip="Clan Settings">
|
||||||
</button>
|
<button
|
||||||
</span>
|
class="link"
|
||||||
|
onClick={() => navigate(`/clans/${window.btoa(d())}`)}
|
||||||
|
>
|
||||||
|
<span class="material-icons">settings</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,210 +0,0 @@
|
|||||||
import { callApi, SuccessQuery } from "@/src/api";
|
|
||||||
import { BackButton } from "@/src/components/BackButton";
|
|
||||||
import { useParams } from "@solidjs/router";
|
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
|
||||||
import { createSignal, For, Match, Switch } from "solid-js";
|
|
||||||
import { Show } from "solid-js";
|
|
||||||
import {
|
|
||||||
createForm,
|
|
||||||
FieldValues,
|
|
||||||
getValue,
|
|
||||||
getValues,
|
|
||||||
setValue,
|
|
||||||
} from "@modular-forms/solid";
|
|
||||||
import { TextInput } from "@/src/components/TextInput";
|
|
||||||
import toast from "solid-toast";
|
|
||||||
|
|
||||||
type AdminData = SuccessQuery<"get_admin_service">["data"];
|
|
||||||
|
|
||||||
interface ClanDetailsProps {
|
|
||||||
admin: AdminData;
|
|
||||||
base_url: string;
|
|
||||||
}
|
|
||||||
interface AdminSettings extends FieldValues {
|
|
||||||
allowedKeys: { name: string; value: string }[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const ClanDetails = (props: ClanDetailsProps) => {
|
|
||||||
const items = () =>
|
|
||||||
Object.entries<string>(
|
|
||||||
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
|
|
||||||
);
|
|
||||||
const [formStore, { Form, Field }] = createForm<AdminSettings>({
|
|
||||||
initialValues: {
|
|
||||||
allowedKeys: items().map(([name, value]) => ({ name, value })),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [keys, setKeys] = createSignal<1[]>(
|
|
||||||
new Array(items().length || 1).fill(1),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubmit = async (values: AdminSettings) => {
|
|
||||||
console.log("submitting", values, getValues(formStore));
|
|
||||||
|
|
||||||
const r = await callApi("set_admin_service", {
|
|
||||||
base_url: props.base_url,
|
|
||||||
allowed_keys: values.allowedKeys.reduce(
|
|
||||||
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
});
|
|
||||||
if (r.status === "success") {
|
|
||||||
toast.success("Successfully updated admin settings");
|
|
||||||
}
|
|
||||||
if (r.status === "error") {
|
|
||||||
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<span class="text-xl text-primary">Clan Admin Settings</span>
|
|
||||||
<Form onSubmit={handleSubmit}>
|
|
||||||
<div class="grid grid-cols-12 gap-2">
|
|
||||||
<span class="col-span-12 text-lg text-neutral">
|
|
||||||
Each of the following keys can be used to authenticate on any
|
|
||||||
machine
|
|
||||||
</span>
|
|
||||||
<For each={keys()}>
|
|
||||||
{(name, idx) => (
|
|
||||||
<>
|
|
||||||
<Field name={`allowedKeys.${idx()}.name`}>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextInput
|
|
||||||
formStore={formStore}
|
|
||||||
inputProps={props}
|
|
||||||
label={"Name"}
|
|
||||||
adornment={{
|
|
||||||
position: "start",
|
|
||||||
content: (
|
|
||||||
<span class="material-icons text-gray-400">key</span>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
error={field.error}
|
|
||||||
class="col-span-4"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name={`allowedKeys.${idx()}.value`}>
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
formStore={formStore}
|
|
||||||
inputProps={props}
|
|
||||||
label={"Value"}
|
|
||||||
value={field.value ?? ""}
|
|
||||||
error={field.error}
|
|
||||||
class="col-span-6"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<span class="tooltip mt-auto" data-tip="Select file">
|
|
||||||
<label
|
|
||||||
class={"form-control w-full"}
|
|
||||||
aria-disabled={formStore.submitting}
|
|
||||||
>
|
|
||||||
<div class="input input-bordered relative flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
value=""
|
|
||||||
// Disable drag n drop
|
|
||||||
onDrop={(e) => e.preventDefault()}
|
|
||||||
class="absolute -ml-4 size-full cursor-pointer opacity-0"
|
|
||||||
type="file"
|
|
||||||
onInput={async (e) => {
|
|
||||||
console.log(e.target.files);
|
|
||||||
if (!e.target.files) return;
|
|
||||||
|
|
||||||
const content = await e.target.files[0].text();
|
|
||||||
console.log(content);
|
|
||||||
setValue(
|
|
||||||
formStore,
|
|
||||||
`allowedKeys.${idx()}.value`,
|
|
||||||
content,
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
!getValue(
|
|
||||||
formStore,
|
|
||||||
`allowedKeys.${idx()}.name`,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
setValue(
|
|
||||||
formStore,
|
|
||||||
`allowedKeys.${idx()}.name`,
|
|
||||||
e.target.files[0].name,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<span class="material-icons">file_open</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<button
|
|
||||||
class="btn btn-ghost col-span-1 self-end"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setKeys((c) => c.filter((_, i) => i !== idx()));
|
|
||||||
setValue(formStore, `allowedKeys.${idx()}.name`, "");
|
|
||||||
setValue(formStore, `allowedKeys.${idx()}.value`, "");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="material-icons">delete</span>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div class="my-2 flex w-full gap-2">
|
|
||||||
<button
|
|
||||||
class="btn btn-square btn-ghost"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setKeys((c) => [...c, 1]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="material-icons">add</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn" type="submit">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Details = () => {
|
|
||||||
const params = useParams();
|
|
||||||
const clan_dir = window.atob(params.id);
|
|
||||||
const query = createQuery(() => ({
|
|
||||||
queryKey: [clan_dir, "get_admin_service"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const result = await callApi("get_admin_service", {
|
|
||||||
base_url: clan_dir,
|
|
||||||
});
|
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
|
||||||
return result.data || null;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="p-2">
|
|
||||||
<BackButton />
|
|
||||||
<Show
|
|
||||||
when={!query.isLoading}
|
|
||||||
fallback={<span class="loading loading-lg"></span>}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={query.data}>
|
|
||||||
{(d) => <ClanDetails admin={query.data} base_url={clan_dir} />}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
import { callApi, OperationResponse, pyApi } from "@/src/api";
|
|
||||||
import { Accessor, Match, Show, Switch } from "solid-js";
|
|
||||||
import {
|
|
||||||
createForm,
|
|
||||||
required,
|
|
||||||
reset,
|
|
||||||
SubmitHandler,
|
|
||||||
} from "@modular-forms/solid";
|
|
||||||
import toast from "solid-toast";
|
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
|
||||||
|
|
||||||
type CreateForm = Meta;
|
|
||||||
|
|
||||||
interface EditClanFormProps {
|
|
||||||
directory: Accessor<string>;
|
|
||||||
done: () => void;
|
|
||||||
}
|
|
||||||
export const EditClanForm = (props: EditClanFormProps) => {
|
|
||||||
const { directory } = props;
|
|
||||||
const details = createQuery(() => ({
|
|
||||||
queryKey: [directory(), "meta"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const result = await callApi("show_clan_meta", { uri: directory() });
|
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
|
||||||
return result.data;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Match when={details?.data}>
|
|
||||||
{(data) => (
|
|
||||||
<FinalEditClanForm
|
|
||||||
initial={data()}
|
|
||||||
directory={directory()}
|
|
||||||
done={props.done}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FinalEditClanFormProps {
|
|
||||||
initial: CreateForm;
|
|
||||||
directory: string;
|
|
||||||
done: () => void;
|
|
||||||
}
|
|
||||||
export const FinalEditClanForm = (props: FinalEditClanFormProps) => {
|
|
||||||
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
|
||||||
initialValues: props.initial,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
|
|
||||||
await toast.promise(
|
|
||||||
(async () => {
|
|
||||||
await callApi("update_clan_meta", {
|
|
||||||
options: {
|
|
||||||
directory: props.directory,
|
|
||||||
meta: values,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})(),
|
|
||||||
{
|
|
||||||
loading: "Updating clan...",
|
|
||||||
success: "Clan Successfully updated",
|
|
||||||
error: "Failed to update clan",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
props.done();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="card card-normal">
|
|
||||||
<Form onSubmit={handleSubmit} shouldActive>
|
|
||||||
<Field name="icon">
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<figure>
|
|
||||||
<Show
|
|
||||||
when={field.value}
|
|
||||||
fallback={
|
|
||||||
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
|
|
||||||
group
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(icon) => (
|
|
||||||
<img
|
|
||||||
class="aspect-square size-60 rounded-lg"
|
|
||||||
src={icon()}
|
|
||||||
alt="Clan Logo"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</figure>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<div class="card-body">
|
|
||||||
<Field
|
|
||||||
name="name"
|
|
||||||
validate={[required("Please enter a unique name for the clan.")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<label class="form-control w-full">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
|
|
||||||
Name
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
{...props}
|
|
||||||
disabled={formStore.submitting}
|
|
||||||
required
|
|
||||||
placeholder="Clan Name"
|
|
||||||
class="input input-bordered"
|
|
||||||
classList={{ "input-error": !!field.error }}
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt">{field.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field name="description">
|
|
||||||
{(field, props) => (
|
|
||||||
<label class="form-control w-full">
|
|
||||||
<div class="label">
|
|
||||||
<span class="label-text">Description</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input
|
|
||||||
{...props}
|
|
||||||
disabled={formStore.submitting}
|
|
||||||
required
|
|
||||||
type="text"
|
|
||||||
placeholder="Some words about your clan"
|
|
||||||
class="input input-bordered"
|
|
||||||
classList={{ "input-error": !!field.error }}
|
|
||||||
value={field.value || ""}
|
|
||||||
/>
|
|
||||||
<div class="label">
|
|
||||||
{field.error && (
|
|
||||||
<span class="label-text-alt">{field.error}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
{
|
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary"
|
|
||||||
type="submit"
|
|
||||||
disabled={formStore.submitting}
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
type Meta = Extract<
|
|
||||||
OperationResponse<"show_clan_meta">,
|
|
||||||
{ status: "success" }
|
|
||||||
>["data"];
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { ClanForm } from "./clanDetails";
|
|
||||||
|
|
||||||
export const CreateClan = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ClanForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -15,7 +15,7 @@ type CreateForm = Meta & {
|
|||||||
template: string;
|
template: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ClanForm = () => {
|
export const CreateClan = () => {
|
||||||
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: "",
|
name: "",
|
||||||
394
pkgs/webview-ui/app/src/routes/clans/details.tsx
Normal file
394
pkgs/webview-ui/app/src/routes/clans/details.tsx
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
import { callApi, SuccessQuery } from "@/src/api";
|
||||||
|
import { BackButton } from "@/src/components/BackButton";
|
||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
import {
|
||||||
|
createQuery,
|
||||||
|
QueryClient,
|
||||||
|
useQueryClient,
|
||||||
|
} from "@tanstack/solid-query";
|
||||||
|
import { createSignal, For, Match, Switch } from "solid-js";
|
||||||
|
import { Show } from "solid-js";
|
||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
FieldValues,
|
||||||
|
getValue,
|
||||||
|
getValues,
|
||||||
|
required,
|
||||||
|
setValue,
|
||||||
|
SubmitHandler,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
|
||||||
|
interface AdminModuleFormProps {
|
||||||
|
admin: AdminData;
|
||||||
|
base_url: string;
|
||||||
|
}
|
||||||
|
interface AdminSettings extends FieldValues {
|
||||||
|
allowedKeys: { name: string; value: string }[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditClanFormProps {
|
||||||
|
initial: GeneralData;
|
||||||
|
directory: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EditClanForm = (props: EditClanFormProps) => {
|
||||||
|
const [formStore, { Form, Field }] = createForm<GeneralData>({
|
||||||
|
initialValues: props.initial,
|
||||||
|
});
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<GeneralData> = async (values, event) => {
|
||||||
|
await toast.promise(
|
||||||
|
(async () => {
|
||||||
|
await callApi("update_clan_meta", {
|
||||||
|
options: {
|
||||||
|
directory: props.directory,
|
||||||
|
meta: values,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})(),
|
||||||
|
{
|
||||||
|
loading: "Updating clan...",
|
||||||
|
success: "Clan Successfully updated",
|
||||||
|
error: "Failed to update clan",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [props.directory, "meta"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const curr_name = () => props.initial.name;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} shouldActive>
|
||||||
|
<Field name="icon">
|
||||||
|
{(field) => (
|
||||||
|
<>
|
||||||
|
<figure class="p-1">
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<div class="text-3xl text-primary">{curr_name()}'s</div>
|
||||||
|
<div class="text-secondary">Wide settings</div>
|
||||||
|
</div>
|
||||||
|
</figure>
|
||||||
|
<figure>
|
||||||
|
<Show
|
||||||
|
when={field.value}
|
||||||
|
fallback={
|
||||||
|
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
|
||||||
|
group
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(icon) => (
|
||||||
|
<img
|
||||||
|
class="aspect-square size-60 rounded-lg"
|
||||||
|
src={icon()}
|
||||||
|
alt="Clan Logo"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
</figure>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="text-xl text-primary">General</span>
|
||||||
|
<Field
|
||||||
|
name="name"
|
||||||
|
validate={[required("Please enter a unique name for the clan.")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
|
||||||
|
Name
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
disabled={formStore.submitting}
|
||||||
|
required
|
||||||
|
placeholder="Clan Name"
|
||||||
|
class="input input-bordered"
|
||||||
|
classList={{ "input-error": !!field.error }}
|
||||||
|
value={field.value}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt">{field.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="description">
|
||||||
|
{(field, props) => (
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Description</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
{...props}
|
||||||
|
disabled={formStore.submitting}
|
||||||
|
required
|
||||||
|
type="text"
|
||||||
|
placeholder="Some words about your clan"
|
||||||
|
class="input input-bordered"
|
||||||
|
classList={{ "input-error": !!field.error }}
|
||||||
|
value={field.value || ""}
|
||||||
|
/>
|
||||||
|
<div class="label">
|
||||||
|
{field.error && (
|
||||||
|
<span class="label-text-alt">{field.error}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={formStore.submitting || !formStore.dirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AdminModuleForm = (props: AdminModuleFormProps) => {
|
||||||
|
const items = () =>
|
||||||
|
Object.entries<string>(
|
||||||
|
(props.admin?.config?.allowedKeys as Record<string, string>) || {},
|
||||||
|
);
|
||||||
|
const [formStore, { Form, Field }] = createForm<AdminSettings>({
|
||||||
|
initialValues: {
|
||||||
|
allowedKeys: items().map(([name, value]) => ({ name, value })),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const [keys, setKeys] = createSignal<1[]>(
|
||||||
|
new Array(items().length || 1).fill(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubmit = async (values: AdminSettings) => {
|
||||||
|
console.log("submitting", values, getValues(formStore));
|
||||||
|
|
||||||
|
const r = await callApi("set_admin_service", {
|
||||||
|
base_url: props.base_url,
|
||||||
|
allowed_keys: values.allowedKeys.reduce(
|
||||||
|
(acc, curr) => ({ ...acc, [curr.name]: curr.value }),
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
});
|
||||||
|
if (r.status === "success") {
|
||||||
|
toast.success("Successfully updated admin settings");
|
||||||
|
}
|
||||||
|
if (r.status === "error") {
|
||||||
|
toast.error(`Failed to update admin settings: ${r.errors[0].message}`);
|
||||||
|
}
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: [props.base_url, "get_admin_service"],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<div class="card-body">
|
||||||
|
<span class="text-xl text-primary">Administration</span>
|
||||||
|
<div class="grid grid-cols-12 gap-2">
|
||||||
|
<span class="col-span-12 text-lg text-neutral">
|
||||||
|
Each of the following keys can be used to authenticate on machines
|
||||||
|
</span>
|
||||||
|
<For each={keys()}>
|
||||||
|
{(name, idx) => (
|
||||||
|
<>
|
||||||
|
<Field name={`allowedKeys.${idx()}.name`}>
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label={"Name"}
|
||||||
|
adornment={{
|
||||||
|
position: "start",
|
||||||
|
content: (
|
||||||
|
<span class="material-icons text-gray-400">key</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
class="col-span-4"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name={`allowedKeys.${idx()}.value`}>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label={"Value"}
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
class="col-span-6"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="tooltip mt-auto" data-tip="Select file">
|
||||||
|
<label
|
||||||
|
class={"form-control w-full"}
|
||||||
|
aria-disabled={formStore.submitting}
|
||||||
|
>
|
||||||
|
<div class="btn btn-secondary relative flex items-center justify-center">
|
||||||
|
<input
|
||||||
|
value=""
|
||||||
|
// Disable drag n drop
|
||||||
|
onDrop={(e) => e.preventDefault()}
|
||||||
|
class="absolute -ml-4 size-full cursor-pointer opacity-0"
|
||||||
|
type="file"
|
||||||
|
onInput={async (e) => {
|
||||||
|
console.log(e.target.files);
|
||||||
|
if (!e.target.files) return;
|
||||||
|
|
||||||
|
const content = await e.target.files[0].text();
|
||||||
|
console.log(content);
|
||||||
|
setValue(
|
||||||
|
formStore,
|
||||||
|
`allowedKeys.${idx()}.value`,
|
||||||
|
content,
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
!getValue(
|
||||||
|
formStore,
|
||||||
|
`allowedKeys.${idx()}.name`,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
setValue(
|
||||||
|
formStore,
|
||||||
|
`allowedKeys.${idx()}.name`,
|
||||||
|
e.target.files[0].name,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span class="material-icons">file_open</span>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost col-span-1 self-end"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setKeys((c) => c.filter((_, i) => i !== idx()));
|
||||||
|
setValue(formStore, `allowedKeys.${idx()}.name`, "");
|
||||||
|
setValue(formStore, `allowedKeys.${idx()}.value`, "");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="material-icons">delete</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div class="my-2 flex w-full gap-2">
|
||||||
|
<button
|
||||||
|
class="btn btn-square btn-ghost"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setKeys((c) => [...c, 1]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span class="material-icons">add</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
<div class="card-actions justify-end">
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
type="submit"
|
||||||
|
disabled={formStore.submitting || !formStore.dirty}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type GeneralData = SuccessQuery<"show_clan_meta">["data"];
|
||||||
|
type AdminData = SuccessQuery<"get_admin_service">["data"];
|
||||||
|
|
||||||
|
export const ClanDetails = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const clan_dir = window.atob(params.id);
|
||||||
|
// Fetch general meta data
|
||||||
|
const clanQuery = createQuery(() => ({
|
||||||
|
queryKey: [clan_dir, "meta"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await callApi("show_clan_meta", { uri: clan_dir });
|
||||||
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// Fetch admin settings
|
||||||
|
const adminQuery = createQuery(() => ({
|
||||||
|
queryKey: [clan_dir, "get_admin_service"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await callApi("get_admin_service", {
|
||||||
|
base_url: clan_dir,
|
||||||
|
});
|
||||||
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
|
return result.data || null;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="card card-normal">
|
||||||
|
<BackButton />
|
||||||
|
<Show
|
||||||
|
when={!adminQuery.isLoading}
|
||||||
|
fallback={
|
||||||
|
<div>
|
||||||
|
<span class="loading loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Match when={clanQuery.data}>
|
||||||
|
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Show>
|
||||||
|
<div class="divider"></div>
|
||||||
|
<Show
|
||||||
|
when={!adminQuery.isLoading}
|
||||||
|
fallback={
|
||||||
|
<div>
|
||||||
|
<span class="loading loading-lg"></span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Switch>
|
||||||
|
<Match when={adminQuery.data}>
|
||||||
|
{(d) => <AdminModuleForm admin={d()} base_url={clan_dir} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
pkgs/webview-ui/app/src/routes/clans/index.ts
Normal file
3
pkgs/webview-ui/app/src/routes/clans/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./list";
|
||||||
|
export * from "./create";
|
||||||
|
export * from "./details";
|
||||||
@@ -4,39 +4,14 @@ import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
|
|||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { useFloating } from "@/src/floating";
|
import { useFloating } from "@/src/floating";
|
||||||
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||||
import { EditClanForm } from "../clan/editClan";
|
|
||||||
import { useNavigate, A } from "@solidjs/router";
|
import { useNavigate, A } from "@solidjs/router";
|
||||||
import { fileURLToPath } from "url";
|
import { registerClan } from "@/src/hooks";
|
||||||
|
|
||||||
export const registerClan = async () => {
|
interface ClanItemProps {
|
||||||
try {
|
|
||||||
const loc = await callApi("open_file", {
|
|
||||||
file_request: { mode: "select_folder" },
|
|
||||||
});
|
|
||||||
if (loc.status === "success" && loc.data) {
|
|
||||||
const data = loc.data[0];
|
|
||||||
setClanList((s) => {
|
|
||||||
const res = new Set([...s, data]);
|
|
||||||
return Array.from(res);
|
|
||||||
});
|
|
||||||
setActiveURI(data);
|
|
||||||
// setRoute((r) => {
|
|
||||||
// if (r === "welcome") return "machines";
|
|
||||||
// return r;
|
|
||||||
// });
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ClanDetailsProps {
|
|
||||||
clan_dir: string;
|
clan_dir: string;
|
||||||
setEditURI: Setter<string | null>;
|
|
||||||
}
|
}
|
||||||
const ClanDetails = (props: ClanDetailsProps) => {
|
const ClanItem = (props: ClanItemProps) => {
|
||||||
const { clan_dir, setEditURI } = props;
|
const { clan_dir } = props;
|
||||||
|
|
||||||
const details = createQuery(() => ({
|
const details = createQuery(() => ({
|
||||||
queryKey: [clan_dir, "meta"],
|
queryKey: [clan_dir, "meta"],
|
||||||
@@ -46,7 +21,7 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
return result.data;
|
return result.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
const navigate = useNavigate();
|
||||||
const [reference, setReference] = createSignal<HTMLElement>();
|
const [reference, setReference] = createSignal<HTMLElement>();
|
||||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||||
|
|
||||||
@@ -87,10 +62,8 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
<div class="stat-figure text-primary">
|
<div class="stat-figure text-primary">
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<button
|
<button
|
||||||
class=" join-item btn-sm"
|
class="join-item btn-sm"
|
||||||
onClick={() => {
|
onClick={() => navigate(`/clans/${window.btoa(clan_dir)}`)}
|
||||||
setEditURI(clan_dir);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span class="material-icons">edit</span>
|
<span class="material-icons">edit</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -157,62 +130,37 @@ const ClanDetails = (props: ClanDetailsProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Settings = () => {
|
export const ClanList = () => {
|
||||||
const [editURI, setEditURI] = createSignal<string | null>(null);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<div class="card card-normal">
|
<div class="card card-normal">
|
||||||
<Switch>
|
<div class="card-body">
|
||||||
<Match when={editURI()}>
|
<div class="label">
|
||||||
{(uri) => (
|
<div class="label-text text-2xl text-neutral">Registered Clans</div>
|
||||||
<EditClanForm
|
<div class="flex gap-2">
|
||||||
directory={uri}
|
<span class="tooltip tooltip-top" data-tip="Register clan">
|
||||||
done={() => {
|
<button class="btn btn-square btn-ghost" onClick={registerClan}>
|
||||||
setEditURI(null);
|
<span class="material-icons">post_add</span>
|
||||||
}}
|
</button>
|
||||||
/>
|
</span>
|
||||||
)}
|
<span class="tooltip tooltip-top" data-tip="Create new clan">
|
||||||
</Match>
|
<button
|
||||||
<Match when={!editURI()}>
|
class="btn btn-square btn-ghost"
|
||||||
<div class="card-body">
|
onClick={() => {
|
||||||
<div class="label">
|
navigate("create");
|
||||||
<div class="label-text text-2xl text-neutral">
|
}}
|
||||||
Registered Clans
|
>
|
||||||
</div>
|
<span class="material-icons">add</span>
|
||||||
<div class="flex gap-2">
|
</button>
|
||||||
<span class="tooltip tooltip-top" data-tip="Register clan">
|
</span>
|
||||||
<button
|
|
||||||
class="btn btn-square btn-ghost"
|
|
||||||
onClick={() => {
|
|
||||||
registerClan();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="material-icons">post_add</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
<span class="tooltip tooltip-top" data-tip="Create new clan">
|
|
||||||
<button
|
|
||||||
class="btn btn-square btn-ghost"
|
|
||||||
onClick={() => {
|
|
||||||
navigate("create");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="material-icons">add</span>
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stats stats-vertical shadow">
|
|
||||||
<For each={clanList()}>
|
|
||||||
{(value) => (
|
|
||||||
<ClanDetails clan_dir={value} setEditURI={setEditURI} />
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</div>
|
||||||
</Switch>
|
<div class="stats stats-vertical shadow">
|
||||||
|
<For each={clanList()}>
|
||||||
|
{(value) => <ClanItem clan_dir={value} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { setActiveURI, setClanList } from "@/src/App";
|
import { setActiveURI } from "@/src/App";
|
||||||
import { registerClan } from "../settings";
|
import { registerClan } from "@/src/hooks";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
|
|
||||||
export const Welcome = () => {
|
export const Welcome = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user