Merge pull request 'lib.values: init getPrio' (#2559) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -101,6 +101,7 @@ in
|
||||
# Those options are interfaced by the CLI
|
||||
# We don't specify the type here, for better performance.
|
||||
inventory = lib.mkOption { type = lib.types.raw; };
|
||||
inventoryValuesPrios = lib.mkOption { type = lib.types.raw; };
|
||||
# all inventory module schemas
|
||||
moduleSchemas = lib.mkOption { type = lib.types.raw; };
|
||||
inventoryFile = lib.mkOption { type = lib.types.raw; };
|
||||
|
||||
@@ -183,6 +183,7 @@ in
|
||||
inherit serviceConfigs;
|
||||
inherit (clan-core) clanModules;
|
||||
inherit inventoryFile;
|
||||
inventoryValuesPrios = (clan-core.lib.values.getPrios { options = inventory.options; });
|
||||
inventory = config.inventory;
|
||||
meta = config.inventory.meta;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ in
|
||||
buildClan = import ./build-clan { inherit lib nixpkgs clan-core; };
|
||||
facts = import ./facts.nix { inherit lib; };
|
||||
inventory = import ./inventory { inherit lib clan-core; };
|
||||
values = import ./values { inherit lib; };
|
||||
jsonschema = import ./jsonschema { inherit lib; };
|
||||
modules = import ./frontmatter {
|
||||
inherit lib;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
./jsonschema/flake-module.nix
|
||||
./inventory/flake-module.nix
|
||||
./build-clan/flake-module.nix
|
||||
./values/flake-module.nix
|
||||
];
|
||||
flake.lib = import ./default.nix {
|
||||
inherit lib inputs;
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{ lib, config, ... }:
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
options,
|
||||
...
|
||||
}:
|
||||
let
|
||||
types = lib.types;
|
||||
|
||||
@@ -92,6 +97,12 @@ in
|
||||
./assertions.nix
|
||||
];
|
||||
options = {
|
||||
options = lib.mkOption {
|
||||
internal = true;
|
||||
visible = false;
|
||||
type = types.raw;
|
||||
default = options;
|
||||
};
|
||||
modules = lib.mkOption {
|
||||
type = types.attrsOf types.path;
|
||||
default = { };
|
||||
|
||||
90
lib/values/default.nix
Normal file
90
lib/values/default.nix
Normal 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;
|
||||
}
|
||||
24
lib/values/flake-module.nix
Normal file
24
lib/values/flake-module.nix
Normal 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
211
lib/values/test.nix
Normal 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;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -119,6 +119,37 @@ def load_inventory_json(
|
||||
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
|
||||
def set_inventory(
|
||||
inventory: Inventory | dict[str, Any], flake_dir: str | Path, message: str
|
||||
@@ -129,6 +160,19 @@ def set_inventory(
|
||||
"""
|
||||
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:
|
||||
if isinstance(inventory, Inventory):
|
||||
json.dump(dataclass_to_dict(inventory), f, indent=2)
|
||||
|
||||
@@ -7,11 +7,16 @@ from typing import Literal
|
||||
|
||||
from clan_cli.api import API
|
||||
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.completions import add_dynamic_completer, complete_tags
|
||||
from clan_cli.dirs import specific_machine_dir
|
||||
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.nix import nix_eval, nix_shell
|
||||
from clan_cli.tags import list_nixos_machines_by_tags
|
||||
@@ -20,12 +25,10 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@API.register
|
||||
def set_machine(flake_url: str | Path, machine_name: str, machine: Machine) -> None:
|
||||
inventory = load_inventory_eval(flake_url)
|
||||
|
||||
inventory.machines[machine_name] = machine
|
||||
|
||||
set_inventory(inventory, flake_url, "machines: edit '{machine_name}'")
|
||||
def set_machine(flake_url: Path, machine_name: str, machine: Machine) -> None:
|
||||
patch_inventory_with(
|
||||
flake_url, f"machines.{machine_name}", dataclass_to_dict(machine)
|
||||
)
|
||||
|
||||
|
||||
@API.register
|
||||
|
||||
36
pkgs/clan-cli/tests/test_patch_inventory.py
Normal file
36
pkgs/clan-cli/tests/test_patch_inventory.py
Normal 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"}}}
|
||||
@@ -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}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
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 { activeURI } from "@/src/App";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { FileInput } from "@/src/components/FileInput";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { RndThumbnail } from "@/src/components/noiseThumbnail";
|
||||
import { SelectInput } from "@/src/components/SelectInput";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import { selectSshKeys } from "@/src/hooks";
|
||||
@@ -54,18 +54,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
|
||||
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) => {
|
||||
console.log("Installing", values);
|
||||
const curr_uri = activeURI();
|
||||
@@ -98,7 +86,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
toast.success("Machine installed successfully");
|
||||
}
|
||||
};
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleDiskConfirm = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
@@ -108,19 +95,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
if (!curr_uri || !disk_id || !props.name) {
|
||||
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) => {
|
||||
@@ -141,7 +115,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
},
|
||||
});
|
||||
toast.dismiss(loading_toast);
|
||||
hwInfoQuery.refetch();
|
||||
// TODO: refresh the machine details
|
||||
|
||||
if (r.status === "error") {
|
||||
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="text-lg font-semibold">Hardware detection</div>
|
||||
|
||||
<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="">
|
||||
<Button
|
||||
variant="light"
|
||||
size="s"
|
||||
class="w-full"
|
||||
onclick={generateReport}
|
||||
endIcon={<Icon icon="Report" />}
|
||||
>
|
||||
<span class="material-icons">manage_search</span>
|
||||
Generate report
|
||||
Run hardware 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field name="disk">
|
||||
{(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>
|
||||
<Field name="disk">{(field, fieldProps) => "disk"}</Field>
|
||||
<div role="alert" class="alert my-4">
|
||||
<span class="material-icons">info</span>
|
||||
<div>
|
||||
@@ -260,16 +189,16 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
class="btn btn-primary btn-wide"
|
||||
onClick={handleDiskConfirm}
|
||||
disabled={!hasDisk()}
|
||||
startIcon={<Icon icon="Flash" />}
|
||||
endIcon={<Icon icon="Flash" />}
|
||||
>
|
||||
Confirm Disk
|
||||
Install
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
class="w-full"
|
||||
type="submit"
|
||||
startIcon={<Icon icon="Flash" />}
|
||||
endIcon={<Icon icon="Flash" />}
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
@@ -291,10 +220,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
|
||||
interface MachineDetailsProps {
|
||||
initialData: MachineData;
|
||||
modules: {
|
||||
name: string;
|
||||
component: JSXElement;
|
||||
}[];
|
||||
}
|
||||
const MachineForm = (props: MachineDetailsProps) => {
|
||||
const [formStore, { Form, Field }] =
|
||||
@@ -308,52 +233,9 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
const machineName = () =>
|
||||
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) => {
|
||||
console.log("submitting", values);
|
||||
|
||||
const curr_uri = activeURI();
|
||||
if (!curr_uri) {
|
||||
return;
|
||||
@@ -426,27 +308,20 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
<figure>
|
||||
<div
|
||||
class="avatar placeholder"
|
||||
classList={{
|
||||
online: onlineStatusQuery.data === "Online",
|
||||
offline: onlineStatusQuery.data === "Offline",
|
||||
}}
|
||||
classList={
|
||||
{
|
||||
// online: onlineStatusQuery.data === "Online",
|
||||
// offline: onlineStatusQuery.data === "Offline",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div class="w-24 rounded-full bg-neutral text-neutral-content">
|
||||
<Show
|
||||
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 class="w-32 rounded-lg border p-2 bg-def-4 border-inv-3">
|
||||
<RndThumbnail name={machineName()} />
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<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, props) => (
|
||||
@@ -461,6 +336,20 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
/>
|
||||
)}
|
||||
</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, props) => (
|
||||
<TextInput
|
||||
@@ -474,6 +363,16 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
/>
|
||||
)}
|
||||
</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">
|
||||
<input type="checkbox" />
|
||||
@@ -542,15 +441,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
</Form>
|
||||
|
||||
<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>
|
||||
|
||||
<span class="text-xl text-primary-800">Actions</span>
|
||||
@@ -581,7 +471,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
name={machineName()}
|
||||
sshKey={sshKey()}
|
||||
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
||||
disks={remoteDiskQuery.data?.blockdevices || []}
|
||||
disks={[]}
|
||||
/>
|
||||
</div>
|
||||
</dialog>
|
||||
@@ -593,7 +483,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
<div class="tooltip w-fit" data-tip="Machine must be online">
|
||||
<Button
|
||||
class="w-full"
|
||||
disabled={!online()}
|
||||
// disabled={!online()}
|
||||
onClick={() => handleUpdate()}
|
||||
endIcon={<Icon icon="Update" />}
|
||||
>
|
||||
@@ -606,8 +496,6 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
type WifiData = ClanService<"iwd">;
|
||||
|
||||
export const MachineDetails = () => {
|
||||
const params = useParams();
|
||||
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 (
|
||||
<div class="card">
|
||||
<BackButton />
|
||||
@@ -653,36 +527,7 @@ export const MachineDetails = () => {
|
||||
>
|
||||
{(data) => (
|
||||
<>
|
||||
<MachineForm
|
||||
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",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<MachineForm initialData={data()} />
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
@@ -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 { callApi, OperationResponse } from "@/src/api";
|
||||
import toast from "solid-toast";
|
||||
@@ -75,9 +82,10 @@ export const MachineListView: Component = () => {
|
||||
startIcon={<Icon icon="Plus" />}
|
||||
></Button>
|
||||
</div>
|
||||
{/* <Show when={filter()}> */}
|
||||
<div class="my-1 flex w-full gap-2 p-2">
|
||||
<div class="h-6 w-6 p-1">
|
||||
<Icon icon="Info" />
|
||||
<Icon icon="Filter" />
|
||||
</div>
|
||||
<For each={filter().tags.sort()}>
|
||||
{(tag) => (
|
||||
@@ -99,6 +107,7 @@ export const MachineListView: Component = () => {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* </Show> */}
|
||||
<Switch>
|
||||
<Match when={inventoryQuery.isLoading}>
|
||||
{/* Loading skeleton */}
|
||||
|
||||
Reference in New Issue
Block a user