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

View File

@@ -18,7 +18,7 @@ export const FieldLayout = (props: LayoutProps) => {
class={cx("grid grid-cols-10 items-center", intern.class)}
{...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>
{props.error && <span class="col-span-full">{props.error}</span>}
</div>

View File

@@ -96,6 +96,7 @@ export const DynForm = (props: FormProps) => {
return (
<>
{/* @ts-expect-error: This happened after solidjs upgrade. TOOD: fixme */}
<ModuleForm {...props.formProps} onSubmit={handleSubmit}>
{props.components?.before}
<SchemaFields

View File

@@ -98,7 +98,6 @@ export async function set_single_service<T extends keyof Services>(
inventory.services = inventory.services || {};
inventory.services[service_name] = inventory.services[service_name] || {};
// @ts-expect-error: This doesn't check
inventory.services[service_name][instance_key] = service_config;
console.log("saving inventory", 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 { SidebarFlyout } from "./SidebarFlyout";
import "./css/sidebar.css";
import Icon from "../icon";
interface SidebarProps {
clanName: string;
@@ -53,9 +54,17 @@ export const SidebarHeader = (props: SidebarProps) => {
return (
<header class="sidebar__header">
<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} />
</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 />}
</header>
);

View File

@@ -21,7 +21,7 @@ export default function Accordion(props: AccordionProps) {
fallback={
<Button
endIcon={<Icon size={12} icon={"CaretDown"} />}
variant="light"
variant="ghost"
size="s"
>
{props.title}
@@ -30,7 +30,7 @@ export default function Accordion(props: AccordionProps) {
>
<Button
endIcon={<Icon size={12} icon={"CaretUp"} />}
variant="dark"
variant="ghost"
size="s"
>
{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-dark.css";
@import "./button-ghost.css";
.button {
@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
),
ghost: cx(
// "shadow-inner-secondary",
!disabled && "hover:bg-secondary-200 hover:text-secondary-900", // Hover state
!disabled && "focus:bg-secondary-200 focus:text-secondary-900", // Focus state
!disabled && "button--light-active", // Active state
!disabled && "button--ghost-hover", // Hover state
!disabled && "button--ghost-focus", // Focus state
!disabled && "button--ghost-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;
}
.accordeon__header::-webkit-details-marker {
summary {
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::marker {
display: none;
}
.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 { InputLabel } from "@/src/components/inputBase";
import { Modal } from "@/src/components/modal";
import Fieldset from "@/src/Form/fieldset";
import Accordion from "@/src/components/accordion";
interface Wifi extends FieldValues {
ssid: string;
@@ -233,16 +235,23 @@ export const Flash = () => {
</div>
</div>
</Modal>
<div class="p-4">
<Typography tag="p" hierarchy="body" size="default" color="primary">
<div class="w-full self-stretch p-8">
{/* <Typography tag="p" hierarchy="body" size="default" color="primary">
USB Utility image.
</Typography>
<Typography tag="p" hierarchy="body" size="default" color="secondary">
Will make bootstrapping new machines easier by providing secure remote
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>
<Form onSubmit={handleSubmit}>
<div class="my-4">
<Field name="sshKeys" type="File[]">
{(field, props) => (
<>
@@ -267,18 +276,18 @@ export const Flash = () => {
}}
value={field.value}
error={field.error}
helperText="Provide your SSH public key. For secure and passwordless SSH connections."
label="Authorized SSH Keys"
//helperText="Provide your SSH public key. For secure and passwordless SSH connections."
//label="Authorized SSH Keys"
multiple
required
/>
</>
)}
</Field>
</div>
</Fieldset>
<Fieldset legend="General">
<Field name="disk" validate={[required("This field is required")]}>
{(field, props) => (
<>
<SelectInput
loading={deviceQuery.isFetching}
selectProps={props}
@@ -299,7 +308,7 @@ export const Flash = () => {
value={field.value || ""}
error={field.error}
required
placeholder="Select a drive where the clan-installer will be flashed to"
placeholder="Select a drive"
options={
deviceQuery.data?.blockdevices.map((d) => ({
value: d.path,
@@ -307,106 +316,32 @@ export const Flash = () => {
})) || []
}
/>
</>
)}
</Field>
</Fieldset>
{/* WiFi Networks */}
<div class="my-4 py-2">
<Fieldset legend="Network Settings">
<FieldLayout
label={<InputLabel class="mb-4">Networks</InputLabel>}
label={<InputLabel>Networks</InputLabel>}
field={
<>
<div class="flex w-full justify-end">
<Button
type="button"
size="s"
variant="light"
onClick={addWifiNetwork}
startIcon={<Icon icon="Plus" />}
startIcon={<Icon size={12} icon="Plus" />}
>
WiFi Network
</Button>
</>
</div>
}
/>
<For each={wifiNetworks()}>
{(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>
</Fieldset>
<div class=" " tabindex="0">
<input type="checkbox" />
<div class=" px-0">
<InputLabel class="mb-4">Advanced</InputLabel>
</div>
<div class="">
<Accordion title="Advanced">
<Fieldset>
<Field
name="machine.flake"
validate={[required("This field is required")]}
@@ -508,10 +443,8 @@ export const Flash = () => {
</>
)}
</Field>
</div>
</div>
<hr></hr>
</Fieldset>
</Accordion>
<div class="mt-2 flex justify-end pt-2">
<Button
class="self-end"

View File

@@ -11,7 +11,6 @@ import { Match, Switch } from "solid-js";
import toast from "solid-toast";
import { MachineAvatar } from "./avatar";
import { DynForm } from "@/src/Form/form";
import { Typography } from "@/src/components/Typography";
import Fieldset from "@/src/Form/fieldset";
import Accordion from "@/src/components/accordion";

View File

@@ -1,9 +1,4 @@
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 {
createForm,
FieldValues,
@@ -14,6 +9,12 @@ import {
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
import { createQuery, useQueryClient } from "@tanstack/solid-query";
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 { MachineAvatar } from "./avatar";
import { Header } from "@/src/layout/header";
@@ -26,6 +27,7 @@ import { DiskStep, DiskValues } from "./install/disk-step";
import { SummaryStep } from "./install/summary-step";
import cx from "classnames";
import { VarsStep, VarsValues } from "./install/vars-step";
import Fieldset from "@/src/Form/fieldset";
type MachineFormInterface = MachineData & {
sshKey?: File;
@@ -242,7 +244,7 @@ const InstallMachine = (props: InstallMachineProps) => {
<div class="flex flex-col items-center gap-3 fg-def-1">
<Typography
classList={{
[cx("bg-inv-4 fg-inv-1")]: idx == step(),
[cx("bg-inv-4 fg-inv-1")]: idx === step(),
[cx("bg-def-4 fg-def-1")]: idx < step(),
}}
color="inherit"
@@ -517,11 +519,66 @@ const MachineForm = (props: MachineDetailsProps) => {
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">
<MachineAvatar name={machineName()} />
</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, props) => (
<TextInput
@@ -534,26 +591,6 @@ const MachineForm = (props: MachineDetailsProps) => {
/>
)}
</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, props) => (
<TextInput
@@ -562,10 +599,34 @@ const MachineForm = (props: MachineDetailsProps) => {
value={field.value ?? ""}
error={field.error}
class="col-span-2"
required
/>
)}
</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, props) => (
<FieldLayout
@@ -574,6 +635,7 @@ const MachineForm = (props: MachineDetailsProps) => {
/>
)}
</Field>
<hr />
<Field name="disk_schema.schema_name">
{(field, props) => (
<>
@@ -584,11 +646,10 @@ const MachineForm = (props: MachineDetailsProps) => {
</>
)}
</Field>
</Fieldset>
<div class=" col-span-full" tabindex="0">
<input type="checkbox" />
<div class=" px-0 text-xl ">Connection Settings</div>
<div class="">
<Accordion title="Connection Settings">
<Fieldset>
<Field name="machine.deploy.targetHost">
{(field, props) => (
<TextInput
@@ -601,42 +662,21 @@ const MachineForm = (props: MachineDetailsProps) => {
/>
)}
</Field>
</div>
</div>
</Fieldset>
</Accordion>
{
<div class=" col-span-full justify-end">
<footer class="flex justify-end gap-y-3 border-t border-secondary-200 pt-5">
<Button
type="submit"
disabled={formStore.submitting || !formStore.dirty}
>
Save
Update edits
</Button>
</div>
</footer>
}
</Form>
</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>
<Modal
@@ -651,23 +691,6 @@ const MachineForm = (props: MachineDetailsProps) => {
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>
</>
);
};

View File

@@ -9,7 +9,7 @@ import {
import { activeURI } from "@/src/App";
import { callApi, OperationResponse } from "@/src/api";
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 { useNavigate } from "@solidjs/router";
import { Button } from "@/src/components/button";
@@ -112,16 +112,6 @@ export const MachineListView: Component = () => {
/>
<div>
<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()}>
{(tag) => (
<button
@@ -142,22 +132,39 @@ export const MachineListView: Component = () => {
)}
</For>
</div>
{/* </Show> */}
<Switch>
<Match when={inventoryQuery.isLoading}>
{/* Loading skeleton */}
<div>
<div class=" m-2 shadow-lg">
<figure class="pl-2">
<div class=" size-12"></div>
</figure>
<div class="">
<h2 class="">
<div class=" h-12 w-80"></div>
</h2>
<div class=" h-8 w-72"></div>
<div class="grid grid-cols-4"></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 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>
</Match>
<Match
@@ -178,10 +185,10 @@ export const MachineListView: Component = () => {
</Match>
<Match when={!inventoryQuery.isLoading}>
<div
class="my-4 flex flex-wrap gap-6 px-3 py-2"
class="my-4 grid gap-6 p-6"
classList={{
"flex-col": view() === "list",
"": view() === "grid",
"grid-cols-1": view() === "list",
"grid-cols-4": view() === "grid",
}}
>
<For each={inventoryMachines()}>

View File

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

View File

@@ -240,17 +240,17 @@ export default plugin.withOptions(
950: toRGB("#162324"),
},
secondary: {
50: toRGB("#f7f9f9"),
100: toRGB("#e7f2f4"),
200: toRGB("#d7e8ea"),
300: toRGB("#afc6ca"),
400: toRGB("#8fb2b6"),
500: toRGB("#7b9a9e"),
600: toRGB("#4f747a"),
700: toRGB("#415e63"),
800: toRGB("#445f64"),
900: toRGB("#2b4347"),
950: toRGB("#0d1415"),
50: toRGB("#F7F9FA"),
100: toRGB("#E7F2F4"),
200: toRGB("#D8E8EB"),
300: toRGB("#AFC6CA"),
400: toRGB("#90B2B7"),
500: toRGB("#7B9B9F"),
600: toRGB("#4F747A"),
700: toRGB("#415E63"),
800: toRGB("#446065"),
900: toRGB("#2C4347"),
950: toRGB("#0D1416"),
},
info: {
50: toRGB("#eff9ff"),

View File

@@ -30,6 +30,9 @@ export default defineConfig({
"@": path.resolve(__dirname, "./"), // Adjust the path as needed
},
},
optimizeDeps: {
include: ["debug", "extend"],
},
plugins: [
/*
Uncomment the following line to enable solid-devtools.