Merge pull request 'lib.values: init getPrio' (#2559) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-12-04 16:53:09 +00:00
14 changed files with 501 additions and 261 deletions

View File

@@ -101,6 +101,7 @@ in
# Those options are interfaced by the CLI # Those options are interfaced by the CLI
# We don't specify the type here, for better performance. # We don't specify the type here, for better performance.
inventory = lib.mkOption { type = lib.types.raw; }; inventory = lib.mkOption { type = lib.types.raw; };
inventoryValuesPrios = lib.mkOption { type = lib.types.raw; };
# all inventory module schemas # all inventory module schemas
moduleSchemas = lib.mkOption { type = lib.types.raw; }; moduleSchemas = lib.mkOption { type = lib.types.raw; };
inventoryFile = lib.mkOption { type = lib.types.raw; }; inventoryFile = lib.mkOption { type = lib.types.raw; };

View File

@@ -183,6 +183,7 @@ in
inherit serviceConfigs; inherit serviceConfigs;
inherit (clan-core) clanModules; inherit (clan-core) clanModules;
inherit inventoryFile; inherit inventoryFile;
inventoryValuesPrios = (clan-core.lib.values.getPrios { options = inventory.options; });
inventory = config.inventory; inventory = config.inventory;
meta = config.inventory.meta; meta = config.inventory.meta;

View File

@@ -15,6 +15,7 @@ in
buildClan = import ./build-clan { inherit lib nixpkgs clan-core; }; buildClan = import ./build-clan { inherit lib nixpkgs clan-core; };
facts = import ./facts.nix { inherit lib; }; facts = import ./facts.nix { inherit lib; };
inventory = import ./inventory { inherit lib clan-core; }; inventory = import ./inventory { inherit lib clan-core; };
values = import ./values { inherit lib; };
jsonschema = import ./jsonschema { inherit lib; }; jsonschema = import ./jsonschema { inherit lib; };
modules = import ./frontmatter { modules = import ./frontmatter {
inherit lib; inherit lib;

View File

@@ -9,6 +9,7 @@
./jsonschema/flake-module.nix ./jsonschema/flake-module.nix
./inventory/flake-module.nix ./inventory/flake-module.nix
./build-clan/flake-module.nix ./build-clan/flake-module.nix
./values/flake-module.nix
]; ];
flake.lib = import ./default.nix { flake.lib = import ./default.nix {
inherit lib inputs; inherit lib inputs;

View File

@@ -1,4 +1,9 @@
{ lib, config, ... }: {
lib,
config,
options,
...
}:
let let
types = lib.types; types = lib.types;
@@ -92,6 +97,12 @@ in
./assertions.nix ./assertions.nix
]; ];
options = { options = {
options = lib.mkOption {
internal = true;
visible = false;
type = types.raw;
default = options;
};
modules = lib.mkOption { modules = lib.mkOption {
type = types.attrsOf types.path; type = types.attrsOf types.path;
default = { }; default = { };

90
lib/values/default.nix Normal file
View File

@@ -0,0 +1,90 @@
{
lib ? import <nixpkgs/lib>,
}:
let
filterOptions = lib.filterAttrs (
name: _:
!builtins.elem name [
"_module"
"_freeformOptions"
]
);
getPrios =
{
options,
}:
let
filteredOptions = filterOptions options;
in
lib.mapAttrs (
_: opt:
let
prio = {
__prio = opt.highestPrio;
};
subOptions = opt.type.getSubOptions opt.loc;
attrDefinitions = (lib.modules.mergeAttrDefinitionsWithPrio opt);
zipDefs = builtins.zipAttrsWith (_ns: vs: vs);
defs = zipDefs opt.definitions;
prioPerValue =
{ type, defs }:
lib.mapAttrs (
attrName: prioSet:
let
# Evaluate the submodule
options = filterOptions subOptions;
modules = (
[
{ inherit options; }
]
++ map (config: { inherit config; }) defs.${attrName}
);
submoduleEval = lib.evalModules {
inherit modules;
};
in
(lib.optionalAttrs (prioSet ? highestPrio) {
__prio = prioSet.highestPrio;
# inherit defs options;
})
// (
if type.nestedTypes.elemType.name == "submodule" then
getPrios { options = submoduleEval.options; }
else
# Nested attrsOf
(lib.optionalAttrs (type.nestedTypes.elemType.name == "attrsOf") (
prioPerValue {
type = type.nestedTypes.elemType;
defs = zipDefs defs.${attrName};
} prioSet.value
))
)
);
attributePrios = prioPerValue {
type = opt.type;
inherit defs;
} attrDefinitions;
in
if opt ? type && opt.type.name == "submodule" then
prio // (getPrios { options = subOptions; })
else if opt ? type && opt.type.name == "attrsOf" then
# prio // attributePrios
# else if
# opt ? type && opt.type.name == "attrsOf" && opt.type.nestedTypes.elemType.name == "attrsOf"
# then
# prio // attributePrios
# else if opt ? type && opt.type.name == "attrsOf" then
prio // attributePrios
else if opt ? type && opt._type == "option" then
prio
else
getPrios { options = opt; }
) filteredOptions;
in
{
inherit getPrios;
}

View File

@@ -0,0 +1,24 @@
{ self, inputs, ... }:
let
inputOverrides = builtins.concatStringsSep " " (
builtins.map (input: " --override-input ${input} ${inputs.${input}}") (builtins.attrNames inputs)
);
in
{
perSystem =
{ pkgs, system, ... }:
{
checks = {
lib-values-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
${inputOverrides} \
--flake ${self}#legacyPackages.${system}.evalTests-inventory
touch $out
'';
};
};
}

211
lib/values/test.nix Normal file
View File

@@ -0,0 +1,211 @@
# tests for the nixos options to jsonschema converter
# run these tests via `nix-unit ./test.nix`
{
lib ? (import <nixpkgs> { }).lib,
slib ? (import ./. { inherit lib; }),
}:
let
eval =
modules:
let
evaledConfig = lib.evalModules {
inherit modules;
};
in
evaledConfig;
in
{
test_default = {
expr = slib.getPrios {
options =
(eval [
{
options.foo.bar = lib.mkOption {
type = lib.types.bool;
description = "Test Description";
default = true;
};
}
]).options;
};
expected = {
foo.bar = {
__prio = 1500;
};
};
};
test_no_default = {
expr = slib.getPrios {
options =
(eval [
{
options.foo.bar = lib.mkOption {
type = lib.types.bool;
};
}
]).options;
};
expected = {
foo.bar = {
__prio = 9999;
};
};
};
test_submodule = {
expr = slib.getPrios {
options =
(eval [
{
options.foo = lib.mkOption {
type = lib.types.submodule {
options = {
bar = lib.mkOption {
type = lib.types.bool;
};
};
};
};
}
]).options;
};
expected = {
foo = {
# Prio of the submodule itself
__prio = 9999;
# Prio of the bar option within the submodule
bar.__prio = 9999;
};
};
};
# TODO(@hsjobeki): Cover this edge case
# test_freeform =
# let
# evaluated = (
# eval [
# {
# freeformType = with lib.types; attrsOf (int);
# options = {
# foo = lib.mkOption {
# type = lib.types.int;
# default = 0;
# };
# };
# }
# {
# bar = lib.mkForce 123;
# baz = 1;
# }
# {
# bar = 10;
# }
# ]
# );
# in
# {
# inherit evaluated;
# expr = slib.getPrios {
# options = evaluated.options;
# };
# expected = {
# };
# };
test_attrsOf_submodule =
let
evaluated = eval [
{
options.foo = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
bar = lib.mkOption {
type = lib.types.int;
default = 0;
};
};
}
);
};
config.foo = {
"nested" = {
"bar" = 2; # <- 100 prio ?
};
"other" = {
"bar" = lib.mkForce 2; # <- 50 prio ?
};
};
}
];
in
{
expr = slib.getPrios { options = evaluated.options; };
expected = {
foo.__prio = 100;
foo.nested.__prio = 100;
foo.other.__prio = 100;
foo.nested.bar.__prio = 100;
foo.other.bar.__prio = 50;
};
};
test_attrsOf_attrsOf_submodule =
let
evaluated = eval [
{
options.foo = lib.mkOption {
type = lib.types.attrsOf (
lib.types.attrsOf (
lib.types.submodule {
options = {
bar = lib.mkOption {
type = lib.types.int;
default = 0;
};
};
}
)
);
};
config.foo = {
a.b = {
bar = 1;
};
a.c = {
bar = 1;
};
x.y = {
bar = 1;
};
x.z = {
bar = 1;
};
};
}
];
in
{
inherit evaluated;
expr = slib.getPrios { options = evaluated.options; };
expected = {
foo.__prio = 100;
# Sub A
foo.a.__prio = 100;
# a.b doesnt have a prio
# a.c doesnt have a prio
foo.a.b.bar.__prio = 100;
foo.a.c.bar.__prio = 100;
# Sub X
foo.x.__prio = 100;
# x.y doesnt have a prio
# x.z doesnt have a prio
foo.x.y.bar.__prio = 100;
foo.x.z.bar.__prio = 100;
};
};
}

