Merge pull request 'Feat(modules): display clan.service modules' (#3537) from hsjobeki/clan-core:module-list into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3537
This commit is contained in:
@@ -45,7 +45,7 @@ let
|
|||||||
inherit inventory directory;
|
inherit inventory directory;
|
||||||
flakeInputs = config.self.inputs;
|
flakeInputs = config.self.inputs;
|
||||||
prefix = config._prefix ++ [ "inventoryClass" ];
|
prefix = config._prefix ++ [ "inventoryClass" ];
|
||||||
localModuleSet = config.self.clan.modules;
|
localModuleSet = config.modules;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -27,10 +27,8 @@ def test_list_modules(test_flake_with_core: FlakeForTest) -> None:
|
|||||||
base_path = test_flake_with_core.path
|
base_path = test_flake_with_core.path
|
||||||
modules_info = list_modules(str(base_path))
|
modules_info = list_modules(str(base_path))
|
||||||
|
|
||||||
assert len(modules_info.items()) > 1
|
assert "localModules" in modules_info
|
||||||
# Random test for those two modules
|
assert "modulesPerSource" in modules_info
|
||||||
assert "borgbackup" in modules_info
|
|
||||||
assert "syncthing" in modules_info
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.impure
|
@pytest.mark.impure
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import json
|
|
||||||
import re
|
import re
|
||||||
import tomllib
|
import tomllib
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, TypedDict
|
from typing import Any, TypedDict
|
||||||
|
|
||||||
from clan_cli.cmd import run_no_stdout
|
from clan_cli.errors import ClanError
|
||||||
from clan_cli.errors import ClanCmdError, ClanError
|
from clan_cli.flake import Flake
|
||||||
from clan_cli.nix import nix_eval
|
|
||||||
|
|
||||||
from . import API
|
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
|
@dataclass
|
||||||
class ModuleInfo:
|
class ModuleInfo:
|
||||||
description: str
|
manifest: ModuleManifest
|
||||||
readme: str
|
roles: dict[str, None]
|
||||||
categories: list[str]
|
|
||||||
roles: list[str] | None
|
|
||||||
features: list[str] = field(default_factory=list)
|
|
||||||
constraints: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
def get_modules(base_path: str) -> dict[str, str]:
|
class ModuleLists(TypedDict):
|
||||||
cmd = nix_eval(
|
modulesPerSource: dict[str, dict[str, ModuleInfo]]
|
||||||
[
|
localModules: dict[str, ModuleInfo]
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@API.register
|
@API.register
|
||||||
def list_modules(base_path: str) -> dict[str, ModuleInfo]:
|
def list_modules(base_path: str) -> ModuleLists:
|
||||||
"""
|
"""
|
||||||
Show information about a module
|
Show information about a module
|
||||||
"""
|
"""
|
||||||
modules = get_modules(base_path)
|
flake = Flake(base_path)
|
||||||
return {
|
modules = flake.select(
|
||||||
module_name: get_module_info(module_name, Path(module_path))
|
"clanInternals.inventoryClass.{?modulesPerSource,?localModules}"
|
||||||
for module_name, module_path in modules.items()
|
)
|
||||||
}
|
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(
|
def get_module_info(
|
||||||
module_name: str,
|
module_name: str,
|
||||||
module_path: Path,
|
module_path: Path,
|
||||||
) -> ModuleInfo:
|
) -> LegacyModuleInfo:
|
||||||
"""
|
"""
|
||||||
Retrieves information about a module
|
Retrieves information about a module
|
||||||
"""
|
"""
|
||||||
@@ -214,7 +209,7 @@ def get_module_info(
|
|||||||
readme, f"{module_path}/README.md"
|
readme, f"{module_path}/README.md"
|
||||||
)
|
)
|
||||||
|
|
||||||
return ModuleInfo(
|
return LegacyModuleInfo(
|
||||||
description=frontmatter.description,
|
description=frontmatter.description,
|
||||||
categories=frontmatter.categories,
|
categories=frontmatter.categories,
|
||||||
roles=get_roles(module_path),
|
roles=get_roles(module_path),
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ export const createModulesQuery = (
|
|||||||
) =>
|
) =>
|
||||||
createQuery(() => ({
|
createQuery(() => ({
|
||||||
queryKey: [uri, "list_modules"],
|
queryKey: [uri, "list_modules"],
|
||||||
placeholderData: [],
|
placeholderData: {
|
||||||
|
localModules: {},
|
||||||
|
modulesPerSource: {},
|
||||||
|
},
|
||||||
enabled: !!uri,
|
enabled: !!uri,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
console.log({ uri });
|
console.log({ uri });
|
||||||
@@ -23,15 +26,13 @@ export const createModulesQuery = (
|
|||||||
if (response.status === "error") {
|
if (response.status === "error") {
|
||||||
toast.error("Failed to fetch data");
|
toast.error("Failed to fetch data");
|
||||||
} else {
|
} else {
|
||||||
if (!filter) {
|
return response.data;
|
||||||
return Object.entries(response.data);
|
|
||||||
}
|
|
||||||
return Object.entries(response.data).filter(([key, value]) =>
|
|
||||||
filter.features.every((f) => (value.features || []).includes(f)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [];
|
return {
|
||||||
|
localModules: {},
|
||||||
|
modulesPerSource: {},
|
||||||
|
};
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ export const ModuleDetails = () => {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<h3 class="text-2xl">{params.id}</h3>
|
<h3 class="text-2xl">{params.id}</h3>
|
||||||
<Switch>
|
{/* <Switch>
|
||||||
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
||||||
{(d) => <AddModule data={d()[1]} id={d()[0]} />}
|
{(d) => <AddModule data={d()[1]} id={d()[0]} />}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -40,7 +40,7 @@ export const AddModule = (props: AddModuleProps) => {
|
|||||||
<Switch fallback="loading">
|
<Switch fallback="loading">
|
||||||
<Match when={tags.data}>
|
<Match when={tags.data}>
|
||||||
{(tags) => (
|
{(tags) => (
|
||||||
<For each={props.data.roles}>
|
<For each={Object.keys(props.data.roles)}>
|
||||||
{(role) => (
|
{(role) => (
|
||||||
<>
|
<>
|
||||||
<div class="text-neutral-600">{role}s</div>
|
<div class="text-neutral-600">{role}s</div>
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ export const ModuleDetails = () => {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<h3 class="text-2xl">{params.id}</h3>
|
<h3 class="text-2xl">{params.id}</h3>
|
||||||
<Switch>
|
{/* <Switch>
|
||||||
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
<Match when={modulesQuery.data?.find((i) => i[0] === params.id)}>
|
||||||
{(d) => <Details data={d()[1]} id={d()[0]} />}
|
{(d) => <Details data={d()[1]} id={d()[0]} />}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -85,19 +85,24 @@ const Details = (props: DetailsProps) => {
|
|||||||
};
|
};
|
||||||
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>
|
{/* TODO: bring this feature back */}
|
||||||
<span class="">Categories</span>
|
{/* <article class="prose">{props.data.description}</article> */}
|
||||||
|
{/* <span class="">Categories</span> */}
|
||||||
<div>
|
<div>
|
||||||
<For each={props.data.categories}>
|
{/* TODO: bring this feature back */}
|
||||||
|
{/* <For each={props.data.categories}>
|
||||||
{(c) => <div class=" m-1">{c}</div>}
|
{(c) => <div class=" m-1">{c}</div>}
|
||||||
</For>
|
</For> */}
|
||||||
</div>
|
</div>
|
||||||
<span class="">Roles</span>
|
<span class="">Roles</span>
|
||||||
<div>
|
<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>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<SolidMarkdown>{props.data.readme}</SolidMarkdown>
|
{/* TODO: bring this feature back */}
|
||||||
|
{/* <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 variant="light" onClick={add} startIcon={<Icon icon="Plus" />}>
|
<Button variant="light" onClick={add} startIcon={<Icon icon="Plus" />}>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { useQueryClient } from "@tanstack/solid-query";
|
|||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
|
|
||||||
export type ModuleInfo = SuccessData<"list_modules">[string];
|
export type ModuleInfo = SuccessData<"list_modules">["localModules"][string];
|
||||||
|
|
||||||
interface CategoryProps {
|
interface CategoryProps {
|
||||||
categories: string[];
|
categories: string[];
|
||||||
@@ -28,7 +28,7 @@ const Categories = (props: CategoryProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface RolesProps {
|
interface RolesProps {
|
||||||
roles: string[];
|
roles: Record<string, null>;
|
||||||
}
|
}
|
||||||
const Roles = (props: RolesProps) => {
|
const Roles = (props: RolesProps) => {
|
||||||
return (
|
return (
|
||||||
@@ -38,7 +38,7 @@ const Roles = (props: RolesProps) => {
|
|||||||
Service
|
Service
|
||||||
</Typography>
|
</Typography>
|
||||||
</span>
|
</span>
|
||||||
{props.roles.map((role) => (
|
{Object.keys(props.roles).map((role) => (
|
||||||
<span class="">{role}</span>
|
<span class="">{role}</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +82,7 @@ const ModuleItem = (props: {
|
|||||||
<A href={`/modules/details/${name}`}>
|
<A href={`/modules/details/${name}`}>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<Categories categories={info.categories} />
|
{/* <Categories categories={info.categories} /> */}
|
||||||
<Typography hierarchy="title" size="m" weight="medium">
|
<Typography hierarchy="title" size="m" weight="medium">
|
||||||
{name}
|
{name}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -92,11 +92,12 @@ const ModuleItem = (props: {
|
|||||||
|
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<Typography hierarchy="body" size="xs">
|
<Typography hierarchy="body" size="xs">
|
||||||
{info.description}
|
description
|
||||||
|
{/* TODO: {info.description} */}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<Roles roles={info.roles || []} />
|
<Roles roles={info.roles || {}} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -161,6 +162,7 @@ export const ModuleList = () => {
|
|||||||
<Switch fallback="Error">
|
<Switch fallback="Error">
|
||||||
<Match when={modulesQuery.isFetching}>Loading....</Match>
|
<Match when={modulesQuery.isFetching}>Loading....</Match>
|
||||||
<Match when={modulesQuery.data}>
|
<Match when={modulesQuery.data}>
|
||||||
|
{(modules) => (
|
||||||
<div
|
<div
|
||||||
class="grid gap-6 p-6"
|
class="grid gap-6 p-6"
|
||||||
classList={{
|
classList={{
|
||||||
@@ -168,16 +170,38 @@ export const ModuleList = () => {
|
|||||||
"grid-cols-2": view() === "grid",
|
"grid-cols-2": view() === "grid",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={modulesQuery.data}>
|
<For each={Object.entries(modules().modulesPerSource)}>
|
||||||
{([k, v]) => (
|
{([sourceName, v]) => (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<Typography size="default" hierarchy="label">
|
||||||
|
{sourceName}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<For each={Object.entries(v)}>
|
||||||
|
{([moduleName, moduleInfo]) => (
|
||||||
<ModuleItem
|
<ModuleItem
|
||||||
info={v}
|
info={moduleInfo}
|
||||||
name={k}
|
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") : ""}
|
class={view() == "grid" ? cx("max-w-md") : ""}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user