Merge pull request 'UI: init flash poc' (#1727) from hsjobeki/clan-core:hsjobeki-feat/clan-init into main

This commit is contained in:
clan-bot
2024-07-10 09:07:09 +00:00
5 changed files with 183 additions and 2 deletions

View File

@@ -55,6 +55,34 @@ class _MethodRegistry:
self._orig: dict[str, Callable[[Any], Any]] = {} self._orig: dict[str, Callable[[Any], Any]] = {}
self._registry: dict[str, Callable[[Any], Any]] = {} self._registry: dict[str, Callable[[Any], Any]] = {}
def register_abstract(self, fn: Callable[..., T]) -> Callable[..., T]:
@wraps(fn)
def wrapper(
*args: Any, op_key: str | None = None, **kwargs: Any
) -> ApiResponse[T]:
raise NotImplementedError(
f"""{fn.__name__} - The platform didn't implement this function.
---
# Example
The function 'open_file()' depends on the platform.
def open_file(file_request: FileRequest) -> str | None:
# In GTK we open a file dialog window
# In Android we open a file picker dialog
# and so on.
pass
# At runtime the clan-app must override platform specific functions
API.register(open_file)
---
"""
)
self.register(wrapper)
return fn
def register(self, fn: Callable[..., T]) -> Callable[..., T]: def register(self, fn: Callable[..., T]) -> Callable[..., T]:
self._orig[fn.__name__] = fn self._orig[fn.__name__] = fn

View File

@@ -28,13 +28,13 @@ class FileRequest:
filters: FileFilter | None = None filters: FileFilter | None = None
@API.register @API.register_abstract
def open_file(file_request: FileRequest) -> str | None: def open_file(file_request: FileRequest) -> str | None:
""" """
Abstract api method to open a file dialog window. Abstract api method to open a file dialog window.
It must return the name of the selected file or None if no file was selected. It must return the name of the selected file or None if no file was selected.
""" """
raise NotImplementedError("Each specific platform should implement this function.") pass
@dataclass @dataclass

View File

@@ -11,6 +11,8 @@ from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from typing import Any from typing import Any
from clan_cli.api import API
from .clan_uri import FlakeId from .clan_uri import FlakeId
from .cmd import Log, run from .cmd import Log, run
from .completions import add_dynamic_completer, complete_machines from .completions import add_dynamic_completer, complete_machines
@@ -22,6 +24,7 @@ from .nix import nix_shell
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@API.register
def flash_machine( def flash_machine(
machine: Machine, machine: Machine,
*, *,

View File

@@ -4,6 +4,7 @@ import { colors } from "./routes/colors/view";
import { clan } from "./routes/clan/view"; import { clan } from "./routes/clan/view";
import { HostList } from "./routes/hosts/view"; import { HostList } from "./routes/hosts/view";
import { BlockDevicesView } from "./routes/blockdevices/view"; import { BlockDevicesView } from "./routes/blockdevices/view";
import { Flash } from "./routes/flash/view";
export type Route = keyof typeof routes; export type Route = keyof typeof routes;
@@ -23,6 +24,11 @@ export const routes = {
label: "hosts", label: "hosts",
icon: "devices_other", icon: "devices_other",
}, },
flash: {
child: Flash,
label: "create_flash_installer",
icon: "devices_other",
},
blockdevices: { blockdevices: {
child: BlockDevicesView, child: BlockDevicesView,
label: "blockdevices", label: "blockdevices",

View File

@@ -0,0 +1,144 @@
import { route } from "@/src/App";
import { OperationArgs, OperationResponse, pyApi } from "@/src/api";
import { SubmitHandler, createForm, required } from "@modular-forms/solid";
import { For, createSignal } from "solid-js";
import { effect } from "solid-js/web";
// type FlashMachineArgs = {
// machine: Omit<OperationArgs<"flash_machine">["machine"], "cached_deployment">;
// } & Omit<Omit<OperationArgs<"flash_machine">, "machine">, "system_config">;
// type FlashMachineArgs = OperationArgs<"flash_machine">;
// type k = keyof FlashMachineArgs;
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type FlashFormValues = {
machine: {
name: string;
flake: string;
};
disk: string;
};
type BlockDevices = Extract<
OperationResponse<"show_block_devices">,
{ status: "success" }
>["data"]["blockdevices"];
export const Flash = () => {
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
const [devices, setDevices] = createSignal<BlockDevices>([]);
pyApi.show_block_devices.receive((r) => {
console.log("block devices", r);
if (r.status === "success") {
setDevices(r.data.blockdevices);
}
});
const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => {
// pyApi.open_file.dispatch({ file_request: { mode: "save" } });
// pyApi.open_file.receive((r) => {
// if (r.status === "success") {
// if (r.data) {
// pyApi.create_clan.dispatch({
// options: { directory: r.data, meta: values },
// });
// }
// return;
// }
// });
console.log("submit", values);
};
effect(() => {
if (route() === "flash") {
pyApi.show_block_devices.dispatch({});
}
});
return (
<div class="">
<Form onSubmit={handleSubmit}>
<Field
name="machine.flake"
validate={[required("This field is required")]}
>
{(field, props) => (
<>
<label class="input input-bordered flex items-center gap-2">
<span class="material-icons">file_download</span>
<input
type="text"
class="grow"
placeholder="Clan URI"
required
{...props}
/>
</label>
<div class="label">
{field.error && (
<span class="label-text-alt font-bold text-error">
{field.error}
</span>
)}
</div>
</>
)}
</Field>
<Field
name="machine.name"
validate={[required("This field is required")]}
>
{(field, props) => (
<>
<label class="input input-bordered flex items-center gap-2">
<span class="material-icons">devices</span>
<input
type="text"
class="grow"
placeholder="Machine Name"
required
{...props}
/>
</label>
<div class="label">
{field.error && (
<span class="label-text-alt font-bold text-error">
{field.error}
</span>
)}
</div>
</>
)}
</Field>
<Field name="disk" validate={[required("This field is required")]}>
{(field, props) => (
<>
<label class="form-control input-bordered flex w-full items-center gap-2">
<select required class="select w-full" {...props}>
{/* <span class="material-icons">devices</span> */}
<For each={devices()}>
{(device) => (
<option value={device.name}>
{device.name} / {device.size} bytes
</option>
)}
</For>
</select>
<div class="label">
{field.error && (
<span class="label-text-alt font-bold text-error">
{field.error}
</span>
)}
</div>
</label>
</>
)}
</Field>
<button class="btn btn-error" type="submit">
<span class="material-icons">bolt</span>Flash Installer
</button>
</Form>
</div>
);
};