View File

@@ -119,6 +119,37 @@ def load_inventory_json(
return inventory return inventory
def patch(d: dict[str, Any], path: str, content: Any) -> None:
"""
Update the value at a specific dot-separated path in a nested dictionary.
:param d: The dictionary to update.
:param path: The dot-separated path to the key (e.g., 'foo.bar').
:param content: The new value to set.
"""
keys = path.split(".")
current = d
for key in keys[:-1]:
current = current.setdefault(key, {})
current[keys[-1]] = content
@API.register
def patch_inventory_with(base_dir: Path, section: str, content: dict[str, Any]) -> None:
inventory_file = get_path(base_dir)
curr_inventory = {}
with inventory_file.open("r") as f:
curr_inventory = json.load(f)
patch(curr_inventory, section, content)
with inventory_file.open("w") as f:
json.dump(curr_inventory, f, indent=2)
commit_file(inventory_file, base_dir, commit_message=f"inventory.{section}: Update")
@API.register @API.register
def set_inventory( def set_inventory(
inventory: Inventory | dict[str, Any], flake_dir: str | Path, message: str inventory: Inventory | dict[str, Any], flake_dir: str | Path, message: str
@@ -129,6 +160,19 @@ def set_inventory(
""" """
inventory_file = get_path(flake_dir) inventory_file = get_path(flake_dir)
# Filter out modules not set via UI.
# It is not possible to set modules from "/nix/store" via the UI
modules = {}
filtered_modules = lambda m: {
key: value for key, value in m.items() if "/nix/store" not in value
}
if isinstance(inventory, dict):
modules = filtered_modules(inventory.get("modules", {})) # type: ignore
inventory["modules"] = modules
else:
modules = filtered_modules(inventory.modules) # type: ignore
inventory.modules = modules
with inventory_file.open("w") as f: with inventory_file.open("w") as f:
if isinstance(inventory, Inventory): if isinstance(inventory, Inventory):
json.dump(dataclass_to_dict(inventory), f, indent=2) json.dump(dataclass_to_dict(inventory), f, indent=2)

View File

@@ -7,11 +7,16 @@ from typing import Literal
from clan_cli.api import API from clan_cli.api import API
from clan_cli.api.modules import parse_frontmatter from clan_cli.api.modules import parse_frontmatter
from clan_cli.api.serde import dataclass_to_dict
from clan_cli.cmd import run_no_stdout from clan_cli.cmd import run_no_stdout
from clan_cli.completions import add_dynamic_completer, complete_tags from clan_cli.completions import add_dynamic_completer, complete_tags
from clan_cli.dirs import specific_machine_dir from clan_cli.dirs import specific_machine_dir
from clan_cli.errors import ClanCmdError, ClanError from clan_cli.errors import ClanCmdError, ClanError
from clan_cli.inventory import Machine, load_inventory_eval, set_inventory from clan_cli.inventory import (
Machine,
load_inventory_eval,
patch_inventory_with,
)
from clan_cli.machines.hardware import HardwareConfig from clan_cli.machines.hardware import HardwareConfig
from clan_cli.nix import nix_eval, nix_shell from clan_cli.nix import nix_eval, nix_shell
from clan_cli.tags import list_nixos_machines_by_tags from clan_cli.tags import list_nixos_machines_by_tags
@@ -20,12 +25,10 @@ log = logging.getLogger(__name__)
@API.register @API.register
def set_machine(flake_url: str | Path, machine_name: str, machine: Machine) -> None: def set_machine(flake_url: Path, machine_name: str, machine: Machine) -> None:
inventory = load_inventory_eval(flake_url) patch_inventory_with(
flake_url, f"machines.{machine_name}", dataclass_to_dict(machine)
inventory.machines[machine_name] = machine )
set_inventory(inventory, flake_url, "machines: edit '{machine_name}'")
@API.register @API.register

View File

@@ -0,0 +1,36 @@
# Functions to test
from clan_cli.inventory import patch
def test_patch_nested() -> None:
orig = {"a": 1, "b": {"a": 2.1, "b": 2.2}, "c": 3}
patch(orig, "b.b", "foo")
# Should only update the nested value
assert orig == {"a": 1, "b": {"a": 2.1, "b": "foo"}, "c": 3}
def test_patch_nested_dict() -> None:
orig = {"a": 1, "b": {"a": 2.1, "b": 2.2}, "c": 3}
# This should update the whole "b" dict
# Which also removes all other keys
patch(orig, "b", {"b": "foo"})
# Should only update the nested value
assert orig == {"a": 1, "b": {"b": "foo"}, "c": 3}
def test_create_missing_paths() -> None:
orig = {"a": 1}
patch(orig, "b.c", "foo")
# Should only update the nested value
assert orig == {"a": 1, "b": {"c": "foo"}}
orig = {}
patch(orig, "a.b.c", "foo")
assert orig == {"a": {"b": {"c": "foo"}}}

View File

@@ -1,37 +0,0 @@
import { QueryClient } from "@tanstack/query-core";
import { get_inventory } from "./inventory";
export const instance_name = (machine_name: string) =>
`${machine_name}-single-disk` as const;
export async function set_single_disk_id(
client: QueryClient,
base_path: string,
machine_name: string,
disk_id: string,
) {
const r = await get_inventory(client, base_path);
if (r.status === "error") {
return r;
}
if (!r.data.services) {
return new Error("No services found in inventory");
}
const inventory = r.data;
inventory.services = inventory.services || {};
inventory.services["single-disk"] = inventory.services["single-disk"] || {};
inventory.services["single-disk"][instance_name(machine_name)] = {
meta: {
name: instance_name(machine_name),
},
roles: {
default: {
machines: [machine_name],
config: {
device: `/dev/disk/by-id/${disk_id}`,
},
},
},
};
}

View File

@@ -1,11 +1,11 @@
import { callApi, ClanService, SuccessData, SuccessQuery } from "@/src/api"; import { callApi, ClanService, SuccessData, SuccessQuery } from "@/src/api";
import { set_single_disk_id } from "@/src/api/disk";
import { get_iwd_service } from "@/src/api/wifi"; import { get_iwd_service } from "@/src/api/wifi";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { BackButton } from "@/src/components/BackButton"; import { BackButton } from "@/src/components/BackButton";
import { Button } from "@/src/components/button"; import { Button } from "@/src/components/button";
import { FileInput } from "@/src/components/FileInput"; import { FileInput } from "@/src/components/FileInput";
import Icon from "@/src/components/icon"; import Icon from "@/src/components/icon";
import { RndThumbnail } from "@/src/components/noiseThumbnail";
import { SelectInput } from "@/src/components/SelectInput"; import { SelectInput } from "@/src/components/SelectInput";
import { TextInput } from "@/src/components/TextInput"; import { TextInput } from "@/src/components/TextInput";
import { selectSshKeys } from "@/src/hooks"; import { selectSshKeys } from "@/src/hooks";
@@ -54,18 +54,6 @@ const InstallMachine = (props: InstallMachineProps) => {
const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk()); const [confirmDisk, setConfirmDisk] = createSignal(!hasDisk());
const hwInfoQuery = createQuery(() => ({
queryKey: [curr, "machine", name, "show_machine_hardware_config"],
queryFn: async () => {
const result = await callApi("show_machine_hardware_config", {
clan_dir: curr,
machine_name: name,
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data === "NIXOS_FACTER";
},
}));
const handleInstall = async (values: InstallForm) => { const handleInstall = async (values: InstallForm) => {
console.log("Installing", values); console.log("Installing", values);
const curr_uri = activeURI(); const curr_uri = activeURI();
@@ -98,7 +86,6 @@ const InstallMachine = (props: InstallMachineProps) => {
toast.success("Machine installed successfully"); toast.success("Machine installed successfully");
} }
}; };
const queryClient = useQueryClient();
const handleDiskConfirm = async (e: Event) => { const handleDiskConfirm = async (e: Event) => {
e.preventDefault(); e.preventDefault();
@@ -108,19 +95,6 @@ const InstallMachine = (props: InstallMachineProps) => {
if (!curr_uri || !disk_id || !props.name) { if (!curr_uri || !disk_id || !props.name) {
return; return;
} }
const r = await set_single_disk_id(
queryClient,
curr_uri,
props.name,
disk_id,
);
if (!r) {
toast.success("Disk set successfully");
setConfirmDisk(true);
} else {
toast.error("Failed to set disk");
}
}; };
const generateReport = async (e: Event) => { const generateReport = async (e: Event) => {
@@ -141,7 +115,7 @@ const InstallMachine = (props: InstallMachineProps) => {
}, },
}); });
toast.dismiss(loading_toast); toast.dismiss(loading_toast);
hwInfoQuery.refetch(); // TODO: refresh the machine details
if (r.status === "error") { if (r.status === "error") {
toast.error(`Failed to generate report. ${r.errors[0].message}`); toast.error(`Failed to generate report. ${r.errors[0].message}`);
@@ -164,81 +138,36 @@ const InstallMachine = (props: InstallMachineProps) => {
<div class="flex flex-col"> <div class="flex flex-col">
<div class="text-lg font-semibold">Hardware detection</div> <div class="text-lg font-semibold">Hardware detection</div>
<div class="flex justify-between py-4"> <div class="flex justify-between py-4">
<Switch>
<Match when={hwInfoQuery.isLoading}>
<span class="loading loading-lg"></span>
</Match>
<Match when={hwInfoQuery.isFetched}>
<Show
when={hwInfoQuery.data}
fallback={
<>
<span class="flex align-middle">
<span class="material-icons text-inherit">close</span>
Not Detected
</span>
<div class="text-neutral">
This might still work, but it is recommended to generate
a hardware report.
</div>
</>
}
>
<span class="flex align-middle">
<span class="material-icons text-inherit">check</span>
Detected
</span>
</Show>
</Match>
</Switch>
<div class=""> <div class="">
<Button <Button
variant="light" variant="light"
size="s" size="s"
class="w-full" class="w-full"
onclick={generateReport} onclick={generateReport}
endIcon={<Icon icon="Report" />}
> >
<span class="material-icons">manage_search</span> Run hardware Report
Generate report </Button>
</div>
</div>
<div class="text-lg font-semibold">Disk schema</div>
<div class="flex justify-between py-4">
<div class="">
<Button
variant="light"
size="s"
class="w-full"
onclick={generateReport}
endIcon={<Icon icon="Flash" />}
>
Select disk Schema
</Button> </Button>
</div> </div>
</div> </div>
</div> </div>
<Field name="disk"> <Field name="disk">{(field, fieldProps) => "disk"}</Field>
{(field, fieldProps) => (
<SelectInput
formStore={formStore}
selectProps={{
...fieldProps,
// @ts-expect-error: disabled is supported by htmlSelect
disabled: confirmDisk(),
}}
label="Remote Disk to use"
value={String(field.value)}
error={field.error}
required
options={
<>
<option disabled>{diskPlaceholder}</option>
<For each={props.disks}>
{(dev) => (
<option value={dev.name}>
{dev.name}
{" -- "}
{dev.size}
{"bytes @"}
{props.targetHost?.split("@")?.[1]}
</option>
)}
</For>
</>
}
/>
)}
</Field>
<div role="alert" class="alert my-4"> <div role="alert" class="alert my-4">
<span class="material-icons">info</span> <span class="material-icons">info</span>
<div> <div>
@@ -260,16 +189,16 @@ const InstallMachine = (props: InstallMachineProps) => {
class="btn btn-primary btn-wide" class="btn btn-primary btn-wide"
onClick={handleDiskConfirm} onClick={handleDiskConfirm}
disabled={!hasDisk()} disabled={!hasDisk()}
startIcon={<Icon icon="Flash" />} endIcon={<Icon icon="Flash" />}
> >
Confirm Disk Install
</Button> </Button>
} }
> >
<Button <Button
class="w-full" class="w-full"
type="submit" type="submit"
startIcon={<Icon icon="Flash" />} endIcon={<Icon icon="Flash" />}
> >
Install Install
</Button> </Button>
@@ -291,10 +220,6 @@ const InstallMachine = (props: InstallMachineProps) => {
interface MachineDetailsProps { interface MachineDetailsProps {
initialData: MachineData; initialData: MachineData;
modules: {
name: string;
component: JSXElement;
}[];
} }
const MachineForm = (props: MachineDetailsProps) => { const MachineForm = (props: MachineDetailsProps) => {
const [formStore, { Form, Field }] = const [formStore, { Form, Field }] =
@@ -308,52 +233,9 @@ const MachineForm = (props: MachineDetailsProps) => {
const machineName = () => const machineName = () =>
getValue(formStore, "machine.name") || props.initialData.machine.name; getValue(formStore, "machine.name") || props.initialData.machine.name;
const onlineStatusQuery = createQuery(() => ({
queryKey: [activeURI(), "machine", targetHost(), "check_machine_online"],
queryFn: async () => {
const curr = activeURI();
if (curr) {
const result = await callApi("check_machine_online", {
flake_url: curr,
machine_name: machineName(),
opts: {
keyfile: sshKey()?.name,
},
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
}
},
// refetchInterval: 10_000, // 10 seconds
}));
const online = () => onlineStatusQuery.data === "Online";
const remoteDiskQuery = createQuery(() => ({
queryKey: [
activeURI(),
"machine",
machineName(),
targetHost(),
"show_block_devices",
],
enabled: online(),
queryFn: async () => {
const curr = activeURI();
if (curr) {
const result = await callApi("show_block_devices", {
options: {
hostname: targetHost(),
keyfile: sshKey()?.name,
},
});
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
}
},
}));
const handleSubmit = async (values: MachineFormInterface) => { const handleSubmit = async (values: MachineFormInterface) => {
console.log("submitting", values);
const curr_uri = activeURI(); const curr_uri = activeURI();
if (!curr_uri) { if (!curr_uri) {
return; return;
@@ -426,27 +308,20 @@ const MachineForm = (props: MachineDetailsProps) => {
<figure> <figure>
<div <div
class="avatar placeholder" class="avatar placeholder"
classList={{ classList={
online: onlineStatusQuery.data === "Online", {
offline: onlineStatusQuery.data === "Offline", // online: onlineStatusQuery.data === "Online",
}} // offline: onlineStatusQuery.data === "Offline",
}
}
> >
<div class="w-24 rounded-full bg-neutral text-neutral-content"> <div class="w-32 rounded-lg border p-2 bg-def-4 border-inv-3">
<Show <RndThumbnail name={machineName()} />
when={onlineStatusQuery.isFetching}
fallback={<span class="material-icons text-4xl">devices</span>}
>
<span class="loading loading-bars loading-sm justify-self-end"></span>
</Show>
</div> </div>
</div> </div>
</figure> </figure>
<div class="card-body"> <div class="card-body">
<span class="text-xl text-primary-800">General</span> <span class="text-xl text-primary-800">General</span>
{/*
<Field name="machine.tags" type="string[]">
{(field, props) => field.value}
</Field> */}
<Field name="machine.name"> <Field name="machine.name">
{(field, props) => ( {(field, props) => (
@@ -461,6 +336,20 @@ const MachineForm = (props: MachineDetailsProps) => {
/> />
)} )}
</Field> </Field>
<Field name="machine.tags" type="string[]">
{(field, props) => (
<For each={field.value}>
{(tag) => (
<label class="p-1">
Tags
<span class="mx-2 rounded-full px-3 py-1 bg-inv-4 fg-inv-1 w-fit">
{tag}
</span>
</label>
)}
</For>
)}
</Field>
<Field name="machine.description"> <Field name="machine.description">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
@@ -474,6 +363,16 @@ const MachineForm = (props: MachineDetailsProps) => {
/> />
)} )}
</Field> </Field>
<Field name="hw_config">
{(field, props) => (
<label>Hardware report: {field.value || "None"}</label>
)}
</Field>
<Field name="disk_schema">
{(field, props) => (
<span>Disk schema: {field.value || "None"}</span>
)}
</Field>
<div class="collapse collapse-arrow" tabindex="0"> <div class="collapse collapse-arrow" tabindex="0">
<input type="checkbox" /> <input type="checkbox" />
@@ -542,15 +441,6 @@ const MachineForm = (props: MachineDetailsProps) => {
</Form> </Form>
<div class="card-body"> <div class="card-body">
<For each={props.modules}>
{(module) => (
<>
<div class="divider"></div>
<span class="text-xl text-primary-800">{module.name}</span>
{module.component}
</>
)}
</For>
<div class="divider"></div> <div class="divider"></div>
<span class="text-xl text-primary-800">Actions</span> <span class="text-xl text-primary-800">Actions</span>
@@ -581,7 +471,7 @@ const MachineForm = (props: MachineDetailsProps) => {
name={machineName()} name={machineName()}
sshKey={sshKey()} sshKey={sshKey()}
targetHost={getValue(formStore, "machine.deploy.targetHost")} targetHost={getValue(formStore, "machine.deploy.targetHost")}
disks={remoteDiskQuery.data?.blockdevices || []} disks={[]}
/> />
</div> </div>
</dialog> </dialog>
@@ -593,7 +483,7 @@ const MachineForm = (props: MachineDetailsProps) => {
<div class="tooltip w-fit" data-tip="Machine must be online"> <div class="tooltip w-fit" data-tip="Machine must be online">
<Button <Button
class="w-full" class="w-full"
disabled={!online()} // disabled={!online()}
onClick={() => handleUpdate()} onClick={() => handleUpdate()}
endIcon={<Icon icon="Update" />} endIcon={<Icon icon="Update" />}
> >
@@ -606,8 +496,6 @@ const MachineForm = (props: MachineDetailsProps) => {
); );
}; };
type WifiData = ClanService<"iwd">;
export const MachineDetails = () => { export const MachineDetails = () => {
const params = useParams(); const params = useParams();
const genericQuery = createQuery(() => ({ const genericQuery = createQuery(() => ({
@@ -630,20 +518,6 @@ export const MachineDetails = () => {
}, },
})); }));
const wifiQuery = createQuery(() => ({
queryKey: [activeURI(), "machine", params.id, "get_iwd_service"],
queryFn: async () => {
const curr = activeURI();
if (curr) {
const result = await get_iwd_service(curr, params.id);
if (!result) throw new Error("Failed to fetch data");
return Object.entries(result?.config?.networks || {}).map(
([name, value]) => ({ name, ssid: value.ssid }),
);
}
},
}));
return ( return (
<div class="card"> <div class="card">
<BackButton /> <BackButton />
@@ -653,36 +527,7 @@ export const MachineDetails = () => {
> >
{(data) => ( {(data) => (
<> <>
<MachineForm <MachineForm initialData={data()} />
initialData={data()}
modules={[
{
component: (
<Show
when={!wifiQuery.isLoading}
fallback={
<div>
<span class="loading loading-lg"></span>
</div>
}
>
<Switch>
<Match when={wifiQuery.data}>
{(d) => (
<WifiModule
initialData={d()}
base_url={activeURI() || ""}
machine_name={data().machine.name}
/>
)}
</Match>
</Switch>
</Show>
),
name: "Wifi",
},
]}
/>
</> </>
)} )}
</Show> </Show>

View File

@@ -1,4 +1,11 @@
import { type Component, createSignal, For, Match, Switch } from "solid-js"; import {
type Component,
createSignal,
For,
Match,
Show,
Switch,
} from "solid-js";
import { activeURI } from "@/src/App"; import { activeURI } from "@/src/App";
import { callApi, OperationResponse } from "@/src/api"; import { callApi, OperationResponse } from "@/src/api";
import toast from "solid-toast"; import toast from "solid-toast";
@@ -75,9 +82,10 @@ export const MachineListView: Component = () => {
startIcon={<Icon icon="Plus" />} startIcon={<Icon icon="Plus" />}
></Button> ></Button>
</div> </div>
{/* <Show when={filter()}> */}
<div class="my-1 flex w-full gap-2 p-2"> <div class="my-1 flex w-full gap-2 p-2">
<div class="h-6 w-6 p-1"> <div class="h-6 w-6 p-1">
<Icon icon="Info" /> <Icon icon="Filter" />
</div> </div>
<For each={filter().tags.sort()}> <For each={filter().tags.sort()}>
{(tag) => ( {(tag) => (
@@ -99,6 +107,7 @@ export const MachineListView: Component = () => {
)} )}
</For> </For>
</div> </div>
{/* </Show> */}
<Switch> <Switch>
<Match when={inventoryQuery.isLoading}> <Match when={inventoryQuery.isLoading}>
{/* Loading skeleton */} {/* Loading skeleton */}