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:
hsjobeki
2025-05-07 14:23:49 +00:00
21 changed files with 3252 additions and 3108 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -142,7 +142,6 @@ export function SelectInput(props: SelectInputpProps) {
</InputLabel> </InputLabel>
} }
field={ field={
<>
<InputBase <InputBase
error={!!props.error} error={!!props.error}
disabled={props.disabled} disabled={props.disabled}
@@ -170,9 +169,7 @@ export function SelectInput(props: SelectInputpProps) {
// popovertargetaction="toggle" // popovertargetaction="toggle"
> >
<Show <Show
when={ when={props.adornment && props.adornment.position === "start"}
props.adornment && props.adornment.position === "start"
}
> >
{props.adornment?.content} {props.adornment?.content}
</Show> </Show>
@@ -222,11 +219,10 @@ export function SelectInput(props: SelectInputpProps) {
> >
{props.adornment?.content} {props.adornment?.content}
</Show> </Show>
<Icon icon="CaretDown" class="ml-auto mr-2"></Icon> <Icon size={12} icon="CaretDown" class="ml-auto mr-2" />
</button> </button>
} }
/> />
</>
} }
/> />

View File

@@ -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>

View File

@@ -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

View File

@@ -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", {

View File

@@ -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>
);
};

View File

@@ -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,9 +54,17 @@ 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} /> */}
<div class="w-full pl-1 text-white">
<ClanTitle clanName={props.clanName} /> <ClanTitle clanName={props.clanName} />
</div> </div>
<Show
when={showFlyout}
fallback={<Icon size={12} class="text-white" icon="CaretDown" />}
>
<Icon size={12} class="text-white" icon="CaretDown" />
</Show>
</div>
{showFlyout() && <SidebarFlyout />} {showFlyout() && <SidebarFlyout />}
</header> </header>
); );

View File

@@ -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}

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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
), ),
}); });

View File

@@ -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;
}

View 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>
);
};

View File

@@ -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;
}
}

View File

