UI/Modules: individual roles members
This commit is contained in:
committed by
hsjobeki
parent
bb6abd44aa
commit
176b07d6d1
@@ -126,7 +126,16 @@ export function SelectInput(props: SelectInputpProps) {
|
|||||||
type="button"
|
type="button"
|
||||||
class="select select-bordered flex items-center gap-2"
|
class="select select-bordered flex items-center gap-2"
|
||||||
ref={setReference}
|
ref={setReference}
|
||||||
popovertarget={_id}
|
formnovalidate
|
||||||
|
onClick={() => {
|
||||||
|
const popover = document.getElementById(_id);
|
||||||
|
if (popover) {
|
||||||
|
popover.togglePopover(); // Show or hide the popover
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
// TODO: Use native popover once Webkti supports it within <form>
|
||||||
|
// popovertarget={_id}
|
||||||
|
// popovertargetaction="toggle"
|
||||||
>
|
>
|
||||||
<Show when={props.adornment && props.adornment.position === "start"}>
|
<Show when={props.adornment && props.adornment.position === "start"}>
|
||||||
{props.adornment?.content}
|
{props.adornment?.content}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { Welcome } from "./routes/welcome";
|
|||||||
import { Toaster } from "solid-toast";
|
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";
|
||||||
|
|
||||||
export const client = new QueryClient();
|
export const client = new QueryClient();
|
||||||
|
|
||||||
@@ -101,11 +102,17 @@ export const routes: AppRoute[] = [
|
|||||||
component: () => <ModuleList />,
|
component: () => <ModuleList />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/:id",
|
path: "details/:id",
|
||||||
label: "Details",
|
label: "Details",
|
||||||
hidden: true,
|
hidden: true,
|
||||||
component: () => <ModuleDetails />,
|
component: () => <ModuleDetails />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/add/:id",
|
||||||
|
label: "Details",
|
||||||
|
hidden: true,
|
||||||
|
component: () => <AddModule />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,3 +34,44 @@ export const createModulesQuery = (
|
|||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export const tagsQuery = (uri: string | null) =>
|
||||||
|
createQuery<string[]>(() => ({
|
||||||
|
queryKey: [uri, "tags"],
|
||||||
|
placeholderData: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!uri) return [];
|
||||||
|
|
||||||
|
const response = await callApi("get_inventory", {
|
||||||
|
base_path: uri,
|
||||||
|
});
|
||||||
|
if (response.status === "error") {
|
||||||
|
toast.error("Failed to fetch data");
|
||||||
|
} else {
|
||||||
|
const machines = response.data.machines || {};
|
||||||
|
const tags = Object.values(machines).flatMap((m) => m.tags || []);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const machinesQuery = (uri: string | null) =>
|
||||||
|
createQuery<string[]>(() => ({
|
||||||
|
queryKey: [uri, "machines"],
|
||||||
|
placeholderData: [],
|
||||||
|
queryFn: async () => {
|
||||||
|
if (!uri) return [];
|
||||||
|
|
||||||
|
const response = await callApi("get_inventory", {
|
||||||
|
base_path: uri,
|
||||||
|
});
|
||||||
|
if (response.status === "error") {
|
||||||
|
toast.error("Failed to fetch data");
|
||||||
|
} else {
|
||||||
|
const machines = response.data.machines || {};
|
||||||
|
return Object.keys(machines);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|||||||
116
pkgs/webview-ui/app/src/routes/modules/add.tsx
Normal file
116
pkgs/webview-ui/app/src/routes/modules/add.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import { activeURI } from "@/src/App";
|
||||||
|
import { BackButton } from "@/src/components/BackButton";
|
||||||
|
import { createModulesQuery, machinesQuery, tagsQuery } from "@/src/queries";
|
||||||
|
import { useParams } from "@solidjs/router";
|
||||||
|
import { For, Match, Switch } from "solid-js";
|
||||||
|
import { ModuleInfo } from "./list";
|
||||||
|
import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid";
|
||||||
|
import { SelectInput } from "@/src/Form/fields/Select";
|
||||||
|
|
||||||
|
export const ModuleDetails = () => {
|
||||||
|
const params = useParams();
|
||||||
|
const modulesQuery = createModulesQuery(activeURI());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="p-1">
|
||||||
|
<BackButton />
|
||||||
|
<div class="p-2">
|
||||||
|
<h3 class="text-2xl">{params.id}</h3>
|
||||||
|
<Switch>
|
||||||
|
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
||||||
|
{(d) => <AddModule data={d()[1]} id={d()[0]} />}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AddModuleProps {
|
||||||
|
data: ModuleInfo;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddModule = (props: AddModuleProps) => {
|
||||||
|
const tags = tagsQuery(activeURI());
|
||||||
|
const machines = machinesQuery(activeURI());
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div>Add to your clan</div>
|
||||||
|
<Switch fallback="loading">
|
||||||
|
<Match when={tags.data}>
|
||||||
|
{(tags) => (
|
||||||
|
<For each={props.data.roles}>
|
||||||
|
{(role) => (
|
||||||
|
<>
|
||||||
|
<div class="text-neutral-600">{role}s</div>
|
||||||
|
<RoleForm
|
||||||
|
avilableTags={tags()}
|
||||||
|
availableMachines={machines.data || []}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RoleFormData extends FieldValues {
|
||||||
|
machines: string[];
|
||||||
|
tags: string[];
|
||||||
|
test: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RoleFormProps {
|
||||||
|
avilableTags: string[];
|
||||||
|
availableMachines: string[];
|
||||||
|
}
|
||||||
|
const RoleForm = (props: RoleFormProps) => {
|
||||||
|
const [formStore, { Field, Form }] = createForm<RoleFormData>({
|
||||||
|
// initialValues: {
|
||||||
|
// machines: ["hugo", "bruno"],
|
||||||
|
// tags: ["network", "backup"],
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<RoleFormData> = (values) => {
|
||||||
|
console.log(values);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit}>
|
||||||
|
<Field name="machines" type="string[]">
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<SelectInput
|
||||||
|
error={field.error}
|
||||||
|
label={"Machines"}
|
||||||
|
value={field.value || []}
|
||||||
|
options={props.availableMachines.map((o) => ({
|
||||||
|
value: o,
|
||||||
|
label: o,
|
||||||
|
}))}
|
||||||
|
multiple
|
||||||
|
selectProps={fieldProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="tags" type="string[]">
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<SelectInput
|
||||||
|
error={field.error}
|
||||||
|
label={"Tags"}
|
||||||
|
value={field.value || []}
|
||||||
|
options={props.avilableTags.map((o) => ({
|
||||||
|
value: o,
|
||||||
|
label: o,
|
||||||
|
}))}
|
||||||
|
multiple
|
||||||
|
selectProps={fieldProps}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,7 +2,7 @@ import { callApi } from "@/src/api";
|
|||||||
import { activeURI } from "@/src/App";
|
import { activeURI } from "@/src/App";
|
||||||
import { BackButton } from "@/src/components/BackButton";
|
import { BackButton } from "@/src/components/BackButton";
|
||||||
import { createModulesQuery } from "@/src/queries";
|
import { createModulesQuery } from "@/src/queries";
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams, useNavigate } from "@solidjs/router";
|
||||||
import {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
@@ -70,6 +70,33 @@ interface DetailsProps {
|
|||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
const Details = (props: DetailsProps) => {
|
const Details = (props: DetailsProps) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const add = async () => {
|
||||||
|
navigate(`/modules/add/${props.id}`);
|
||||||
|
// const uri = activeURI();
|
||||||
|
// if (!uri) return;
|
||||||
|
// const res = await callApi("get_inventory", { base_path: uri });
|
||||||
|
// if (res.status === "error") {
|
||||||
|
// toast.error("Failed to fetch inventory");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const inventory = res.data;
|
||||||
|
// const newInventory = deepMerge(inventory, {
|
||||||
|
// services: {
|
||||||
|
// [props.id]: {
|
||||||
|
// default: {
|
||||||
|
// enabled: false,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// callApi("set_inventory", {
|
||||||
|
// flake_dir: uri,
|
||||||
|
// inventory: newInventory,
|
||||||
|
// message: `Add module: ${props.id} in 'default' instance`,
|
||||||
|
// });
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div class="flex w-full flex-col gap-2">
|
<div class="flex w-full flex-col gap-2">
|
||||||
<article class="prose">{props.data.description}</article>
|
<article class="prose">{props.data.description}</article>
|
||||||
@@ -89,37 +116,11 @@ const Details = (props: DetailsProps) => {
|
|||||||
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
|
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
|
||||||
</div>
|
</div>
|
||||||
<div class="my-2 flex w-full gap-2">
|
<div class="my-2 flex w-full gap-2">
|
||||||
<button
|
<button class="btn btn-primary" onClick={add}>
|
||||||
class="btn btn-primary"
|
|
||||||
onClick={async () => {
|
|
||||||
const uri = activeURI();
|
|
||||||
if (!uri) return;
|
|
||||||
const res = await callApi("get_inventory", { base_path: uri });
|
|
||||||
if (res.status === "error") {
|
|
||||||
toast.error("Failed to fetch inventory");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const inventory = res.data;
|
|
||||||
const newInventory = deepMerge(inventory, {
|
|
||||||
services: {
|
|
||||||
[props.id]: {
|
|
||||||
default: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
callApi("set_inventory", {
|
|
||||||
flake_dir: uri,
|
|
||||||
inventory: newInventory,
|
|
||||||
message: `Add module: ${props.id} in 'default' instance`,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span class="material-icons ">add</span>
|
<span class="material-icons ">add</span>
|
||||||
Add to Clan
|
Add to Clan
|
||||||
</button>
|
</button>
|
||||||
|
{/* Add -> Select (required) roles, assign Machine */}
|
||||||
</div>
|
</div>
|
||||||
<ModuleForm id={props.id} />
|
<ModuleForm id={props.id} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,11 +18,12 @@ const ModuleListItem = (props: { name: string; info: ModuleInfo }) => {
|
|||||||
<div class="join">more</div>
|
<div class="join">more</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<A href={`/modules/${name}`}>
|
<A href={`/modules/details/${name}`}>
|
||||||
<div class="stat-value underline">{name}</div>
|
<div class="stat-value underline">{name}</div>
|
||||||
</A>
|
</A>
|
||||||
|
|
||||||
<div>{info.description}</div>
|
<div>{info.description}</div>
|
||||||
|
<div>{JSON.stringify(info.constraints)}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import typography from "@tailwindcss/typography";
|
import typography from "@tailwindcss/typography";
|
||||||
import daisyui from "daisyui";
|
import daisyui from "daisyui";
|
||||||
import core from "./tailwind/core-plugin";
|
import core from "./tailwind/core-plugin";
|
||||||
|
// @ts-expect-error: Doesn't have types
|
||||||
|
import { parseColor } from "tailwindcss/lib/util/color";
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user