Merge pull request 'UI: init flash poc' (#1727) from hsjobeki/clan-core:hsjobeki-feat/clan-init into main
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
144
pkgs/webview-ui/app/src/routes/flash/view.tsx
Normal file
144
pkgs/webview-ui/app/src/routes/flash/view.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user