Merge pull request 'modules: add more categories' (#2438) from hsjobeki/clan-core:hsjobeki-main into main

Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/2438
This commit is contained in:
hsjobeki
2024-11-19 15:59:23 +00:00
11 changed files with 213 additions and 35 deletions

View File

@@ -1,4 +1,5 @@
---
description = "Generates a uuid for use in disk device naming"
features = [ "inventory" ]
categories = [ "System" ]
---

View File

@@ -1,6 +1,7 @@
---
description = "Automatically provisions wifi credentials"
features = [ "inventory" ]
categories = [ "Network" ]
---
!!! Warning

View File

@@ -1,6 +1,6 @@
---
description = "Enables secure remote access to the machine over ssh."
categories = ["System"]
categories = ["System", "Network"]
features = [ "inventory" ]
---
@@ -9,5 +9,3 @@ It will generate a host key for each machine
## Roles
###

View File

@@ -1,6 +1,7 @@
---
description = "Configures [Zerotier VPN](https://zerotier.com) secure and efficient networking within a Clan.."
features = [ "inventory" ]
categories = [ "Network", "System" ]
[constraints]
roles.controller.min = 1

View File

@@ -126,7 +126,16 @@ export function SelectInput(props: SelectInputpProps) {
type="button"
class="select select-bordered flex items-center gap-2"
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"}>
{props.adornment?.content}

View File

@@ -17,6 +17,7 @@ import { Welcome } from "./routes/welcome";
import { Toaster } from "solid-toast";
import { ModuleList } from "./routes/modules/list";
import { ModuleDetails } from "./routes/modules/details";
import { ModuleDetails as AddModule } from "./routes/modules/add";
export const client = new QueryClient();
@@ -101,11 +102,17 @@ export const routes: AppRoute[] = [
component: () => <ModuleList />,
},
{
path: "/:id",
path: "details/:id",
label: "Details",
hidden: true,
component: () => <ModuleDetails />,
},
{
path: "/add/:id",
label: "Details",
hidden: true,
component: () => <AddModule />,
},
],
},
{

View File

@@ -34,3 +34,44 @@ export const createModulesQuery = (
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 [];
},
}));

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

View File

@@ -2,7 +2,7 @@ import { callApi } from "@/src/api";
import { activeURI } from "@/src/App";
import { BackButton } from "@/src/components/BackButton";
import { createModulesQuery } from "@/src/queries";
import { useParams } from "@solidjs/router";
import { useParams, useNavigate } from "@solidjs/router";
import {
createEffect,
createSignal,
@@ -70,6 +70,33 @@ interface DetailsProps {
id: string;
}
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 (
<div class="flex w-full flex-col gap-2">
<article class="prose">{props.data.description}</article>
@@ -89,37 +116,11 @@ const Details = (props: DetailsProps) => {
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
</div>
<div class="my-2 flex w-full gap-2">
<button
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`,
});
}}
>
<button class="btn btn-primary" onClick={add}>
<span class="material-icons ">add</span>
Add to Clan
</button>
{/* Add -> Select (required) roles, assign Machine */}
</div>
<ModuleForm id={props.id} />
</div>

View File

@@ -18,11 +18,12 @@ const ModuleListItem = (props: { name: string; info: ModuleInfo }) => {
<div class="join">more</div>
</div>
<A href={`/modules/${name}`}>
<A href={`/modules/details/${name}`}>
<div class="stat-value underline">{name}</div>
</A>
<div>{info.description}</div>
<div>{JSON.stringify(info.constraints)}</div>
</div>
);
};

View File

@@ -1,6 +1,8 @@
import typography from "@tailwindcss/typography";
import daisyui from "daisyui";
import core from "./tailwind/core-plugin";
// @ts-expect-error: Doesn't have types
import { parseColor } from "tailwindcss/lib/util/color";
/** @type {import('tailwindcss').Config} */
const config = {