Merge pull request 'Clan-app: edit clan, memoize active clan' (#1819) from hsjobeki/clan-core:hsjobeki-main into main

This commit is contained in:
clan-bot
2024-07-29 15:10:00 +00:00
7 changed files with 286 additions and 78 deletions

View File

@@ -7,10 +7,10 @@ from types import ModuleType
# These imports are unused, but necessary for @API.register to run once.
from clan_cli.api import directory, mdns_discovery, modules
from clan_cli.arg_actions import AppendOptionAction
from clan_cli.clan import show
from clan_cli.clan import show, update
# API endpoints that are not used in the cli.
__all__ = ["directory", "mdns_discovery", "modules"]
__all__ = ["directory", "mdns_discovery", "modules", "update"]
from . import (
backups,

View File

@@ -1,4 +1,4 @@
import { createSignal, type Component } from "solid-js";
import { createEffect, createSignal, type Component } from "solid-js";
import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes";
import { Toaster } from "solid-toast";
@@ -7,9 +7,20 @@ import { makePersisted } from "@solid-primitives/storage";
// Some global state
const [route, setRoute] = createSignal<Route>("machines");
createEffect(() => {
console.log(route());
});
export { route, setRoute };
const [activeURI, setActiveURI] = createSignal<string | null>(null);
const [activeURI, setActiveURI] = makePersisted(
createSignal<string | null>(null),
{
name: "activeURI",
storage: localStorage,
}
);
export { activeURI, setActiveURI };
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
@@ -17,8 +28,6 @@ const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
storage: localStorage,
});
clanList() && setActiveURI(clanList()[0]);
export { clanList, setClanList };
const App: Component = () => {

View File

@@ -1,15 +1,20 @@
import { createQuery } from "@tanstack/solid-query";
import { activeURI, setRoute } from "../App";
import { callApi } from "../api";
import { Show } from "solid-js";
import { Accessor, createEffect, Show } from "solid-js";
export const Header = () => {
const { isLoading, data } = createQuery(() => ({
queryKey: [`${activeURI()}:meta`],
interface HeaderProps {
clan_dir: Accessor<string | null>;
}
export const Header = (props: HeaderProps) => {
const { clan_dir } = props;
const query = createQuery(() => ({
queryKey: [clan_dir(), "meta"],
queryFn: async () => {
const currUri = activeURI();
if (currUri) {
const result = await callApi("show_clan_meta", { uri: currUri });
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;
}
@@ -29,16 +34,25 @@ export const Header = () => {
</span>
</div>
<div class="flex-1">
<div class="tooltip tooltip-right" data-tip={data?.name || activeURI()}>
<div class="avatar placeholder online mx-4">
<div class="w-10 rounded-full bg-slate-700 text-neutral-content">
<span class="text-xl">C</span>
<Show when={data?.name}>
{(name) => <span class="text-xl">{name()}</span>}
</Show>
<Show when={!query.isFetching && query.data}>
{(meta) => (
<div class="tooltip tooltip-right" data-tip={activeURI()}>
<div class="avatar placeholder online mx-4">
<div class="w-10 rounded-full bg-slate-700 text-3xl text-neutral-content">
{meta().name.slice(0, 1)}
</div>
</div>
</div>
</div>
</div>
)}
</Show>
<span class="flex flex-col">
<Show when={!query.isFetching && query.data}>
{(meta) => [
<span class="text-primary">{meta().name}</span>,
<span class="text-neutral">{meta()?.description}</span>,
]}
</Show>
</span>
</div>
<div class="flex-none">
<span class="tooltip tooltip-bottom" data-tip="Settings">

View File

@@ -1,7 +1,7 @@
import { Component, JSXElement, Show } from "solid-js";
import { Header } from "./header";
import { Sidebar } from "../Sidebar";
import { clanList, route, setRoute } from "../App";
import { activeURI, clanList, route, setRoute } from "../App";
interface LayoutProps {
children: JSXElement;
@@ -18,7 +18,7 @@ export const Layout: Component<LayoutProps> = (props) => {
/>
<div class="drawer-content">
<Show when={route() !== "welcome"}>
<Header />
<Header clan_dir={activeURI} />
</Show>
{props.children}
</div>

View File

@@ -0,0 +1,175 @@
import { OperationResponse, callApi, pyApi } from "@/src/api";
import { Accessor, Show, Switch, Match } from "solid-js";
import {
SubmitHandler,
createForm,
required,
reset,
} from "@modular-forms/solid";
import toast from "solid-toast";
import { createQuery } from "@tanstack/solid-query";
type CreateForm = Meta;
interface EditClanFormProps {
directory: Accessor<string>;
done: () => void;
}
export const EditClanForm = (props: EditClanFormProps) => {
const { directory } = props;
const details = createQuery(() => ({
queryKey: [directory(), "meta"],
queryFn: async () => {
const result = await callApi("show_clan_meta", { uri: directory() });
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
},
}));
return (
<Switch>
<Match when={details?.data}>
{(data) => (
<FinalEditClanForm
initial={data()}
directory={directory()}
done={props.done}
/>
)}
</Match>
</Switch>
);
};
interface FinalEditClanFormProps {
initial: CreateForm;
directory: string;
done: () => void;
}
export const FinalEditClanForm = (props: FinalEditClanFormProps) => {
const [formStore, { Form, Field }] = createForm<CreateForm>({
initialValues: props.initial,
});
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
await toast.promise(
(async () => {
await callApi("update_clan_meta", {
options: {
directory: props.directory,
meta: values,
},
});
})(),
{
loading: "Updating clan...",
success: "Clan Successfully updated",
error: "Failed to update clan",
}
);
props.done();
};
return (
<div class="card card-normal">
<Form onSubmit={handleSubmit} shouldActive>
<Field name="icon">
{(field, props) => (
<>
<figure>
<Show
when={field.value}
fallback={
<span class="material-icons aspect-square size-60 rounded-lg text-[18rem]">
group
</span>
}
>
{(icon) => (
<img
class="aspect-square size-60 rounded-lg"
src={icon()}
alt="Clan Logo"
/>
)}
</Show>
</figure>
</>
)}
</Field>
<div class="card-body">
<Field
name="name"
validate={[required("Please enter a unique name for the clan.")]}
>
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text block after:ml-0.5 after:text-primary after:content-['*']">
Name
</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
placeholder="Clan Name"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
<Field name="description">
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text">Description</span>
</div>
<input
{...props}
disabled={formStore.submitting}
required
type="text"
placeholder="Some words about your clan"
class="input input-bordered"
classList={{ "input-error": !!field.error }}
value={field.value || ""}
/>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
{
<div class="card-actions justify-end">
<button
class="btn btn-primary"
type="submit"
disabled={formStore.submitting}
>
Save
</button>
</div>
}
</div>
</Form>
</div>
);
};
type Meta = Extract<
OperationResponse<"show_clan_meta">,
{ status: "success" }
>["data"];

View File

@@ -2,24 +2,24 @@ import { callApi } from "@/src/api";
import { activeURI } from "@/src/App";
import { createQuery } from "@tanstack/solid-query";
import { createEffect } from "solid-js";
import toast from "solid-toast";
export function DiskView() {
const query = createQuery(() => ({
queryKey: ["disk", activeURI],
queryKey: ["disk", activeURI()],
queryFn: async () => {
const currUri = activeURI();
if (currUri) {
// Example of calling an API
const result = await callApi("get_inventory", { base_path: currUri });
if (result.status === "error") throw new Error("Failed to fetch data");
return result.data;
}
},
}));
createEffect(() => {
// Example debugging the data
console.log(query.data);
console.log(query);
});
return (
<div>

View File

@@ -12,18 +12,19 @@ import {
setRoute,
clanList,
} from "@/src/App";
import { createEffect, createSignal, For, Show } from "solid-js";
import {
createEffect,
createSignal,
For,
Match,
Setter,
Show,
Switch,
} from "solid-js";
import { createQuery } from "@tanstack/solid-query";
import { useFloating } from "@/src/floating";
import {
arrow,
autoUpdate,
flip,
hide,
offset,
shift,
size,
} from "@floating-ui/dom";
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
import { EditClanForm } from "../clan/editClan";
export const registerClan = async () => {
try {
@@ -51,9 +52,10 @@ export const registerClan = async () => {
interface ClanDetailsProps {
clan_dir: string;
setEditURI: Setter<string | null>;
}
const ClanDetails = (props: ClanDetailsProps) => {
const { clan_dir } = props;
const { clan_dir, setEditURI } = props;
const details = createQuery(() => ({
queryKey: [clan_dir, "meta"],
@@ -66,7 +68,6 @@ const ClanDetails = (props: ClanDetailsProps) => {
const [reference, setReference] = createSignal<HTMLElement>();
const [floating, setFloating] = createSignal<HTMLElement>();
const [arrowEl, setArrowEl] = createSignal<HTMLElement>();
// `position` is a reactive object.
const position = useFloating(reference, floating, {
@@ -92,6 +93,14 @@ const ClanDetails = (props: ClanDetailsProps) => {
<div class="stat">
<div class="stat-figure text-primary">
<div class="join">
<button
class=" join-item btn-sm"
onClick={() => {
setEditURI(clan_dir);
}}
>
<span class="material-icons">edit</span>
</button>
<button
class=" join-item btn-sm"
classList={{
@@ -145,56 +154,57 @@ const ClanDetails = (props: ClanDetailsProps) => {
</div>
</div>
</div>
<div class="stat-title">Clan URI</div>
<div class="stat-title">{clan_dir}</div>
<Show when={details.isSuccess}>
<div
class="stat-value"
// classList={{
// "text-primary": activeURI() === clan_dir,
// }}
>
{details.data?.name}
</div>
<div class="stat-value">{details.data?.name}</div>
</Show>
<Show
when={details.isSuccess && details.data?.description}
fallback={<div class="stat-desc text-lg">{clan_dir}</div>}
>
<div
class="stat-desc text-lg"
// classList={{
// "text-primary": activeURI() === clan_dir,
// }}
>
{details.data?.description}
</div>
<Show when={details.isSuccess && details.data?.description}>
<div class="stat-desc text-lg">{details.data?.description}</div>
</Show>
</div>
);
};
export const Settings = () => {
const [editURI, setEditURI] = createSignal<string | null>(null);
return (
<div class="card card-normal">
<div class="card-body">
<div class="label">
<div class="label-text">Registered Clans</div>
<button
class="btn btn-square btn-primary"
onClick={() => {
registerClan();
}}
>
<span class="material-icons">add</span>
</button>
</div>
<div class="stats stats-vertical shadow">
<For each={clanList()}>
{(value) => <ClanDetails clan_dir={value} />}
</For>
</div>
</div>
<Switch>
<Match when={editURI()}>
{(uri) => (
<EditClanForm
directory={uri}
done={() => {
setEditURI(null);
}}
/>
)}
</Match>
<Match when={!editURI()}>
<div class="card-body">
<div class="label">
<div class="label-text">Registered Clans</div>
<button
class="btn btn-square btn-primary"
onClick={() => {
registerClan();
}}
>
<span class="material-icons">add</span>
</button>
</div>
<div class="stats stats-vertical shadow">
<For each={clanList()}>
{(value) => (
<ClanDetails clan_dir={value} setEditURI={setEditURI} />
)}
</For>
</div>
</div>
</Match>
</Switch>
</div>
);
};