Merge pull request 'feat(UI): design fixups in {machineList, machineItem, machineDetails, sidebar, sidebarHeader, button, sidebar}' (#3528) from amunsen/clan-core:ui-improvements into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3528
This commit is contained in:
5045
pkgs/webview-ui/app/package-lock.json
generated
5045
pkgs/webview-ui/app/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -142,91 +142,87 @@ export function SelectInput(props: SelectInputpProps) {
|
|||||||
</InputLabel>
|
</InputLabel>
|
||||||
}
|
}
|
||||||
field={
|
field={
|
||||||
<>
|
<InputBase
|
||||||
<InputBase
|
error={!!props.error}
|
||||||
error={!!props.error}
|
disabled={props.disabled}
|
||||||
disabled={props.disabled}
|
required={props.required}
|
||||||
required={props.required}
|
class="!justify-start"
|
||||||
class="!justify-start"
|
divRef={setReference}
|
||||||
divRef={setReference}
|
inputElem={
|
||||||
inputElem={
|
<button
|
||||||
<button
|
// TODO: Keyboard acessibililty
|
||||||
// TODO: Keyboard acessibililty
|
// Currently the popover only opens with onClick
|
||||||
// Currently the popover only opens with onClick
|
// Options are not selectable with keyboard
|
||||||
// Options are not selectable with keyboard
|
tabIndex={-1}
|
||||||
tabIndex={-1}
|
disabled={props.disabled}
|
||||||
disabled={props.disabled}
|
onClick={() => {
|
||||||
onClick={() => {
|
const popover = document.getElementById(_id);
|
||||||
const popover = document.getElementById(_id);
|
if (popover) {
|
||||||
if (popover) {
|
popover.togglePopover(); // Show or hide the popover
|
||||||
popover.togglePopover(); // Show or hide the popover
|
}
|
||||||
}
|
}}
|
||||||
}}
|
type="button"
|
||||||
type="button"
|
class="flex w-full items-center gap-2"
|
||||||
class="flex w-full items-center gap-2"
|
formnovalidate
|
||||||
formnovalidate
|
// TODO: Use native popover once Webkit supports it within <form>
|
||||||
// TODO: Use native popover once Webkit supports it within <form>
|
// popovertarget={_id}
|
||||||
// popovertarget={_id}
|
// popovertargetaction="toggle"
|
||||||
// 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
|
<Show
|
||||||
when={
|
when={
|
||||||
props.adornment && props.adornment.position === "start"
|
getValues() &&
|
||||||
|
getValues.length !== 1 &&
|
||||||
|
getValues()[0] !== ""
|
||||||
}
|
}
|
||||||
|
fallback={props.placeholder}
|
||||||
>
|
>
|
||||||
{props.adornment?.content}
|
<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=""
|
||||||
|
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>
|
</Show>
|
||||||
{props.inlineLabel}
|
</div>
|
||||||
<div class="flex cursor-default flex-row gap-2">
|
<Show
|
||||||
<Show
|
when={props.adornment && props.adornment.position === "end"}
|
||||||
when={
|
>
|
||||||
getValues() &&
|
{props.adornment?.content}
|
||||||
getValues.length !== 1 &&
|
</Show>
|
||||||
getValues()[0] !== ""
|
<Icon size={12} icon="CaretDown" class="ml-auto mr-2" />
|
||||||
}
|
</button>
|
||||||
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=""
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const FieldLayout = (props: LayoutProps) => {
|
|||||||
class={cx("grid grid-cols-10 items-center", intern.class)}
|
class={cx("grid grid-cols-10 items-center", intern.class)}
|
||||||
{...divProps}
|
{...divProps}
|
||||||
>
|
>
|
||||||
<label class="col-span-5">{props.label}</label>
|
<div class="col-span-5 flex items-center">{props.label}</div>
|
||||||
<div class="col-span-5">{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>
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export const DynForm = (props: FormProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{/* @ts-expect-error: This happened after solidjs upgrade. TOOD: fixme */}
|
||||||
<ModuleForm {...props.formProps} onSubmit={handleSubmit}>
|
<ModuleForm {...props.formProps} onSubmit={handleSubmit}>
|
||||||
{props.components?.before}
|
{props.components?.before}
|
||||||
<SchemaFields
|
<SchemaFields
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ export async function set_single_service<T extends keyof Services>(
|
|||||||
inventory.services = inventory.services || {};
|
inventory.services = inventory.services || {};
|
||||||
inventory.services[service_name] = inventory.services[service_name] || {};
|
inventory.services[service_name] = inventory.services[service_name] || {};
|
||||||
|
|
||||||
// @ts-expect-error: This doesn't check
|
|
||||||
inventory.services[service_name][instance_key] = service_config;
|
inventory.services[service_name][instance_key] = service_config;
|
||||||
console.log("saving inventory", inventory);
|
console.log("saving inventory", inventory);
|
||||||
return callApi("set_inventory", {
|
return callApi("set_inventory", {
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
import { createSignal, For, Setter, Show } from "solid-js";
|
|
||||||
import { callApi, SuccessQuery } from "../api";
|
|
||||||
import { Menu } from "./Menu";
|
|
||||||
import { activeURI } from "../App";
|
|
||||||
import toast from "solid-toast";
|
|
||||||
import { A, useNavigate } from "@solidjs/router";
|
|
||||||
import { RndThumbnail } from "./noiseThumbnail";
|
|
||||||
import Icon from "./icon";
|
|
||||||
import { Filter } from "../routes/machines";
|
|
||||||
import { Typography } from "./Typography";
|
|
||||||
import { Button } from "./button";
|
|
||||||
|
|
||||||
type MachineDetails = SuccessQuery<"list_machines">["data"][string];
|
|
||||||
|
|
||||||
interface MachineListItemProps {
|
|
||||||
name: string;
|
|
||||||
info?: MachineDetails;
|
|
||||||
nixOnly?: boolean;
|
|
||||||
setFilter: Setter<Filter>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MachineListItem = (props: MachineListItemProps) => {
|
|
||||||
const { name, info, nixOnly } = props;
|
|
||||||
|
|
||||||
// Bootstrapping
|
|
||||||
const [installing, setInstalling] = createSignal<boolean>(false);
|
|
||||||
|
|
||||||
// Later only updates
|
|
||||||
const [updating, setUpdating] = createSignal<boolean>(false);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleInstall = async () => {
|
|
||||||
if (!info?.deploy?.targetHost || installing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const active_clan = activeURI();
|
|
||||||
if (!active_clan) {
|
|
||||||
toast.error("No active clan selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!info?.deploy?.targetHost) {
|
|
||||||
toast.error(
|
|
||||||
"Machine does not have a target host. Specify where the machine should be deployed.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setInstalling(true);
|
|
||||||
await toast.promise(
|
|
||||||
callApi("install_machine", {
|
|
||||||
opts: {
|
|
||||||
machine: {
|
|
||||||
name: name,
|
|
||||||
flake: {
|
|
||||||
identifier: active_clan,
|
|
||||||
},
|
|
||||||
override_target_host: info?.deploy.targetHost,
|
|
||||||
},
|
|
||||||
no_reboot: true,
|
|
||||||
debug: true,
|
|
||||||
nix_options: [],
|
|
||||||
password: null,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
loading: "Installing...",
|
|
||||||
success: "Installed",
|
|
||||||
error: "Failed to install",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setInstalling(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
|
||||||
if (!info?.deploy?.targetHost || installing()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const active_clan = activeURI();
|
|
||||||
if (!active_clan) {
|
|
||||||
toast.error("No active clan selected");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!info?.deploy.targetHost) {
|
|
||||||
toast.error(
|
|
||||||
"Machine does not have a target host. Specify where the machine should be deployed.",
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setUpdating(true);
|
|
||||||
await toast.promise(
|
|
||||||
callApi("update_machines", {
|
|
||||||
base_path: active_clan,
|
|
||||||
machines: [
|
|
||||||
{
|
|
||||||
name: name,
|
|
||||||
deploy: {
|
|
||||||
targetHost: info?.deploy.targetHost,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
loading: "Updating...",
|
|
||||||
success: "Updated",
|
|
||||||
error: "Failed to update",
|
|
||||||
},
|
|
||||||
);
|
|
||||||
setUpdating(false);
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div class="m-2 w-64 rounded-lg border p-3 border-def-2">
|
|
||||||
<figure class="h-fit rounded-xl border bg-def-2 border-def-5">
|
|
||||||
<RndThumbnail name={name} width={220} height={120} />
|
|
||||||
</figure>
|
|
||||||
<div class="flex-row justify-between gap-4 px-2 pt-2">
|
|
||||||
<div class="flex flex-col">
|
|
||||||
<A href={`/machines/${name}`}>
|
|
||||||
<Typography hierarchy="title" size="m" weight="bold">
|
|
||||||
{name}
|
|
||||||
</Typography>
|
|
||||||
</A>
|
|
||||||
<div class="flex justify-between text-slate-600">
|
|
||||||
<div class="flex flex-nowrap">
|
|
||||||
<span class="h-4">
|
|
||||||
<Icon icon="Flash" class="h-4" font-size="inherit" />
|
|
||||||
</span>
|
|
||||||
<Typography hierarchy="body" size="s" weight="medium">
|
|
||||||
<Show when={info}>
|
|
||||||
{(d) => d()?.description || "no description"}
|
|
||||||
</Show>
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="self-end">
|
|
||||||
<Menu
|
|
||||||
popoverid={`menu-${props.name}`}
|
|
||||||
label={<Icon icon={"More"} />}
|
|
||||||
>
|
|
||||||
<ul class="z-[1] w-64 bg-white p-2 shadow ">
|
|
||||||
<li>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full"
|
|
||||||
onClick={() => {
|
|
||||||
navigate("/machines/" + name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
classList={{
|
|
||||||
disabled: !info?.deploy?.targetHost || installing(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full"
|
|
||||||
onClick={handleInstall}
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
classList={{
|
|
||||||
disabled: !info?.deploy?.targetHost || updating(),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
class="w-full"
|
|
||||||
onClick={handleUpdate}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/* <div class="text-slate-600">
|
|
||||||
<Show when={info}>
|
|
||||||
{(d) => (
|
|
||||||
<>
|
|
||||||
<Show when={d().tags}>
|
|
||||||
{(tags) => (
|
|
||||||
<span class="flex gap-1">
|
|
||||||
<For each={tags()}>
|
|
||||||
{(tag) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() =>
|
|
||||||
props.setFilter((prev) => {
|
|
||||||
if (prev.tags.includes(tag)) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
tags: [...prev.tags, tag],
|
|
||||||
};
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span class="rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
{d()?.deploy?.targetHost}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</div> */}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createSignal, Show } from "solid-js";
|
||||||
import { Typography } from "@/src/components/Typography";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import { SidebarFlyout } from "./SidebarFlyout";
|
import { SidebarFlyout } from "./SidebarFlyout";
|
||||||
import "./css/sidebar.css";
|
import "./css/sidebar.css";
|
||||||
|
import Icon from "../icon";
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
clanName: string;
|
clanName: string;
|
||||||
@@ -53,8 +54,16 @@ export const SidebarHeader = (props: SidebarProps) => {
|
|||||||
return (
|
return (
|
||||||
<header class="sidebar__header">
|
<header class="sidebar__header">
|
||||||
<div onClick={handleClick} class="sidebar__header__inner">
|
<div onClick={handleClick} class="sidebar__header__inner">
|
||||||
<ClanProfile clanName={props.clanName} showFlyout={showFlyout} />
|
{/* <ClanProfile clanName={props.clanName} showFlyout={showFlyout} /> */}
|
||||||
<ClanTitle clanName={props.clanName} />
|
<div class="w-full pl-1 text-white">
|
||||||
|
<ClanTitle clanName={props.clanName} />
|
||||||
|
</div>
|
||||||
|
<Show
|
||||||
|
when={showFlyout}
|
||||||
|
fallback={<Icon size={12} class="text-white" icon="CaretDown" />}
|
||||||
|
>
|
||||||
|
<Icon size={12} class="text-white" icon="CaretDown" />
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
{showFlyout() && <SidebarFlyout />}
|
{showFlyout() && <SidebarFlyout />}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export default function Accordion(props: AccordionProps) {
|
|||||||
fallback={
|
fallback={
|
||||||
<Button
|
<Button
|
||||||
endIcon={<Icon size={12} icon={"CaretDown"} />}
|
endIcon={<Icon size={12} icon={"CaretDown"} />}
|
||||||
variant="light"
|
variant="ghost"
|
||||||
size="s"
|
size="s"
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
@@ -30,7 +30,7 @@ export default function Accordion(props: AccordionProps) {
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
endIcon={<Icon size={12} icon={"CaretUp"} />}
|
endIcon={<Icon size={12} icon={"CaretUp"} />}
|
||||||
variant="dark"
|
variant="ghost"
|
||||||
size="s"
|
size="s"
|
||||||
>
|
>
|
||||||
{props.title}
|
{props.title}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
.button--ghost-hover:hover {
|
||||||
|
@apply hover:bg-secondary-100 hover:text-secondary-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--ghost-focus:focus {
|
||||||
|
@apply focus:bg-secondary-200 focus:text-secondary-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button--ghost-active:active {
|
||||||
|
@apply active:bg-secondary-200 active:text-secondary-900 active:shadow-inner-primary-active;
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "./button-light.css";
|
@import "./button-light.css";
|
||||||
@import "./button-dark.css";
|
@import "./button-dark.css";
|
||||||
|
@import "./button-ghost.css";
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply inline-flex items-center flex-shrink gap-1 justify-center p-4 font-semibold;
|
@apply inline-flex items-center flex-shrink gap-1 justify-center p-4 font-semibold;
|
||||||
|
|||||||
@@ -26,10 +26,9 @@ const variantColors: (
|
|||||||
!disabled && "button--light-active", // Active state
|
!disabled && "button--light-active", // Active state
|
||||||
),
|
),
|
||||||
ghost: cx(
|
ghost: cx(
|
||||||
// "shadow-inner-secondary",
|
!disabled && "button--ghost-hover", // Hover state
|
||||||
!disabled && "hover:bg-secondary-200 hover:text-secondary-900", // Hover state
|
!disabled && "button--ghost-focus", // Focus state
|
||||||
!disabled && "focus:bg-secondary-200 focus:text-secondary-900", // Focus state
|
!disabled && "button--ghost-active", // Active state
|
||||||
!disabled && "button--light-active", // Active state
|
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
.machine-item {
|
||||||
|
@apply col-span-1 flex flex-col items-center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
padding: theme(padding.2);
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item__thumb-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding: theme(padding.4);
|
||||||
|
|
||||||
|
border-radius: theme(borderRadius.md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item__thumb {
|
||||||
|
@apply rounded-md bg-secondary-100 border border-secondary-200;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
transition: transform 0.24s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item__header {
|
||||||
|
@apply flex flex-col justify-center items-center;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
transition: transform 0.18s 0.04s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item__pseudo {
|
||||||
|
@apply bg-secondary-50;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid theme(borderColor.secondary.100);
|
||||||
|
border-radius: theme(borderRadius.md);
|
||||||
|
|
||||||
|
transition:
|
||||||
|
transform 0.16s ease-in-out,
|
||||||
|
opacity 0.08s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item:hover {
|
||||||
|
& .machine-item__pseudo {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .machine-item__thumb {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px rgba(0, 0, 0, 0.1),
|
||||||
|
0 8px 20px rgba(0, 0, 0, 0.15),
|
||||||
|
0 12px 40px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .machine-item__header {
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item:not(:hover) .machine-item__pseudo {
|
||||||
|
transform: scale(0.94);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
135
pkgs/webview-ui/app/src/components/machine-list-item/index.tsx
Normal file
135
pkgs/webview-ui/app/src/components/machine-list-item/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { createSignal, For, Setter, Show } from "solid-js";
|
||||||
|
import { callApi, SuccessQuery } from "../../api";
|
||||||
|
|
||||||
|
import { activeURI } from "../../App";
|
||||||
|
import toast from "solid-toast";
|
||||||
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
|
import { RndThumbnail } from "../noiseThumbnail";
|
||||||
|
|
||||||
|
import { Filter } from "../../routes/machines";
|
||||||
|
import { Typography } from "../Typography";
|
||||||
|
import "./css/index.css";
|
||||||
|
|
||||||
|
type MachineDetails = SuccessQuery<"list_machines">["data"][string];
|
||||||
|
|
||||||
|
interface MachineListItemProps {
|
||||||
|
name: string;
|
||||||
|
info?: MachineDetails;
|
||||||
|
nixOnly?: boolean;
|
||||||
|
setFilter: Setter<Filter>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MachineListItem = (props: MachineListItemProps) => {
|
||||||
|
const { name, info, nixOnly } = props;
|
||||||
|
|
||||||
|
// Bootstrapping
|
||||||
|
const [installing, setInstalling] = createSignal<boolean>(false);
|
||||||
|
|
||||||
|
// Later only updates
|
||||||
|
const [updating, setUpdating] = createSignal<boolean>(false);
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleInstall = async () => {
|
||||||
|
if (!info?.deploy?.targetHost || installing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active_clan = activeURI();
|
||||||
|
if (!active_clan) {
|
||||||
|
toast.error("No active clan selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!info?.deploy?.targetHost) {
|
||||||
|
toast.error(
|
||||||
|
"Machine does not have a target host. Specify where the machine should be deployed.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInstalling(true);
|
||||||
|
await toast.promise(
|
||||||
|
callApi("install_machine", {
|
||||||
|
opts: {
|
||||||
|
machine: {
|
||||||
|
name: name,
|
||||||
|
flake: {
|
||||||
|
identifier: active_clan,
|
||||||
|
},
|
||||||
|
override_target_host: info?.deploy.targetHost,
|
||||||
|
},
|
||||||
|
no_reboot: true,
|
||||||
|
debug: true,
|
||||||
|
nix_options: [],
|
||||||
|
password: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Installing...",
|
||||||
|
success: "Installed",
|
||||||
|
error: "Failed to install",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setInstalling(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async () => {
|
||||||
|
if (!info?.deploy?.targetHost || installing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const active_clan = activeURI();
|
||||||
|
if (!active_clan) {
|
||||||
|
toast.error("No active clan selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!info?.deploy.targetHost) {
|
||||||
|
toast.error(
|
||||||
|
"Machine does not have a target host. Specify where the machine should be deployed.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setUpdating(true);
|
||||||
|
await toast.promise(
|
||||||
|
callApi("update_machines", {
|
||||||
|
base_path: active_clan,
|
||||||
|
machines: [
|
||||||
|
{
|
||||||
|
name: name,
|
||||||
|
deploy: {
|
||||||
|
targetHost: info?.deploy.targetHost,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: "Updating...",
|
||||||
|
success: "Updated",
|
||||||
|
error: "Failed to update",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setUpdating(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div class="machine-item">
|
||||||
|
<A href={`/machines/${name}`}>
|
||||||
|
<div class="machine-item__thumb-wrapper">
|
||||||
|
<div class="machine-item__thumb">
|
||||||
|
<RndThumbnail name={name} width={100} height={100} />
|
||||||
|
</div>
|
||||||
|
<div class="machine-item__pseudo" />
|
||||||
|
</div>
|
||||||
|
<header class="machine-item__header">
|
||||||
|
<Typography
|
||||||
|
class="text-center"
|
||||||
|
hierarchy="body"
|
||||||
|
size="s"
|
||||||
|
weight="bold"
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Typography>
|
||||||
|
</header>
|
||||||
|
</A>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -96,9 +96,102 @@ html {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordeon__header::-webkit-details-marker {
|
summary {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary::marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.accordeon__body {
|
.accordeon__body {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.machine-item-loader {
|
||||||
|
@apply col-span-1 flex flex-col items-center;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
padding: theme(padding.2);
|
||||||
|
border-radius: theme(borderRadius.md);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__thumb-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
padding: theme(padding.4);
|
||||||
|
|
||||||
|
border-radius: theme(borderRadius.md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__thumb {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
background: theme(backgroundColor.secondary.100);
|
||||||
|
border-radius: theme(borderRadius.md);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__headline {
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
width: 90%;
|
||||||
|
height: 20px;
|
||||||
|
|
||||||
|
background: theme(backgroundColor.secondary.100);
|
||||||
|
border-radius: theme(borderRadius.sm);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__cover {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__loader {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 20%,
|
||||||
|
theme(backgroundColor.secondary.200) 50%,
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
|
background-size: 400px 100%;
|
||||||
|
|
||||||
|
animation: loader 4s linear infinite;
|
||||||
|
transition: all 0.56s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.machine-item-loader__cover .machine-item-loader__loader {
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
transparent 20%,
|
||||||
|
theme(backgroundColor.secondary.50) 50%,
|
||||||
|
transparent 80%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader {
|
||||||
|
0% {
|
||||||
|
background-position: -1000px 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 1000px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import toast from "solid-toast";
|
|||||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||||
import { InputLabel } from "@/src/components/inputBase";
|
import { InputLabel } from "@/src/components/inputBase";
|
||||||
import { Modal } from "@/src/components/modal";
|
import { Modal } from "@/src/components/modal";
|
||||||
|
import Fieldset from "@/src/Form/fieldset";
|
||||||
|
import Accordion from "@/src/components/accordion";
|
||||||
|
|
||||||
interface Wifi extends FieldValues {
|
interface Wifi extends FieldValues {
|
||||||
ssid: string;
|
ssid: string;
|
||||||
@@ -233,16 +235,23 @@ export const Flash = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
<div class="p-4">
|
<div class="w-full self-stretch p-8">
|
||||||
<Typography tag="p" hierarchy="body" size="default" color="primary">
|
{/* <Typography tag="p" hierarchy="body" size="default" color="primary">
|
||||||
USB Utility image.
|
USB Utility image.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
||||||
Will make bootstrapping new machines easier by providing secure remote
|
Will make bootstrapping new machines easier by providing secure remote
|
||||||
connection to any machine when plugged in.
|
connection to any machine when plugged in.
|
||||||
</Typography>
|
</Typography> */}
|
||||||
<Form onSubmit={handleSubmit}>
|
<Form
|
||||||
<div class="my-4">
|
onSubmit={handleSubmit}
|
||||||
|
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
||||||
|
>
|
||||||
|
<Fieldset legend="Authorized SSH Keys">
|
||||||
|
<Typography hierarchy="body" size="s" weight="medium">
|
||||||
|
Provide your SSH public key. For secure and passwordless SSH
|
||||||
|
connections.
|
||||||
|
</Typography>
|
||||||
<Field name="sshKeys" type="File[]">
|
<Field name="sshKeys" type="File[]">
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<>
|
<>
|
||||||
@@ -267,146 +276,72 @@ export const Flash = () => {
|
|||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
helperText="Provide your SSH public key. For secure and passwordless SSH connections."
|
//helperText="Provide your SSH public key. For secure and passwordless SSH connections."
|
||||||
label="Authorized SSH Keys"
|
//label="Authorized SSH Keys"
|
||||||
multiple
|
multiple
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</Fieldset>
|
||||||
|
<Fieldset legend="General">
|
||||||
<Field name="disk" validate={[required("This field is required")]}>
|
<Field name="disk" validate={[required("This field is required")]}>
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<SelectInput
|
|
||||||
loading={deviceQuery.isFetching}
|
|
||||||
selectProps={props}
|
|
||||||
label="Flash Disk"
|
|
||||||
labelProps={{
|
|
||||||
labelAction: (
|
|
||||||
<Button
|
|
||||||
disabled={isFlashing()}
|
|
||||||
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 drive where the clan-installer will be flashed to"
|
|
||||||
options={
|
|
||||||
deviceQuery.data?.blockdevices.map((d) => ({
|
|
||||||
value: d.path,
|
|
||||||
label: `${d.path} -- ${d.size} bytes`,
|
|
||||||
})) || []
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
{/* WiFi Networks */}
|
|
||||||
<div class="my-4 py-2">
|
|
||||||
<FieldLayout
|
|
||||||
label={<InputLabel class="mb-4">Networks</InputLabel>}
|
|
||||||
field={
|
|
||||||
<>
|
<>
|
||||||
|
<SelectInput
|
||||||
|
loading={deviceQuery.isFetching}
|
||||||
|
selectProps={props}
|
||||||
|
label="Flash Disk"
|
||||||
|
labelProps={{
|
||||||
|
labelAction: (
|
||||||
|
<Button
|
||||||
|
disabled={isFlashing()}
|
||||||
|
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 drive"
|
||||||
|
options={
|
||||||
|
deviceQuery.data?.blockdevices.map((d) => ({
|
||||||
|
value: d.path,
|
||||||
|
label: `${d.path} -- ${d.size} bytes`,
|
||||||
|
})) || []
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
|
<Fieldset legend="Network Settings">
|
||||||
|
<FieldLayout
|
||||||
|
label={<InputLabel>Networks</InputLabel>}
|
||||||
|
field={
|
||||||
|
<div class="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="s"
|
size="s"
|
||||||
variant="light"
|
variant="light"
|
||||||
onClick={addWifiNetwork}
|
onClick={addWifiNetwork}
|
||||||
startIcon={<Icon icon="Plus" />}
|
startIcon={<Icon size={12} icon="Plus" />}
|
||||||
>
|
>
|
||||||
WiFi Network
|
WiFi Network
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<For each={wifiNetworks()}>
|
</Fieldset>
|
||||||
{(network, index) => (
|
|
||||||
<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}
|
|
||||||
label="SSID"
|
|
||||||
value={field.value ?? ""}
|
|
||||||
error={field.error}
|
|
||||||
class="col-span-full "
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
name={`wifi.${index()}.password`}
|
|
||||||
validate={[required("Password is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextInput
|
|
||||||
class="col-span-full"
|
|
||||||
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
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="light"
|
|
||||||
class="h-10"
|
|
||||||
size="s"
|
|
||||||
onClick={() => removeWifiNetwork(index())}
|
|
||||||
startIcon={<Icon icon="Trash" />}
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class=" " tabindex="0">
|
<Accordion title="Advanced">
|
||||||
<input type="checkbox" />
|
<Fieldset>
|
||||||
<div class=" px-0">
|
|
||||||
<InputLabel class="mb-4">Advanced</InputLabel>
|
|
||||||
</div>
|
|
||||||
<div class="">
|
|
||||||
<Field
|
<Field
|
||||||
name="machine.flake"
|
name="machine.flake"
|
||||||
validate={[required("This field is required")]}
|
validate={[required("This field is required")]}
|
||||||
@@ -508,10 +443,8 @@ export const Flash = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
</Fieldset>
|
||||||
</div>
|
</Accordion>
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
<div class="mt-2 flex justify-end pt-2">
|
<div class="mt-2 flex justify-end pt-2">
|
||||||
<Button
|
<Button
|
||||||
class="self-end"
|
class="self-end"
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { Match, Switch } from "solid-js";
|
|||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { MachineAvatar } from "./avatar";
|
import { MachineAvatar } from "./avatar";
|
||||||
import { DynForm } from "@/src/Form/form";
|
import { DynForm } from "@/src/Form/form";
|
||||||
import { Typography } from "@/src/components/Typography";
|
|
||||||
import Fieldset from "@/src/Form/fieldset";
|
import Fieldset from "@/src/Form/fieldset";
|
||||||
import Accordion from "@/src/components/accordion";
|
import Accordion from "@/src/components/accordion";
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import { callApi, SuccessData } from "@/src/api";
|
import { callApi, SuccessData } from "@/src/api";
|
||||||
import { activeURI } from "@/src/App";
|
|
||||||
import { Button } from "@/src/components/button";
|
|
||||||
import Icon from "@/src/components/icon";
|
|
||||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
FieldValues,
|
FieldValues,
|
||||||
@@ -14,6 +9,12 @@ import {
|
|||||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
|
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
|
||||||
|
|
||||||
|
import { activeURI } from "@/src/App";
|
||||||
|
import { Button } from "@/src/components/button";
|
||||||
|
import Icon from "@/src/components/icon";
|
||||||
|
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||||
|
import Accordion from "@/src/components/accordion";
|
||||||
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";
|
||||||
@@ -26,6 +27,7 @@ import { DiskStep, DiskValues } from "./install/disk-step";
|
|||||||
import { SummaryStep } from "./install/summary-step";
|
import { SummaryStep } from "./install/summary-step";
|
||||||
import cx from "classnames";
|
import cx from "classnames";
|
||||||
import { VarsStep, VarsValues } from "./install/vars-step";
|
import { VarsStep, VarsValues } from "./install/vars-step";
|
||||||
|
import Fieldset from "@/src/Form/fieldset";
|
||||||
|
|
||||||
type MachineFormInterface = MachineData & {
|
type MachineFormInterface = MachineData & {
|
||||||
sshKey?: File;
|
sshKey?: File;
|
||||||
@@ -242,7 +244,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
|||||||
<div class="flex flex-col items-center gap-3 fg-def-1">
|
<div class="flex flex-col items-center gap-3 fg-def-1">
|
||||||
<Typography
|
<Typography
|
||||||
classList={{
|
classList={{
|
||||||
[cx("bg-inv-4 fg-inv-1")]: idx == step(),
|
[cx("bg-inv-4 fg-inv-1")]: idx === step(),
|
||||||
[cx("bg-def-4 fg-def-1")]: idx < step(),
|
[cx("bg-def-4 fg-def-1")]: idx < step(),
|
||||||
}}
|
}}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
@@ -517,83 +519,71 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div class="flex flex-col gap-6 p-4">
|
<div class="flex flex-col gap-6">
|
||||||
<span class="mb-2 flex w-full justify-center">
|
<div class="sticky top-0 flex items-center justify-end gap-2 border-b border-secondary-100 bg-secondary-50 px-4 py-2">
|
||||||
<MachineAvatar name={machineName()} />
|
<div class="flex items-center gap-3">
|
||||||
</span>
|
<div class="w-fit" data-tip="Machine must be online">
|
||||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-6">
|
{/* <Button
|
||||||
<Field name="machine.name">
|
class="w-full"
|
||||||
{(field, props) => (
|
size="s"
|
||||||
<TextInput
|
// disabled={!online()}
|
||||||
inputProps={props}
|
onClick={() => {
|
||||||
label="Name"
|
setInstallModalOpen(true);
|
||||||
value={field.value ?? ""}
|
}}
|
||||||
error={field.error}
|
endIcon={<Icon icon="Flash" />}
|
||||||
class="col-span-2"
|
>
|
||||||
required
|
Install
|
||||||
/>
|
</Button> */}
|
||||||
)}
|
</div>
|
||||||
</Field>
|
{/* <Typography hierarchy="label" size="default">
|
||||||
<Field name="machine.tags" type="string[]">
|
Installs the system for the first time. Used to bootstrap the
|
||||||
{(field, props) => (
|
remote device.
|
||||||
<>
|
</Typography> */}
|
||||||
<FieldLayout
|
</div>
|
||||||
label={<InputLabel>Tags</InputLabel>}
|
<div class="flex items-center gap-3">
|
||||||
field={
|
<div class="button-group flex">
|
||||||
<span class="col-span-10">
|
<Button
|
||||||
<For each={field.value}>
|
variant="light"
|
||||||
{(tag) => (
|
class="w-full"
|
||||||
<span class="mx-2 w-fit rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
size="s"
|
||||||
{tag}
|
onClick={() => {
|
||||||
</span>
|
setInstallModalOpen(true);
|
||||||
)}
|
}}
|
||||||
</For>
|
endIcon={<Icon size={14} icon="Flash" />}
|
||||||
</span>
|
>
|
||||||
}
|
Install
|
||||||
/>
|
</Button>
|
||||||
</>
|
<Button
|
||||||
)}
|
variant="light"
|
||||||
</Field>
|
class="w-full"
|
||||||
<Field name="machine.description">
|
size="s"
|
||||||
{(field, props) => (
|
onClick={() => handleUpdate()}
|
||||||
<TextInput
|
endIcon={<Icon size={12} icon="Update" />}
|
||||||
inputProps={props}
|
>
|
||||||
label="Description"
|
Update
|
||||||
value={field.value ?? ""}
|
</Button>
|
||||||
error={field.error}
|
</div>
|
||||||
class="col-span-2"
|
<div class=" w-fit" data-tip="Machine must be online"></div>
|
||||||
required
|
{/* <Typography hierarchy="label" size="default">
|
||||||
/>
|
Update the system if changes should be synced after the
|
||||||
)}
|
installation process.
|
||||||
</Field>
|
</Typography> */}
|
||||||
<Field name="hw_config">
|
</div>
|
||||||
{(field, props) => (
|
</div>
|
||||||
<FieldLayout
|
<div class="p-4">
|
||||||
label={<InputLabel>Hardware Configuration</InputLabel>}
|
<span class="mb-2 flex w-full justify-center">
|
||||||
field={<span>{field.value || "None"}</span>}
|
<MachineAvatar name={machineName()} />
|
||||||
/>
|
</span>
|
||||||
)}
|
<Form
|
||||||
</Field>
|
onSubmit={handleSubmit}
|
||||||
<Field name="disk_schema.schema_name">
|
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
||||||
{(field, props) => (
|
>
|
||||||
<>
|
<Fieldset legend="General">
|
||||||
<FieldLayout
|
<Field name="machine.name">
|
||||||
label={<InputLabel>Disk schema</InputLabel>}
|
|
||||||
field={<span>{field.value || "None"}</span>}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<div class=" col-span-full" tabindex="0">
|
|
||||||
<input type="checkbox" />
|
|
||||||
<div class=" px-0 text-xl ">Connection Settings</div>
|
|
||||||
<div class="">
|
|
||||||
<Field name="machine.deploy.targetHost">
|
|
||||||
{(field, props) => (
|
{(field, props) => (
|
||||||
<TextInput
|
<TextInput
|
||||||
inputProps={props}
|
inputProps={props}
|
||||||
label="Target Host"
|
label="Name"
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
class="col-span-2"
|
class="col-span-2"
|
||||||
@@ -601,73 +591,106 @@ const MachineForm = (props: MachineDetailsProps) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
<Field name="machine.description">
|
||||||
</div>
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
inputProps={props}
|
||||||
|
label="Description"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
class="col-span-2"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field name="machine.tags" type="string[]">
|
||||||
|
{(field, props) => (
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Typography hierarchy="label" size="default" weight="bold">
|
||||||
|
Tags{" "}
|
||||||
|
</Typography>
|
||||||
|
<For each={field.value}>
|
||||||
|
{(tag) => (
|
||||||
|
<span class="mx-2 w-fit rounded-full px-3 py-0.5 bg-inv-4 fg-inv-1">
|
||||||
|
<Typography
|
||||||
|
hierarchy="label"
|
||||||
|
size="s"
|
||||||
|
inverted={true}
|
||||||
|
>
|
||||||
|
{tag}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
{
|
<Fieldset legend="Hardware">
|
||||||
<div class=" col-span-full justify-end">
|
<Field name="hw_config">
|
||||||
<Button
|
{(field, props) => (
|
||||||
type="submit"
|
<FieldLayout
|
||||||
disabled={formStore.submitting || !formStore.dirty}
|
label={<InputLabel>Hardware Configuration</InputLabel>}
|
||||||
>
|
field={<span>{field.value || "None"}</span>}
|
||||||
Save
|
/>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
</Field>
|
||||||
}
|
<hr />
|
||||||
</Form>
|
<Field name="disk_schema.schema_name">
|
||||||
</div>
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<FieldLayout
|
||||||
|
label={<InputLabel>Disk schema</InputLabel>}
|
||||||
|
field={<span>{field.value || "None"}</span>}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
|
||||||
<div class="">
|
<Accordion title="Connection Settings">
|
||||||
<div class=""></div>
|
<Fieldset>
|
||||||
|
<Field name="machine.deploy.targetHost">
|
||||||
|
{(field, props) => (
|
||||||
|
<TextInput
|
||||||
|
inputProps={props}
|
||||||
|
label="Target Host"
|
||||||
|
value={field.value ?? ""}
|
||||||
|
error={field.error}
|
||||||
|
class="col-span-2"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</Fieldset>
|
||||||
|
</Accordion>
|
||||||
|
|
||||||
<span class="text-xl text-primary-800">Actions</span>
|
{
|
||||||
<div class="my-4 flex flex-col gap-6">
|
<footer class="flex justify-end gap-y-3 border-t border-secondary-200 pt-5">
|
||||||
<span class="max-w-md">
|
<Button
|
||||||
Installs the system for the first time. Used to bootstrap the remote
|
type="submit"
|
||||||
device.
|
disabled={formStore.submitting || !formStore.dirty}
|
||||||
</span>
|
>
|
||||||
<div class=" w-fit" data-tip="Machine must be online">
|
Update edits
|
||||||
<Button
|
</Button>
|
||||||
class="w-full"
|
</footer>
|
||||||
// disabled={!online()}
|
}
|
||||||
onClick={() => {
|
</Form>
|
||||||
setInstallModalOpen(true);
|
|
||||||
}}
|
|
||||||
endIcon={<Icon icon="Flash" />}
|
|
||||||
>
|
|
||||||
Install
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Modal
|
|
||||||
title={`Install machine`}
|
|
||||||
open={installModalOpen()}
|
|
||||||
handleClose={() => setInstallModalOpen(false)}
|
|
||||||
class="min-w-[600px]"
|
|
||||||
>
|
|
||||||
<InstallMachine
|
|
||||||
name={machineName()}
|
|
||||||
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
|
||||||
machine={props.initialData}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<span class="max-w-md">
|
|
||||||
Update the system if changes should be synced after the installation
|
|
||||||
process.
|
|
||||||
</span>
|
|
||||||
<div class=" w-fit" data-tip="Machine must be online">
|
|
||||||
<Button
|
|
||||||
class="w-full"
|
|
||||||
// disabled={!online()}
|
|
||||||
onClick={() => handleUpdateButton()}
|
|
||||||
endIcon={<Icon icon="Update" />}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={`Install machine`}
|
||||||
|
open={installModalOpen()}
|
||||||
|
handleClose={() => setInstallModalOpen(false)}
|
||||||
|
class="min-w-[600px]"
|
||||||
|
>
|
||||||
|
<InstallMachine
|
||||||
|
name={machineName()}
|
||||||
|
targetHost={getValue(formStore, "machine.deploy.targetHost")}
|
||||||
|
machine={props.initialData}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { activeURI } from "@/src/App";
|
import { activeURI } from "@/src/App";
|
||||||
import { callApi, OperationResponse } from "@/src/api";
|
import { callApi, OperationResponse } from "@/src/api";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { MachineListItem } from "@/src/components/MachineListItem";
|
import { MachineListItem } from "@/src/components/machine-list-item";
|
||||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { Button } from "@/src/components/button";
|
import { Button } from "@/src/components/button";
|
||||||
@@ -112,16 +112,6 @@ export const MachineListView: Component = () => {
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<div class="my-1 flex w-full gap-2 p-2">
|
<div class="my-1 flex w-full gap-2 p-2">
|
||||||
<div class="flex w-full justify-end px-4 py-1">
|
|
||||||
<div class="flex">
|
|
||||||
<Button
|
|
||||||
// onClick={() => navigate("create")}
|
|
||||||
size="s"
|
|
||||||
variant="light"
|
|
||||||
startIcon={<Icon icon="Filter" />}
|
|
||||||
></Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<For each={filter().tags.sort()}>
|
<For each={filter().tags.sort()}>
|
||||||
{(tag) => (
|
{(tag) => (
|
||||||
<button
|
<button
|
||||||
@@ -142,22 +132,39 @@ export const MachineListView: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
{/* </Show> */}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={inventoryQuery.isLoading}>
|
<Match when={inventoryQuery.isLoading}>
|
||||||
{/* Loading skeleton */}
|
{/* Loading skeleton */}
|
||||||
<div>
|
<div class="grid grid-cols-4"></div>
|
||||||
<div class=" m-2 shadow-lg">
|
<div class="machine-item-loader">
|
||||||
<figure class="pl-2">
|
<div class="machine-item-loader__thumb-wrapper">
|
||||||
<div class=" size-12"></div>
|
<div class="machine-item-loader__thumb">
|
||||||
</figure>
|
<div class="machine-item-loader__loader" />
|
||||||
<div class="">
|
|
||||||
<h2 class="">
|
|
||||||
<div class=" h-12 w-80"></div>
|
|
||||||
</h2>
|
|
||||||
<div class=" h-8 w-72"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="machine-item-loader__headline">
|
||||||
|
<div class="machine-item-loader__loader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="machine-item-loader">
|
||||||
|
<div class="machine-item-loader__thumb-wrapper">
|
||||||
|
<div class="machine-item-loader__thumb">
|
||||||
|
<div class="machine-item-loader__loader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="machine-item-loader__headline">
|
||||||
|
<div class="machine-item-loader__loader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="machine-item-loader">
|
||||||
|
<div class="machine-item-loader__thumb-wrapper">
|
||||||
|
<div class="machine-item-loader__thumb">
|
||||||
|
<div class="machine-item-loader__loader" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="machine-item-loader__headline">
|
||||||
|
<div class="machine-item-loader__loader" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match
|
<Match
|
||||||
@@ -178,10 +185,10 @@ export const MachineListView: Component = () => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={!inventoryQuery.isLoading}>
|
<Match when={!inventoryQuery.isLoading}>
|
||||||
<div
|
<div
|
||||||
class="my-4 flex flex-wrap gap-6 px-3 py-2"
|
class="my-4 grid gap-6 p-6"
|
||||||
classList={{
|
classList={{
|
||||||
"flex-col": view() === "list",
|
"grid-cols-1": view() === "list",
|
||||||
"": view() === "grid",
|
"grid-cols-4": view() === "grid",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={inventoryMachines()}>
|
<For each={inventoryMachines()}>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const ModuleList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
title="Modules"
|
title="App Store"
|
||||||
toolbar={
|
toolbar={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -240,17 +240,17 @@ export default plugin.withOptions(
|
|||||||
950: toRGB("#162324"),
|
950: toRGB("#162324"),
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
50: toRGB("#f7f9f9"),
|
50: toRGB("#F7F9FA"),
|
||||||
100: toRGB("#e7f2f4"),
|
100: toRGB("#E7F2F4"),
|
||||||
200: toRGB("#d7e8ea"),
|
200: toRGB("#D8E8EB"),
|
||||||
300: toRGB("#afc6ca"),
|
300: toRGB("#AFC6CA"),
|
||||||
400: toRGB("#8fb2b6"),
|
400: toRGB("#90B2B7"),
|
||||||
500: toRGB("#7b9a9e"),
|
500: toRGB("#7B9B9F"),
|
||||||
600: toRGB("#4f747a"),
|
600: toRGB("#4F747A"),
|
||||||
700: toRGB("#415e63"),
|
700: toRGB("#415E63"),
|
||||||
800: toRGB("#445f64"),
|
800: toRGB("#446065"),
|
||||||
900: toRGB("#2b4347"),
|
900: toRGB("#2C4347"),
|
||||||
950: toRGB("#0d1415"),
|
950: toRGB("#0D1416"),
|
||||||
},
|
},
|
||||||
info: {
|
info: {
|
||||||
50: toRGB("#eff9ff"),
|
50: toRGB("#eff9ff"),
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ export default defineConfig({
|
|||||||
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
|
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ["debug", "extend"],
|
||||||
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
/*
|
/*
|
||||||
Uncomment the following line to enable solid-devtools.
|
Uncomment the following line to enable solid-devtools.
|
||||||
|
|||||||
Reference in New Issue
Block a user