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;
|
||||
tag?: Tag;
|
||||
class?: string;
|
||||
classList?: Record<string, boolean>;
|
||||
}
|
||||
|
||||
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 { 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";
|
||||
@@ -114,16 +114,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
|
||||
@@ -144,7 +134,6 @@ export const MachineListView: Component = () => {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* </Show> */}
|
||||
<Switch>
|
||||
<Match when={inventoryQuery.isLoading}>
|
||||
{/* Loading skeleton */}
|
||||
@@ -180,10 +169,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
|
||||
|
||||
Reference in New Issue
Block a user