Merge pull request 'feat(ui): add a clan context provider' (#3744) from feat/clan-uri-context into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/3744
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import { createSignal } from "solid-js";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import { callApi } from "./api";
|
||||
|
||||
const [activeURI, setActiveURI] = makePersisted(
|
||||
createSignal<string | null>(null),
|
||||
{
|
||||
name: "activeURI",
|
||||
storage: localStorage,
|
||||
},
|
||||
);
|
||||
|
||||
export { activeURI, setActiveURI };
|
||||
|
||||
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||
name: "clanList",
|
||||
storage: localStorage,
|
||||
});
|
||||
|
||||
export { clanList, setClanList };
|
||||
|
||||
(async function () {
|
||||
const curr = activeURI();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: curr },
|
||||
});
|
||||
console.log("refetched meta for ", curr);
|
||||
if (result.status === "error") {
|
||||
result.errors.forEach((error) => {
|
||||
if (error.description === "clan directory does not exist") {
|
||||
setActiveURI(null);
|
||||
setClanList((clans) => clans.filter((clan) => clan !== curr));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// ensure to null out activeURI on startup if the clan was deleted
|
||||
// => throws user back to the view for selecting a clan
|
||||
@@ -1,14 +1,12 @@
|
||||
import { For, createEffect, Show, type JSX, children } from "solid-js";
|
||||
import { A, RouteSectionProps } from "@solidjs/router";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { callApi } from "@/src/api";
|
||||
import { AppRoute, routes } from "@/src/index";
|
||||
import { For, type JSX, Show } from "solid-js";
|
||||
import { RouteSectionProps } from "@solidjs/router";
|
||||
import { AppRoute, routes } from "@/src";
|
||||
import { SidebarHeader } from "./SidebarHeader";
|
||||
import { SidebarListItem } from "./SidebarListItem";
|
||||
import { Typography } from "../Typography";
|
||||
import "./css/sidebar.css";
|
||||
import Icon, { IconVariant } from "../icon";
|
||||
import { clanMetaQuery } from "@/src/queries/clan-meta";
|
||||
|
||||
export const SidebarSection = (props: {
|
||||
title: string;
|
||||
@@ -42,26 +40,7 @@ export const SidebarSection = (props: {
|
||||
};
|
||||
|
||||
export const Sidebar = (props: RouteSectionProps) => {
|
||||
createEffect(() => {
|
||||
console.log("machines");
|
||||
console.log(routes);
|
||||
});
|
||||
|
||||
const query = createQuery(() => ({
|
||||
queryKey: [activeURI(), "meta"],
|
||||
queryFn: async () => {
|
||||
const curr = activeURI();
|
||||
if (curr) {
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: curr },
|
||||
});
|
||||
console.log("refetched meta for ", curr);
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
|
||||
return result.data;
|
||||
}
|
||||
},
|
||||
}));
|
||||
const query = clanMetaQuery();
|
||||
|
||||
return (
|
||||
<div class="sidebar">
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { createSignal, For, Setter, Show } from "solid-js";
|
||||
import { createSignal, Setter } 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";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type MachineDetails = SuccessQuery<"list_inv_machines">["data"][string];
|
||||
|
||||
@@ -28,6 +27,8 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
// Later only updates
|
||||
const [updating, setUpdating] = createSignal<boolean>(false);
|
||||
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleInstall = async () => {
|
||||
@@ -35,7 +36,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const active_clan = activeURI();
|
||||
const active_clan = activeClanURI();
|
||||
if (!active_clan) {
|
||||
console.error("No active clan selected");
|
||||
return;
|
||||
@@ -70,7 +71,7 @@ export const MachineListItem = (props: MachineListItemProps) => {
|
||||
return;
|
||||
}
|
||||
|
||||
const active_clan = activeURI();
|
||||
const active_clan = activeClanURI();
|
||||
if (!active_clan) {
|
||||
console.error("No active clan selected");
|
||||
return;
|
||||
|
||||
65
pkgs/clan-app/ui/src/contexts/clan.tsx
Normal file
65
pkgs/clan-app/ui/src/contexts/clan.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { createContext, createEffect, JSX, useContext } from "solid-js";
|
||||
import { callApi } from "@/src/api";
|
||||
import {
|
||||
activeClanURI,
|
||||
addClanURI,
|
||||
clanURIs,
|
||||
removeClanURI,
|
||||
setActiveClanURI,
|
||||
store,
|
||||
} from "@/src/stores/clan";
|
||||
import { redirect } from "@solidjs/router";
|
||||
|
||||
// Create the context
|
||||
interface ClanContextType {
|
||||
activeClanURI: typeof activeClanURI;
|
||||
setActiveClanURI: typeof setActiveClanURI;
|
||||
clanURIs: typeof clanURIs;
|
||||
addClanURI: typeof addClanURI;
|
||||
removeClanURI: typeof removeClanURI;
|
||||
}
|
||||
|
||||
const ClanContext = createContext<ClanContextType>({
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
});
|
||||
|
||||
interface ClanProviderProps {
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function ClanProvider(props: ClanProviderProps) {
|
||||
// redirect to welcome if there's no active clan and no clan URIs
|
||||
createEffect(async () => {
|
||||
if (!store.activeClanURI && store.clanURIs.length == 0) {
|
||||
redirect("/welcome");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<ClanContext.Provider
|
||||
value={{
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</ClanContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
// Export a hook that provides access to the context
|
||||
export function useClanContext() {
|
||||
const context = useContext(ClanContext);
|
||||
if (!context) {
|
||||
throw new Error("useClanContext must be used within a ClanProvider");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import { callApi } from "../api";
|
||||
import { setActiveURI, setClanList } from "../App";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export const registerClan = async () => {
|
||||
const { setActiveClanURI, addClanURI } = useClanContext();
|
||||
|
||||
try {
|
||||
const loc = await callApi("open_file", {
|
||||
file_request: { mode: "select_folder" },
|
||||
});
|
||||
if (loc.status === "success" && loc.data) {
|
||||
const data = loc.data[0];
|
||||
setClanList((s) => {
|
||||
const res = new Set([...s, data]);
|
||||
return Array.from(res);
|
||||
});
|
||||
setActiveURI(data);
|
||||
addClanURI(data);
|
||||
setActiveClanURI(data);
|
||||
return data;
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -5,12 +5,12 @@ import { Navigate, RouteDefinition, Router } from "@solidjs/router";
|
||||
import "./index.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/solid-query";
|
||||
import {
|
||||
CreateMachine,
|
||||
MachineDetails,
|
||||
MachineListView,
|
||||
CreateMachine,
|
||||
} from "./routes/machines";
|
||||
import { Layout } from "./layout/layout";
|
||||
import { ClanList, CreateClan, ClanDetails } from "./routes/clans";
|
||||
import { ClanDetails, ClanList, CreateClan } from "./routes/clans";
|
||||
import { Flash } from "./routes/flash/view";
|
||||
import { HostList } from "./routes/hosts/view";
|
||||
import { Welcome } from "./routes/welcome";
|
||||
@@ -21,9 +21,9 @@ import { ModuleDetails as AddModule } from "./routes/modules/add";
|
||||
import { ApiTester } from "./api_test";
|
||||
import { IconVariant } from "./components/icon";
|
||||
import { Components } from "./routes/components";
|
||||
import { activeURI } from "./App";
|
||||
import { VarsPage, VarsForm } from "./routes/machines/install/vars-step";
|
||||
import { VarsPage } from "./routes/machines/install/vars-step";
|
||||
import { ThreePlayground } from "./three";
|
||||
import { ClanProvider } from "./contexts/clan";
|
||||
|
||||
export const client = new QueryClient();
|
||||
|
||||
@@ -186,7 +186,9 @@ render(
|
||||
<Toaster position="top-right" containerClassName="z-[9999]" />
|
||||
</Portal>
|
||||
<QueryClientProvider client={client}>
|
||||
<Router root={Layout}>{routes}</Router>
|
||||
<ClanProvider>
|
||||
<Router root={Layout}>{routes}</Router>
|
||||
</ClanProvider>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
),
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { Component, createEffect } from "solid-js";
|
||||
import { Sidebar } from "@/src/components/Sidebar";
|
||||
import { clanList } from "../App";
|
||||
import { RouteSectionProps, useNavigate } from "@solidjs/router";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export const Layout: Component<RouteSectionProps> = (props) => {
|
||||
const navigate = useNavigate();
|
||||
const { clanURIs } = useClanContext();
|
||||
createEffect(() => {
|
||||
console.log(
|
||||
"empty ClanList, redirect to welcome page",
|
||||
clanList().length === 0,
|
||||
clanURIs().length === 0,
|
||||
);
|
||||
if (clanList().length === 0) {
|
||||
if (clanURIs().length === 0) {
|
||||
navigate("/welcome");
|
||||
}
|
||||
});
|
||||
|
||||
37
pkgs/clan-app/ui/src/queries/clan-meta.ts
Normal file
37
pkgs/clan-app/ui/src/queries/clan-meta.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { callApi } from "@/src/api";
|
||||
import { activeClanURI, removeClanURI } from "@/src/stores/clan";
|
||||
|
||||
export const clanMetaQuery = (uri: string | undefined = undefined) =>
|
||||
useQuery(() => {
|
||||
const clanURI = uri || activeClanURI();
|
||||
const enabled = !!clanURI;
|
||||
|
||||
return {
|
||||
enabled,
|
||||
queryKey: [clanURI, "meta"],
|
||||
queryFn: async () => {
|
||||
console.log("fetching clan meta", clanURI);
|
||||
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: clanURI! },
|
||||
});
|
||||
|
||||
console.log("result", result);
|
||||
|
||||
if (result.status === "error") {
|
||||
// check if the clan directory no longer exists
|
||||
// remove from the clan list if not
|
||||
result.errors.forEach((error) => {
|
||||
if (error.description === "clan directory does not exist") {
|
||||
removeClanURI(clanURI!);
|
||||
}
|
||||
});
|
||||
|
||||
throw new Error("Failed to fetch data");
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -6,7 +6,7 @@ export interface ModulesFilter {
|
||||
features: string[];
|
||||
}
|
||||
export const createModulesQuery = (
|
||||
uri: string | null,
|
||||
uri: string | undefined,
|
||||
filter?: ModulesFilter,
|
||||
) =>
|
||||
createQuery(() => ({
|
||||
@@ -34,7 +34,7 @@ export const createModulesQuery = (
|
||||
},
|
||||
}));
|
||||
|
||||
export const tagsQuery = (uri: string | null) =>
|
||||
export const tagsQuery = (uri: string | undefined) =>
|
||||
createQuery<string[]>(() => ({
|
||||
queryKey: [uri, "tags"],
|
||||
placeholderData: [],
|
||||
@@ -55,7 +55,7 @@ export const tagsQuery = (uri: string | null) =>
|
||||
},
|
||||
}));
|
||||
|
||||
export const machinesQuery = (uri: string | null) =>
|
||||
export const machinesQuery = (uri: string | undefined) =>
|
||||
createQuery<string[]>(() => ({
|
||||
queryKey: [uri, "machines"],
|
||||
placeholderData: [],
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
SubmitHandler,
|
||||
} from "@modular-forms/solid";
|
||||
import toast from "solid-toast";
|
||||
import { activeURI, setActiveURI, setClanList } from "@/src/App";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type CreateForm = Meta & {
|
||||
template: string;
|
||||
@@ -27,6 +27,8 @@ export const CreateClan = () => {
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { setActiveClanURI, addClanURI } = useClanContext();
|
||||
|
||||
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
|
||||
const { template, ...meta } = values;
|
||||
const response = await callApi("open_file", {
|
||||
@@ -74,8 +76,10 @@ export const CreateClan = () => {
|
||||
|
||||
if (r.status === "success") {
|
||||
toast.success("Clan Successfully Created");
|
||||
setActiveURI(target_dir[0]);
|
||||
setClanList((list) => [...list, target_dir[0]]);
|
||||
|
||||
addClanURI(target_dir[0]);
|
||||
setActiveClanURI(target_dir[0]);
|
||||
|
||||
navigate("/machines");
|
||||
reset(formStore);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import toast from "solid-toast";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { Header } from "@/src/layout/header";
|
||||
import { clanMetaQuery } from "@/src/queries/clan-meta";
|
||||
|
||||
interface EditClanFormProps {
|
||||
initial: GeneralData;
|
||||
@@ -44,7 +45,7 @@ const EditClanForm = (props: EditClanFormProps) => {
|
||||
error: "Failed to update clan",
|
||||
},
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [props.directory, "meta"],
|
||||
});
|
||||
};
|
||||
@@ -142,16 +143,7 @@ export const ClanDetails = () => {
|
||||
const params = useParams();
|
||||
const clan_dir = window.atob(params.id);
|
||||
// Fetch general meta data
|
||||
const clanQuery = createQuery(() => ({
|
||||
queryKey: [clan_dir, "inventory", "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: clan_dir },
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
const clanQuery = clanMetaQuery(clan_dir);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { activeURI, clanList, setActiveURI, setClanList } from "@/src/App";
|
||||
import { createSignal, For, Match, Setter, Show, Switch } from "solid-js";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createSignal, For, Show } from "solid-js";
|
||||
import { useFloating } from "@/src/floating";
|
||||
import { autoUpdate, flip, hide, offset, shift } from "@floating-ui/dom";
|
||||
import { useNavigate, A } from "@solidjs/router";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { registerClan } from "@/src/hooks";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
import { clanURIs, setActiveClanURI } from "@/src/stores/clan";
|
||||
import { clanMetaQuery } from "@/src/queries/clan-meta";
|
||||
|
||||
interface ClanItemProps {
|
||||
clan_dir: string;
|
||||
@@ -15,20 +15,14 @@ interface ClanItemProps {
|
||||
const ClanItem = (props: ClanItemProps) => {
|
||||
const { clan_dir } = props;
|
||||
|
||||
const details = createQuery(() => ({
|
||||
queryKey: [clan_dir, "meta"],
|
||||
queryFn: async () => {
|
||||
const result = await callApi("show_clan_meta", {
|
||||
flake: { identifier: clan_dir },
|
||||
});
|
||||
if (result.status === "error") throw new Error("Failed to fetch data");
|
||||
return result.data;
|
||||
},
|
||||
}));
|
||||
const details = clanMetaQuery(clan_dir);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [reference, setReference] = createSignal<HTMLElement>();
|
||||
const [floating, setFloating] = createSignal<HTMLElement>();
|
||||
|
||||
const { activeClanURI, removeClanURI } = useClanContext();
|
||||
|
||||
// `position` is a reactive object.
|
||||
const position = useFloating(reference, floating, {
|
||||
placement: "top",
|
||||
@@ -50,15 +44,7 @@ const ClanItem = (props: ClanItemProps) => {
|
||||
});
|
||||
|
||||
const handleRemove = () => {
|
||||
setClanList((s) =>
|
||||
s.filter((v, idx) => {
|
||||
if (v == clan_dir) {
|
||||
setActiveURI(clanList()[idx - 1] || clanList()[idx + 1] || null);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
removeClanURI(clan_dir);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -77,10 +63,10 @@ const ClanItem = (props: ClanItemProps) => {
|
||||
variant="light"
|
||||
class=" "
|
||||
onClick={() => {
|
||||
setActiveURI(clan_dir);
|
||||
setActiveClanURI(clan_dir);
|
||||
}}
|
||||
>
|
||||
{activeURI() === clan_dir ? "active" : "select"}
|
||||
{activeClanURI() === clan_dir ? "active" : "select"}
|
||||
</Button>
|
||||
<Button
|
||||
size="s"
|
||||
@@ -117,7 +103,7 @@ const ClanItem = (props: ClanItemProps) => {
|
||||
<div
|
||||
class=""
|
||||
classList={{
|
||||
"": activeURI() === clan_dir,
|
||||
"": activeClanURI() === clan_dir,
|
||||
}}
|
||||
>
|
||||
{clan_dir}
|
||||
@@ -164,7 +150,7 @@ export const ClanList = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div class=" shadow">
|
||||
<For each={clanList()}>
|
||||
<For each={clanURIs()}>
|
||||
{(value) => <ClanItem clan_dir={value} />}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { createEffect } from "solid-js";
|
||||
import { useQuery } from "@tanstack/solid-query";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export function DiskView() {
|
||||
const query = createQuery(() => ({
|
||||
queryKey: ["disk", activeURI()],
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const query = useQuery(() => ({
|
||||
queryKey: ["disk", activeClanURI()],
|
||||
queryFn: async () => {
|
||||
const currUri = activeURI();
|
||||
const currUri = activeClanURI();
|
||||
if (currUri) {
|
||||
// Example of calling an API
|
||||
const result = await callApi("get_inventory", {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { callApi, OperationArgs } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
@@ -13,16 +12,19 @@ import { MachineAvatar } from "./avatar";
|
||||
import { DynForm } from "@/src/Form/form";
|
||||
import Fieldset from "@/src/Form/fieldset";
|
||||
import Accordion from "@/src/components/accordion";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type CreateMachineForm = OperationArgs<"create_machine">;
|
||||
|
||||
export function CreateMachine() {
|
||||
const navigate = useNavigate();
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const [formStore, { Form, Field }] = createForm<CreateMachineForm>({
|
||||
initialValues: {
|
||||
opts: {
|
||||
clan_dir: {
|
||||
identifier: activeURI() || "",
|
||||
identifier: activeClanURI() || "",
|
||||
},
|
||||
machine: {
|
||||
tags: ["all"],
|
||||
@@ -39,7 +41,7 @@ export function CreateMachine() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const handleSubmit = async (values: CreateMachineForm) => {
|
||||
const active_dir = activeURI();
|
||||
const active_dir = activeClanURI();
|
||||
if (!active_dir) {
|
||||
toast.error("Open a clan to create the machine within");
|
||||
return;
|
||||
@@ -60,9 +62,10 @@ export function CreateMachine() {
|
||||
toast.success(`Successfully created ${values.opts.machine.name}`);
|
||||
reset(formStore);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [activeURI(), "list_inv_machines"],
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [active_dir, "list_inv_machines"],
|
||||
});
|
||||
|
||||
navigate("/machines");
|
||||
} else {
|
||||
toast.error(
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
setValue,
|
||||
} from "@modular-forms/solid";
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { createQuery, useQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js";
|
||||
|
||||
import { activeURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { TextInput } from "@/src/Form/fields/TextInput";
|
||||
@@ -29,9 +28,11 @@ import cx from "classnames";
|
||||
import { VarsStep, VarsValues } from "./install/vars-step";
|
||||
import Fieldset from "@/src/Form/fieldset";
|
||||
import {
|
||||
FileSelectorField,
|
||||
type FileDialogOptions,
|
||||
FileSelectorField,
|
||||
} from "@/src/components/fileSelect";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type MachineFormInterface = MachineData & {
|
||||
sshKey?: File;
|
||||
disk?: string;
|
||||
@@ -81,7 +82,9 @@ interface InstallMachineProps {
|
||||
machine: MachineData;
|
||||
}
|
||||
const InstallMachine = (props: InstallMachineProps) => {
|
||||
const curr = activeURI();
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const curr = activeClanURI();
|
||||
const { name } = props;
|
||||
if (!curr || !name) {
|
||||
return <span>No Clan selected</span>;
|
||||
@@ -95,7 +98,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
|
||||
const handleInstall = async (values: AllStepsValues) => {
|
||||
console.log("Installing", values);
|
||||
const curr_uri = activeURI();
|
||||
const curr_uri = activeClanURI();
|
||||
|
||||
const target = values["1"].target;
|
||||
const diskValues = values["2"];
|
||||
@@ -257,7 +260,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
machine_id={props.name}
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
dir={activeURI()}
|
||||
dir={activeClanURI()}
|
||||
handleNext={(data) => {
|
||||
const prev = getValue(formStore, "1");
|
||||
setValue(formStore, "1", { ...prev, ...data });
|
||||
@@ -277,7 +280,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
machine_id={props.name}
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
dir={activeURI()}
|
||||
dir={activeClanURI()}
|
||||
footer={<Footer />}
|
||||
handleNext={(data) => {
|
||||
const prev = getValue(formStore, "2");
|
||||
@@ -297,7 +300,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
machine_id={props.name}
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
dir={activeURI()}
|
||||
dir={activeClanURI()}
|
||||
handleNext={(data) => {
|
||||
const prev = getValue(formStore, "3");
|
||||
setValue(formStore, "3", { ...prev, ...data });
|
||||
@@ -312,7 +315,7 @@ const InstallMachine = (props: InstallMachineProps) => {
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
machine_id={props.name}
|
||||
// @ts-expect-error: This cannot be undefined in this context.
|
||||
dir={activeURI()}
|
||||
dir={activeClanURI()}
|
||||
handleNext={() => handleNext()}
|
||||
// @ts-expect-error: This cannot be known.
|
||||
initial={getValues(formStore)}
|
||||
@@ -394,11 +397,12 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const queryClient = useQueryClient();
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const handleSubmit = async (values: MachineFormInterface) => {
|
||||
console.log("submitting", values);
|
||||
|
||||
const curr_uri = activeURI();
|
||||
const curr_uri = activeClanURI();
|
||||
if (!curr_uri) {
|
||||
return;
|
||||
}
|
||||
@@ -418,18 +422,18 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
),
|
||||
},
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [activeURI(), "machine", machineName(), "get_machine_details"],
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: [curr_uri, "machine", machineName(), "get_machine_details"],
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const generatorsQuery = createQuery(() => ({
|
||||
queryKey: [activeURI(), machineName(), "generators"],
|
||||
queryKey: [activeClanURI(), machineName(), "generators"],
|
||||
queryFn: async () => {
|
||||
const machine_name = machineName();
|
||||
const base_dir = activeURI();
|
||||
const base_dir = activeClanURI();
|
||||
if (!machine_name || !base_dir) {
|
||||
return [];
|
||||
}
|
||||
@@ -460,7 +464,7 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
if (isUpdating()) {
|
||||
return;
|
||||
}
|
||||
const curr_uri = activeURI();
|
||||
const curr_uri = activeClanURI();
|
||||
if (!curr_uri) {
|
||||
return;
|
||||
}
|
||||
@@ -697,10 +701,12 @@ const MachineForm = (props: MachineDetailsProps) => {
|
||||
|
||||
export const MachineDetails = () => {
|
||||
const params = useParams();
|
||||
const genericQuery = createQuery(() => ({
|
||||
queryKey: [activeURI(), "machine", params.id, "get_machine_details"],
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const genericQuery = useQuery(() => ({
|
||||
queryKey: [activeClanURI(), "machine", params.id, "get_machine_details"],
|
||||
queryFn: async () => {
|
||||
const curr = activeURI();
|
||||
const curr = activeClanURI();
|
||||
if (curr) {
|
||||
const result = await callApi("get_machine_details", {
|
||||
machine: {
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { callApi } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { InputError, InputLabel } from "@/src/components/inputBase";
|
||||
import { FieldLayout } from "@/src/Form/fields/layout";
|
||||
import {
|
||||
createForm,
|
||||
SubmitHandler,
|
||||
FieldValues,
|
||||
validate,
|
||||
required,
|
||||
getValue,
|
||||
submit,
|
||||
required,
|
||||
setValue,
|
||||
FormStore,
|
||||
submit,
|
||||
SubmitHandler,
|
||||
validate,
|
||||
} from "@modular-forms/solid";
|
||||
import { createEffect, createSignal, JSX, Match, Switch } from "solid-js";
|
||||
import { TextInput } from "@/src/Form/fields";
|
||||
@@ -21,9 +19,10 @@ import { createQuery } from "@tanstack/solid-query";
|
||||
import { Badge } from "@/src/components/badge";
|
||||
import { Group } from "@/src/components/group";
|
||||
import {
|
||||
FileSelectorField,
|
||||
type FileDialogOptions,
|
||||
FileSelectorField,
|
||||
} from "@/src/components/fileSelect";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export type HardwareValues = FieldValues & {
|
||||
report: boolean;
|
||||
@@ -76,8 +75,10 @@ export const HWStep = (props: StepProps<HardwareValues>) => {
|
||||
}
|
||||
});
|
||||
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const generateReport = async (e: Event) => {
|
||||
const curr_uri = activeURI();
|
||||
const curr_uri = activeClanURI();
|
||||
if (!curr_uri) return;
|
||||
|
||||
await validate(formStore, "target");
|
||||
|
||||
@@ -12,10 +12,10 @@ import { For, JSX, Match, Show, Switch } from "solid-js";
|
||||
import { TextInput } from "@/src/Form/fields";
|
||||
import toast from "solid-toast";
|
||||
import { useNavigate, useParams, useSearchParams } from "@solidjs/router";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { StepProps } from "./hardware-step";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export type VarsValues = FieldValues & Record<string, Record<string, string>>;
|
||||
|
||||
@@ -206,6 +206,7 @@ export const VarsStep = (props: VarsStepProps) => {
|
||||
export const VarsPage = () => {
|
||||
const params = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { activeClanURI } = useClanContext();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const handleNext = (values: VarsValues) => {
|
||||
if (searchParams?.action === "update") {
|
||||
@@ -234,7 +235,7 @@ export const VarsPage = () => {
|
||||
</div>
|
||||
|
||||
{/* VarsStep component */}
|
||||
<Show when={activeURI()}>
|
||||
<Show when={activeClanURI()}>
|
||||
{(uri) => (
|
||||
<VarsStep
|
||||
machine_id={params.id}
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
import {
|
||||
type Component,
|
||||
createSignal,
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { type Component, createSignal, For, Match, Switch } from "solid-js";
|
||||
import { callApi, OperationResponse } from "@/src/api";
|
||||
import toast from "solid-toast";
|
||||
import { MachineListItem } from "@/src/components/machine-list-item";
|
||||
import { createQuery, useQueryClient } from "@tanstack/solid-query";
|
||||
import { useQuery, 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";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
type MachinesModel = Extract<
|
||||
OperationResponse<"list_inv_machines">,
|
||||
@@ -30,19 +22,22 @@ export const MachineListView: Component = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [filter, setFilter] = createSignal<Filter>({ tags: [] });
|
||||
const { activeClanURI } = useClanContext();
|
||||
|
||||
const inventoryQuery = createQuery<MachinesModel>(() => ({
|
||||
queryKey: [activeURI(), "list_inv_machines"],
|
||||
const inventoryQuery = useQuery<MachinesModel>(() => ({
|
||||
queryKey: [activeClanURI(), "list_inv_machines"],
|
||||
placeholderData: {},
|
||||
enabled: !!activeURI(),
|
||||
enabled: !!activeClanURI(),
|
||||
queryFn: async () => {
|
||||
const uri = activeURI();
|
||||
console.log("fetching inventory", activeClanURI());
|
||||
const uri = activeClanURI();
|
||||
if (uri) {
|
||||
const response = await callApi("list_inv_machines", {
|
||||
flake: {
|
||||
identifier: uri,
|
||||
},
|
||||
});
|
||||
console.log("response", response);
|
||||
if (response.status === "error") {
|
||||
console.error("Failed to fetch data");
|
||||
} else {
|
||||
@@ -54,9 +49,18 @@ export const MachineListView: Component = () => {
|
||||
}));
|
||||
|
||||
const refresh = async () => {
|
||||
queryClient.invalidateQueries({
|
||||
const clanURI = activeClanURI();
|
||||
|
||||
// do nothing if there is no active URI
|
||||
if (!clanURI) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("refreshing", clanURI);
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
// Invalidates the cache for of all types of machine list at once
|
||||
queryKey: [activeURI(), "list_inv_machines"],
|
||||
queryKey: [clanURI, "list_inv_machines"],
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { activeURI } from "@/src/App";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { createModulesQuery, machinesQuery, tagsQuery } from "@/src/queries";
|
||||
import { useParams } from "@solidjs/router";
|
||||
@@ -6,10 +5,12 @@ import { For, Match, Switch } from "solid-js";
|
||||
import { ModuleInfo } from "./list";
|
||||
import { createForm, FieldValues, SubmitHandler } from "@modular-forms/solid";
|
||||
import { SelectInput } from "@/src/Form/fields/Select";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export const ModuleDetails = () => {
|
||||
const params = useParams();
|
||||
const modulesQuery = createModulesQuery(activeURI());
|
||||
const { activeClanURI } = useClanContext();
|
||||
const modulesQuery = createModulesQuery(activeClanURI());
|
||||
|
||||
return (
|
||||
<div class="p-1">
|
||||
@@ -32,8 +33,9 @@ interface AddModuleProps {
|
||||
}
|
||||
|
||||
export const AddModule = (props: AddModuleProps) => {
|
||||
const tags = tagsQuery(activeURI());
|
||||
const machines = machinesQuery(activeURI());
|
||||
const { activeClanURI } = useClanContext();
|
||||
const tags = tagsQuery(activeClanURI());
|
||||
const machines = machinesQuery(activeClanURI());
|
||||
return (
|
||||
<div>
|
||||
<div>Add to your clan</div>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { activeURI } from "@/src/App";
|
||||
import { BackButton } from "@/src/components/BackButton";
|
||||
import { createModulesQuery } from "@/src/queries";
|
||||
import { useParams, useNavigate } from "@solidjs/router";
|
||||
import { useNavigate, useParams } from "@solidjs/router";
|
||||
import { createEffect, For, Match, Switch } from "solid-js";
|
||||
import { SolidMarkdown } from "solid-markdown";
|
||||
import { ModuleInfo } from "./list";
|
||||
import { createQuery } from "@tanstack/solid-query";
|
||||
import { JSONSchema7 } from "json-schema";
|
||||
@@ -11,10 +9,13 @@ import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { DynForm } from "@/src/Form/form";
|
||||
import { Button } from "@/src/components/button";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
import { activeClanURI } from "@/src/stores/clan";
|
||||
|
||||
export const ModuleDetails = () => {
|
||||
const params = useParams();
|
||||
const modulesQuery = createModulesQuery(activeURI());
|
||||
const { activeClanURI } = useClanContext();
|
||||
const modulesQuery = createModulesQuery(activeClanURI());
|
||||
|
||||
return (
|
||||
<div class="p-1">
|
||||
@@ -143,7 +144,7 @@ export const ModuleForm = (props: { id: string }) => {
|
||||
// TODO: Fetch the synced schema for all the modules at runtime
|
||||
// We use static schema file at build time for now. (Different versions might have different schema at runtime)
|
||||
const schemaQuery = createQuery(() => ({
|
||||
queryKey: [activeURI(), "modules_schema"],
|
||||
queryKey: [activeClanURI(), "modules_schema"],
|
||||
queryFn: async () => {
|
||||
const moduleSchema = await import(
|
||||
"../../../api/modules_schemas.json"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SuccessData } from "@/src/api";
|
||||
import { activeURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { Header } from "@/src/layout/header";
|
||||
import { createModulesQuery } from "@/src/queries";
|
||||
@@ -11,6 +10,7 @@ import { makePersisted } from "@solid-primitives/storage";
|
||||
import { useQueryClient } from "@tanstack/solid-query";
|
||||
import cx from "classnames";
|
||||
import Icon from "@/src/components/icon";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export type ModuleInfo = SuccessData<"list_modules">["localModules"][string];
|
||||
|
||||
@@ -110,7 +110,8 @@ const ModuleItem = (props: {
|
||||
|
||||
export const ModuleList = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const modulesQuery = createModulesQuery(activeURI(), {
|
||||
const { activeClanURI } = useClanContext();
|
||||
const modulesQuery = createModulesQuery(activeClanURI(), {
|
||||
features: ["inventory"],
|
||||
});
|
||||
|
||||
@@ -120,9 +121,16 @@ export const ModuleList = () => {
|
||||
});
|
||||
|
||||
const refresh = async () => {
|
||||
queryClient.invalidateQueries({
|
||||
const clanURI = activeClanURI();
|
||||
|
||||
// do nothing if there is no active URI
|
||||
if (!clanURI) {
|
||||
return;
|
||||
}
|
||||
|
||||
await queryClient.invalidateQueries({
|
||||
// Invalidates the cache for of all types of machine list at once
|
||||
queryKey: [activeURI(), "list_modules"],
|
||||
queryKey: [clanURI, "list_modules"],
|
||||
});
|
||||
};
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { setActiveURI } from "@/src/App";
|
||||
import { Button } from "@/src/components/button";
|
||||
import { registerClan } from "@/src/hooks";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { useClanContext } from "@/src/contexts/clan";
|
||||
|
||||
export const Welcome = () => {
|
||||
const navigate = useNavigate();
|
||||
const { setActiveClanURI } = useClanContext();
|
||||
return (
|
||||
<div class="min-h-[calc(100vh-10rem)]">
|
||||
<div class="mb-32 text-center">
|
||||
@@ -21,7 +22,7 @@ export const Welcome = () => {
|
||||
onClick={async () => {
|
||||
const uri = await registerClan();
|
||||
if (uri) {
|
||||
setActiveURI(uri);
|
||||
setActiveClanURI(uri);
|
||||
navigate("/machines");
|
||||
}
|
||||
}}
|
||||
|
||||
89
pkgs/clan-app/ui/src/stores/clan.tsx
Normal file
89
pkgs/clan-app/ui/src/stores/clan.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createStore, produce } from "solid-js/store";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
interface ClanStoreType {
|
||||
clanURIs: string[];
|
||||
activeClanURI?: string;
|
||||
}
|
||||
|
||||
const [store, setStore] = makePersisted(
|
||||
createStore<ClanStoreType>({
|
||||
clanURIs: [],
|
||||
}),
|
||||
{
|
||||
name: "clanStore",
|
||||
storage: localStorage,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Retrieves the active clan URI from the store.
|
||||
*
|
||||
* @function
|
||||
* @returns {string} The URI of the active clan.
|
||||
*/
|
||||
const activeClanURI = (): string | undefined => store.activeClanURI;
|
||||
|
||||
/**
|
||||
* Updates the active Clan URI in the store.
|
||||
*
|
||||
* @param {string} uri - The URI to be set as the active Clan URI.
|
||||
*/
|
||||
const setActiveClanURI = (uri: string) => setStore("activeClanURI", uri);
|
||||
|
||||
/**
|
||||
* Retrieves the current list of clan URIs from the store.
|
||||
*
|
||||
* @function clanURIs
|
||||
* @returns {*} The clan URIs from the store.
|
||||
*/
|
||||
const clanURIs = (): string[] => store.clanURIs;
|
||||
|
||||
/**
|
||||
* Adds a new clan URI to the list of clan URIs in the store.
|
||||
*
|
||||
* @param {string} uri - The URI of the clan to be added.
|
||||
*
|
||||
*/
|
||||
const addClanURI = (uri: string) =>
|
||||
setStore("clanURIs", store.clanURIs.length, uri);
|
||||
|
||||
/**
|
||||
* Removes a specified URI from the clan URI list and updates the active clan URI.
|
||||
*
|
||||
* This function modifies the store in the following ways:
|
||||
* - Removes the specified URI from the `clanURIs` array.
|
||||
* - Clears the `activeClanURI` if the removed URI matches the currently active URI.
|
||||
* - Sets a new active clan URI to the last URI in the `clanURIs` array if the active clan URI is undefined
|
||||
* and there are remaining clan URIs in the list.
|
||||
*
|
||||
* @param {string} uri - The URI to be removed from the clan list.
|
||||
*/
|
||||
const removeClanURI = (uri: string) => {
|
||||
setStore(
|
||||
produce((state) => {
|
||||
// remove from the clan list
|
||||
state.clanURIs = state.clanURIs.filter((el) => el !== uri);
|
||||
|
||||
// clear active clan uri if it's the one being removed
|
||||
if (state.activeClanURI === uri) {
|
||||
state.activeClanURI = undefined;
|
||||
}
|
||||
|
||||
// select a new active URI if at least one remains
|
||||
if (!state.activeClanURI && state.clanURIs.length > 0) {
|
||||
state.activeClanURI = state.clanURIs[state.clanURIs.length - 1];
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
store,
|
||||
setStore,
|
||||
activeClanURI,
|
||||
setActiveClanURI,
|
||||
clanURIs,
|
||||
addClanURI,
|
||||
removeClanURI,
|
||||
};
|
||||
Reference in New Issue
Block a user