Merge pull request 'API: Display only local block devices, remote ones should be retrieved from hw-report' (#2642) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
site_name: Clan Documentation
|
||||
site_url: https://docs.clan.lol
|
||||
repo_url: https://git.clan.lol/clan/clan-core/
|
||||
repo_name: clan-core
|
||||
repo_name: "_>"
|
||||
edit_uri: _edit/main/docs/docs/
|
||||
|
||||
validation:
|
||||
@@ -148,11 +148,13 @@ theme:
|
||||
features:
|
||||
- navigation.instant
|
||||
- navigation.tabs
|
||||
- navigation.tabs.sticky
|
||||
- navigation.footer
|
||||
- content.code.annotate
|
||||
- content.code.copy
|
||||
- content.tabs.link
|
||||
icon:
|
||||
repo: fontawesome/brands/git-alt
|
||||
repo: fontawesome/brands/git
|
||||
custom_dir: overrides
|
||||
|
||||
palette:
|
||||
@@ -181,7 +183,7 @@ extra:
|
||||
social:
|
||||
- icon: fontawesome/regular/comment
|
||||
link: https://matrix.to/#/#clan:clan.lol
|
||||
- icon: fontawesome/brands/gitlab
|
||||
- icon: fontawesome/brands/git
|
||||
link: https://git.clan.lol/clan/clan-core
|
||||
- icon: fontawesome/brands/github
|
||||
link: https://github.com/clan-lol/clan-core
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
# Authoring a clanModule
|
||||
|
||||
!!! Danger ":fontawesome-solid-road-barrier: Under Construction :fontawesome-solid-road-barrier:"
|
||||
Currently under construction use with caution
|
||||
|
||||
:fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier:
|
||||
|
||||
This site will guide you through authoring your first module. Explaining which conventions must be followed, such that others will have an enjoyable experience and the module can be used with minimal effort.
|
||||
|
||||
:fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier:
|
||||
Under construction
|
||||
:fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier:
|
||||
|
||||
!!! Tip
|
||||
External ClanModules can be ad-hoc loaded via [`clan.inventory.modules`](../reference/nix-api/inventory.md#modules)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
# Disk Templates
|
||||
|
||||
|
||||
!!! Danger "!!! Under construction !!!"
|
||||
!!! Danger ":fontawesome-solid-road-barrier: Under Construction :fontawesome-solid-road-barrier:"
|
||||
Currently under construction use with caution
|
||||
|
||||
:fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier: :fontawesome-solid-road-barrier:
|
||||
|
||||
|
||||
## Structure
|
||||
|
||||
|
||||
@@ -118,37 +118,17 @@ def blk_from_dict(data: dict) -> BlkInfo:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BlockDeviceOptions:
|
||||
hostname: str | None = None
|
||||
keyfile: str | None = None
|
||||
|
||||
|
||||
@API.register
|
||||
def show_block_devices(options: BlockDeviceOptions) -> Blockdevices:
|
||||
def show_block_devices() -> Blockdevices:
|
||||
"""
|
||||
Abstract api method to show block devices.
|
||||
Api method to show local block devices.
|
||||
|
||||
It must return a list of block devices.
|
||||
"""
|
||||
keyfile = options.keyfile
|
||||
remote = (
|
||||
[
|
||||
"ssh",
|
||||
*(["-i", f"{keyfile}"] if keyfile else []),
|
||||
# Disable strict host key checking
|
||||
"-o StrictHostKeyChecking=accept-new",
|
||||
# Disable known hosts file
|
||||
"-o UserKnownHostsFile=/dev/null",
|
||||
f"{options.hostname}",
|
||||
]
|
||||
if options.hostname
|
||||
else []
|
||||
)
|
||||
|
||||
cmd = nix_shell(
|
||||
["nixpkgs#util-linux", *(["nixpkgs#openssh"] if options.hostname else [])],
|
||||
["nixpkgs#util-linux"],
|
||||
[
|
||||
*remote,
|
||||
"lsblk",
|
||||
"--json",
|
||||
"--output",
|
||||
|
||||
@@ -7,14 +7,22 @@ import {
|
||||
createMemo,
|
||||
} from "solid-js";
|
||||
import { Portal } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
import { Label } from "../base/label";
|
||||
import { useFloating } from "../base";
|
||||
import { autoUpdate, flip, hide, shift, size } from "@floating-ui/dom";
|
||||
import { autoUpdate, flip, hide, offset, shift, size } from "@floating-ui/dom";
|
||||
import { Button } from "@/src/components/button";
|
||||
import {
|
||||
InputBase,
|
||||
InputError,
|
||||
InputLabel,
|
||||
InputLabelProps,
|
||||
} from "@/src/components/inputBase";
|
||||
import { FieldLayout } from "./layout";
|
||||
import Icon from "@/src/components/icon";
|
||||
|
||||
export interface Option {
|
||||
value: string;
|
||||
label: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface SelectInputpProps {
|
||||
@@ -22,7 +30,7 @@ interface SelectInputpProps {
|
||||
selectProps: JSX.InputHTMLAttributes<HTMLSelectElement>;
|
||||
options: Option[];
|
||||
label: JSX.Element;
|
||||
altLabel?: JSX.Element;
|
||||
labelProps?: InputLabelProps;
|
||||
helperText?: JSX.Element;
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
@@ -36,6 +44,7 @@ interface SelectInputpProps {
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
multiple?: boolean;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function SelectInput(props: SelectInputpProps) {
|
||||
@@ -61,6 +70,7 @@ export function SelectInput(props: SelectInputpProps) {
|
||||
});
|
||||
},
|
||||
}),
|
||||
offset({ mainAxis: 2 }),
|
||||
shift(),
|
||||
flip(),
|
||||
hide({
|
||||
@@ -114,126 +124,144 @@ export function SelectInput(props: SelectInputpProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<label
|
||||
class={cx("form-control w-full", props.class)}
|
||||
aria-disabled={props.disabled}
|
||||
>
|
||||
<div class="label">
|
||||
<Label label={props.label} required={props.required} />
|
||||
<span class="label-text-alt block">{props.altLabel}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="select select-bordered flex items-center gap-2"
|
||||
ref={setReference}
|
||||
formnovalidate
|
||||
onClick={() => {
|
||||
const popover = document.getElementById(_id);
|
||||
if (popover) {
|
||||
popover.togglePopover(); // Show or hide the popover
|
||||
}
|
||||
}}
|
||||
// TODO: Use native popover once Webkti supports it within <form>
|
||||
// popovertarget={_id}
|
||||
// popovertargetaction="toggle"
|
||||
>
|
||||
<Show when={props.adornment && props.adornment.position === "start"}>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
{props.inlineLabel}
|
||||
<div class="flex cursor-default flex-row gap-2">
|
||||
<For each={getValues()} fallback={"Select"}>
|
||||
{(item) => (
|
||||
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
|
||||
{item}
|
||||
<Show when={props.multiple}>
|
||||
<button
|
||||
class="btn-ghost btn-xs"
|
||||
type="button"
|
||||
onClick={(_e) => {
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
props.selectProps.onInput({
|
||||
currentTarget: {
|
||||
options: getValues()
|
||||
.filter((o) => o !== item)
|
||||
.map((value) => ({
|
||||
value,
|
||||
selected: true,
|
||||
disabled: false,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<select
|
||||
class="hidden"
|
||||
multiple
|
||||
{...props.selectProps}
|
||||
<FieldLayout
|
||||
error={props.error && <InputError error={props.error} />}
|
||||
label={
|
||||
<InputLabel
|
||||
description={""}
|
||||
required={props.required}
|
||||
{...props.labelProps}
|
||||
>
|
||||
<For each={props.options}>
|
||||
{({ label, value }) => (
|
||||
<option value={value} selected={getValues().includes(value)}>
|
||||
{label}
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</select>
|
||||
<Show when={props.adornment && props.adornment.position === "end"}>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
</button>
|
||||
<Portal mount={document.body}>
|
||||
<div
|
||||
id={_id}
|
||||
popover
|
||||
ref={setFloating}
|
||||
style={{
|
||||
margin: 0,
|
||||
position: position.strategy,
|
||||
top: `${position.y ?? 0}px`,
|
||||
left: `${position.x ?? 0}px`,
|
||||
}}
|
||||
class="dropdown-content z-[1] rounded-b-box bg-base-100 shadow"
|
||||
>
|
||||
<ul class="menu flex max-h-96 flex-col gap-1 overflow-x-hidden overflow-y-scroll">
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
}
|
||||
field={
|
||||
<>
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
class="!justify-start"
|
||||
divRef={setReference}
|
||||
inputElem={
|
||||
<button
|
||||
// TODO: Keyboard acessibililty
|
||||
// Currently the popover only opens with onClick
|
||||
// Options are not selectable with keyboard
|
||||
tabIndex={-1}
|
||||
onClick={() => {
|
||||
const popover = document.getElementById(_id);
|
||||
if (popover) {
|
||||
popover.togglePopover(); // Show or hide the popover
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2"
|
||||
formnovalidate
|
||||
// TODO: Use native popover once Webkit supports it within <form>
|
||||
// popovertarget={_id}
|
||||
// popovertargetaction="toggle"
|
||||
>
|
||||
<Show
|
||||
when={
|
||||
props.adornment && props.adornment.position === "start"
|
||||
}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
{props.inlineLabel}
|
||||
<div class="flex cursor-default flex-row gap-2">
|
||||
<Show
|
||||
when={
|
||||
getValues() &&
|
||||
getValues.length !== 1 &&
|
||||
getValues()[0] !== ""
|
||||
}
|
||||
fallback={props.placeholder}
|
||||
>
|
||||
<For each={getValues()} fallback={"Select"}>
|
||||
{(item) => (
|
||||
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
|
||||
{item}
|
||||
<Show when={props.multiple}>
|
||||
<button
|
||||
class="btn-ghost btn-xs"
|
||||
type="button"
|
||||
onClick={(_e) => {
|
||||
// @ts-expect-error: fieldName is not known ahead of time
|
||||
props.selectProps.onInput({
|
||||
currentTarget: {
|
||||
options: getValues()
|
||||
.filter((o) => o !== item)
|
||||
.map((value) => ({
|
||||
value,
|
||||
selected: true,
|
||||
disabled: false,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
<Show
|
||||
when={props.adornment && props.adornment.position === "end"}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
<Icon icon="CaretDown" class="ml-auto mr-2"></Icon>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<Portal mount={document.body}>
|
||||
<div
|
||||
id={_id}
|
||||
popover
|
||||
ref={setFloating}
|
||||
style={{
|
||||
margin: 0,
|
||||
position: position.strategy,
|
||||
top: `${position.y ?? 0}px`,
|
||||
left: `${position.x ?? 0}px`,
|
||||
}}
|
||||
class="z-[1] shadow"
|
||||
>
|
||||
<ul class="menu flex max-h-96 flex-col gap-1 overflow-x-hidden overflow-y-scroll">
|
||||
<Show when={!props.loading} fallback={"Loading ...."}>
|
||||
<For each={props.options}>
|
||||
{(opt) => (
|
||||
<>
|
||||
<li>
|
||||
<button
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="!justify-start"
|
||||
onClick={() => handleClickOption(opt)}
|
||||
disabled={opt.disabled}
|
||||
classList={{
|
||||
active: getValues().includes(opt.value),
|
||||
active:
|
||||
!opt.disabled && getValues().includes(opt.value),
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
</Button>
|
||||
</li>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</Portal>
|
||||
<div class="label">
|
||||
{props.helperText && (
|
||||
<span class="label-text text-neutral">{props.helperText}</span>
|
||||
)}
|
||||
{props.error && (
|
||||
<span class="label-text-alt font-bold text-error-700">
|
||||
{props.error}
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
</ul>
|
||||
</div>
|
||||
</label>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,62 +1,51 @@
|
||||
import { createEffect, Show, type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Label } from "../base/label";
|
||||
import { InputBase, InputLabel } from "@/src/components/inputBase";
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import { InputBase, InputError, InputLabel } from "@/src/components/inputBase";
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import { FieldLayout } from "./layout";
|
||||
|
||||
interface TextInputProps {
|
||||
value: string;
|
||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||
label: JSX.Element;
|
||||
altLabel?: JSX.Element;
|
||||
helperText?: JSX.Element;
|
||||
// Common
|
||||
error?: string;
|
||||
required?: boolean;
|
||||
type?: string;
|
||||
inlineLabel?: JSX.Element;
|
||||
class?: string;
|
||||
adornment?: {
|
||||
position: "start" | "end";
|
||||
content: JSX.Element;
|
||||
};
|
||||
disabled?: boolean;
|
||||
// Passed to input
|
||||
value: string;
|
||||
inputProps?: JSX.InputHTMLAttributes<HTMLInputElement>;
|
||||
placeholder?: string;
|
||||
// Passed to label
|
||||
label: JSX.Element;
|
||||
help?: string;
|
||||
// Passed to layouad
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export function TextInput(props: TextInputProps) {
|
||||
// createEffect(() => {
|
||||
// console.log("TextInput", props.error, props.value);
|
||||
// });
|
||||
const [layoutProps, rest] = splitProps(props, ["class"]);
|
||||
return (
|
||||
<div
|
||||
class="grid grid-cols-12"
|
||||
classList={{
|
||||
"mb-[14.5px]": !props.error,
|
||||
}}
|
||||
>
|
||||
<InputLabel
|
||||
class="col-span-2"
|
||||
required={props.required}
|
||||
error={!!props.error}
|
||||
>
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
class="col-span-10"
|
||||
{...props.inputProps}
|
||||
value={props.value}
|
||||
/>
|
||||
{props.error && (
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
size="xxs"
|
||||
weight="medium"
|
||||
class="col-span-full px-1 !fg-semantic-4"
|
||||
<FieldLayout
|
||||
label={
|
||||
<InputLabel
|
||||
class="col-span-2"
|
||||
required={props.required}
|
||||
error={!!props.error}
|
||||
help={props.help}
|
||||
>
|
||||
{props.error}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
{props.label}
|
||||
</InputLabel>
|
||||
}
|
||||
field={
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
placeholder={props.placeholder}
|
||||
class="col-span-10"
|
||||
{...props.inputProps}
|
||||
value={props.value}
|
||||
/>
|
||||
}
|
||||
error={props.error && <InputError error={props.error} />}
|
||||
{...layoutProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
29
pkgs/webview-ui/app/src/Form/fields/layout.tsx
Normal file
29
pkgs/webview-ui/app/src/Form/fields/layout.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { JSX, splitProps } from "solid-js";
|
||||
import cx from "classnames";
|
||||
|
||||
interface LayoutProps extends JSX.HTMLAttributes<HTMLDivElement> {
|
||||
field?: JSX.Element;
|
||||
label?: JSX.Element;
|
||||
error?: JSX.Element;
|
||||
}
|
||||
export const FieldLayout = (props: LayoutProps) => {
|
||||
const [intern, divProps] = splitProps(props, [
|
||||
"field",
|
||||
"label",
|
||||
"error",
|
||||
"class",
|
||||
]);
|
||||
return (
|
||||
<div
|
||||
class={cx("grid grid-cols-12 items-center", intern.class)}
|
||||
classList={{
|
||||
"mb-[14.5px]": !props.error,
|
||||
}}
|
||||
{...divProps}
|
||||
>
|
||||
<label class="col-span-2">{props.label}</label>
|
||||
<div class="col-span-10">{props.field}</div>
|
||||
{props.error && <span class="col-span-full">{props.error}</span>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -248,7 +248,7 @@ export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
{(options) => (
|
||||
<SelectInput
|
||||
error={field.error}
|
||||
altLabel={props.schema.title}
|
||||
// altLabel={props.schema.title}
|
||||
label={props.path.join(".")}
|
||||
helperText={props.schema.description}
|
||||
value={field.value || []}
|
||||
@@ -276,7 +276,7 @@ export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
<TextInput
|
||||
inputProps={{ ...fieldProps, readonly }}
|
||||
value={field.value as unknown as string}
|
||||
type="password"
|
||||
// type="password"
|
||||
error={field.error}
|
||||
{...commonProps}
|
||||
// required
|
||||
@@ -525,7 +525,7 @@ export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
<SelectInput
|
||||
multiple
|
||||
error={field.error}
|
||||
altLabel={props.schema.title}
|
||||
// altLabel={props.schema.title}
|
||||
label={listFieldName}
|
||||
helperText={props.schema.description}
|
||||
value={field.value || ""}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Dynamic } from "solid-js/web";
|
||||
import cx from "classnames";
|
||||
import "./css/typography.css";
|
||||
|
||||
type Hierarchy = "body" | "title" | "headline" | "label";
|
||||
export type Hierarchy = "body" | "title" | "headline" | "label";
|
||||
type Color = "primary" | "secondary" | "tertiary";
|
||||
type Weight = "normal" | "medium" | "bold";
|
||||
type Tag = "span" | "p" | "h1" | "h2" | "h3" | "h4";
|
||||
@@ -75,7 +75,7 @@ const weightMap: Record<Weight, string> = {
|
||||
bold: cx("fnt-weight-bold"),
|
||||
};
|
||||
|
||||
interface TypographyProps<H extends Hierarchy> {
|
||||
interface _TypographyProps<H extends Hierarchy> {
|
||||
hierarchy: H;
|
||||
size: AllowedSizes<H>;
|
||||
children: JSX.Element;
|
||||
@@ -86,7 +86,8 @@ interface TypographyProps<H extends Hierarchy> {
|
||||
class?: string;
|
||||
classList?: Record<string, boolean>;
|
||||
}
|
||||
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
||||
|
||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||
return (
|
||||
<Dynamic
|
||||
component={props.tag || "span"}
|
||||
@@ -103,3 +104,5 @@ export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
||||
</Dynamic>
|
||||
);
|
||||
};
|
||||
|
||||
export type TypographyProps = _TypographyProps<Hierarchy>;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import cx from "classnames";
|
||||
import { createEffect, JSX, splitProps } from "solid-js";
|
||||
import { JSX, Ref, Show, splitProps } from "solid-js";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { Typography } from "../Typography";
|
||||
import { Typography, TypographyProps } from "../Typography";
|
||||
|
||||
type Variants = "outlined" | "ghost";
|
||||
interface InputBaseProps {
|
||||
@@ -17,6 +17,9 @@ interface InputBaseProps {
|
||||
readonly?: boolean;
|
||||
error?: boolean;
|
||||
icon?: IconVariant;
|
||||
/** Overrides the input element */
|
||||
inputElem?: JSX.Element;
|
||||
divRef?: Ref<HTMLDivElement>;
|
||||
}
|
||||
|
||||
const variantBorder: Record<Variants, string> = {
|
||||
@@ -27,11 +30,7 @@ const variantBorder: Record<Variants, string> = {
|
||||
const fgStateClasses = cx("aria-disabled:fg-def-4 aria-readonly:fg-def-3");
|
||||
|
||||
export const InputBase = (props: InputBaseProps) => {
|
||||
const [, inputProps] = splitProps(props, ["class"]);
|
||||
|
||||
createEffect(() => {
|
||||
console.log("InputBase", props.value, props.variant);
|
||||
});
|
||||
const [internal, inputProps] = splitProps(props, ["class", "divRef"]);
|
||||
return (
|
||||
<div
|
||||
class={cx(
|
||||
@@ -66,6 +65,7 @@ export const InputBase = (props: InputBaseProps) => {
|
||||
aria-readonly={props.readonly}
|
||||
tabIndex={0}
|
||||
role="textbox"
|
||||
ref={internal.divRef}
|
||||
>
|
||||
{props.icon && (
|
||||
<i
|
||||
@@ -77,32 +77,39 @@ export const InputBase = (props: InputBaseProps) => {
|
||||
<Icon icon={props.icon} font-size="inherit" color="inherit" />
|
||||
</i>
|
||||
)}
|
||||
<input
|
||||
tabIndex={-1}
|
||||
class="w-full bg-transparent outline-none aria-readonly:cursor-no-drop"
|
||||
value={props.value}
|
||||
type={props.type ? props.type : "text"}
|
||||
readOnly={props.readonly}
|
||||
placeholder={`${props.placeholder || ""}`}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
aria-invalid={props.error}
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
{...inputProps}
|
||||
/>
|
||||
<Show when={!props.inputElem} fallback={props.inputElem}>
|
||||
<input
|
||||
tabIndex={-1}
|
||||
class="w-full bg-transparent outline-none"
|
||||
value={props.value}
|
||||
type={props.type ? props.type : "text"}
|
||||
readOnly={props.readonly}
|
||||
placeholder={`${props.placeholder || ""}`}
|
||||
required={props.required}
|
||||
disabled={props.disabled}
|
||||
aria-invalid={props.error}
|
||||
aria-disabled={props.disabled}
|
||||
aria-readonly={props.readonly}
|
||||
{...inputProps}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface InputLabelProps extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
export interface InputLabelProps
|
||||
extends JSX.LabelHTMLAttributes<HTMLLabelElement> {
|
||||
description?: string;
|
||||
required?: boolean;
|
||||
error?: boolean;
|
||||
help?: string;
|
||||
labelAction?: JSX.Element;
|
||||
}
|
||||
export const InputLabel = (props: InputLabelProps) => {
|
||||
const [labelProps, forwardProps] = splitProps(props, ["class"]);
|
||||
const [labelProps, forwardProps] = splitProps(props, [
|
||||
"class",
|
||||
"labelAction",
|
||||
]);
|
||||
return (
|
||||
<label
|
||||
class={cx("flex items-center gap-1", labelProps.class)}
|
||||
@@ -112,7 +119,7 @@ export const InputLabel = (props: InputLabelProps) => {
|
||||
hierarchy="label"
|
||||
size="default"
|
||||
weight="bold"
|
||||
class="!fg-def-1"
|
||||
class="inline-flex gap-1 align-middle !fg-def-1"
|
||||
classList={{
|
||||
[cx("!fg-semantic-1")]: !!props.error,
|
||||
}}
|
||||
@@ -138,9 +145,33 @@ export const InputLabel = (props: InputLabelProps) => {
|
||||
</span>
|
||||
)}
|
||||
</Typography>
|
||||
{props.labelAction}
|
||||
<Typography hierarchy="body" size="xs" weight="normal" color="secondary">
|
||||
{props.description}
|
||||
</Typography>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
interface InputErrorProps {
|
||||
error: string;
|
||||
typographyProps?: TypographyProps;
|
||||
}
|
||||
export const InputError = (props: InputErrorProps) => {
|
||||
const [typoClasses, rest] = splitProps(
|
||||
props.typographyProps || { class: "" },
|
||||
["class"],
|
||||
);
|
||||
return (
|
||||
<Typography
|
||||
hierarchy="body"
|
||||
// @ts-expect-error: Dependent type is to complex to check how it is coupled to the override for now
|
||||
size="xxs"
|
||||
weight="medium"
|
||||
class={cx("col-span-full px-1 !fg-semantic-4", typoClasses)}
|
||||
{...rest}
|
||||
>
|
||||
{props.error}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { Component, For, Show } from "solid-js";
|
||||
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
|
||||
export const BlockDevicesView: Component = () => {
|
||||
const {
|
||||
data: devices,
|
||||
refetch: loadDevices,
|
||||
isFetching,
|
||||
} = createQuery(() => ({
|
||||
queryKey: ["block_devices"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_block_devices", { options: {} });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
|
||||
return result.data;
|
||||
},
|
||||
staleTime: 1000 * 60 * 5,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||
<Button
|
||||
onClick={() => loadDevices()}
|
||||
startIcon={<Icon icon="Update" />}
|
||||
></Button>
|
||||
</div>
|
||||
<div class="flex max-w-screen-lg flex-col gap-4">
|
||||
{isFetching ? (
|
||||
<span class="loading loading-bars"></span>
|
||||
) : (
|
||||
<Show when={devices}>
|
||||
{(devices) => (
|
||||
<For each={devices().blockdevices}>
|
||||
{(device) => (
|
||||
<div class="stats shadow">
|
||||
<div class="stat w-28 py-8">
|
||||
<div class="stat-title">Name</div>
|
||||
<div class="stat-value">
|
||||
{" "}
|
||||
<span class="material-icons">storage</span>{" "}
|
||||
{device.name}
|
||||
</div>
|
||||
<div class="stat-desc"></div>
|
||||
</div>
|
||||
|
||||
<div class="stat w-28">
|
||||
<div class="stat-title">Size</div>
|
||||
<div class="stat-value">{device.size}</div>
|
||||
<div class="stat-desc"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</Show>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -159,12 +159,12 @@ export const CreateClan = () => {
|
||||
<div class="collapse-title link font-medium ">Advanced</div>
|
||||
<div class="collapse-content">
|
||||
<TextInput
|
||||
adornment={{
|
||||
content: (
|
||||
<span class="-mr-1 text-neutral-500">clan-core #</span>
|
||||
),
|
||||
position: "start",
|
||||
}}
|
||||
// adornment={{
|
||||
// content: (
|
||||
// <span class="-mr-1 text-neutral-500">clan-core #</span>
|
||||
// ),
|
||||
// position: "start",
|
||||
// }}
|
||||
inputProps={props}
|
||||
label="Template to use"
|
||||
value={field.value ?? ""}
|
||||
|
||||
@@ -217,12 +217,12 @@ const AdminModuleForm = (props: AdminModuleFormProps) => {
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
label={"Name"}
|
||||
adornment={{
|
||||
position: "start",
|
||||
content: (
|
||||
<span class="material-icons text-gray-400">key</span>
|
||||
),
|
||||
}}
|
||||
// adornment={{
|
||||
// position: "start",
|
||||
// content: (
|
||||
// <span class="material-icons text-gray-400">key</span>
|
||||
// ),
|
||||
// }}
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-4"
|
||||
|
||||
@@ -2,10 +2,12 @@ import { callApi } from "@/src/api";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { FileInput } from "@/src/components/FileInput";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { SelectInput } from "@/src/components/SelectInput";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
|
||||
import { Typography } from "@/src/components/Typography";
|
||||
import { Header } from "@/src/layout/header";
|
||||
|
||||
import { SelectInput } from "@/src/Form/fields/Select";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
import {
|
||||
createForm,
|
||||
required,
|
||||
@@ -16,6 +18,8 @@ import {
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createEffect, createSignal, For, Show } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||
import { InputLabel } from "@/src/components/inputBase";
|
||||
|
||||
interface Wifi extends FieldValues {
|
||||
ssid: string;
|
||||
@@ -89,9 +93,7 @@ export const Flash = () => {
|
||||
const deviceQuery = createQuery(() => ({
|
||||
queryKey: ["block_devices"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_block_devices", {
|
||||
options: {},
|
||||
});
|
||||
const result = await callApi("show_block_devices", {});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
@@ -145,41 +147,43 @@ export const Flash = () => {
|
||||
|
||||
const handleSubmit = async (values: FlashFormValues) => {
|
||||
console.log("Submit:", values);
|
||||
try {
|
||||
await callApi("flash_machine", {
|
||||
machine: {
|
||||
name: values.machine.devicePath,
|
||||
flake: {
|
||||
loc: values.machine.flake,
|
||||
},
|
||||
},
|
||||
mode: "format",
|
||||
disks: [{ name: "main", device: values.disk }],
|
||||
system_config: {
|
||||
language: values.language,
|
||||
keymap: values.keymap,
|
||||
ssh_keys_path: values.sshKeys.map((file) => file.name),
|
||||
},
|
||||
dry_run: false,
|
||||
write_efi_boot_entries: false,
|
||||
debug: false,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(`Error could not flash disk: ${error}`);
|
||||
console.error("Error submitting form:", error);
|
||||
}
|
||||
toast.error("Not fully implemented yet");
|
||||
// Disabled for now. To prevent accidental flashing of local disks
|
||||
// try {
|
||||
// await callApi("flash_machine", {
|
||||
// machine: {
|
||||
// name: values.machine.devicePath,
|
||||
// flake: {
|
||||
// loc: values.machine.flake,
|
||||
// },
|
||||
// },
|
||||
// mode: "format",
|
||||
// disks: [{ name: "main", device: values.disk }],
|
||||
// system_config: {
|
||||
// language: values.language,
|
||||
// keymap: values.keymap,
|
||||
// ssh_keys_path: values.sshKeys.map((file) => file.name),
|
||||
// },
|
||||
// dry_run: false,
|
||||
// write_efi_boot_entries: false,
|
||||
// debug: false,
|
||||
// });
|
||||
// } catch (error) {
|
||||
// toast.error(`Error could not flash disk: ${error}`);
|
||||
// console.error("Error submitting form:", error);
|
||||
// }
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header title="Flash installer" />
|
||||
<div class="p-4">
|
||||
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
||||
<Typography tag="p" hierarchy="body" size="default" color="primary">
|
||||
USB Utility image.
|
||||
</Typography>
|
||||
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
||||
This will make bootstrapping a new machine easier by providing secure
|
||||
remote connection to any machine when plugged in.
|
||||
Will make bootstrapping new machines easier by providing secure remote
|
||||
connection to any machine when plugged in.
|
||||
</Typography>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div class="my-4">
|
||||
@@ -220,37 +224,30 @@ export const Flash = () => {
|
||||
<Field name="disk" validate={[required("This field is required")]}>
|
||||
{(field, props) => (
|
||||
<SelectInput
|
||||
topRightLabel={
|
||||
<Button
|
||||
size="s"
|
||||
variant="light"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
deviceQuery.refetch();
|
||||
}}
|
||||
startIcon={<Icon icon="Update" />}
|
||||
></Button>
|
||||
}
|
||||
formStore={formStore}
|
||||
loading={deviceQuery.isFetching}
|
||||
selectProps={props}
|
||||
label="Flash Disk"
|
||||
value={String(field.value)}
|
||||
labelProps={{
|
||||
labelAction: (
|
||||
<Button
|
||||
class="ml-auto"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
type="button"
|
||||
startIcon={<Icon icon="Update" />}
|
||||
onClick={() => deviceQuery.refetch()}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
value={field.value || ""}
|
||||
error={field.error}
|
||||
required
|
||||
placeholder="Select a thing where the installer will be flashed to"
|
||||
options={
|
||||
<>
|
||||
<option value="" disabled>
|
||||
Select a disk where the installer will be flashed to
|
||||
</option>
|
||||
|
||||
<For each={deviceQuery.data?.blockdevices}>
|
||||
{(device) => (
|
||||
<option value={device.path}>
|
||||
{device.path} -- {device.size} bytes
|
||||
</option>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
deviceQuery.data?.blockdevices.map((d) => ({
|
||||
value: d.path,
|
||||
label: `${d.path} -- ${d.size} bytes`,
|
||||
})) || []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
@@ -258,94 +255,100 @@ export const Flash = () => {
|
||||
|
||||
{/* WiFi Networks */}
|
||||
<div class="my-4 py-2">
|
||||
<h3 class="mb-2 text-lg font-semibold">WiFi Networks</h3>
|
||||
<span class="mb-2 text-sm">Add preconfigured networks</span>
|
||||
<FieldLayout
|
||||
label={<InputLabel class="mb-4">Networks</InputLabel>}
|
||||
field={
|
||||
<>
|
||||
<Button
|
||||
type="button"
|
||||
size="s"
|
||||
variant="light"
|
||||
onClick={addWifiNetwork}
|
||||
startIcon={<Icon icon="Plus" />}
|
||||
>
|
||||
WiFi Network
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<For each={wifiNetworks()}>
|
||||
{(network, index) => (
|
||||
<div class="mb-2 grid grid-cols-7 gap-2">
|
||||
<Field
|
||||
name={`wifi.${index()}.ssid`}
|
||||
validate={[required("SSID is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
label="SSID"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-3"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`wifi.${index()}.password`}
|
||||
validate={[required("Password is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<div class="relative col-span-3 w-full">
|
||||
<div class="flex w-full gap-2">
|
||||
<div class="mb-2 grid w-full grid-cols-6 gap-2 align-middle">
|
||||
<Field
|
||||
name={`wifi.${index()}.ssid`}
|
||||
validate={[required("SSID is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
type={
|
||||
passwordVisibility()[index()] ? "text" : "password"
|
||||
}
|
||||
label="Password"
|
||||
label="SSID"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
adornment={{
|
||||
position: "end",
|
||||
content: (
|
||||
<Button
|
||||
variant="light"
|
||||
type="button"
|
||||
class="flex justify-center opacity-70"
|
||||
onClick={() =>
|
||||
togglePasswordVisibility(index())
|
||||
}
|
||||
startIcon={
|
||||
passwordVisibility()[index()] ? (
|
||||
<Icon icon="EyeClose" />
|
||||
) : (
|
||||
<Icon icon="EyeOpen" />
|
||||
)
|
||||
}
|
||||
></Button>
|
||||
),
|
||||
}}
|
||||
class="col-span-3"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<div class="col-span-1 self-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
class="h-12"
|
||||
onClick={() => removeWifiNetwork(index())}
|
||||
startIcon={<Icon icon="Trash" />}
|
||||
></Button>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name={`wifi.${index()}.password`}
|
||||
validate={[required("Password is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<div class="relative col-span-3 w-full">
|
||||
<TextInput
|
||||
inputProps={{
|
||||
...props,
|
||||
type: passwordVisibility()[index()]
|
||||
? "text"
|
||||
: "password",
|
||||
}}
|
||||
label="Password"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
// adornment={{
|
||||
// position: "end",
|
||||
// content: (
|
||||
// <Button
|
||||
// variant="light"
|
||||
// type="button"
|
||||
// class="flex justify-center opacity-70"
|
||||
// onClick={() =>
|
||||
// togglePasswordVisibility(index())
|
||||
// }
|
||||
// startIcon={
|
||||
// passwordVisibility()[index()] ? (
|
||||
// <Icon icon="EyeClose" />
|
||||
// ) : (
|
||||
// <Icon icon="EyeOpen" />
|
||||
// )
|
||||
// }
|
||||
// ></Button>
|
||||
// ),
|
||||
// }}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="light"
|
||||
class="h-10"
|
||||
size="s"
|
||||
onClick={() => removeWifiNetwork(index())}
|
||||
startIcon={<Icon icon="Trash" />}
|
||||
></Button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
<div class="">
|
||||
<Button
|
||||
type="button"
|
||||
size="s"
|
||||
variant="light"
|
||||
onClick={addWifiNetwork}
|
||||
startIcon={<Icon icon="Plus" />}
|
||||
>
|
||||
Add WiFi Network
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="collapse collapse-arrow" tabindex="0">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title link font-medium ">
|
||||
Advanced Settings
|
||||
<div class="collapse-title px-0">
|
||||
<InputLabel class="mb-4">Advanced</InputLabel>
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<Field
|
||||
@@ -358,9 +361,6 @@ export const Flash = () => {
|
||||
inputProps={props}
|
||||
label="Source (flake URL)"
|
||||
value={String(field.value)}
|
||||
inlineLabel={
|
||||
<span class="material-icons">file_download</span>
|
||||
}
|
||||
error={field.error}
|
||||
required
|
||||
/>
|
||||
@@ -377,21 +377,26 @@ export const Flash = () => {
|
||||
inputProps={props}
|
||||
label="Image Name (attribute name)"
|
||||
value={String(field.value)}
|
||||
inlineLabel={<span class="material-icons">devices</span>}
|
||||
error={field.error}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<div class="my-2 py-2">
|
||||
<span class="text-sm text-neutral-600">Source URL: </span>
|
||||
<span class="text-sm text-neutral-600">
|
||||
{getValue(formStore, "machine.flake") +
|
||||
"#" +
|
||||
getValue(formStore, "machine.devicePath")}
|
||||
</span>
|
||||
</div>
|
||||
<FieldLayout
|
||||
label={
|
||||
<InputLabel help="Computed reference">Source Url</InputLabel>
|
||||
}
|
||||
field={
|
||||
<InputLabel>
|
||||
{getValue(formStore, "machine.flake") +
|
||||
"#" +
|
||||
getValue(formStore, "machine.devicePath")}
|
||||
</InputLabel>
|
||||
}
|
||||
/>
|
||||
<hr class="mb-6"></hr>
|
||||
|
||||
<Field
|
||||
name="language"
|
||||
validate={[required("This field is required")]}
|
||||
@@ -399,22 +404,22 @@ export const Flash = () => {
|
||||
{(field, props) => (
|
||||
<>
|
||||
<SelectInput
|
||||
formStore={formStore}
|
||||
selectProps={props}
|
||||
label="Language"
|
||||
value={String(field.value)}
|
||||
error={field.error}
|
||||
required
|
||||
options={
|
||||
<>
|
||||
<option value={"en_US.UTF-8"}>{"en_US.UTF-8"}</option>
|
||||
<For each={langQuery.data}>
|
||||
{(language) => (
|
||||
<option value={language}>{language}</option>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
}
|
||||
loading={langQuery.isLoading}
|
||||
options={[
|
||||
{
|
||||
label: "en_US.UTF-8",
|
||||
value: "en_US.UTF-8",
|
||||
},
|
||||
...(langQuery.data?.map((lang) => ({
|
||||
label: lang,
|
||||
value: lang,
|
||||
})) || []),
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -427,22 +432,22 @@ export const Flash = () => {
|
||||
{(field, props) => (
|
||||
<>
|
||||
<SelectInput
|
||||
formStore={formStore}
|
||||
selectProps={props}
|
||||
label="Keymap"
|
||||
value={String(field.value)}
|
||||
error={field.error}
|
||||
required
|
||||
options={
|
||||
<>
|
||||
<option value={"en"}>{"en"}</option>
|
||||
<For each={keymapQuery.data}>
|
||||
{(keymap) => (
|
||||
<option value={keymap}>{keymap}</option>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
}
|
||||
loading={keymapQuery.isLoading}
|
||||
options={[
|
||||
{
|
||||
label: "en",
|
||||
value: "en",
|
||||
},
|
||||
...(keymapQuery.data?.map((keymap) => ({
|
||||
label: keymap,
|
||||
value: keymap,
|
||||
})) || []),
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -18,6 +18,7 @@ import toast from "solid-toast";
|
||||
import { MachineAvatar } from "./avatar";
|
||||
import { Header } from "@/src/layout/header";
|
||||
import { InputLabel } from "@/src/components/inputBase";
|
||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||
|
||||
type MachineFormInterface = MachineData & {
|
||||
sshKey?: File;
|
||||
@@ -308,7 +309,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
<div class="card-body">
|
||||
<span class="text-xl text-primary-800">General</span>
|
||||
<MachineAvatar name={machineName()} />
|
||||
<Form onSubmit={handleSubmit} class="grid grid-cols-12 gap-y-4">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Field name="machine.name">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
@@ -324,16 +325,20 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
<Field name="machine.tags" type="string[]">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<InputLabel class="col-span-2">Tags</InputLabel>
|
||||
<span class="col-span-10">
|
||||
<For each={field.value}>
|
||||
{(tag) => (
|
||||
<span class="mx-2 w-fit rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
||||
{tag}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
<FieldLayout
|
||||
label={<InputLabel>Tags</InputLabel>}
|
||||
field={
|
||||
<span class="col-span-10">
|
||||
<For each={field.value}>
|
||||
{(tag) => (
|
||||
<span class="mx-2 w-fit rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
||||
{tag}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
@@ -351,19 +356,19 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
</Field>
|
||||
<Field name="hw_config">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<InputLabel class="col-span-2">
|
||||
Hardware Configuration
|
||||
</InputLabel>
|
||||
<span class="col-span-10">{field.value || "None"}</span>
|
||||
</>
|
||||
<FieldLayout
|
||||
label={<InputLabel>Hardware Configuration</InputLabel>}
|
||||
field={<span>{field.value || "None"}</span>}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="disk_schema">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<InputLabel class="col-span-2">Disk schema</InputLabel>
|
||||
<span class="col-span-10">{field.value || "None"}</span>
|
||||
<FieldLayout
|
||||
label={<InputLabel>Disk schema</InputLabel>}
|
||||
field={<span>{field.value || "None"}</span>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
@@ -608,7 +613,8 @@ function WifiModule(props: MachineWifiProps) {
|
||||
label="Password"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
type="password"
|
||||
// todo
|
||||
// type="password"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -74,7 +74,14 @@ const mkBorderUtils = (
|
||||
|
||||
export default plugin.withOptions(
|
||||
(_options = {}) =>
|
||||
({ addUtilities, theme }) => {
|
||||
({ addUtilities, theme, addVariant, e }) => {
|
||||
// @ts-expect-error: lib of tailwind has no types
|
||||
addVariant("popover-open", ({ modifySelectors, separator }) => {
|
||||
// @ts-expect-error: lib of tailwind has no types
|
||||
modifySelectors(({ className }) => {
|
||||
return `.${e(`popover-open${separator}${className}`)}:popover-open`;
|
||||
});
|
||||
});
|
||||
addUtilities({
|
||||
// Background colors
|
||||
".bg-def-1": {
|
||||
|
||||
Reference in New Issue
Block a user