Feat(modules): display clan.service modules

This commit is contained in:
Johannes Kirschbauer
2025-05-07 15:27:22 +02:00
parent f8723ab897
commit baf686e83f
5 changed files with 104 additions and 79 deletions

View File

@@ -1,13 +1,11 @@
import json
import re
import tomllib
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, TypedDict
from clan_cli.cmd import run_no_stdout
from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.nix import nix_eval
from clan_cli.errors import ClanError
from clan_cli.flake import Flake
from . import API
@@ -143,53 +141,50 @@ def get_roles(module_path: Path) -> None | list[str]:
]
class ModuleManifest(TypedDict):
name: str
features: dict[str, bool]
@dataclass
class ModuleInfo:
description: str
readme: str
categories: list[str]
roles: list[str] | None
features: list[str] = field(default_factory=list)
constraints: dict[str, Any] = field(default_factory=dict)
manifest: ModuleManifest
roles: dict[str, None]
def get_modules(base_path: str) -> dict[str, str]:
cmd = nix_eval(
[
f"{base_path}#clanInternals.inventory.modules",
"--json",
]
)
try:
proc = run_no_stdout(cmd)
res = proc.stdout.strip()
except ClanCmdError as e:
msg = "clanInternals might not have inventory.modules attributes"
raise ClanError(
msg,
location=f"list_modules {base_path}",
description="Evaluation failed on clanInternals.inventory.modules attribute",
) from e
modules: dict[str, str] = json.loads(res)
return modules
class ModuleLists(TypedDict):
modulesPerSource: dict[str, dict[str, ModuleInfo]]
localModules: dict[str, ModuleInfo]
@API.register
def list_modules(base_path: str) -> dict[str, ModuleInfo]:
def list_modules(base_path: str) -> ModuleLists:
"""
Show information about a module
"""
modules = get_modules(base_path)
return {
module_name: get_module_info(module_name, Path(module_path))
for module_name, module_path in modules.items()
}
flake = Flake(base_path)
modules = flake.select(
"clanInternals.inventoryClass.{?modulesPerSource,?localModules}"
)
print("Modules found:", modules)
return modules
@dataclass
class LegacyModuleInfo:
description: str
categories: list[str]
roles: None | list[str]
readme: str
features: list[str]
constraints: dict[str, Any]
def get_module_info(
module_name: str,
module_path: Path,
) -> ModuleInfo:
) -> LegacyModuleInfo:
"""
Retrieves information about a module
"""
@@ -214,7 +209,7 @@ def get_module_info(
readme, f"{module_path}/README.md"
)
return ModuleInfo(
return LegacyModuleInfo(
description=frontmatter.description,
categories=frontmatter.categories,
roles=get_roles(module_path),

View File

@@ -11,7 +11,10 @@ export const createModulesQuery = (
) =>
createQuery(() => ({
queryKey: [uri, "list_modules"],
placeholderData: [],
placeholderData: {
localModules: {},
modulesPerSource: {},
},
enabled: !!uri,
queryFn: async () => {
console.log({ uri });
@@ -23,15 +26,13 @@ export const createModulesQuery = (
if (response.status === "error") {
toast.error("Failed to fetch data");
} else {
if (!filter) {
return Object.entries(response.data);
}
return Object.entries(response.data).filter(([key, value]) =>
filter.features.every((f) => (value.features || []).includes(f)),
);
return response.data;
}
}
return [];
return {
localModules: {},
modulesPerSource: {},
};
},
}));

View File

@@ -16,11 +16,11 @@ export const ModuleDetails = () => {
<BackButton />
<div class="p-2">
<h3 class="text-2xl">{params.id}</h3>
<Switch>
{/* <Switch>
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
{(d) => <AddModule data={d()[1]} id={d()[0]} />}
</Match>
</Switch>
</Switch> */}
</div>
</div>
);
@@ -40,7 +40,7 @@ export const AddModule = (props: AddModuleProps) => {
<Switch fallback="loading">
<Match when={tags.data}>
{(tags) => (
<For each={props.data.roles}>
<For each={Object.keys(props.data.roles)}>
{(role) => (
<>
<div class="text-neutral-600">{role}s</div>

View File

@@ -21,11 +21,11 @@ export const ModuleDetails = () => {
<BackButton />
<div class="p-2">
<h3 class="text-2xl">{params.id}</h3>
<Switch>
{/* <Switch>
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
{(d) => <Details data={d()[1]} id={d()[0]} />}
</Match>
</Switch>
</Switch> */}
</div>
</div>
);
@@ -85,19 +85,24 @@ const Details = (props: DetailsProps) => {
};
return (
<div class="flex w-full flex-col gap-2">
<article class="prose">{props.data.description}</article>
<span class="">Categories</span>
{/* TODO: bring this feature back */}
{/* <article class="prose">{props.data.description}</article> */}
{/* <span class="">Categories</span> */}
<div>
<For each={props.data.categories}>
{/* TODO: bring this feature back */}
{/* <For each={props.data.categories}>
{(c) => <div class=" m-1">{c}</div>}
</For>
</For> */}
</div>
<span class="">Roles</span>
<div>
<For each={props.data.roles}>{(r) => <div class=" m-1">{r}</div>}</For>
<For each={Object.keys(props.data.roles)}>
{(r) => <div class=" m-1">{r}</div>}
</For>
</div>
<div class="p-2">
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
{/* TODO: bring this feature back */}
{/* <SolidMarkdown>{props.data.readme}</SolidMarkdown> */}
</div>
<div class="my-2 flex w-full gap-2">
<Button variant="light" onClick={add} startIcon={<Icon icon="Plus" />}>

View File

@@ -12,7 +12,7 @@ import { useQueryClient } from "@tanstack/solid-query";
import cx from "classnames";
import Icon from "@/src/components/icon";
export type ModuleInfo = SuccessData<"list_modules">[string];
export type ModuleInfo = SuccessData<"list_modules">["localModules"][string];
interface CategoryProps {
categories: string[];
@@ -28,7 +28,7 @@ const Categories = (props: CategoryProps) => {
};
interface RolesProps {
roles: string[];
roles: Record<string, null>;
}
const Roles = (props: RolesProps) => {
return (
@@ -38,7 +38,7 @@ const Roles = (props: RolesProps) => {
Service
</Typography>
</span>
{props.roles.map((role) => (
{Object.keys(props.roles).map((role) => (
<span class="">{role}</span>
))}
</div>
@@ -82,7 +82,7 @@ const ModuleItem = (props: {
<A href={`/modules/details/${name}`}>
<div class="">
<div class="flex flex-col">
<Categories categories={info.categories} />
{/* <Categories categories={info.categories} /> */}
<Typography hierarchy="title" size="m" weight="medium">
{name}
</Typography>
@@ -92,11 +92,12 @@ const ModuleItem = (props: {
<div class="w-full">
<Typography hierarchy="body" size="xs">
{info.description}
description
{/* TODO: {info.description} */}
</Typography>
</div>
</header>
<Roles roles={info.roles || []} />
<Roles roles={info.roles || {}} />
</div>
);
};
@@ -161,6 +162,7 @@ export const ModuleList = () => {
<Switch fallback="Error">
<Match when={modulesQuery.isFetching}>Loading....</Match>
<Match when={modulesQuery.data}>
{(modules) => (
<div
class="grid gap-6 p-6"
classList={{
@@ -168,16 +170,38 @@ export const ModuleList = () => {
"grid-cols-2": view() === "grid",
}}
>
<For each={modulesQuery.data}>
{([k, v]) => (
<For each={Object.entries(modules().modulesPerSource)}>
{([sourceName, v]) => (
<>
<div>
<Typography size="default" hierarchy="label">
{sourceName}
</Typography>
</div>
<For each={Object.entries(v)}>
{([moduleName, moduleInfo]) => (
<ModuleItem
info={v}
name={k}
info={moduleInfo}
name={moduleName}
class={view() == "grid" ? cx("max-w-md") : ""}
/>
)}
</For>
</>
)}
</For>
<div>{"localModules"}</div>
<For each={Object.entries(modules().localModules)}>
{([moduleName, moduleInfo]) => (
<ModuleItem
info={moduleInfo}
name={moduleName}
class={view() == "grid" ? cx("max-w-md") : ""}
/>
)}
</For>
</div>
)}
</Match>
</Switch>
</>