Merge pull request 'UI: general improvments on layout and responsiveness' (#2595) from hsjobeki/clan-core:hsjobeki-main into main
This commit is contained in:
@@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* This script generates a custom index.html file for the webview UI.
|
||||||
|
* It reads the manifest.json file generated by Vite and uses it to generate the HTML file.
|
||||||
|
* It also processes the CSS files to rewrite the URLs in the CSS files to match the new location of the assets.
|
||||||
|
* The script is run after the Vite build is complete.
|
||||||
|
*
|
||||||
|
* This is necessary because the webview UI is loaded from the local file system and the URLs in the CSS files need to be rewritten to match the new location of the assets.
|
||||||
|
* The generated index.html file is then used as the entry point for the webview UI.
|
||||||
|
*/
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import postcss from "postcss";
|
import postcss from "postcss";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ export const BackButton = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="ghost"
|
||||||
class="w-fit"
|
size="s"
|
||||||
|
class="mr-2"
|
||||||
onClick={() => navigate(-1)}
|
onClick={() => navigate(-1)}
|
||||||
startIcon={<Icon icon="CaretRight" />}
|
startIcon={<Icon icon="CaretLeft" />}
|
||||||
></Button>
|
></Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -106,109 +106,104 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
|||||||
setUpdating(false);
|
setUpdating(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<li>
|
<div class="card card-side m-2">
|
||||||
<div class="card card-side m-2">
|
<figure class="h-fit rounded-xl border bg-def-2 border-def-5">
|
||||||
<figure class="h-fit rounded-xl border bg-def-2 border-def-5">
|
<RndThumbnail name={name} width={220} height={120} />
|
||||||
<RndThumbnail name={name} width={220} height={120} />
|
</figure>
|
||||||
</figure>
|
<div class="card-body flex-row justify-between ">
|
||||||
<div class="card-body flex-row justify-between ">
|
<div class="flex flex-col">
|
||||||
<div class="flex flex-col">
|
<A href={`/machines/${name}`}>
|
||||||
<A href={`/machines/${name}`}>
|
<h2
|
||||||
<h2
|
class="card-title underline"
|
||||||
class="card-title underline"
|
classList={{
|
||||||
classList={{
|
"text-neutral-500": nixOnly,
|
||||||
"text-neutral-500": nixOnly,
|
}}
|
||||||
}}
|
|
||||||
>
|
|
||||||
{name}
|
|
||||||
</h2>
|
|
||||||
</A>
|
|
||||||
<div class="text-slate-600">
|
|
||||||
<Show when={info}>{(d) => d()?.description}</Show>
|
|
||||||
</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 class="flex flex-row flex-wrap gap-4 py-2"></div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Menu
|
|
||||||
popoverid={`menu-${props.name}`}
|
|
||||||
label={<Icon icon={"More"} />}
|
|
||||||
>
|
>
|
||||||
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
|
{name}
|
||||||
<li>
|
</h2>
|
||||||
<a
|
</A>
|
||||||
onClick={() => {
|
<div class="text-slate-600">
|
||||||
navigate("/machines/" + name);
|
<Show when={info}>{(d) => d()?.description}</Show>
|
||||||
}}
|
|
||||||
>
|
|
||||||
Details
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
classList={{
|
|
||||||
disabled: !info?.deploy?.targetHost || installing(),
|
|
||||||
}}
|
|
||||||
onClick={handleInstall}
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<Show when={info?.deploy?.targetHost} fallback={"Deploy"}>
|
|
||||||
{(d) => `Install to ${d()}`}
|
|
||||||
</Show>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
classList={{
|
|
||||||
disabled: !info?.deploy?.targetHost || updating(),
|
|
||||||
}}
|
|
||||||
onClick={handleUpdate}
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<Show when={info?.deploy?.targetHost} fallback={"Deploy"}>
|
|
||||||
{(d) => `Update (${d()})`}
|
|
||||||
</Show>
|
|
||||||
</a>
|
|
||||||
</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 class="flex flex-row flex-wrap gap-4 py-2"></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Menu popoverid={`menu-${props.name}`} label={<Icon icon={"More"} />}>
|
||||||
|
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
navigate("/machines/" + name);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Details
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
classList={{
|
||||||
|
disabled: !info?.deploy?.targetHost || installing(),
|
||||||
|
}}
|
||||||
|
onClick={handleInstall}
|
||||||
|
>
|
||||||
|
<a>
|
||||||
|
<Show when={info?.deploy?.targetHost} fallback={"Deploy"}>
|
||||||
|
{(d) => `Install to ${d()}`}
|
||||||
|
</Show>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
classList={{
|
||||||
|
disabled: !info?.deploy?.targetHost || updating(),
|
||||||
|
}}
|
||||||
|
onClick={handleUpdate}
|
||||||
|
>
|
||||||
|
<a>
|
||||||
|
<Show when={info?.deploy?.targetHost} fallback={"Deploy"}>
|
||||||
|
{(d) => `Update (${d()})`}
|
||||||
|
</Show>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import { createSignal, Show } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
import { Typography } from "@/src/components/Typography";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import { SidebarFlyout } from "./SidebarFlyout";
|
import { SidebarFlyout } from "./SidebarFlyout";
|
||||||
|
import "./css/sidebar.css";
|
||||||
|
|
||||||
interface SidebarHeader {
|
interface SidebarProps {
|
||||||
clanName: string;
|
clanName: string;
|
||||||
|
showFlyout?: () => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SidebarHeader = (props: SidebarHeader) => {
|
const ClanProfile = (props: SidebarProps) => {
|
||||||
const { clanName } = props;
|
return (
|
||||||
|
|
||||||
const [showFlyout, toggleFlyout] = createSignal(false);
|
|
||||||
|
|
||||||
function handleClick() {
|
|
||||||
toggleFlyout(!showFlyout());
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderClanProfile = () => (
|
|
||||||
<div
|
<div
|
||||||
class={`sidebar__profile ${showFlyout() ? "sidebar__profile--flyout" : ""}`}
|
class={`sidebar__profile ${props.showFlyout?.() ? "sidebar__profile--flyout" : ""}`}
|
||||||
>
|
>
|
||||||
<Typography
|
<Typography
|
||||||
classes="sidebar__profile__character"
|
class="sidebar__profile__character"
|
||||||
tag="span"
|
tag="span"
|
||||||
hierarchy="title"
|
hierarchy="title"
|
||||||
size="m"
|
size="m"
|
||||||
@@ -29,14 +22,15 @@ export const SidebarHeader = (props: SidebarHeader) => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
inverted={true}
|
inverted={true}
|
||||||
>
|
>
|
||||||
{clanName.slice(0, 1).toUpperCase()}
|
{props.clanName.slice(0, 1).toUpperCase()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderClanTitle = () => (
|
const ClanTitle = (props: SidebarProps) => {
|
||||||
|
return (
|
||||||
<Typography
|
<Typography
|
||||||
classes="sidebar__title"
|
|
||||||
tag="h3"
|
tag="h3"
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="default"
|
size="default"
|
||||||
@@ -44,15 +38,23 @@ export const SidebarHeader = (props: SidebarHeader) => {
|
|||||||
color="primary"
|
color="primary"
|
||||||
inverted={true}
|
inverted={true}
|
||||||
>
|
>
|
||||||
{clanName}
|
{props.clanName}
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SidebarHeader = (props: SidebarProps) => {
|
||||||
|
const [showFlyout, toggleFlyout] = createSignal(false);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
toggleFlyout(!showFlyout());
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header class="sidebar__header">
|
<header class="sidebar__header">
|
||||||
<div onClick={handleClick} class="sidebar__header__inner">
|
<div onClick={handleClick} class="sidebar__header__inner">
|
||||||
{renderClanProfile()}
|
<ClanProfile clanName={props.clanName} showFlyout={showFlyout} />
|
||||||
{renderClanTitle()}
|
<ClanTitle clanName={props.clanName} />
|
||||||
</div>
|
</div>
|
||||||
{showFlyout() && <SidebarFlyout />}
|
{showFlyout() && <SidebarFlyout />}
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const SidebarListItem = (props: SidebarListItem) => {
|
|||||||
<li class="sidebar__list__item">
|
<li class="sidebar__list__item">
|
||||||
<A class="sidebar__list__link" href={href}>
|
<A class="sidebar__list__link" href={href}>
|
||||||
<Typography
|
<Typography
|
||||||
classes="sidebar__list__content"
|
class="sidebar__list__content"
|
||||||
tag="span"
|
tag="span"
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="s"
|
size="s"
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const SidebarSection = (props: {
|
|||||||
<details class="sidebar__section accordeon" open>
|
<details class="sidebar__section accordeon" open>
|
||||||
<summary class="accordeon__header">
|
<summary class="accordeon__header">
|
||||||
<Typography
|
<Typography
|
||||||
classes="uppercase"
|
class="uppercase"
|
||||||
tag="p"
|
tag="p"
|
||||||
hierarchy="body"
|
hierarchy="body"
|
||||||
size="xs"
|
size="xs"
|
||||||
@@ -47,7 +47,7 @@ export const Sidebar = (props: RouteSectionProps) => {
|
|||||||
const curr = activeURI();
|
const curr = activeURI();
|
||||||
if (curr) {
|
if (curr) {
|
||||||
const result = await callApi("show_clan_meta", { uri: curr });
|
const result = await callApi("show_clan_meta", { uri: curr });
|
||||||
|
console.log("refetched meta for ", curr);
|
||||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||||
|
|
||||||
return result.data;
|
return result.data;
|
||||||
|
|||||||
@@ -67,38 +67,27 @@ const weightMap: Record<Weight, string> = {
|
|||||||
|
|
||||||
interface TypographyProps<H extends Hierarchy> {
|
interface TypographyProps<H extends Hierarchy> {
|
||||||
hierarchy: H;
|
hierarchy: H;
|
||||||
|
size: AllowedSizes<H>;
|
||||||
|
children: JSX.Element;
|
||||||
weight?: Weight;
|
weight?: Weight;
|
||||||
color?: Color;
|
color?: Color;
|
||||||
inverted?: boolean;
|
inverted?: boolean;
|
||||||
size: AllowedSizes<H>;
|
|
||||||
tag?: Tag;
|
tag?: Tag;
|
||||||
children: JSX.Element;
|
class?: string;
|
||||||
classes?: string;
|
|
||||||
}
|
}
|
||||||
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
export const Typography = <H extends Hierarchy>(props: TypographyProps<H>) => {
|
||||||
const {
|
|
||||||
size,
|
|
||||||
color = "primary",
|
|
||||||
inverted,
|
|
||||||
hierarchy,
|
|
||||||
weight = "normal",
|
|
||||||
tag = "span",
|
|
||||||
children,
|
|
||||||
classes,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={tag}
|
component={props.tag || "span"}
|
||||||
class={cx(
|
class={cx(
|
||||||
classes,
|
props.class,
|
||||||
colorMap[color],
|
colorMap[props.color || "primary"],
|
||||||
inverted && "fnt-clr--inverted",
|
props.inverted && "fnt-clr--inverted",
|
||||||
sizeHierarchyMap[hierarchy][size] as string,
|
sizeHierarchyMap[props.hierarchy][props.size] as string,
|
||||||
weightMap[weight],
|
weightMap[props.weight || "normal"],
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{props.children}
|
||||||
</Dynamic>
|
</Dynamic>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { JSX } from "solid-js";
|
import { JSX } from "solid-js";
|
||||||
import { Typography } from "../components/Typography";
|
import { Typography } from "../components/Typography";
|
||||||
|
import { BackButton } from "../components/BackButton";
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
title: string;
|
title: string;
|
||||||
toolbar?: JSX.Element;
|
toolbar?: JSX.Element;
|
||||||
|
showBack?: boolean;
|
||||||
}
|
}
|
||||||
export const Header = (props: HeaderProps) => {
|
export const Header = (props: HeaderProps) => {
|
||||||
return (
|
return (
|
||||||
<div class="navbar border-b px-6 py-4 border-def-3">
|
<div class="navbar border-b px-6 py-4 border-def-3">
|
||||||
<div class="flex-none">
|
<div class="flex-none">
|
||||||
|
{props.showBack && <BackButton />}
|
||||||
<span class="tooltip tooltip-bottom lg:hidden" data-tip="Menu">
|
<span class="tooltip tooltip-bottom lg:hidden" data-tip="Menu">
|
||||||
<label
|
<label
|
||||||
class="btn btn-square btn-ghost drawer-button"
|
class="btn btn-square btn-ghost drawer-button"
|
||||||
@@ -19,7 +22,7 @@ export const Header = (props: HeaderProps) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<Typography hierarchy="title" size="m" weight="medium">
|
<Typography hierarchy="title" size="m" weight="medium" class="">
|
||||||
{props.title}
|
{props.title}
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export const BlockDevicesView: Component = () => {
|
|||||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => loadDevices()}
|
onClick={() => loadDevices()}
|
||||||
startIcon={<Icon icon="Reload" />}
|
startIcon={<Icon icon="Update" />}
|
||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex max-w-screen-lg flex-col gap-4">
|
<div class="flex max-w-screen-lg flex-col gap-4">
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { callApi, ClanServiceInstance, SuccessQuery } from "@/src/api";
|
import { callApi, ClanServiceInstance, SuccessQuery } from "@/src/api";
|
||||||
import { BackButton } from "@/src/components/BackButton";
|
|
||||||
import { useParams } from "@solidjs/router";
|
import { useParams } from "@solidjs/router";
|
||||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||||
import { createSignal, For, Match, Switch } from "solid-js";
|
import { createSignal, For, Match, Switch } from "solid-js";
|
||||||
import { Show } from "solid-js";
|
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
FieldValues,
|
FieldValues,
|
||||||
@@ -15,9 +13,10 @@ import {
|
|||||||
} from "@modular-forms/solid";
|
} from "@modular-forms/solid";
|
||||||
import { TextInput } from "@/src/components/TextInput";
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { get_single_service, set_single_service } from "@/src/api/inventory";
|
import { set_single_service } from "@/src/api/inventory";
|
||||||
import { Button } from "@/src/components/button";
|
import { Button } from "@/src/components/button";
|
||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
|
import { Header } from "@/src/layout/header";
|
||||||
|
|
||||||
interface AdminModuleFormProps {
|
interface AdminModuleFormProps {
|
||||||
admin: AdminData;
|
admin: AdminData;
|
||||||
@@ -65,30 +64,17 @@ const EditClanForm = (props: EditClanFormProps) => {
|
|||||||
<Field name="icon">
|
<Field name="icon">
|
||||||
{(field) => (
|
{(field) => (
|
||||||
<>
|
<>
|
||||||
<figure class="p-1">
|
<div class="flex flex-col items-center">
|
||||||
<div class="flex flex-col items-center">
|
<div class="text-3xl text-primary-800">{curr_name()}</div>
|
||||||
<div class="text-3xl text-primary-800">{curr_name()}</div>
|
<div class="text-secondary-800">Wide settings</div>
|
||||||
<div class="text-secondary-800">Wide settings</div>
|
<Icon
|
||||||
</div>
|
class="mt-4"
|
||||||
</figure>
|
icon="ClanIcon"
|
||||||
<figure>
|
viewBox="0 0 72 89"
|
||||||
<Show
|
width={96}
|
||||||
when={field.value}
|
height={96}
|
||||||
fallback={
|
/>
|
||||||
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
|
</div>
|
||||||
group
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(icon) => (
|
|
||||||
<img
|
|
||||||
class="aspect-square size-60 rounded-lg"
|
|
||||||
src={icon()}
|
|
||||||
alt="Clan Logo"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
</figure>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
@@ -344,7 +330,6 @@ type AdminData = ClanServiceInstance<"admin">;
|
|||||||
|
|
||||||
export const ClanDetails = () => {
|
export const ClanDetails = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const clan_dir = window.atob(params.id);
|
const clan_dir = window.atob(params.id);
|
||||||
// Fetch general meta data
|
// Fetch general meta data
|
||||||
const clanQuery = createQuery(() => ({
|
const clanQuery = createQuery(() => ({
|
||||||
@@ -355,48 +340,17 @@ export const ClanDetails = () => {
|
|||||||
return result.data;
|
return result.data;
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
// Fetch admin settings
|
|
||||||
const adminQuery = createQuery(() => ({
|
|
||||||
queryKey: [clan_dir, "inventory", "services", "admin"],
|
|
||||||
queryFn: async () => {
|
|
||||||
const result = await get_single_service(queryClient, clan_dir, "admin");
|
|
||||||
if (!result) throw new Error("Failed to fetch data");
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="card card-normal">
|
<>
|
||||||
<BackButton />
|
<Header title={clan_dir} showBack />
|
||||||
<Show
|
<div class="flex flex-col justify-center">
|
||||||
when={!adminQuery.isLoading}
|
|
||||||
fallback={
|
|
||||||
<div>
|
|
||||||
<span class="loading loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Switch fallback={<>General data not available</>}>
|
<Switch fallback={<>General data not available</>}>
|
||||||
<Match when={clanQuery.data}>
|
<Match when={clanQuery.data}>
|
||||||
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
|
{(d) => <EditClanForm initial={d()} directory={clan_dir} />}
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Show>
|
</div>
|
||||||
<div class="divider"></div>
|
</>
|
||||||
<Show
|
|
||||||
when={!adminQuery.isLoading}
|
|
||||||
fallback={
|
|
||||||
<div>
|
|
||||||
<span class="loading loading-lg"></span>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Switch fallback={<>Admin data not available</>}>
|
|
||||||
<Match when={adminQuery.data}>
|
|
||||||
{(d) => <AdminModuleForm admin={d()} base_url={clan_dir} />}
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { FileInput } from "@/src/components/FileInput";
|
|||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
import { SelectInput } from "@/src/components/SelectInput";
|
import { SelectInput } from "@/src/components/SelectInput";
|
||||||
import { TextInput } from "@/src/components/TextInput";
|
import { TextInput } from "@/src/components/TextInput";
|
||||||
|
import { Typography } from "@/src/components/Typography";
|
||||||
|
import { Header } from "@/src/layout/header";
|
||||||
import {
|
import {
|
||||||
createForm,
|
createForm,
|
||||||
required,
|
required,
|
||||||
@@ -169,292 +171,308 @@ export const Flash = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="m-4 rounded-lg bg-slate-50 p-4 pt-8 shadow-sm shadow-slate-400">
|
<>
|
||||||
<Form onSubmit={handleSubmit}>
|
<Header title="Flash installer" />
|
||||||
<div class="my-4">
|
<div class="p-4">
|
||||||
<Field name="sshKeys" type="File[]">
|
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
||||||
{(field, props) => (
|
USB Utility image.
|
||||||
<>
|
</Typography>
|
||||||
<FileInput
|
<Typography tag="p" hierarchy="body" size="default" color="secondary">
|
||||||
{...props}
|
This will make bootstrapping a new machine easier by providing secure
|
||||||
onClick={async (event) => {
|
remote connection to any machine when plugged in.
|
||||||
event.preventDefault(); // Prevent the native file dialog from opening
|
</Typography>
|
||||||
const input = event.target;
|
<Form onSubmit={handleSubmit}>
|
||||||
const files = await selectSshKeys();
|
<div class="my-4">
|
||||||
|
<Field name="sshKeys" type="File[]">
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<FileInput
|
||||||
|
{...props}
|
||||||
|
onClick={async (event) => {
|
||||||
|
event.preventDefault(); // Prevent the native file dialog from opening
|
||||||
|
const input = event.target;
|
||||||
|
const files = await selectSshKeys();
|
||||||
|
|
||||||
// Set the files
|
// Set the files
|
||||||
Object.defineProperty(input, "files", {
|
Object.defineProperty(input, "files", {
|
||||||
value: files,
|
value: files,
|
||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
// Define the files property on the input element
|
// Define the files property on the input element
|
||||||
const changeEvent = new Event("input", {
|
const changeEvent = new Event("input", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
input.dispatchEvent(changeEvent);
|
input.dispatchEvent(changeEvent);
|
||||||
}}
|
}}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
helperText="Provide your SSH public key. For secure and passwordless SSH connections."
|
helperText="Provide your SSH public key. For secure and passwordless SSH connections."
|
||||||
label="Authorized SSH Keys"
|
label="Authorized SSH Keys"
|
||||||
multiple
|
multiple
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Field name="disk" validate={[required("This field is required")]}>
|
||||||
|
{(field, props) => (
|
||||||
|
<SelectInput
|
||||||
|
topRightLabel={
|
||||||
|
<Button
|
||||||
|
size="s"
|
||||||
|
variant="light"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
deviceQuery.refetch();
|
||||||
|
}}
|
||||||
|
startIcon={<Icon icon="Update" />}
|
||||||
|
></Button>
|
||||||
|
}
|
||||||
|
formStore={formStore}
|
||||||
|
selectProps={props}
|
||||||
|
label="Flash Disk"
|
||||||
|
value={String(field.value)}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
options={
|
||||||
|
<>
|
||||||
|
<option value="" disabled>
|
||||||
|
Select a disk where the installer will be flashed to
|
||||||
|
</option>
|
||||||
|
|
||||||
|
<For each={deviceQuery.data?.blockdevices}>
|
||||||
|
{(device) => (
|
||||||
|
<option value={device.path}>
|
||||||
|
{device.path} -- {device.size} bytes
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Field>
|
</Field>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Field name="disk" validate={[required("This field is required")]}>
|
{/* WiFi Networks */}
|
||||||
{(field, props) => (
|
<div class="my-4 py-2">
|
||||||
<SelectInput
|
<h3 class="mb-2 text-lg font-semibold">WiFi Networks</h3>
|
||||||
topRightLabel={
|
<span class="mb-2 text-sm">Add preconfigured networks</span>
|
||||||
<Button
|
<For each={wifiNetworks()}>
|
||||||
size="s"
|
{(network, index) => (
|
||||||
variant="light"
|
<div class="mb-2 grid grid-cols-7 gap-2">
|
||||||
onClick={(e) => {
|
<Field
|
||||||
e.preventDefault();
|
name={`wifi.${index()}.ssid`}
|
||||||
deviceQuery.refetch();
|
validate={[required("SSID is required")]}
|
||||||
}}
|
>
|
||||||
startIcon={<Icon icon="Reload" />}
|
{(field, props) => (
|
||||||
></Button>
|
|
||||||
}
|
|
||||||
formStore={formStore}
|
|
||||||
selectProps={props}
|
|
||||||
label="Flash Disk"
|
|
||||||
value={String(field.value)}
|
|
||||||
error={field.error}
|
|
||||||
required
|
|
||||||
options={
|
|
||||||
<>
|
|
||||||
<option value="" disabled>
|
|
||||||
Select a disk where the installer will be flashed to
|
|
||||||
</option>
|
|
||||||
|
|
||||||
<For each={deviceQuery.data?.blockdevices}>
|
|
||||||
{(device) => (
|
|
||||||
<option value={device.path}>
|
|
||||||
{device.path} -- {device.size} bytes
|
|
||||||
</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
{/* WiFi Networks */}
|
|
||||||
<div class="my-4 py-2">
|
|
||||||
<h3 class="mb-2 text-lg font-semibold">WiFi Networks</h3>
|
|
||||||
<span class="mb-2 text-sm">Add preconfigured networks</span>
|
|
||||||
<For each={wifiNetworks()}>
|
|
||||||
{(network, index) => (
|
|
||||||
<div class="mb-2 grid grid-cols-7 gap-2">
|
|
||||||
<Field
|
|
||||||
name={`wifi.${index()}.ssid`}
|
|
||||||
validate={[required("SSID is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<TextInput
|
|
||||||
formStore={formStore}
|
|
||||||
inputProps={props}
|
|
||||||
label="SSID"
|
|
||||||
value={field.value ?? ""}
|
|
||||||
error={field.error}
|
|
||||||
class="col-span-3"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
name={`wifi.${index()}.password`}
|
|
||||||
validate={[required("Password is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<div class="relative col-span-3 w-full">
|
|
||||||
<TextInput
|
<TextInput
|
||||||
formStore={formStore}
|
formStore={formStore}
|
||||||
inputProps={props}
|
inputProps={props}
|
||||||
type={
|
label="SSID"
|
||||||
passwordVisibility()[index()] ? "text" : "password"
|
|
||||||
}
|
|
||||||
label="Password"
|
|
||||||
value={field.value ?? ""}
|
value={field.value ?? ""}
|
||||||
error={field.error}
|
error={field.error}
|
||||||
adornment={{
|
class="col-span-3"
|
||||||
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
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</Field>
|
||||||
</Field>
|
<Field
|
||||||
<div class="col-span-1 self-end">
|
name={`wifi.${index()}.password`}
|
||||||
<Button
|
validate={[required("Password is required")]}
|
||||||
type="button"
|
>
|
||||||
variant="light"
|
{(field, props) => (
|
||||||
class="h-12"
|
<div class="relative col-span-3 w-full">
|
||||||
onClick={() => removeWifiNetwork(index())}
|
<TextInput
|
||||||
startIcon={<Icon icon="Trash" />}
|
formStore={formStore}
|
||||||
></Button>
|
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
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<div class="col-span-1 self-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="light"
|
||||||
|
class="h-12"
|
||||||
|
onClick={() => removeWifiNetwork(index())}
|
||||||
|
startIcon={<Icon icon="Trash" />}
|
||||||
|
></Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
<div class="">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="s"
|
||||||
|
variant="light"
|
||||||
|
onClick={addWifiNetwork}
|
||||||
|
startIcon={<Icon icon="Plus" />}
|
||||||
|
>
|
||||||
|
Add WiFi Network
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse collapse-arrow" tabindex="0">
|
||||||
|
<input type="checkbox" />
|
||||||
|
<div class="collapse-title link font-medium ">
|
||||||
|
Advanced Settings
|
||||||
|
</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<Field
|
||||||
|
name="machine.flake"
|
||||||
|
validate={[required("This field is required")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label="Source (flake URL)"
|
||||||
|
value={String(field.value)}
|
||||||
|
inlineLabel={
|
||||||
|
<span class="material-icons">file_download</span>
|
||||||
|
}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
name="machine.devicePath"
|
||||||
|
validate={[required("This field is required")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
formStore={formStore}
|
||||||
|
inputProps={props}
|
||||||
|
label="Image Name (attribute name)"
|
||||||
|
value={String(field.value)}
|
||||||
|
inlineLabel={<span class="material-icons">devices</span>}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
<div class="my-2 py-2">
|
||||||
|
<span class="text-sm text-neutral-600">Source URL: </span>
|
||||||
|
<span class="text-sm text-neutral-600">
|
||||||
|
{getValue(formStore, "machine.flake") +
|
||||||
|
"#" +
|
||||||
|
getValue(formStore, "machine.devicePath")}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
<Field
|
||||||
</For>
|
name="language"
|
||||||
<div class="">
|
validate={[required("This field is required")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
formStore={formStore}
|
||||||
|
selectProps={props}
|
||||||
|
label="Language"
|
||||||
|
value={String(field.value)}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
options={
|
||||||
|
<>
|
||||||
|
<option value={"en_US.UTF-8"}>{"en_US.UTF-8"}</option>
|
||||||
|
<For each={langQuery.data}>
|
||||||
|
{(language) => (
|
||||||
|
<option value={language}>{language}</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
name="keymap"
|
||||||
|
validate={[required("This field is required")]}
|
||||||
|
>
|
||||||
|
{(field, props) => (
|
||||||
|
<>
|
||||||
|
<SelectInput
|
||||||
|
formStore={formStore}
|
||||||
|
selectProps={props}
|
||||||
|
label="Keymap"
|
||||||
|
value={String(field.value)}
|
||||||
|
error={field.error}
|
||||||
|
required
|
||||||
|
options={
|
||||||
|
<>
|
||||||
|
<option value={"en"}>{"en"}</option>
|
||||||
|
<For each={keymapQuery.data}>
|
||||||
|
{(keymap) => (
|
||||||
|
<option value={keymap}>{keymap}</option>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr></hr>
|
||||||
|
<div class="mt-2 flex justify-end pt-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
class="self-end"
|
||||||
size="s"
|
type="submit"
|
||||||
variant="light"
|
disabled={formStore.submitting}
|
||||||
onClick={addWifiNetwork}
|
startIcon={
|
||||||
startIcon={<Icon icon="Plus" />}
|
formStore.submitting ? (
|
||||||
|
<Icon icon="Load" />
|
||||||
|
) : (
|
||||||
|
<Icon icon="Flash" />
|
||||||
|
)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
Add WiFi Network
|
{formStore.submitting ? "Flashing..." : "Flash Installer"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Form>
|
||||||
|
</div>
|
||||||
<div class="collapse collapse-arrow" tabindex="0">
|
</>
|
||||||
<input type="checkbox" />
|
|
||||||
<div class="collapse-title link font-medium ">Advanced Settings</div>
|
|
||||||
<div class="collapse-content">
|
|
||||||
<Field
|
|
||||||
name="machine.flake"
|
|
||||||
validate={[required("This field is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
formStore={formStore}
|
|
||||||
inputProps={props}
|
|
||||||
label="Source (flake URL)"
|
|
||||||
value={String(field.value)}
|
|
||||||
inlineLabel={
|
|
||||||
<span class="material-icons">file_download</span>
|
|
||||||
}
|
|
||||||
error={field.error}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<Field
|
|
||||||
name="machine.devicePath"
|
|
||||||
validate={[required("This field is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<TextInput
|
|
||||||
formStore={formStore}
|
|
||||||
inputProps={props}
|
|
||||||
label="Image Name (attribute name)"
|
|
||||||
value={String(field.value)}
|
|
||||||
inlineLabel={<span class="material-icons">devices</span>}
|
|
||||||
error={field.error}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
<div class="my-2 py-2">
|
|
||||||
<span class="text-sm text-neutral-600">Source URL: </span>
|
|
||||||
<span class="text-sm text-neutral-600">
|
|
||||||
{getValue(formStore, "machine.flake") +
|
|
||||||
"#" +
|
|
||||||
getValue(formStore, "machine.devicePath")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<Field
|
|
||||||
name="language"
|
|
||||||
validate={[required("This field is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<SelectInput
|
|
||||||
formStore={formStore}
|
|
||||||
selectProps={props}
|
|
||||||
label="Language"
|
|
||||||
value={String(field.value)}
|
|
||||||
error={field.error}
|
|
||||||
required
|
|
||||||
options={
|
|
||||||
<>
|
|
||||||
<option value={"en_US.UTF-8"}>{"en_US.UTF-8"}</option>
|
|
||||||
<For each={langQuery.data}>
|
|
||||||
{(language) => (
|
|
||||||
<option value={language}>{language}</option>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
|
|
||||||
<Field
|
|
||||||
name="keymap"
|
|
||||||
validate={[required("This field is required")]}
|
|
||||||
>
|
|
||||||
{(field, props) => (
|
|
||||||
<>
|
|
||||||
<SelectInput
|
|
||||||
formStore={formStore}
|
|
||||||
selectProps={props}
|
|
||||||
label="Keymap"
|
|
||||||
value={String(field.value)}
|
|
||||||
error={field.error}
|
|
||||||
required
|
|
||||||
options={
|
|
||||||
<>
|
|
||||||
<option value={"en"}>{"en"}</option>
|
|
||||||
<For each={keymapQuery.data}>
|
|
||||||
{(keymap) => <option value={keymap}>{keymap}</option>}
|
|
||||||
</For>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr></hr>
|
|
||||||
<div class="mt-2 flex justify-end pt-2">
|
|
||||||
<Button
|
|
||||||
class="self-end"
|
|
||||||
type="submit"
|
|
||||||
disabled={formStore.submitting}
|
|
||||||
startIcon={
|
|
||||||
formStore.submitting ? (
|
|
||||||
<Icon icon="Load" />
|
|
||||||
) : (
|
|
||||||
<Icon icon="Flash" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formStore.submitting ? "Flashing..." : "Flash Installer"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { callApi, SuccessData, SuccessQuery } from "@/src/api";
|
import { callApi, SuccessData, SuccessQuery } from "@/src/api";
|
||||||
import { activeURI } from "@/src/App";
|
import { activeURI } from "@/src/App";
|
||||||
import { BackButton } from "@/src/components/BackButton";
|
|
||||||
import { Button } from "@/src/components/button";
|
import { Button } from "@/src/components/button";
|
||||||
import { FileInput } from "@/src/components/FileInput";
|
import { FileInput } from "@/src/components/FileInput";
|
||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
@@ -17,6 +16,7 @@ import { createQuery } from "@tanstack/solid-query";
|
|||||||
import { createSignal, For, Show } from "solid-js";
|
import { createSignal, For, Show } from "solid-js";
|
||||||
import toast from "solid-toast";
|
import toast from "solid-toast";
|
||||||
import { MachineAvatar } from "./avatar";
|
import { MachineAvatar } from "./avatar";
|
||||||
|
import { Header } from "@/src/layout/header";
|
||||||
|
|
||||||
type MachineFormInterface = MachineData & {
|
type MachineFormInterface = MachineData & {
|
||||||
sshKey?: File;
|
sshKey?: File;
|
||||||
@@ -502,19 +502,21 @@ export const MachineDetails = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="card">
|
<>
|
||||||
<BackButton />
|
<Header title={`${params.id} machine`} showBack />
|
||||||
<Show
|
<div class="card">
|
||||||
when={genericQuery.data}
|
<Show
|
||||||
fallback={<span class="loading loading-lg"></span>}
|
when={genericQuery.data}
|
||||||
>
|
fallback={<span class="loading loading-lg"></span>}
|
||||||
{(data) => (
|
>
|
||||||
<>
|
{(data) => (
|
||||||
<MachineForm initialData={data()} />
|
<>
|
||||||
</>
|
<MachineForm initialData={data()} />
|
||||||
)}
|
</>
|
||||||
</Show>
|
)}
|
||||||
</div>
|
</Show>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { useNavigate } from "@solidjs/router";
|
|||||||
import { Button } from "@/src/components/button";
|
import { Button } from "@/src/components/button";
|
||||||
import Icon from "@/src/components/icon";
|
import Icon from "@/src/components/icon";
|
||||||
import { Header } from "@/src/layout/header";
|
import { Header } from "@/src/layout/header";
|
||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
|
||||||
type MachinesModel = Extract<
|
type MachinesModel = Extract<
|
||||||
OperationResponse<"list_inventory_machines">,
|
OperationResponse<"list_inventory_machines">,
|
||||||
@@ -31,7 +32,7 @@ export const MachineListView: Component = () => {
|
|||||||
const [filter, setFilter] = createSignal<Filter>({ tags: [] });
|
const [filter, setFilter] = createSignal<Filter>({ tags: [] });
|
||||||
|
|
||||||
const inventoryQuery = createQuery<MachinesModel>(() => ({
|
const inventoryQuery = createQuery<MachinesModel>(() => ({
|
||||||
queryKey: [activeURI(), "list_machines", "inventory"],
|
queryKey: [activeURI(), "list_inventory_machines"],
|
||||||
placeholderData: {},
|
placeholderData: {},
|
||||||
enabled: !!activeURI(),
|
enabled: !!activeURI(),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -65,6 +66,10 @@ export const MachineListView: Component = () => {
|
|||||||
});
|
});
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [view, setView] = makePersisted(createSignal<"list" | "grid">("list"), {
|
||||||
|
name: "machines_view",
|
||||||
|
storage: localStorage,
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header
|
<Header
|
||||||
@@ -76,21 +81,23 @@ export const MachineListView: Component = () => {
|
|||||||
variant="light"
|
variant="light"
|
||||||
size="s"
|
size="s"
|
||||||
onClick={() => refresh()}
|
onClick={() => refresh()}
|
||||||
startIcon={<Icon icon="Reload" />}
|
startIcon={<Icon icon="Update" />}
|
||||||
></Button>
|
></Button>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="border border-def-3">
|
<div class="border border-def-3">
|
||||||
<span class="tooltip tooltip-bottom" data-tip="List View">
|
<span class="tooltip tooltip-bottom" data-tip="List View">
|
||||||
<Button
|
<Button
|
||||||
variant="dark"
|
onclick={() => setView("list")}
|
||||||
|
variant={view() == "list" ? "dark" : "light"}
|
||||||
size="s"
|
size="s"
|
||||||
startIcon={<Icon icon="List" />}
|
startIcon={<Icon icon="List" />}
|
||||||
></Button>
|
></Button>
|
||||||
</span>
|
</span>
|
||||||
<span class="tooltip tooltip-bottom" data-tip="Grid View">
|
<span class="tooltip tooltip-bottom" data-tip="Grid View">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
onclick={() => setView("grid")}
|
||||||
|
variant={view() == "grid" ? "dark" : "light"}
|
||||||
size="s"
|
size="s"
|
||||||
startIcon={<Icon icon="Grid" />}
|
startIcon={<Icon icon="Grid" />}
|
||||||
></Button>
|
></Button>
|
||||||
@@ -110,7 +117,6 @@ export const MachineListView: Component = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
{/* <Show when={filter()}> */}
|
|
||||||
<div class="my-1 flex w-full gap-2 p-2">
|
<div class="my-1 flex w-full gap-2 p-2">
|
||||||
<div class="size-6 p-1">
|
<div class="size-6 p-1">
|
||||||
<Icon icon="Filter" />
|
<Icon icon="Filter" />
|
||||||
@@ -170,7 +176,13 @@ export const MachineListView: Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={!inventoryQuery.isLoading}>
|
<Match when={!inventoryQuery.isLoading}>
|
||||||
<ul>
|
<div
|
||||||
|
class="my-4 flex flex-wrap gap-6 px-3 py-2"
|
||||||
|
classList={{
|
||||||
|
"flex-col": view() === "list",
|
||||||
|
"": view() === "grid",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<For each={inventoryMachines()}>
|
<For each={inventoryMachines()}>
|
||||||
{([name, info]) => (
|
{([name, info]) => (
|
||||||
<MachineListItem
|
<MachineListItem
|
||||||
@@ -180,7 +192,7 @@ export const MachineListView: Component = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</ul>
|
</div>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,48 +1,179 @@
|
|||||||
import { callApi, SuccessData } from "@/src/api";
|
import { SuccessData } from "@/src/api";
|
||||||
import { activeURI } from "@/src/App";
|
import { activeURI } from "@/src/App";
|
||||||
|
import { Button } from "@/src/components/button";
|
||||||
|
import { Header } from "@/src/layout/header";
|
||||||
import { createModulesQuery } from "@/src/queries";
|
import { createModulesQuery } from "@/src/queries";
|
||||||
import { A, useNavigate } from "@solidjs/router";
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
import { createSignal, For, Match, Switch } from "solid-js";
|
||||||
import { createEffect, For, Match, Switch } from "solid-js";
|
import { Typography } from "@/src/components/Typography";
|
||||||
import { SolidMarkdown } from "solid-markdown";
|
import { Menu } from "@/src/components/Menu";
|
||||||
|
import { makePersisted } from "@solid-primitives/storage";
|
||||||
|
import { useQueryClient } from "@tanstack/solid-query";
|
||||||
|
import cx from "classnames";
|
||||||
|
import Icon from "@/src/components/icon";
|
||||||
|
|
||||||
export type ModuleInfo = SuccessData<"list_modules">[string];
|
export type ModuleInfo = SuccessData<"list_modules">[string];
|
||||||
|
|
||||||
const ModuleListItem = (props: { name: string; info: ModuleInfo }) => {
|
interface CategoryProps {
|
||||||
|
categories: string[];
|
||||||
|
}
|
||||||
|
const Categories = (props: CategoryProps) => {
|
||||||
|
return (
|
||||||
|
<span class="ml-6 inline-flex h-full align-middle">
|
||||||
|
{props.categories.map((category) => (
|
||||||
|
<span class="badge badge-secondary">{category}</span>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface RolesProps {
|
||||||
|
roles: string[];
|
||||||
|
}
|
||||||
|
const Roles = (props: RolesProps) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
<Typography hierarchy="body" size="xs">
|
||||||
|
Service Typography{" "}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
{props.roles.map((role) => (
|
||||||
|
<span class="badge badge-ghost">{role}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ModuleItem = (props: {
|
||||||
|
name: string;
|
||||||
|
info: ModuleInfo;
|
||||||
|
class?: string;
|
||||||
|
}) => {
|
||||||
const { name, info } = props;
|
const { name, info } = props;
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="stat">
|
<div class={cx("stat rounded-lg shadow-md", props.class)}>
|
||||||
<div class="stat-figure text-primary-800">
|
<div class="stat-figure text-primary-800">
|
||||||
<div class="join">more</div>
|
<div class="join">
|
||||||
|
<Menu popoverid={`menu-${props.name}`} label={<Icon icon={"More"} />}>
|
||||||
|
<ul class="menu z-[1] w-52 rounded-box bg-base-100 p-2 shadow">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
onClick={() => {
|
||||||
|
navigate(`/modules/details/${name}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Configure
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Menu>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<A href={`/modules/details/${name}`}>
|
<A href={`/modules/details/${name}`}>
|
||||||
<div class="stat-value underline">{name}</div>
|
<div class="stat-value underline">
|
||||||
|
{name}
|
||||||
|
<Categories categories={info.categories} />
|
||||||
|
</div>
|
||||||
</A>
|
</A>
|
||||||
|
|
||||||
<div>{info.description}</div>
|
<div class="w-full">
|
||||||
<div>{JSON.stringify(info.constraints)}</div>
|
<Typography hierarchy="body" size="default">
|
||||||
|
{info.description}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<Roles roles={info.roles || []} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ModuleList = () => {
|
export const ModuleList = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
const modulesQuery = createModulesQuery(activeURI(), {
|
const modulesQuery = createModulesQuery(activeURI(), {
|
||||||
features: ["inventory"],
|
features: ["inventory"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [view, setView] = makePersisted(createSignal<"list" | "grid">("list"), {
|
||||||
|
name: "modules_view",
|
||||||
|
storage: localStorage,
|
||||||
|
});
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
// Invalidates the cache for of all types of machine list at once
|
||||||
|
queryKey: [activeURI(), "list_modules"],
|
||||||
|
});
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Switch fallback="Shit">
|
<>
|
||||||
<Match when={modulesQuery.isLoading}>Loading....</Match>
|
<Header
|
||||||
<Match when={modulesQuery.data}>
|
title="Modules"
|
||||||
<div>
|
toolbar={
|
||||||
Show Modules
|
<>
|
||||||
<For each={modulesQuery.data}>
|
<span class="tooltip tooltip-bottom" data-tip="Reload">
|
||||||
{([k, v]) => <ModuleListItem info={v} name={k} />}
|
<Button
|
||||||
</For>
|
variant="light"
|
||||||
</div>
|
size="s"
|
||||||
</Match>
|
onClick={() => refresh()}
|
||||||
</Switch>
|
startIcon={<Icon icon="Update" />}
|
||||||
|
></Button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="border border-def-3">
|
||||||
|
<span class="tooltip tooltip-bottom" data-tip="List View">
|
||||||
|
<Button
|
||||||
|
onclick={() => setView("list")}
|
||||||
|
variant={view() == "list" ? "dark" : "light"}
|
||||||
|
size="s"
|
||||||
|
startIcon={<Icon icon="List" />}
|
||||||
|
></Button>
|
||||||
|
</span>
|
||||||
|
<span class="tooltip tooltip-bottom" data-tip="Grid View">
|
||||||
|
<Button
|
||||||
|
onclick={() => setView("grid")}
|
||||||
|
variant={view() == "grid" ? "dark" : "light"}
|
||||||
|
size="s"
|
||||||
|
startIcon={<Icon icon="Grid" />}
|
||||||
|
></Button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="tooltip tooltip-bottom" data-tip="New Machine">
|
||||||
|
<Button
|
||||||
|
size="s"
|
||||||
|
variant="light"
|
||||||
|
startIcon={<Icon icon="CaretUp" />}
|
||||||
|
>
|
||||||
|
Import Module
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Switch fallback="Error">
|
||||||
|
<Match when={modulesQuery.isFetching}>Loading....</Match>
|
||||||
|
<Match when={modulesQuery.data}>
|
||||||
|
<div
|
||||||
|
class="my-4 flex flex-wrap gap-6 px-3 py-2"
|
||||||
|
classList={{
|
||||||
|
"flex-col": view() === "list",
|
||||||
|
"": view() === "grid",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<For each={modulesQuery.data}>
|
||||||
|
{([k, v]) => (
|
||||||
|
<ModuleItem
|
||||||
|
info={v}
|
||||||
|
name={k}
|
||||||
|
class={view() == "grid" ? cx("max-w-md") : ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user