@@ -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> */}
<Form
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> </Typography>
<Form onSubmit={handleSubmit}>
<div class="my-4">
<Field name="sshKeys" type="File[]"> <Field name="sshKeys" type="File[]">
{(field, props) => ( {(field, props) => (
<> <>
@@ -267,18 +276,18 @@ 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 <SelectInput
loading={deviceQuery.isFetching} loading={deviceQuery.isFetching}
selectProps={props} selectProps={props}
@@ -299,7 +308,7 @@ export const Flash = () => {
value={field.value || ""} value={field.value || ""}
error={field.error} error={field.error}
required required
placeholder="Select a drive where the clan-installer will be flashed to" placeholder="Select a drive"
options={ options={
deviceQuery.data?.blockdevices.map((d) => ({ deviceQuery.data?.blockdevices.map((d) => ({
value: d.path, value: d.path,
@@ -307,106 +316,32 @@ export const Flash = () => {
})) || [] })) || []
} }
/> />
</>
)} )}
</Field> </Field>
</Fieldset>
{/* WiFi Networks */} <Fieldset legend="Network Settings">
<div class="my-4 py-2">
<FieldLayout <FieldLayout
label={<InputLabel class="mb-4">Networks</InputLabel>} label={<InputLabel>Networks</InputLabel>}
field={ 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"

View File

@@ -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";

View File

@@ -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,11 +519,66 @@ const MachineForm = (props: MachineDetailsProps) => {
return ( return (
<> <>
<div class="flex flex-col gap-6 p-4"> <div class="flex flex-col gap-6">
<div class="sticky top-0 flex items-center justify-end gap-2 border-b border-secondary-100 bg-secondary-50 px-4 py-2">
<div class="flex items-center gap-3">
<div class="w-fit" data-tip="Machine must be online">
{/* <Button
class="w-full"
size="s"
// disabled={!online()}
onClick={() => {
setInstallModalOpen(true);
}}
endIcon={<Icon icon="Flash" />}
>
Install
</Button> */}
</div>
{/* <Typography hierarchy="label" size="default">
Installs the system for the first time. Used to bootstrap the
remote device.
</Typography> */}
</div>
<div class="flex items-center gap-3">
<div class="button-group flex">
<Button
variant="light"
class="w-full"
size="s"
onClick={() => {
setInstallModalOpen(true);
}}
endIcon={<Icon size={14} icon="Flash" />}
>
Install
</Button>
<Button
variant="light"
class="w-full"
size="s"
onClick={() => handleUpdate()}
endIcon={<Icon size={12} icon="Update" />}
>
Update
</Button>
</div>
<div class=" w-fit" data-tip="Machine must be online"></div>
{/* <Typography hierarchy="label" size="default">
Update the system if changes should be synced after the
installation process.
</Typography> */}
</div>
</div>
<div class="p-4">
<span class="mb-2 flex w-full justify-center"> <span class="mb-2 flex w-full justify-center">
<MachineAvatar name={machineName()} /> <MachineAvatar name={machineName()} />
</span> </span>
<Form onSubmit={handleSubmit} class="flex flex-col gap-6"> <Form
onSubmit={handleSubmit}
class="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
>
<Fieldset legend="General">
<Field name="machine.name"> <Field name="machine.name">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
@@ -534,26 +591,6 @@ const MachineForm = (props: MachineDetailsProps) => {
/> />
)} )}
</Field> </Field>
<Field name="machine.tags" type="string[]">
{(field, props) => (
<>
<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>
<Field name="machine.description"> <Field name="machine.description">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
@@ -562,10 +599,34 @@ const MachineForm = (props: MachineDetailsProps) => {
value={field.value ?? ""} value={field.value ?? ""}
error={field.error} error={field.error}
class="col-span-2" class="col-span-2"
required
/> />
)} )}
</Field> </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">
<Field name="hw_config"> <Field name="hw_config">
{(field, props) => ( {(field, props) => (
<FieldLayout <FieldLayout
@@ -574,6 +635,7 @@ const MachineForm = (props: MachineDetailsProps) => {
/> />
)} )}
</Field> </Field>
<hr />
<Field name="disk_schema.schema_name"> <Field name="disk_schema.schema_name">
{(field, props) => ( {(field, props) => (
<> <>
@@ -584,11 +646,10 @@ const MachineForm = (props: MachineDetailsProps) => {
</> </>
)} )}
</Field> </Field>
</Fieldset>
<div class=" col-span-full" tabindex="0"> <Accordion title="Connection Settings">
<input type="checkbox" /> <Fieldset>
<div class=" px-0 text-xl ">Connection Settings</div>
<div class="">
<Field name="machine.deploy.targetHost"> <Field name="machine.deploy.targetHost">
{(field, props) => ( {(field, props) => (
<TextInput <TextInput
@@ -601,42 +662,21 @@ const MachineForm = (props: MachineDetailsProps) => {
/> />
)} )}
</Field> </Field>
</div> </Fieldset>
</div> </Accordion>
{ {
<div class=" col-span-full justify-end"> <footer class="flex justify-end gap-y-3 border-t border-secondary-200 pt-5">
<Button <Button
type="submit" type="submit"
disabled={formStore.submitting || !formStore.dirty} disabled={formStore.submitting || !formStore.dirty}
> >
Save Update edits
</Button> </Button>
</div> </footer>
} }
</Form> </Form>
</div> </div>
<div class="">
<div class=""></div>
<span class="text-xl text-primary-800">Actions</span>
<div class="my-4 flex flex-col gap-6">
<span class="max-w-md">
Installs the system for the first time. Used to bootstrap the remote
device.
</span>
<div class=" w-fit" data-tip="Machine must be online">
<Button
class="w-full"
// disabled={!online()}
onClick={() => {
setInstallModalOpen(true);
}}
endIcon={<Icon icon="Flash" />}
>
Install
</Button>
</div> </div>
<Modal <Modal
@@ -651,23 +691,6 @@ const MachineForm = (props: MachineDetailsProps) => {
machine={props.initialData} machine={props.initialData}
/> />
</Modal> </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>
</> </>
); );
}; };

View File

@@ -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()}>

View File

@@ -121,7 +121,7 @@ export const ModuleList = () => {
return ( return (
<> <>
<Header <Header
title="Modules" title="App Store"
toolbar={ toolbar={
<> <>
<Button <Button

View File

@@ -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"),

View File

@@ -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.