feat(ui): display machine install state and install button

This commit is contained in:
Brian McGee
2025-08-14 14:05:33 +01:00
parent abf6893714
commit 9aebf02f05
24 changed files with 6636 additions and 128 deletions

View File

@@ -1 +1 @@
/nix/store/q012qk78pwldxl3qjy09nwrx9jlamivm-nixpkgs
/nix/store/apspgd56g9qy6fca8d44qnhdaiqrdf2c-nixpkgs

View File

@@ -1,3 +1,4 @@
from enum import StrEnum
from typing import TypedDict
from clan_lib.api import API
@@ -25,6 +26,18 @@ class ListOptions(TypedDict):
filter: MachineFilter
class MachineStatus(StrEnum):
NOT_INSTALLED = "not_installed"
OFFLINE = "offline"
OUT_OF_SYNC = "out_of_sync"
ONLINE = "online"
class MachineState(TypedDict):
status: MachineStatus
# add more info later when retrieving remote state
@API.register
def list_machines(
flake: Flake, opts: ListOptions | None = None
@@ -154,3 +167,47 @@ def get_machine_fields_schema(machine: Machine) -> dict[str, FieldSchema]:
}
for field in field_names
}
@API.register
def list_machine_state(flake: Flake) -> dict[str, MachineState]:
"""
Retrieve the current state of all machines in the clan.
Args:
flake (Flake): The flake object representing the configuration source.
"""
inventory_store = InventoryStore(flake=flake)
inventory = inventory_store.read()
# todo integrate with remote state when implementing https://git.clan.lol/clan/clan-core/issues/4748
machines = inventory.get("machines", {})
return {
machine_name: MachineState(
status=MachineStatus.OFFLINE
if get_value_by_path(machine, "installedAt", None)
else MachineStatus.NOT_INSTALLED
)
for machine_name, machine in machines.items()
}
@API.register
def get_machine_state(machine: Machine) -> MachineState:
"""
Retrieve the current state of the machine.
Args:
machine (Machine): The machine object for which we want to retrieve the latest state.
"""
inventory_store = InventoryStore(flake=machine.flake)
inventory = inventory_store.read()
# todo integrate with remote state when implementing https://git.clan.lol/clan/clan-core/issues/4748
return MachineState(
status=MachineStatus.OFFLINE
if get_value_by_path(inventory, f"machines.{machine.name}.installedAt", None)
else MachineStatus.NOT_INSTALLED
)

View File

@@ -1,3 +1,4 @@
import time
from collections.abc import Callable
from typing import cast
from unittest.mock import ANY, patch
@@ -12,7 +13,16 @@ from clan_lib.nix_models.clan import Clan, InventoryMachine, Unknown
from clan_lib.persist.inventory_store import InventoryStore
from clan_lib.persist.util import get_value_by_path, set_value_by_path
from .actions import get_machine, get_machine_fields_schema, list_machines, set_machine
from .actions import (
MachineState,
MachineStatus,
get_machine,
get_machine_fields_schema,
get_machine_state,
list_machine_state,
list_machines,
set_machine,
)
@pytest.mark.with_core
@@ -219,7 +229,44 @@ def test_get_machine_writeability(clan_flake: Callable[..., Flake]) -> None:
"deploy.buildHost",
"description",
"icon",
"installedAt",
}
assert read_only_fields == {"machineClass", "name"}
assert write_info["tags"]["readonly_members"] == ["nix1", "all", "nixos"]
@pytest.mark.with_core
def test_machine_state(clan_flake: Callable[..., Flake]) -> None:
now = int(time.time())
yesterday = now - 86400
last_week = now - 604800
flake = clan_flake(
# clan.nix, cannot be changed
clan={
"inventory": {
"machines": {
"jon": {},
"sara": {"installedAt": yesterday},
"bob": {"installedAt": last_week},
},
}
},
)
assert list_machine_state(flake) == {
"jon": MachineState(status=MachineStatus.NOT_INSTALLED),
"sara": MachineState(status=MachineStatus.OFFLINE),
"bob": MachineState(status=MachineStatus.OFFLINE),
}
assert get_machine_state(Machine("jon", flake)) == MachineState(
status=MachineStatus.NOT_INSTALLED
)
assert get_machine_state(Machine("sara", flake)) == MachineState(
status=MachineStatus.OFFLINE
)
assert get_machine_state(Machine("bob", flake)) == MachineState(
status=MachineStatus.OFFLINE
)

View File

@@ -72,6 +72,7 @@ class InventoryMachineDeploy(TypedDict):
InventoryMachineDeployType = InventoryMachineDeploy
InventoryMachineDescriptionType = str | None
InventoryMachineIconType = str | None
InventoryMachineInstalledatType = int | None
InventoryMachineMachineclassType = Literal["nixos", "darwin"]
InventoryMachineNameType = str
InventoryMachineTagsType = list[str]
@@ -80,6 +81,7 @@ class InventoryMachine(TypedDict):
deploy: NotRequired[InventoryMachineDeployType]
description: NotRequired[InventoryMachineDescriptionType]
icon: NotRequired[InventoryMachineIconType]
installedAt: NotRequired[InventoryMachineInstalledatType]
machineClass: NotRequired[InventoryMachineMachineclassType]
name: NotRequired[InventoryMachineNameType]
tags: NotRequired[InventoryMachineTagsType]