Merge pull request 'UI: Improvements on install workflow.' (#2682) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -233,16 +233,18 @@ def construct_value(
|
|||||||
|
|
||||||
# Enums
|
# Enums
|
||||||
if origin is Enum:
|
if origin is Enum:
|
||||||
if field_value not in origin.__members__:
|
try:
|
||||||
msg = f"Expected one of {', '.join(origin.__members__)}, got {field_value}"
|
return t(field_value) # type: ignore
|
||||||
raise ClanError(msg, location=f"{loc}")
|
except ValueError:
|
||||||
return origin.__members__[field_value] # type: ignore
|
msg = f"Expected one of {', '.join(str(origin))}, got {field_value}"
|
||||||
|
raise ClanError(msg, location=f"{loc}") from ValueError
|
||||||
|
|
||||||
if isinstance(t, type) and issubclass(t, Enum):
|
if isinstance(t, type) and issubclass(t, Enum):
|
||||||
if field_value not in t.__members__:
|
try:
|
||||||
|
return t(field_value) # type: ignore
|
||||||
|
except ValueError:
|
||||||
msg = f"Expected one of {', '.join(t.__members__)}, got {field_value}"
|
msg = f"Expected one of {', '.join(t.__members__)}, got {field_value}"
|
||||||
raise ClanError(msg, location=f"{loc}")
|
raise ClanError(msg, location=f"{loc}") from ValueError
|
||||||
return t.__members__[field_value] # type: ignore
|
|
||||||
|
|
||||||
if origin is Annotated:
|
if origin is Annotated:
|
||||||
(base_type,) = get_args(t)
|
(base_type,) = get_args(t)
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ from typing import (
|
|||||||
is_typeddict,
|
is_typeddict,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from clan_cli.api.serde import dataclass_to_dict
|
||||||
|
|
||||||
|
|
||||||
class JSchemaTypeError(Exception):
|
class JSchemaTypeError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -257,7 +259,8 @@ def type_to_dict(
|
|||||||
if type(t) is EnumType:
|
if type(t) is EnumType:
|
||||||
return {
|
return {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": list(t.__members__),
|
# Construct every enum value and use the same method as the serde module for converting it into the same literal string
|
||||||
|
"enum": [dataclass_to_dict(t(value)) for value in t], # type: ignore
|
||||||
}
|
}
|
||||||
if t is Any:
|
if t is Any:
|
||||||
msg = f"{scope} - Usage of the Any type is not supported for API functions. In: {scope}"
|
msg = f"{scope} - Usage of the Any type is not supported for API functions. In: {scope}"
|
||||||
|
|||||||
@@ -394,7 +394,8 @@ def run(
|
|||||||
|
|
||||||
if options.check and process.returncode != 0:
|
if options.check and process.returncode != 0:
|
||||||
err = ClanCmdError(cmd_out)
|
err = ClanCmdError(cmd_out)
|
||||||
err.msg = "Command has been cancelled"
|
err.msg = str(stderr_buf)
|
||||||
|
err.description = "Command has been cancelled"
|
||||||
raise err
|
raise err
|
||||||
|
|
||||||
return cmd_out
|
return cmd_out
|
||||||
|
|||||||
@@ -158,7 +158,6 @@ def find_deleted_paths(
|
|||||||
for key, p_value in persisted.items():
|
for key, p_value in persisted.items():
|
||||||
current_path = f"{parent_key}.{key}" if parent_key else key
|
current_path = f"{parent_key}.{key}" if parent_key else key
|
||||||
# Check if this key exists in update
|
# Check if this key exists in update
|
||||||
# breakpoint()
|
|
||||||
if key not in update:
|
if key not in update:
|
||||||
# Key doesn't exist at all -> entire branch deleted
|
# Key doesn't exist at all -> entire branch deleted
|
||||||
deleted_paths.add(current_path)
|
deleted_paths.add(current_path)
|
||||||
|
|||||||
@@ -266,3 +266,31 @@ def test_literal_field() -> None:
|
|||||||
with pytest.raises(ClanError):
|
with pytest.raises(ClanError):
|
||||||
# Not a valid value
|
# Not a valid value
|
||||||
from_dict(Person, {"name": "open"})
|
from_dict(Person, {"name": "open"})
|
||||||
|
|
||||||
|
|
||||||
|
def test_enum_roundtrip() -> None:
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class MyEnum(Enum):
|
||||||
|
FOO = "abc"
|
||||||
|
BAR = 2
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Person:
|
||||||
|
name: MyEnum
|
||||||
|
|
||||||
|
# Both are equivalent
|
||||||
|
data = {"name": "abc"} # JSON Representation
|
||||||
|
expected = Person(name=MyEnum.FOO) # Data representation
|
||||||
|
|
||||||
|
assert from_dict(Person, data) == expected
|
||||||
|
|
||||||
|
assert dataclass_to_dict(expected) == data
|
||||||
|
|
||||||
|
# Same test for integer values
|
||||||
|
data2 = {"name": 2} # JSON Representation
|
||||||
|
expected2 = Person(name=MyEnum.BAR) # Data representation
|
||||||
|
|
||||||
|
assert from_dict(Person, data2) == expected2
|
||||||
|
|
||||||
|
assert dataclass_to_dict(expected2) == data2
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export interface Option {
|
|||||||
|
|
||||||
interface SelectInputpProps {
|
interface SelectInputpProps {
|
||||||
value: string[] | string;
|
value: string[] | string;
|
||||||
selectProps: JSX.InputHTMLAttributes<HTMLSelectElement>;
|
selectProps?: JSX.InputHTMLAttributes<HTMLSelectElement>;
|
||||||
options: Option[];
|
options: Option[];
|
||||||
label: JSX.Element;
|
label: JSX.Element;
|
||||||
labelProps?: InputLabelProps;
|
labelProps?: InputLabelProps;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { splitProps, type JSX } from "solid-js";
|
import { splitProps, type JSX } from "solid-js";
|
||||||
import { InputBase, InputError, InputLabel } from "@/src/components/inputBase";
|
import {
|
||||||
|
InputBase,
|
||||||
|
InputError,
|
||||||
|
InputLabel,
|
||||||
|
InputVariant,
|
||||||
|
} from "@/src/components/inputBase";
|
||||||
import { Typography } from "@/src/components/Typography";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import { FieldLayout } from "./layout";
|
import { FieldLayout } from "./layout";
|
||||||
|
|
||||||
@@ -12,10 +17,11 @@ interface TextInputProps {
|
|||||||
value: string;
|
value: string;
|
||||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
|
variant?: InputVariant;
|
||||||
// Passed to label
|
// Passed to label
|
||||||
label: JSX.Element;
|
label: JSX.Element;
|
||||||
help?: string;
|
help?: string;
|
||||||
// Passed to layouad
|
// Passed to layout
|
||||||
class?: string;
|
class?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +41,7 @@ export function TextInput(props: TextInputProps) {
|
|||||||
}
|
}
|
||||||
field={
|
field={
|
||||||
<InputBase
|
<InputBase
|
||||||
|
variant={props.variant}
|
||||||
error={!!props.error}
|
error={!!props.error}
|
||||||
required={props.required}
|
required={props.required}
|
||||||
disabled={props.disabled}
|
disabled={props.disabled}
|
||||||
|
|||||||
@@ -15,14 +15,11 @@ export const FieldLayout = (props: LayoutProps) => {
|
|||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class={cx("grid grid-cols-12 items-center", intern.class)}
|
class={cx("grid grid-cols-10 items-center", intern.class)}
|
||||||
classList={{
|
|
||||||
"mb-[14.5px]": !props.error,
|
|
||||||
}}
|
|
||||||
{...divProps}
|
{...divProps}
|
||||||
>
|
>
|
||||||
<label class="col-span-2">{props.label}</label>
|
<label class="col-span-5">{props.label}</label>
|
||||||
<div class="col-span-10">{props.field}</div>
|
<div class="col-span-5">{props.field}</div>
|
||||||
{props.error && <span class="col-span-full">{props.error}</span>}
|
{props.error && <span class="col-span-full">{props.error}</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
@import "./typography-color.css";
|
@import "./typography-color.css";
|
||||||
|
|
||||||
.fnt-weight-normal {
|
.fnt-weight-normal {
|
||||||
font-weight: normal;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fnt-weight-medium {
|
.fnt-weight-medium {
|
||||||
font-weight: medium;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fnt-weight-bold {
|
.fnt-weight-bold {
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fnt-weight-normal.fnt-clr--inverted {
|
.fnt-weight-normal.fnt-clr--inverted {
|
||||||
font-weight: light;
|
font-weight: 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fnt-weight-medium.fnt-clr--inverted {
|
.fnt-weight-medium.fnt-clr--inverted {
|
||||||
font-weight: normal;
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fnt-weight-bold.fnt-clr--inverted {
|
.fnt-weight-bold.fnt-clr--inverted {
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ interface _TypographyProps<H extends Hierarchy> {
|
|||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
class?: string;
|
class?: string;
|
||||||
classList?: Record<string, boolean>;
|
classList?: Record<string, boolean>;
|
||||||
|
// Disable using the color prop
|
||||||
|
// A font color is provided via class / classList or inherited
|
||||||
|
useExternColor?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||||
@@ -92,7 +95,7 @@ export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
|||||||
<Dynamic
|
<Dynamic
|
||||||
component={props.tag || "span"}
|
component={props.tag || "span"}
|
||||||
class={cx(
|
class={cx(
|
||||||
colorMap[props.color || "primary"],
|
!props.useExternColor && colorMap[props.color || "primary"],
|
||||||
props.inverted && "fnt-clr--inverted",
|
props.inverted && "fnt-clr--inverted",
|
||||||
sizeHierarchyMap[props.hierarchy][props.size] as string,
|
sizeHierarchyMap[props.hierarchy][props.size] as string,
|
||||||
weightMap[props.weight || "normal"],
|
weightMap[props.weight || "normal"],
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { Typography } from "../Typography";
|
|||||||
type Variants = "dark" | "light" | "ghost";
|
type Variants = "dark" | "light" | "ghost";
|
||||||
type Size = "default" | "s";
|
type Size = "default" | "s";
|
||||||
|
|
||||||
const variantColors: Record<Variants, string> = {
|
const variantColors: (
|
||||||
|
disabled: boolean | undefined,
|
||||||
|
) => Record<Variants, string> = (disabled) => ({
|
||||||
dark: cx(
|
dark: cx(
|
||||||
"border border-solid",
|
"border border-solid",
|
||||||
"border-secondary-950 bg-primary-900 text-white",
|
"border-secondary-950 bg-primary-900 text-white",
|
||||||
@@ -13,8 +15,9 @@ const variantColors: Record<Variants, string> = {
|
|||||||
// Hover state
|
// Hover state
|
||||||
// Focus state
|
// Focus state
|
||||||
// Active state
|
// Active state
|
||||||
"hover:border-secondary-900 hover:bg-secondary-700",
|
!disabled && "hover:border-secondary-900 hover:bg-secondary-700",
|
||||||
"focus:border-secondary-900",
|
!disabled && "focus:border-secondary-900",
|
||||||
|
!disabled &&
|
||||||
"active:border-secondary-900 active:shadow-inner-primary-active",
|
"active:border-secondary-900 active:shadow-inner-primary-active",
|
||||||
// Disabled
|
// Disabled
|
||||||
"disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300",
|
"disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300",
|
||||||
@@ -26,8 +29,9 @@ const variantColors: Record<Variants, string> = {
|
|||||||
// Hover state
|
// Hover state
|
||||||
// Focus state
|
// Focus state
|
||||||
// Active state
|
// Active state
|
||||||
"hover:bg-secondary-200 hover:text-secondary-900",
|
!disabled && "hover:bg-secondary-200 hover:text-secondary-900",
|
||||||
"focus:bg-secondary-200 focus:text-secondary-900",
|
!disabled && "focus:bg-secondary-200 focus:text-secondary-900",
|
||||||
|
!disabled &&
|
||||||
"active:bg-secondary-200 active:text-secondary-950 active:shadow-inner-secondary-active",
|
"active:bg-secondary-200 active:text-secondary-950 active:shadow-inner-secondary-active",
|
||||||
// Disabled
|
// Disabled
|
||||||
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
||||||
@@ -37,13 +41,14 @@ const variantColors: Record<Variants, string> = {
|
|||||||
// Hover state
|
// Hover state
|
||||||
// Focus state
|
// Focus state
|
||||||
// Active state
|
// Active state
|
||||||
"hover:bg-secondary-200 hover:text-secondary-900",
|
!disabled && "hover:bg-secondary-200 hover:text-secondary-900",
|
||||||
"focus:bg-secondary-200 focus:text-secondary-900",
|
!disabled && "focus:bg-secondary-200 focus:text-secondary-900",
|
||||||
|
!disabled &&
|
||||||
"active:bg-secondary-200 active:text-secondary-950 active:shadow-inner-secondary-active",
|
"active:bg-secondary-200 active:text-secondary-950 active:shadow-inner-secondary-active",
|
||||||
// Disabled
|
// Disabled
|
||||||
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
||||||
),
|
),
|
||||||
};
|
});
|
||||||
|
|
||||||
const sizePaddings: Record<Size, string> = {
|
const sizePaddings: Record<Size, string> = {
|
||||||
default: cx("rounded-[0.1875rem] px-4 py-2"),
|
default: cx("rounded-[0.1875rem] px-4 py-2"),
|
||||||
@@ -82,7 +87,7 @@ export const Button = (props: ButtonProps) => {
|
|||||||
"p-4",
|
"p-4",
|
||||||
sizePaddings[local.size || "default"],
|
sizePaddings[local.size || "default"],
|
||||||
// Colors
|
// Colors
|
||||||
variantColors[local.variant || "dark"],
|
variantColors(props.disabled)[local.variant || "dark"],
|
||||||
//Font
|
//Font
|
||||||
"leading-none font-semibold",
|
"leading-none font-semibold",
|
||||||
sizeFont[local.size || "default"],
|
sizeFont[local.size || "default"],
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import ArrowBottom from "@/icons/arrow-bottom.svg";
|
|||||||
import ArrowLeft from "@/icons/arrow-left.svg";
|
import ArrowLeft from "@/icons/arrow-left.svg";
|
||||||
import ArrowRight from "@/icons/arrow-right.svg";
|
import ArrowRight from "@/icons/arrow-right.svg";
|
||||||
import ArrowTop from "@/icons/arrow-top.svg";
|
import ArrowTop from "@/icons/arrow-top.svg";
|
||||||
|
import Attention from "@/icons/attention.svg";
|
||||||
import CaretDown from "@/icons/caret-down.svg";
|
import CaretDown from "@/icons/caret-down.svg";
|
||||||
import CaretUp from "@/icons/caret-up.svg";
|
|
||||||
import CaretLeft from "@/icons/caret-left.svg";
|
import CaretLeft from "@/icons/caret-left.svg";
|
||||||
import CaretRight from "@/icons/caret-right.svg";
|
import CaretRight from "@/icons/caret-right.svg";
|
||||||
|
import CaretUp from "@/icons/caret-up.svg";
|
||||||
import Checkmark from "@/icons/checkmark.svg";
|
import Checkmark from "@/icons/checkmark.svg";
|
||||||
import ClanIcon from "@/icons/clan-icon.svg";
|
import ClanIcon from "@/icons/clan-icon.svg";
|
||||||
import ClanLogo from "@/icons/clan-logo.svg";
|
import ClanLogo from "@/icons/clan-logo.svg";
|
||||||
@@ -16,31 +17,33 @@ import Edit from "@/icons/edit.svg";
|
|||||||
import Expand from "@/icons/expand.svg";
|
import Expand from "@/icons/expand.svg";
|
||||||
import EyeClose from "@/icons/eye-close.svg";
|
import EyeClose from "@/icons/eye-close.svg";
|
||||||
import EyeOpen from "@/icons/eye-open.svg";
|
import EyeOpen from "@/icons/eye-open.svg";
|
||||||
import Flash from "@/icons/flash.svg";
|
|
||||||
import Filter from "@/icons/filter.svg";
|
import Filter from "@/icons/filter.svg";
|
||||||
|
import Flash from "@/icons/flash.svg";
|
||||||
import Folder from "@/icons/folder.svg";
|
import Folder from "@/icons/folder.svg";
|
||||||
import More from "@/icons/more.svg";
|
|
||||||
import Report from "@/icons/report.svg";
|
|
||||||
import Search from "@/icons/search.svg";
|
|
||||||
import Grid from "@/icons/grid.svg";
|
import Grid from "@/icons/grid.svg";
|
||||||
import Info from "@/icons/info.svg";
|
import Info from "@/icons/info.svg";
|
||||||
import List from "@/icons/list.svg";
|
import List from "@/icons/list.svg";
|
||||||
import Load from "@/icons/load.svg";
|
import Load from "@/icons/load.svg";
|
||||||
|
import More from "@/icons/more.svg";
|
||||||
import Paperclip from "@/icons/paperclip.svg";
|
import Paperclip from "@/icons/paperclip.svg";
|
||||||
import Plus from "@/icons/plus.svg";
|
import Plus from "@/icons/plus.svg";
|
||||||
import Reload from "@/icons/reload.svg";
|
import Reload from "@/icons/reload.svg";
|
||||||
|
import Report from "@/icons/report.svg";
|
||||||
|
import Search from "@/icons/search.svg";
|
||||||
import Settings from "@/icons/settings.svg";
|
import Settings from "@/icons/settings.svg";
|
||||||
import Trash from "@/icons/trash.svg";
|
import Trash from "@/icons/trash.svg";
|
||||||
import Update from "@/icons/update.svg";
|
import Update from "@/icons/update.svg";
|
||||||
|
import Warning from "@/icons/warning.svg";
|
||||||
|
|
||||||
const icons = {
|
const icons = {
|
||||||
ArrowBottom,
|
ArrowBottom,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowTop,
|
ArrowTop,
|
||||||
|
Attention,
|
||||||
CaretDown,
|
CaretDown,
|
||||||
CaretRight,
|
|
||||||
CaretLeft,
|
CaretLeft,
|
||||||
|
CaretRight,
|
||||||
CaretUp,
|
CaretUp,
|
||||||
Checkmark,
|
Checkmark,
|
||||||
ClanIcon,
|
ClanIcon,
|
||||||
@@ -52,21 +55,22 @@ const icons = {
|
|||||||
EyeClose,
|
EyeClose,
|
||||||
EyeOpen,
|
EyeOpen,
|
||||||
Filter,
|
Filter,
|
||||||
Folder,
|
|
||||||
More,
|
|
||||||
Report,
|
|
||||||
Search,
|
|
||||||
Flash,
|
Flash,
|
||||||
|
Folder,
|
||||||
Grid,
|
Grid,
|
||||||
Info,
|
Info,
|
||||||
List,
|
List,
|
||||||
Load,
|
Load,
|
||||||
|
More,
|
||||||
Paperclip,
|
Paperclip,
|
||||||
Plus,
|
Plus,
|
||||||
Reload,
|
Reload,
|
||||||
|
Report,
|
||||||
|
Search,
|
||||||
Settings,
|
Settings,
|
||||||
Trash,
|
Trash,
|
||||||
Update,
|
Update,
|
||||||
|
Warning,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconVariant = keyof typeof icons;
|
export type IconVariant = keyof typeof icons;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { JSX, Ref, Show, splitProps } from "solid-js";
|
|||||||
import Icon, { IconVariant } from "../icon";
|
import Icon, { IconVariant } from "../icon";
|
||||||
import { Typography, TypographyProps } from "../Typography";
|
import { Typography, TypographyProps } from "../Typography";
|
||||||
|
|
||||||
type Variants = "outlined" | "ghost";
|
export type InputVariant = "outlined" | "ghost";
|
||||||
interface InputBaseProps {
|
interface InputBaseProps {
|
||||||
variant?: Variants;
|
variant?: InputVariant;
|
||||||
value?: string;
|
value?: string;
|
||||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -22,7 +22,7 @@ interface InputBaseProps {
|
|||||||
divRef?: Ref<HTMLDivElement>;
|
divRef?: Ref<HTMLDivElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantBorder: Record<Variants, string> = {
|
const variantBorder: Record<InputVariant, string> = {
|
||||||
outlined: "border border-inv-3",
|
outlined: "border border-inv-3",
|
||||||
ghost: "",
|
ghost: "",
|
||||||
};
|
};
|
||||||
@@ -115,6 +115,8 @@ export const InputLabel = (props: InputLabelProps) => {
|
|||||||
class={cx("flex items-center gap-1", labelProps.class)}
|
class={cx("flex items-center gap-1", labelProps.class)}
|
||||||
{...forwardProps}
|
{...forwardProps}
|
||||||
>
|
>
|
||||||
|
<span class="flex flex-col justify-center">
|
||||||
|
<span>
|
||||||
<Typography
|
<Typography
|
||||||
hierarchy="label"
|
hierarchy="label"
|
||||||
size="default"
|
size="default"
|
||||||
@@ -126,10 +128,17 @@ export const InputLabel = (props: InputLabelProps) => {
|
|||||||
aria-invalid={props.error}
|
aria-invalid={props.error}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
|
</Typography>
|
||||||
{props.required && (
|
{props.required && (
|
||||||
<span class="inline-flex px-1 align-bottom leading-[0.5] fg-def-3">
|
<Typography
|
||||||
{"*"}
|
class="inline-flex px-1 align-text-top leading-[0.5] fg-def-4"
|
||||||
</span>
|
useExternColor={true}
|
||||||
|
hierarchy="label"
|
||||||
|
weight="bold"
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
{"∗"}
|
||||||
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{props.help && (
|
{props.help && (
|
||||||
<span
|
<span
|
||||||
@@ -144,11 +153,17 @@ export const InputLabel = (props: InputLabelProps) => {
|
|||||||
<Icon class="inline fg-def-3" icon={"Info"} width={"0.8125rem"} />
|
<Icon class="inline fg-def-3" icon={"Info"} width={"0.8125rem"} />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Typography>
|
</span>
|
||||||
{props.labelAction}
|
<Typography
|
||||||
<Typography hierarchy="body" size="xs" weight="normal" color="secondary">
|
hierarchy="body"
|
||||||
|
size="xs"
|
||||||
|
weight="normal"
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
{props.description}
|
{props.description}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</span>
|
||||||
|
{props.labelAction}
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ interface ModalProps {
|
|||||||
handleClose: () => void;
|
handleClose: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
children: JSX.Element;
|
children: JSX.Element;
|
||||||
|
class?: string;
|
||||||
}
|
}
|
||||||
export const Modal = (props: ModalProps) => {
|
export const Modal = (props: ModalProps) => {
|
||||||
const [dragging, setDragging] = createSignal(false);
|
const [dragging, setDragging] = createSignal(false);
|
||||||
@@ -46,7 +47,10 @@ export const Modal = (props: ModalProps) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog.Content
|
<Dialog.Content
|
||||||
class="absolute left-1/3 top-1/3 z-50 min-w-[320px] rounded-md border border-def-4 focus-visible:outline-none"
|
class={cx(
|
||||||
|
"overflow-hidden absolute left-1/3 top-1/3 z-50 min-w-[560px] rounded-md border border-def-4 focus-visible:outline-none",
|
||||||
|
props.class,
|
||||||
|
)}
|
||||||
classList={{
|
classList={{
|
||||||
"!cursor-grabbing": dragging(),
|
"!cursor-grabbing": dragging(),
|
||||||
[cx("scale-105 transition-transform")]: dragging(),
|
[cx("scale-105 transition-transform")]: dragging(),
|
||||||
@@ -63,7 +67,7 @@ export const Modal = (props: ModalProps) => {
|
|||||||
>
|
>
|
||||||
<Dialog.Label
|
<Dialog.Label
|
||||||
as="div"
|
as="div"
|
||||||
class="flex w-full justify-center rounded-t-md border-b-2 px-4 py-2 align-middle bg-def-3 border-def-5"
|
class="flex w-full justify-center border-b-2 px-4 py-2 align-middle bg-def-3 border-def-5"
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@@ -79,7 +83,9 @@ export const Modal = (props: ModalProps) => {
|
|||||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||||
<hr class="h-px w-full border-none bg-secondary-300" />
|
<hr class="h-px w-full border-none bg-secondary-300" />
|
||||||
</div>
|
</div>
|
||||||
<span class="mx-2"> {props.title}</span>
|
<span class="mx-2 select-none whitespace-nowrap">
|
||||||
|
{props.title}
|
||||||
|
</span>
|
||||||
<div
|
<div
|
||||||
class="flex w-full cursor-move flex-col gap-px py-1 "
|
class="flex w-full cursor-move flex-col gap-px py-1 "
|
||||||
classList={{
|
classList={{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { callApi, SuccessData, SuccessQuery } from "@/src/api";
|
|||||||
import { activeURI } from "@/src/App";
|
import { activeURI } from "@/src/App";
|
||||||
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, { IconVariant } from "@/src/components/icon";
|
||||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||||
import { selectSshKeys } from "@/src/hooks";
|
import { selectSshKeys } from "@/src/hooks";
|
||||||
import {
|
import {
|
||||||
@@ -13,12 +13,17 @@ import {
|
|||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { createQuery } from "@tanstack/solid-query";
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, JSX, Match, Show, Switch } from "solid-js";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { MachineAvatar } from "./avatar";
|
import { MachineAvatar } from "./avatar";
|
||||||
import { Header } from "@/src/layout/header";
|
import { Header } from "@/src/layout/header";
|
||||||
import { InputLabel } from "@/src/components/inputBase";
|
import { InputLabel } from "@/src/components/inputBase";
|
||||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||||
|
import { Modal } from "@/src/components/modal";
|
||||||
|
import { Typography } from "@/src/components/Typography";
|
||||||
|
import cx from "classnames";
|
||||||
|
import { SelectInput } from "@/src/Form/fields/Select";
|
||||||
|
import { HWStep } from "./install/hardware-step";
|
||||||
|
|
||||||
type MachineFormInterface = MachineData & {
|
type MachineFormInterface = MachineData & {
|
||||||
sshKey?: File;
|
sshKey?: File;
|
||||||
@@ -33,6 +38,65 @@ interface InstallForm extends FieldValues {
|
|||||||
disk?: string;
|
disk?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
export const Group = (props: GroupProps) => (
|
||||||
|
<div class="flex flex-col gap-8 rounded-md border px-4 py-5 bg-def-2 border-def-2">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
type AdmonitionVariant = "attention" | "danger";
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
variant: AdmonitionVariant;
|
||||||
|
headline: JSX.Element;
|
||||||
|
}
|
||||||
|
const variantColorsMap: Record<AdmonitionVariant, string> = {
|
||||||
|
attention: cx("bg-[#9BD8F2] fg-def-1"),
|
||||||
|
danger: cx("bg-semantic-2 fg-semantic-2"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantIconColorsMap: Record<AdmonitionVariant, string> = {
|
||||||
|
attention: cx("fg-def-1"),
|
||||||
|
danger: cx("fg-semantic-3"),
|
||||||
|
};
|
||||||
|
|
||||||
|
const variantIconMap: Record<AdmonitionVariant, IconVariant> = {
|
||||||
|
attention: "Attention",
|
||||||
|
danger: "Warning",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SectionHeader = (props: SectionHeaderProps) => (
|
||||||
|
<div
|
||||||
|
class={cx(
|
||||||
|
"flex items-center gap-3 rounded-md px-3 py-2",
|
||||||
|
variantColorsMap[props.variant],
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
<Icon
|
||||||
|
icon={variantIconMap[props.variant]}
|
||||||
|
class={cx("size-5", variantIconColorsMap[props.variant])}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{props.headline}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const steps = {
|
||||||
|
"1": "Hardware detection",
|
||||||
|
"2": "Disk schema",
|
||||||
|
"3": "Installation",
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
const Section = (props: SectionProps) => (
|
||||||
|
<div class="flex flex-col gap-3">{props.children}</div>
|
||||||
|
);
|
||||||
|
|
||||||
interface InstallMachineProps {
|
interface InstallMachineProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
targetHost?: string | null;
|
targetHost?: string | null;
|
||||||
@@ -90,7 +154,6 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDiskConfirm = async (e: Event) => {
|
const handleDiskConfirm = async (e: Event) => {
|
||||||
e.preventDefault();
|
|
||||||
const curr_uri = activeURI();
|
const curr_uri = activeURI();
|
||||||
const disk = getValue(formStore, "disk");
|
const disk = getValue(formStore, "disk");
|
||||||
const disk_id = props.disks.find((d) => d.name === disk)?.id_link;
|
const disk_id = props.disks.find((d) => d.name === disk)?.id_link;
|
||||||
@@ -98,9 +161,9 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const [stepsDone, setStepsDone] = createSignal<StepIdx[]>([]);
|
||||||
|
|
||||||
const generateReport = async (e: Event) => {
|
const generateReport = async (e: Event) => {
|
||||||
e.preventDefault();
|
|
||||||
const curr_uri = activeURI();
|
const curr_uri = activeURI();
|
||||||
if (!curr_uri || !props.name) {
|
if (!curr_uri || !props.name) {
|
||||||
return;
|
return;
|
||||||
@@ -113,7 +176,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
machine: props.name,
|
machine: props.name,
|
||||||
keyfile: props.sshKey?.name,
|
keyfile: props.sshKey?.name,
|
||||||
target_host: props.targetHost,
|
target_host: props.targetHost,
|
||||||
backend: "NIXOS_FACTER",
|
backend: "nixos-facter",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
toast.dismiss(loading_toast);
|
toast.dismiss(loading_toast);
|
||||||
@@ -126,97 +189,205 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
toast.success("Report generated successfully");
|
toast.success("Report generated successfully");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form onSubmit={handleInstall}>
|
|
||||||
<h3 class="text-lg font-bold">
|
|
||||||
<span class="font-normal">Install: </span>
|
|
||||||
{props.name}
|
|
||||||
</h3>
|
|
||||||
<p class="py-4">
|
|
||||||
Install the system for the first time. This will erase the disk and
|
|
||||||
bootstrap a new device.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-col">
|
type StepIdx = keyof typeof steps;
|
||||||
<div class="text-lg font-semibold">Hardware detection</div>
|
const [step, setStep] = createSignal<StepIdx>("1");
|
||||||
<div class="flex justify-between py-4">
|
|
||||||
<div class="">
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
size="s"
|
|
||||||
class="w-full"
|
|
||||||
onclick={generateReport}
|
|
||||||
endIcon={<Icon icon="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) => "disk"}</Field>
|
const handleNext = () => {
|
||||||
<div role="alert" class="alert my-4">
|
console.log("Next");
|
||||||
<span class="material-icons">info</span>
|
setStep((c) => `${+c + 1}` as StepIdx);
|
||||||
<div>
|
};
|
||||||
<div class="font-semibold">Summary:</div>
|
const handlePrev = () => {
|
||||||
<div class="mb-2">
|
console.log("Next");
|
||||||
Install to <b>{props.targetHost}</b> using{" "}
|
setStep((c) => `${+c - 1}` as StepIdx);
|
||||||
<b>{props.sshKey?.name || "default ssh key"}</b> for
|
};
|
||||||
authentication.
|
|
||||||
</div>
|
const Footer = () => (
|
||||||
This may take ~15 minutes depending on the initial closure and the
|
<div class="flex justify-between">
|
||||||
environmental setup.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-action">
|
|
||||||
<Show
|
|
||||||
when={confirmDisk()}
|
|
||||||
fallback={
|
|
||||||
<Button
|
<Button
|
||||||
class="btn btn-primary btn-wide"
|
startIcon={<Icon icon="ArrowLeft" />}
|
||||||
onClick={handleDiskConfirm}
|
variant="light"
|
||||||
disabled={!hasDisk()}
|
type="button"
|
||||||
endIcon={<Icon icon="Flash" />}
|
onClick={handlePrev}
|
||||||
|
disabled={step() === "1"}
|
||||||
>
|
>
|
||||||
Install
|
Previous
|
||||||
</Button>
|
</Button>
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
class="w-full"
|
endIcon={<Icon icon="ArrowRight" />}
|
||||||
type="submit"
|
type="submit"
|
||||||
endIcon={<Icon icon="Flash" />}
|
// IMPORTANT: The step itself will try to submit and call the next step
|
||||||
|
// onClick={(e: Event) => handleNext()}
|
||||||
>
|
>
|
||||||
Install
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
|
||||||
<form method="dialog">
|
|
||||||
<Button
|
|
||||||
variant="light"
|
|
||||||
onClick={() => setConfirmDisk(false)}
|
|
||||||
class="btn"
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
);
|
||||||
</>
|
return (
|
||||||
|
<div>
|
||||||
|
<div class="select-none px-6 py-2">
|
||||||
|
<Typography hierarchy="label" size="default">
|
||||||
|
Install:{" "}
|
||||||
|
</Typography>
|
||||||
|
<Typography hierarchy="label" size="default" weight="bold">
|
||||||
|
{props.name}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{/* Stepper container */}
|
||||||
|
<div class="flex items-center justify-evenly gap-2 border py-3 bg-def-3 border-def-2">
|
||||||
|
{/* A Step with a circle a number inside. Label is below */}
|
||||||
|
<For each={Object.entries(steps)}>
|
||||||
|
{([idx, label]) => (
|
||||||
|
<div class="flex flex-col items-center gap-3 fg-def-1">
|
||||||
|
<Typography
|
||||||
|
classList={{
|
||||||
|
[cx("bg-inv-4 fg-inv-1")]: idx == step(),
|
||||||
|
[cx("bg-def-4 fg-def-1")]: idx < step(),
|
||||||
|
}}
|
||||||
|
useExternColor={true}
|
||||||
|
hierarchy="label"
|
||||||
|
size="default"
|
||||||
|
weight="bold"
|
||||||
|
class="flex size-6 items-center justify-center rounded-full text-center align-middle bg-def-1"
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
when={idx >= step()}
|
||||||
|
fallback={<Icon icon="Checkmark" class="size-5" />}
|
||||||
|
>
|
||||||
|
{idx}
|
||||||
|
</Show>
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
useExternColor={true}
|
||||||
|
hierarchy="label"
|
||||||
|
size="xs"
|
||||||
|
weight="medium"
|
||||||
|
class="text-center align-top fg-def-3"
|
||||||
|
classList={{
|
||||||
|
[cx("!fg-def-1")]: idx == step(),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-6 p-6">
|
||||||
|
<Switch fallback={"Undefined content. This Step seems to not exist."}>
|
||||||
|
<Match when={step() === "1"}>
|
||||||
|
<HWStep
|
||||||
|
initial={{
|
||||||
|
target: props.targetHost || "",
|
||||||
|
}}
|
||||||
|
// TODO: Context wrapper that redirects
|
||||||
|
// @ts-expect-error: This cannot be undefined in this context.
|
||||||
|
machine_id={props.name}
|
||||||
|
// @ts-expect-error: This cannot be undefined in this context.
|
||||||
|
dir={activeURI()}
|
||||||
|
handleNext={() => handleNext()}
|
||||||
|
footer={<Footer />}
|
||||||
|
/>
|
||||||
|
</Match>
|
||||||
|
<Match when={step() === "2"}>
|
||||||
|
<span class="flex flex-col gap-4">
|
||||||
|
<Typography hierarchy="body" size="default" weight="bold">
|
||||||
|
Single Disk
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="xs"
|
||||||
|
weight="medium"
|
||||||
|
class="underline"
|
||||||
|
>
|
||||||
|
Change schema
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
<Group>
|
||||||
|
<SelectInput required label="Main Disk" options={[]} value={[]} />
|
||||||
|
</Group>
|
||||||
|
<Footer />
|
||||||
|
</Match>
|
||||||
|
<Match when={step() === "3"}>
|
||||||
|
<Section>
|
||||||
|
<Typography
|
||||||
|
hierarchy="label"
|
||||||
|
size="xs"
|
||||||
|
weight="medium"
|
||||||
|
class="uppercase"
|
||||||
|
>
|
||||||
|
Hardware Report
|
||||||
|
</Typography>
|
||||||
|
<Group>
|
||||||
|
<FieldLayout
|
||||||
|
label={<InputLabel>Target</InputLabel>}
|
||||||
|
field={
|
||||||
|
<Typography hierarchy="body" size="xs" weight="bold">
|
||||||
|
192.157.124.81
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
></FieldLayout>
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
<Section>
|
||||||
|
<Typography
|
||||||
|
hierarchy="label"
|
||||||
|
size="xs"
|
||||||
|
weight="medium"
|
||||||
|
class="uppercase"
|
||||||
|
>
|
||||||
|
Disk Configuration
|
||||||
|
</Typography>
|
||||||
|
<Group>
|
||||||
|
<FieldLayout
|
||||||
|
label={<InputLabel>Disk Layout</InputLabel>}
|
||||||
|
field={
|
||||||
|
<Typography hierarchy="body" size="xs" weight="bold">
|
||||||
|
Single Disk
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
></FieldLayout>
|
||||||
|
<hr class="h-px w-full border-none bg-acc-3"></hr>
|
||||||
|
<FieldLayout
|
||||||
|
label={<InputLabel>Main Disk</InputLabel>}
|
||||||
|
field={
|
||||||
|
<Typography hierarchy="body" size="xs" weight="bold">
|
||||||
|
Samsung evo 850 efkjhasd
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
></FieldLayout>
|
||||||
|
</Group>
|
||||||
|
</Section>
|
||||||
|
<SectionHeader
|
||||||
|
variant="danger"
|
||||||
|
headline={
|
||||||
|
<span>
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="s"
|
||||||
|
weight="bold"
|
||||||
|
useExternColor
|
||||||
|
>
|
||||||
|
Setup your device.
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
hierarchy="body"
|
||||||
|
size="s"
|
||||||
|
weight="medium"
|
||||||
|
useExternColor
|
||||||
|
>
|
||||||
|
This will erase the disk and bootstrap fresh.
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Footer></Footer>
|
||||||
|
<Button startIcon={<Icon icon="Flash" />}>Install</Button>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -235,6 +406,8 @@ 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 [installModalOpen, setInstallModalOpen] = createSignal(false);
|
||||||
|
|
||||||
const handleSubmit = async (values: MachineFormInterface) => {
|
const handleSubmit = async (values: MachineFormInterface) => {
|
||||||
console.log("submitting", values);
|
console.log("submitting", values);
|
||||||
|
|
||||||
@@ -309,7 +482,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
<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>
|
||||||
<MachineAvatar name={machineName()} />
|
<MachineAvatar name={machineName()} />
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form onSubmit={handleSubmit} class="flex flex-col gap-6">
|
||||||
<Field name="machine.name">
|
<Field name="machine.name">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -452,10 +625,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
class="w-full"
|
class="w-full"
|
||||||
// disabled={!online()}
|
// disabled={!online()}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const modal = document.getElementById(
|
setInstallModalOpen(true);
|
||||||
"install_modal",
|
|
||||||
) as HTMLDialogElement | null;
|
|
||||||
modal?.showModal();
|
|
||||||
}}
|
}}
|
||||||
endIcon={<Icon icon="Flash" />}
|
endIcon={<Icon icon="Flash" />}
|
||||||
>
|
>
|
||||||
@@ -463,16 +633,19 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<dialog id="install_modal" class="modal backdrop:bg-transparent">
|
<Modal
|
||||||
<div class="modal-box w-11/12 max-w-5xl">
|
title={`Install machine`}
|
||||||
|
open={installModalOpen()}
|
||||||
|
handleClose={() => setInstallModalOpen(false)}
|
||||||
|
class="min-w-[600px]"
|
||||||
|
>
|
||||||
<InstallMachine
|
<InstallMachine
|
||||||
name={machineName()}
|
name={machineName()}
|
||||||
sshKey={sshKey()}
|
sshKey={sshKey()}
|
||||||
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
||||||
disks={[]}
|
disks={[]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Modal>
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<span class="max-w-md text-neutral">
|
<span class="max-w-md text-neutral">
|
||||||
Update the system if changes should be synced after the installation
|
Update the system if changes should be synced after the installation
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import { callApi } from "@/src/api";
|
||||||
|
import { activeURI } from "@/src/App";
|
||||||
|
import { Button } from "@/src/components/button";
|
||||||
|
import Icon from "@/src/components/icon";
|
||||||
|
import { InputError, InputLabel } from "@/src/components/inputBase";
|
||||||
|
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||||
|
import {
|
||||||
|
createForm,
|
||||||
|
SubmitHandler,
|
||||||
|
FieldValues,
|
||||||
|
validate,
|
||||||
|
required,
|
||||||
|
getValue,
|
||||||
|
submit,
|
||||||
|
setValue,
|
||||||
|
} from "@modular-forms/solid";
|
||||||
|
import { createEffect, createSignal, JSX, Match, Switch } from "solid-js";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
import { Group } from "../details";
|
||||||
|
import { TextInput } from "@/src/Form/fields";
|
||||||
|
import { createQuery } from "@tanstack/solid-query";
|
||||||
|
|
||||||
|
interface Hardware extends FieldValues {
|
||||||
|
report: boolean;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StepProps {
|
||||||
|
machine_id: string;
|
||||||
|
dir: string;
|
||||||
|
handleNext: () => void;
|
||||||
|
footer: JSX.Element;
|
||||||
|
initial?: Partial<Hardware>;
|
||||||
|
}
|
||||||
|
export const HWStep = (props: StepProps) => {
|
||||||
|
const [formStore, { Form, Field }] = createForm<Hardware>({
|
||||||
|
initialValues: props.initial || {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit: SubmitHandler<Hardware> = async (values, event) => {
|
||||||
|
console.log("Submit Hardware", { values });
|
||||||
|
const valid = await validate(formStore);
|
||||||
|
console.log("Valid", valid);
|
||||||
|
if (!valid) return;
|
||||||
|
props.handleNext();
|
||||||
|
};
|
||||||
|
|
||||||
|
const [isGenerating, setIsGenerating] = createSignal(false);
|
||||||
|
|
||||||
|
const hwReportQuery = createQuery(() => ({
|
||||||
|
queryKey: [props.dir, props.machine_id, "hw_report"],
|
||||||
|
queryFn: async () => {
|
||||||
|
const result = await callApi("show_machine_hardware_config", {
|
||||||
|
clan_dir: props.dir,
|
||||||
|
machine_name: props.machine_id,
|
||||||
|
});
|
||||||
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
|
return result.data;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
// Workaround to set the form state
|
||||||
|
createEffect(() => {
|
||||||
|
const report = hwReportQuery.data;
|
||||||
|
if (report === "nixos-facter" || report === "nixos-generate-config") {
|
||||||
|
setValue(formStore, "report", true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const generateReport = async (e: Event) => {
|
||||||
|
const curr_uri = activeURI();
|
||||||
|
if (!curr_uri) return;
|
||||||
|
|
||||||
|
const loading_toast = toast.loading("Generating hardware report...");
|
||||||
|
|
||||||
|
await validate(formStore, "target");
|
||||||
|
const target = getValue(formStore, "target");
|
||||||
|
|
||||||
|
if (!target) {
|
||||||
|
toast.error("Target ip must be provided");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsGenerating(true);
|
||||||
|
const r = await callApi("generate_machine_hardware_info", {
|
||||||
|
opts: {
|
||||||
|
flake: { loc: curr_uri },
|
||||||
|
machine: props.machine_id,
|
||||||
|
target_host: target,
|
||||||
|
backend: "nixos-facter",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setIsGenerating(false);
|
||||||
|
toast.dismiss(loading_toast);
|
||||||
|
// TODO: refresh the machine details
|
||||||
|
|
||||||
|
if (r.status === "error") {
|
||||||
|
toast.error(`Failed to generate report. ${r.errors[0].message}`);
|
||||||
|
}
|
||||||
|
if (r.status === "success") {
|
||||||
|
toast.success("Report generated successfully");
|
||||||
|
}
|
||||||
|
hwReportQuery.refetch();
|
||||||
|
submit(formStore);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} class="flex flex-col gap-6">
|
||||||
|
<Group>
|
||||||
|
<Field name="target" validate={required("Target must be provided")}>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<TextInput
|
||||||
|
error={field.error}
|
||||||
|
variant="ghost"
|
||||||
|
label="Target ip"
|
||||||
|
value={field.value || ""}
|
||||||
|
inputProps={fieldProps}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
<Field
|
||||||
|
name="report"
|
||||||
|
type="boolean"
|
||||||
|
validate={required("Report must be generated")}
|
||||||
|
>
|
||||||
|
{(field, fieldProps) => (
|
||||||
|
<FieldLayout
|
||||||
|
error={field.error && <InputError error={field.error} />}
|
||||||
|
label={
|
||||||
|
<InputLabel
|
||||||
|
required
|
||||||
|
help="Detect hardware specific drivers from target ip"
|
||||||
|
>
|
||||||
|
Hardware report
|
||||||
|
</InputLabel>
|
||||||
|
}
|
||||||
|
field={
|
||||||
|
<Switch>
|
||||||
|
<Match when={hwReportQuery.isLoading}>
|
||||||
|
<div>Loading...</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={hwReportQuery.error}>
|
||||||
|
<div>Error...</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={hwReportQuery.data}>
|
||||||
|
{(data) => (
|
||||||
|
<>
|
||||||
|
<Switch>
|
||||||
|
<Match when={data() === "none"}>
|
||||||
|
<Button
|
||||||
|
disabled={isGenerating()}
|
||||||
|
startIcon={<Icon icon="Report" />}
|
||||||
|
class="w-full"
|
||||||
|
onClick={generateReport}
|
||||||
|
>
|
||||||
|
Run hardware report
|
||||||
|
</Button>
|
||||||
|
</Match>
|
||||||
|
<Match when={data() === "nixos-facter"}>
|
||||||
|
<div>Detected</div>
|
||||||
|
</Match>
|
||||||
|
<Match when={data() === "nixos-generate-config"}>
|
||||||
|
<div>Nixos report Detected</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Group>
|
||||||
|
{props.footer}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -128,6 +128,21 @@ export default plugin.withOptions(
|
|||||||
".bg-acc-4": {
|
".bg-acc-4": {
|
||||||
backgroundColor: theme("colors.secondary.300"),
|
backgroundColor: theme("colors.secondary.300"),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// bg inverse accent
|
||||||
|
".bg-semantic-1": {
|
||||||
|
backgroundColor: theme("colors.error.50"),
|
||||||
|
},
|
||||||
|
".bg-semantic-2": {
|
||||||
|
backgroundColor: theme("colors.error.100"),
|
||||||
|
},
|
||||||
|
".bg-semantic-3": {
|
||||||
|
backgroundColor: theme("colors.error.200"),
|
||||||
|
},
|
||||||
|
".bg-semantic-4": {
|
||||||
|
backgroundColor: theme("colors.error.300"),
|
||||||
|
},
|
||||||
|
|
||||||
// bg inverse accent
|
// bg inverse accent
|
||||||
".bg-inv-acc-1": {
|
".bg-inv-acc-1": {
|
||||||
backgroundColor: theme("colors.secondary.500"),
|
backgroundColor: theme("colors.secondary.500"),
|
||||||
|
|||||||
Reference in New Issue
Block a user