machine-item: updates design and unifies
This commit is contained in:
committed by
Johannes Kirschbauer
parent
16256440e6
commit
51950329a3
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -84,7 +84,6 @@ interface _TypographyProps<H extends Hierarchy> {
|
|||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
class?: string;
|
class?: string;
|
||||||
classList?: Record<string, boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
export const Typography = <H extends Hierarchy>(props: _TypographyProps<H>) => {
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
.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;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 20;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
@@ -114,16 +114,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
|
||||||
@@ -144,7 +134,6 @@ export const MachineListView: Component = () => {
|
|||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
{/* </Show> */}
|
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={inventoryQuery.isLoading}>
|
<Match when={inventoryQuery.isLoading}>
|
||||||
{/* Loading skeleton */}
|
{/* Loading skeleton */}
|
||||||
@@ -180,10 +169,10 @@ export const MachineListView: Component = () => {
|
|||||||
</Match>
|
</Match>
|
||||||
<Match when={!inventoryQuery.isLoading}>
|
<Match when={!inventoryQuery.isLoading}>
|
||||||
<div
|
<div
|
||||||
class="my-4 flex flex-wrap gap-6 px-3 py-2"
|
class="my-4 grid gap-6 p-6"
|
||||||
classList={{
|
classList={{
|
||||||
"flex-col": view() === "list",
|
"grid-cols-1": view() === "list",
|
||||||
"": view() === "grid",
|
"grid-cols-4": view() === "grid",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<For each={inventoryMachines()}>
|
<For each={inventoryMachines()}>
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export const ModuleList = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
title="Modules"
|
title="App Store"
|
||||||
toolbar={
|
toolbar={
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
Reference in New Issue
Block a user