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:
@@ -1,4 +1,5 @@
|
||||
---
|
||||
description = "Generates a uuid for use in disk device naming"
|
||||
features = [ "inventory" ]
|
||||
categories = [ "System" ]
|
||||
---
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
description = "Automatically provisions wifi credentials"
|
||||
features = [ "inventory" ]
|
||||
categories = [ "Network" ]
|
||||
---
|
||||
|
||||
!!! Warning
|
||||
|
||||
@@ -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
|
||||
|
||||
###
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 />,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 [];
|
||||
},
|
||||
}));
|
||||
|
||||
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 { 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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user