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>
|
||||
}
|
||||
field={
|
||||
<>
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
class="!justify-start"
|
||||
divRef={setReference}
|
||||
inputElem={
|
||||
<button
|
||||
// TODO: Keyboard acessibililty
|
||||
// Currently the popover only opens with onClick
|
||||
// Options are not selectable with keyboard
|
||||
tabIndex={-1}
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
const popover = document.getElementById(_id);
|
||||
if (popover) {
|
||||
popover.togglePopover(); // Show or hide the popover
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2"
|
||||
formnovalidate
|
||||
// TODO: Use native popover once Webkit supports it within <form>
|
||||
// popovertarget={_id}
|
||||
// popovertargetaction="toggle"
|
||||
<InputBase
|
||||
error={!!props.error}
|
||||
disabled={props.disabled}
|
||||
required={props.required}
|
||||
class="!justify-start"
|
||||
divRef={setReference}
|
||||
inputElem={
|
||||
<button
|
||||
// TODO: Keyboard acessibililty
|
||||
// Currently the popover only opens with onClick
|
||||
// Options are not selectable with keyboard
|
||||
tabIndex={-1}
|
||||
disabled={props.disabled}
|
||||
onClick={() => {
|
||||
const popover = document.getElementById(_id);
|
||||
if (popover) {
|
||||
popover.togglePopover(); // Show or hide the popover
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2"
|
||||
formnovalidate
|
||||
// TODO: Use native popover once Webkit supports it within <form>
|
||||
// popovertarget={_id}
|
||||
// popovertargetaction="toggle"
|
||||
>
|
||||
<Show
|
||||
when={props.adornment && props.adornment.position === "start"}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
{props.inlineLabel}
|
||||
<div class="flex cursor-default flex-row gap-2">
|
||||
<Show
|
||||
when={
|
||||
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>
|
||||
{props.inlineLabel}
|
||||
<div class="flex cursor-default flex-row gap-2">
|
||||
<Show
|
||||
when={
|
||||
getValues() &&
|
||||
getValues.length !== 1 &&
|
||||
getValues()[0] !== ""
|
||||
}
|
||||
fallback={props.placeholder}
|
||||
>
|
||||
<For each={getValues()} fallback={"Select"}>
|
||||
{(item) => (
|
||||
<div class="rounded-xl bg-slate-800 px-4 py-1 text-sm text-white">
|
||||
{item}
|
||||
<Show when={props.multiple}>
|
||||
<button
|
||||
class=""
|
||||
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>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<Show
|
||||
when={props.adornment && props.adornment.position === "end"}
|
||||
>
|
||||
{props.adornment?.content}
|
||||
</Show>
|
||||
<Icon size={12} icon="CaretDown" class="ml-auto mr-2" />
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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 { SidebarFlyout } from "./SidebarFlyout";
|
||||
import "./css/sidebar.css";
|
||||
import Icon from "../icon";
|
||||
|
||||
interface SidebarProps {
|
||||
clanName: string;
|
||||
@@ -53,8 +54,16 @@ export const SidebarHeader = (props: SidebarProps) => {
|
||||
return (
|
||||
<header class="sidebar__header">
|
||||
<div onClick={handleClick} class="sidebar__header__inner">
|
||||
<ClanProfile clanName={props.clanName} showFlyout={showFlyout} />
|
||||
<ClanTitle clanName={props.clanName} />
|
||||
{/* <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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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-dark.css";
|
||||
@import "./button-ghost.css";
|
||||
|
||||
.button {
|
||||
@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
|
||||
),
|
||||
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
|
||||
),
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
<div class="my-4">
|
||||
</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>
|
||||
<Field name="sshKeys" type="File[]">
|
||||
{(field, props) => (
|
||||
<>
|
||||
@@ -267,146 +276,72 @@ 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>
|
||||
|
||||
<Field name="disk" validate={[required("This field is required")]}>
|
||||
{(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={
|
||||
</Fieldset>
|
||||
<Fieldset legend="General">
|
||||
<Field name="disk" validate={[required("This field is required")]}>
|
||||
{(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"
|
||||
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
|
||||
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"
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,83 +519,71 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="flex flex-col gap-6 p-4">
|
||||
<span class="mb-2 flex w-full justify-center">
|
||||
<MachineAvatar name={machineName()} />
|
||||
</span>
|
||||
<Form onSubmit={handleSubmit} class="flex flex-col gap-6">
|
||||
<Field name="machine.name">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
label="Name"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-2"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</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
|
||||
inputProps={props}
|
||||
label="Description"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-2"
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="hw_config">
|
||||
{(field, props) => (
|
||||
<FieldLayout
|
||||
label={<InputLabel>Hardware Configuration</InputLabel>}
|
||||
field={<span>{field.value || "None"}</span>}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="disk_schema.schema_name">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<FieldLayout
|
||||
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">
|
||||
<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="mx-auto flex w-full max-w-2xl flex-col gap-y-6"
|
||||
>
|
||||
<Fieldset legend="General">
|
||||
<Field name="machine.name">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
label="Target Host"
|
||||
label="Name"
|
||||
value={field.value ?? ""}
|
||||
error={field.error}
|
||||
class="col-span-2"
|
||||
@@ -601,73 +591,106 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<Field name="machine.description">
|
||||
{(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>
|
||||
|
||||
{
|
||||
<div class=" col-span-full justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formStore.submitting || !formStore.dirty}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
</Form>
|
||||
</div>
|
||||
<Fieldset legend="Hardware">
|
||||
<Field name="hw_config">
|
||||
{(field, props) => (
|
||||
<FieldLayout
|
||||
label={<InputLabel>Hardware Configuration</InputLabel>}
|
||||
field={<span>{field.value || "None"}</span>}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<hr />
|
||||
<Field name="disk_schema.schema_name">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<FieldLayout
|
||||
label={<InputLabel>Disk schema</InputLabel>}
|
||||
field={<span>{field.value || "None"}</span>}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</Fieldset>
|
||||
|
||||
<div class="">
|
||||
<div class=""></div>
|
||||
<Accordion title="Connection Settings">
|
||||
<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">
|
||||
<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
|
||||
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>
|
||||
{
|
||||
<footer class="flex justify-end gap-y-3 border-t border-secondary-200 pt-5">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formStore.submitting || !formStore.dirty}
|
||||
>
|
||||
Update edits
|
||||
</Button>
|
||||
</footer>
|
||||
}
|
||||
</Form>
|
||||
</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 { 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()}>
|
||||
|
||||
@@ -121,7 +121,7 @@ export const ModuleList = () => {
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
title="Modules"
|
||||
title="App Store"
|
||||
toolbar={
|
||||
<>
|
||||
<Button
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user