Merge pull request 'UI: Layout improvements & serde fix.' (#2585) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -2,36 +2,21 @@ import {
|
||||
createForm,
|
||||
Field,
|
||||
FieldArray,
|
||||
FieldElement,
|
||||
FieldValues,
|
||||
FormStore,
|
||||
getValue,
|
||||
minLength,
|
||||
pattern,
|
||||
ResponseData,
|
||||
setValue,
|
||||
getValues,
|
||||
insert,
|
||||
SubmitHandler,
|
||||
swap,
|
||||
reset,
|
||||
remove,
|
||||
move,
|
||||
setError,
|
||||
setValues,
|
||||
} from "@modular-forms/solid";
|
||||
import { JSONSchema7, JSONSchema7Type, validate } from "json-schema";
|
||||
import { JSONSchema7, JSONSchema7Type } from "json-schema";
|
||||
import { TextInput } from "../fields/TextInput";
|
||||
import {
|
||||
children,
|
||||
Component,
|
||||
createEffect,
|
||||
For,
|
||||
JSX,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { createEffect, For, JSX, Match, Show, Switch } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Label } from "../base/label";
|
||||
import { SelectInput } from "../fields/Select";
|
||||
@@ -218,7 +203,7 @@ export function StringField<T extends FieldValues, R extends ResponseData>(
|
||||
(r) => r === props.path[props.path.length - 1],
|
||||
),
|
||||
};
|
||||
const readonly = props.readonly;
|
||||
const readonly = !!props.readonly;
|
||||
return (
|
||||
<Switch fallback={<Unsupported schema={props.schema} />}>
|
||||
<Match
|
||||
@@ -388,20 +373,27 @@ export function ListValueDisplay<T extends FieldValues, R extends ResponseData>(
|
||||
{props.children}
|
||||
<div class="ml-4 min-w-fit pb-4">
|
||||
<Button
|
||||
variant="light"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
type="button"
|
||||
onClick={moveItemBy(1)}
|
||||
disabled={topMost()}
|
||||
startIcon={<Icon icon="ArrowBottom" />}
|
||||
class="h-12"
|
||||
></Button>
|
||||
<Button
|
||||
variant="light"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
onClick={moveItemBy(-1)}
|
||||
disabled={bottomMost()}
|
||||
class="h-12"
|
||||
startIcon={<Icon icon="ArrowTop" />}
|
||||
></Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
class="h-12"
|
||||
startIcon={<Icon icon="Trash" />}
|
||||
onClick={removeItem}
|
||||
@@ -600,7 +592,9 @@ export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
each={fieldArray.items}
|
||||
fallback={
|
||||
// Empty list
|
||||
<span class="text-neutral-500">No items</span>
|
||||
<span class="text-neutral-500">
|
||||
No {itemsSchema().title || "entries"} yet.
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{(item, idx) => (
|
||||
@@ -644,7 +638,10 @@ export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
formProps={{
|
||||
class: cx("px-2 w-full"),
|
||||
}}
|
||||
schema={{ ...itemsSchema(), title: "Add entry" }}
|
||||
schema={{
|
||||
...itemsSchema(),
|
||||
title: itemsSchema().title || "thing",
|
||||
}}
|
||||
initialPath={["root"]}
|
||||
// Reset the input field for list items
|
||||
resetOnSubmit={true}
|
||||
@@ -655,10 +652,12 @@ export function ArrayFields<T extends FieldValues, R extends ResponseData>(
|
||||
components={{
|
||||
before: (
|
||||
<Button
|
||||
variant="light"
|
||||
variant="ghost"
|
||||
type="submit"
|
||||
endIcon={<Icon icon={"Plus"} />}
|
||||
class="capitalize"
|
||||
>
|
||||
Add
|
||||
Add {itemsSchema().title}
|
||||
</Button>
|
||||
),
|
||||
}}
|
||||
@@ -847,7 +846,7 @@ export function ObjectFields<T extends FieldValues, R extends ResponseData>(
|
||||
{key}
|
||||
</span>
|
||||
<Button
|
||||
variant="light"
|
||||
variant="ghost"
|
||||
class="ml-auto"
|
||||
size="s"
|
||||
type="button"
|
||||
|
||||
@@ -168,7 +168,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
<div>
|
||||
<Menu
|
||||
popoverid={`menu-${props.name}`}
|
||||
label={<Icon icon={"Expand"} />}
|
||||
label={<Icon icon={"More"} />}
|
||||
>
|
||||
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
|
||||
<li>
|
||||
|
||||
@@ -55,14 +55,12 @@ export const Menu = (props: MenuProps) => {
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="light"
|
||||
variant="ghost"
|
||||
size="s"
|
||||
popovertarget={props.popoverid}
|
||||
popovertargetaction="toggle"
|
||||
ref={setReference}
|
||||
class={cx(
|
||||
"btn btn-ghost btn-outline join-item btn-sm",
|
||||
props.buttonClass,
|
||||
)}
|
||||
class={cx("join-item", props.buttonClass)}
|
||||
{...props.buttonProps}
|
||||
>
|
||||
{props.label}
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
||||
inverted,
|
||||
hierarchy,
|
||||
weight = "normal",
|
||||
tag,
|
||||
tag = "span",
|
||||
children,
|
||||
classes,
|
||||
} = props;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { splitProps, type JSX } from "solid-js";
|
||||
import cx from "classnames";
|
||||
import { Typography } from "../Typography";
|
||||
|
||||
type Variants = "dark" | "light";
|
||||
type Variants = "dark" | "light" | "ghost";
|
||||
type Size = "default" | "s";
|
||||
|
||||
const variantColors: Record<Variants, string> = {
|
||||
dark: cx(
|
||||
"border border-solid",
|
||||
"border-secondary-950 bg-primary-900 text-white",
|
||||
"shadow-inner-primary",
|
||||
// Hover state
|
||||
@@ -18,6 +20,7 @@ const variantColors: Record<Variants, string> = {
|
||||
"disabled:bg-secondary-200 disabled:text-secondary-700 disabled:border-secondary-300",
|
||||
),
|
||||
light: cx(
|
||||
"border border-solid",
|
||||
"border-secondary-800 bg-secondary-100 text-secondary-800",
|
||||
"shadow-inner-secondary",
|
||||
// Hover state
|
||||
@@ -29,6 +32,17 @@ const variantColors: Record<Variants, string> = {
|
||||
// Disabled
|
||||
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
||||
),
|
||||
ghost: cx(
|
||||
// "shadow-inner-secondary",
|
||||
// Hover state
|
||||
// Focus state
|
||||
// Active state
|
||||
"hover:bg-secondary-200 hover:text-secondary-900",
|
||||
"focus:bg-secondary-200 focus:text-secondary-900",
|
||||
"active:bg-secondary-200 active:text-secondary-950 active:shadow-inner-secondary-active",
|
||||
// Disabled
|
||||
"disabled:bg-secondary-50 disabled:text-secondary-200 disabled:border-secondary-700",
|
||||
),
|
||||
};
|
||||
|
||||
const sizePaddings: Record<Size, string> = {
|
||||
@@ -36,6 +50,11 @@ const sizePaddings: Record<Size, string> = {
|
||||
s: cx("rounded-sm py-[0.375rem] px-3"),
|
||||
};
|
||||
|
||||
const sizeFont: Record<Size, string> = {
|
||||
default: cx("text-[0.8125rem]"),
|
||||
s: cx("text-[0.75rem]"),
|
||||
};
|
||||
|
||||
interface ButtonProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: Variants;
|
||||
size?: Size;
|
||||
@@ -60,11 +79,13 @@ export const Button = (props: ButtonProps) => {
|
||||
// Layout
|
||||
"inline-flex items-center flex-shrink gap-2 justify-center",
|
||||
// Styles
|
||||
"border border-solid",
|
||||
"p-4",
|
||||
sizePaddings[local.size || "default"],
|
||||
// Colors
|
||||
variantColors[local.variant || "dark"],
|
||||
//Font
|
||||
"leading-none font-semibold",
|
||||
sizeFont[local.size || "default"],
|
||||
)}
|
||||
{...other}
|
||||
>
|
||||
|
||||
@@ -100,13 +100,12 @@ interface RndThumbnailProps {
|
||||
height?: number;
|
||||
}
|
||||
export const RndThumbnail = (props: RndThumbnailProps) => {
|
||||
const { name } = props;
|
||||
const seed = Array.from(name).reduce(
|
||||
(acc, char) => acc + char.charCodeAt(0),
|
||||
0,
|
||||
); // Seed from name
|
||||
const imageSrc = generatePatternedImage(seed, props.width, props.height);
|
||||
return <img src={imageSrc} alt={name} />;
|
||||
const seed = () =>
|
||||
Array.from(props.name).reduce((acc, char) => acc + char.charCodeAt(0), 0); // Seed from name
|
||||
const imageSrc = () =>
|
||||
generatePatternedImage(seed(), props.width, props.height);
|
||||
|
||||
return <img src={imageSrc()} alt={props.name} />;
|
||||
};
|
||||
|
||||
export const RndThumbnailShow = () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @refresh reload */
|
||||
import { Portal, render } from "solid-js/web";
|
||||
import { RouteDefinition, Router } from "@solidjs/router";
|
||||
import { Navigate, RouteDefinition, Router } from "@solidjs/router";
|
||||
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
@@ -46,6 +46,12 @@ export type AppRoute = Omit<RouteDefinition, "children"> & {
|
||||
};
|
||||
|
||||
export const routes: AppRoute[] = [
|
||||
{
|
||||
path: "/",
|
||||
label: "",
|
||||
hidden: true,
|
||||
component: () => <Navigate href="/machines" />,
|
||||
},
|
||||
{
|
||||
path: "/machines",
|
||||
label: "Machines",
|
||||
@@ -139,7 +145,6 @@ export const routes: AppRoute[] = [
|
||||
hidden: false,
|
||||
component: () => <Welcome />,
|
||||
},
|
||||
|
||||
{
|
||||
path: "/api_testing",
|
||||
label: "api_testing",
|
||||
|
||||
@@ -1,33 +1,13 @@
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { activeURI } from "../App";
|
||||
import { callApi } from "../api";
|
||||
import { Accessor, Show } from "solid-js";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
|
||||
import Icon from "../components/icon";
|
||||
import { Button } from "../components/button";
|
||||
import { JSX } from "solid-js";
|
||||
import { Typography } from "../components/Typography";
|
||||
|
||||
interface HeaderProps {
|
||||
clan_dir: Accessor<string | null>;
|
||||
title: string;
|
||||
toolbar?: JSX.Element;
|
||||
}
|
||||
export const Header = (props: HeaderProps) => {
|
||||
const { clan_dir } = props;
|
||||
const navigate = useNavigate();
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: [clan_dir(), "meta"],
|
||||
queryFn: async () => {
|
||||
const curr = clan_dir();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", { uri: curr });
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return (
|
||||
<div class="navbar">
|
||||
<div class="navbar border-b px-6 py-4 border-def-3">
|
||||
<div class="flex-none">
|
||||
<span class="tooltip tooltip-bottom lg:hidden" data-tip="Menu">
|
||||
<label
|
||||
@@ -38,19 +18,13 @@ export const Header = (props: HeaderProps) => {
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div class="flex-none">
|
||||
<Show when={activeURI()}>
|
||||
{(d) => (
|
||||
<span class="tooltip tooltip-bottom" data-tip="Clan Settings">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => navigate(`/clans/${window.btoa(d())}`)}
|
||||
startIcon={<Icon icon="Settings" />}
|
||||
></Button>
|
||||
</span>
|
||||
)}
|
||||
</Show>
|
||||
<div class="flex-1">
|
||||
<Typography hierarchy="title" size="m" weight="medium">
|
||||
{props.title}
|
||||
</Typography>
|
||||
</div>
|
||||
<div class="flex-none items-center justify-center gap-3">
|
||||
{props.toolbar}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { Component, createEffect, Show } from "solid-js";
|
||||
import { Header } from "./header";
|
||||
import { Component, createEffect } from "solid-js";
|
||||
import { Sidebar } from "@/src/components/Sidebar";
|
||||
import { activeURI, clanList } from "../App";
|
||||
import { clanList } from "../App";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
|
||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
createEffect(() => {
|
||||
console.log("Layout props", props.location);
|
||||
console.log(
|
||||
"empty ClanList, redirect to welcome page",
|
||||
clanList().length === 0,
|
||||
@@ -25,10 +23,7 @@ export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
type="checkbox"
|
||||
class="drawer-toggle hidden"
|
||||
/>
|
||||
<div class="drawer-content overflow-x-hidden overflow-y-scroll p-2">
|
||||
<Show when={props.location.pathname !== "welcome"}>
|
||||
<Header clan_dir={activeURI} />
|
||||
</Show>
|
||||
<div class="drawer-content my-2 ml-8 overflow-x-hidden overflow-y-scroll rounded-lg border bg-def-1 border-def-3">
|
||||
{props.children}
|
||||
</div>
|
||||
<div
|
||||
|
||||
22
pkgs/webview-ui/app/src/routes/machines/avatar.tsx
Normal file
22
pkgs/webview-ui/app/src/routes/machines/avatar.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { RndThumbnail } from "@/src/components/noiseThumbnail";
|
||||
import cx from "classnames";
|
||||
interface AvatarProps {
|
||||
name?: string;
|
||||
class?: string;
|
||||
}
|
||||
export const MachineAvatar = (props: AvatarProps) => {
|
||||
return (
|
||||
<figure>
|
||||
<div class="avatar placeholder">
|
||||
<div
|
||||
class={cx(
|
||||
"rounded-lg border p-2 bg-def-1 border-def-3 size-36",
|
||||
props.class,
|
||||
)}
|
||||
>
|
||||
<RndThumbnail name={props.name || ""} />
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
);
|
||||
};
|
||||
@@ -3,11 +3,14 @@ import { activeURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import { Header } from "@/src/layout/header";
|
||||
import { createForm, required, reset } from "@modular-forms/solid";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { useQueryClient } from "@tanstack/solid-query";
|
||||
import { Match, Switch } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { MachineAvatar } from "./avatar";
|
||||
import { DynForm } from "@/src/Form/form";
|
||||
|
||||
type CreateMachineForm = OperationArgs<"create_machine">;
|
||||
|
||||
@@ -66,68 +69,111 @@ export function CreateMachine() {
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div class="flex w-full justify-center">
|
||||
<div class="mt-4 w-full max-w-3xl self-stretch px-2">
|
||||
<span class="px-2">Create new Machine</span>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="opts.machine.name"
|
||||
validate={[required("This field is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
formStore={formStore}
|
||||
value={`${field.value}`}
|
||||
label={"name"}
|
||||
error={field.error}
|
||||
required
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="opts.machine.description">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
formStore={formStore}
|
||||
value={`${field.value}`}
|
||||
label={"description"}
|
||||
error={field.error}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="opts.machine.deploy.targetHost">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<>
|
||||
<Header title="Create Machine" />
|
||||
<div class="flex w-full p-4">
|
||||
<div class="mt-4 w-full self-stretch px-2">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Field
|
||||
name="opts.machine.name"
|
||||
validate={[required("This field is required")]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<>
|
||||
<div class="flex justify-center">
|
||||
<MachineAvatar name={field.value} />
|
||||
</div>
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
formStore={formStore}
|
||||
value={`${field.value}`}
|
||||
label={"name"}
|
||||
error={field.error}
|
||||
required
|
||||
placeholder="New_machine"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="opts.machine.description">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
formStore={formStore}
|
||||
value={`${field.value}`}
|
||||
label={"Deployment target"}
|
||||
label={"description"}
|
||||
error={field.error}
|
||||
placeholder="My awesome machine"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
<div class="mt-12 flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formStore.submitting}
|
||||
startIcon={
|
||||
formStore.submitting ? (
|
||||
<Icon icon="Load" />
|
||||
) : (
|
||||
<Icon icon="Plus" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Switch fallback={<>Creating</>}>
|
||||
<Match when={!formStore.submitting}>Create</Match>
|
||||
</Switch>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Field>
|
||||
<Field name="opts.machine.tags" type="string[]">
|
||||
{(field, props) => (
|
||||
<div class="p-2">
|
||||
<DynForm
|
||||
initialValues={{ tags: ["all"] }}
|
||||
components={{
|
||||
before: <div>Tags</div>,
|
||||
}}
|
||||
schema={{
|
||||
type: "object",
|
||||
properties: {
|
||||
tags: {
|
||||
type: "array",
|
||||
items: {
|
||||
title: "Tag",
|
||||
type: "string",
|
||||
},
|
||||
uniqueItems: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
<div class="collapse collapse-arrow" tabindex="0">
|
||||
<input type="checkbox" />
|
||||
<div class="collapse-title link font-medium ">
|
||||
Deployment Settings
|
||||
</div>
|
||||
<div class="collapse-content">
|
||||
<Field name="opts.machine.deploy.targetHost">
|
||||
{(field, props) => (
|
||||
<>
|
||||
<TextInput
|
||||
inputProps={props}
|
||||
formStore={formStore}
|
||||
value={`${field.value}`}
|
||||
label={"Target"}
|
||||
error={field.error}
|
||||
placeholder="e.g. 192.168.188.64"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-12 flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={formStore.submitting}
|
||||
startIcon={
|
||||
formStore.submitting ? (
|
||||
<Icon icon="Load" />
|
||||
) : (
|
||||
<Icon icon="Plus" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Switch fallback={<>Creating</>}>
|
||||
<Match when={!formStore.submitting}>Create</Match>
|
||||
</Switch>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { callApi, ClanService, SuccessData, SuccessQuery } from "@/src/api";
|
||||
import { get_iwd_service } from "@/src/api/wifi";
|
||||
import { callApi, SuccessData, SuccessQuery } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { FileInput } from "@/src/components/FileInput";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { RndThumbnail } from "@/src/components/noiseThumbnail";
|
||||
import { SelectInput } from "@/src/components/SelectInput";
|
||||
import { TextInput } from "@/src/components/TextInput";
|
||||
import { selectSshKeys } from "@/src/hooks";
|
||||
import {
|
||||
@@ -16,9 +13,10 @@ import {
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { useParams } from "@solidjs/router";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Show, Switch, Match, JSXElement } from "solid-js";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { MachineAvatar } from "./avatar";
|
||||
|
||||
type MachineFormInterface = MachineData & {
|
||||
sshKey?: File;
|
||||
@@ -305,24 +303,9 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<figure>
|
||||
<div
|
||||
class="avatar placeholder"
|
||||
classList={
|
||||
{
|
||||
// online: onlineStatusQuery.data === "Online",
|
||||
// offline: onlineStatusQuery.data === "Offline",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div class="w-32 rounded-lg border p-2 bg-def-4 border-inv-3">
|
||||
<RndThumbnail name={machineName() || "M"} />
|
||||
</div>
|
||||
</div>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<span class="text-xl text-primary-800">General</span>
|
||||
|
||||
<MachineAvatar name={machineName()} />
|
||||
<Field name="machine.name">
|
||||
{(field, props) => (
|
||||
<TextInput
|
||||
|
||||
@@ -14,6 +14,7 @@ import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { Header } from "@/src/layout/header";
|
||||
|
||||
type MachinesModel = Extract<
|
||||
OperationResponse<"list_inventory_machines">,
|
||||
@@ -65,96 +66,124 @@ export const MachineListView: Component = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Open Clan"></div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => refresh()}
|
||||
startIcon={<Icon icon="Reload" />}
|
||||
></Button>
|
||||
</div>
|
||||
<>
|
||||
<Header
|
||||
title="Your Machines"
|
||||
toolbar={
|
||||
<>
|
||||
<span class="tooltip tooltip-bottom" data-tip="Reload">
|
||||
<Button
|
||||
variant="light"
|
||||
size="s"
|
||||
onClick={() => refresh()}
|
||||
startIcon={<Icon icon="Reload" />}
|
||||
></Button>
|
||||
</span>
|
||||
|
||||
<div class="tooltip tooltip-bottom" data-tip="Create machine">
|
||||
<Button
|
||||
variant="light"
|
||||
onClick={() => navigate("create")}
|
||||
startIcon={<Icon icon="Plus" />}
|
||||
></Button>
|
||||
</div>
|
||||
{/* <Show when={filter()}> */}
|
||||
<div class="my-1 flex w-full gap-2 p-2">
|
||||
<div class="size-6 p-1">
|
||||
<Icon icon="Filter" />
|
||||
</div>
|
||||
<For each={filter().tags.sort()}>
|
||||
{(tag) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilter((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
tags: prev.tags.filter((t) => t !== tag),
|
||||
};
|
||||
})
|
||||
}
|
||||
>
|
||||
<span class="rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
||||
{tag}
|
||||
<div class="border border-def-3">
|
||||
<span class="tooltip tooltip-bottom" data-tip="List View">
|
||||
<Button
|
||||
variant="dark"
|
||||
size="s"
|
||||
startIcon={<Icon icon="List" />}
|
||||
></Button>
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* </Show> */}
|
||||
<Switch>
|
||||
<Match when={inventoryQuery.isLoading}>
|
||||
{/* Loading skeleton */}
|
||||
<div>
|
||||
<div class="card card-side m-2 bg-base-100 shadow-lg">
|
||||
<figure class="pl-2">
|
||||
<div class="skeleton size-12"></div>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<div class="skeleton h-12 w-80"></div>
|
||||
</h2>
|
||||
<div class="skeleton h-8 w-72"></div>
|
||||
<span class="tooltip tooltip-bottom" data-tip="Grid View">
|
||||
<Button
|
||||
variant="light"
|
||||
size="s"
|
||||
startIcon={<Icon icon="Grid" />}
|
||||
></Button>
|
||||
</span>
|
||||
</div>
|
||||
<span class="tooltip tooltip-bottom" data-tip="New Machine">
|
||||
<Button
|
||||
onClick={() => navigate("create")}
|
||||
size="s"
|
||||
variant="light"
|
||||
startIcon={<Icon icon="Plus" />}
|
||||
>
|
||||
New Machine
|
||||
</Button>
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
{/* <Show when={filter()}> */}
|
||||
<div class="my-1 flex w-full gap-2 p-2">
|
||||
<div class="size-6 p-1">
|
||||
<Icon icon="Filter" />
|
||||
</div>
|
||||
<For each={filter().tags.sort()}>
|
||||
{(tag) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setFilter((prev) => {
|
||||
return {
|
||||
...prev,
|
||||
tags: prev.tags.filter((t) => t !== tag),
|
||||
};
|
||||
})
|
||||
}
|
||||
>
|
||||
<span class="rounded-full px-3 py-1 bg-inv-4 fg-inv-1">
|
||||
{tag}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
{/* </Show> */}
|
||||
<Switch>
|
||||
<Match when={inventoryQuery.isLoading}>
|
||||
{/* Loading skeleton */}
|
||||
<div>
|
||||
<div class="card card-side m-2 bg-base-100 shadow-lg">
|
||||
<figure class="pl-2">
|
||||
<div class="skeleton size-12"></div>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<div class="skeleton h-12 w-80"></div>
|
||||
</h2>
|
||||
<div class="skeleton h-8 w-72"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={!inventoryQuery.isLoading && inventoryMachines().length === 0}
|
||||
>
|
||||
<div class="mt-8 flex w-full flex-col items-center justify-center gap-2">
|
||||
<span class="text-lg text-neutral">
|
||||
No machines defined yet. Click below to define one.
|
||||
</span>
|
||||
<Button
|
||||
variant="light"
|
||||
class="size-28 overflow-hidden p-2"
|
||||
onClick={() => navigate("/machines/create")}
|
||||
>
|
||||
<span class="material-icons text-6xl font-light">add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!inventoryQuery.isLoading}>
|
||||
<ul>
|
||||
<For each={inventoryMachines()}>
|
||||
{([name, info]) => (
|
||||
<MachineListItem
|
||||
name={name}
|
||||
info={info}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</Match>
|
||||
<Match
|
||||
when={!inventoryQuery.isLoading && inventoryMachines().length === 0}
|
||||
>
|
||||
<div class="mt-8 flex w-full flex-col items-center justify-center gap-2">
|
||||
<span class="text-lg text-neutral">
|
||||
No machines defined yet. Click below to define one.
|
||||
</span>
|
||||
<Button
|
||||
variant="light"
|
||||
class="size-28 overflow-hidden p-2"
|
||||
onClick={() => navigate("/machines/create")}
|
||||
>
|
||||
<span class="material-icons text-6xl font-light">add</span>
|
||||
</Button>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={!inventoryQuery.isLoading}>
|
||||
<ul>
|
||||
<For each={inventoryMachines()}>
|
||||
{([name, info]) => (
|
||||
<MachineListItem
|
||||
name={name}
|
||||
info={info}
|
||||
setFilter={setFilter}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user