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:
brianmcgee
2025-05-26 09:26:07 +00:00
23 changed files with 344 additions and 202 deletions

View File

@@ -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

View File

@@ -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">

View File

@@ -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;

View 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;
}

View File

@@ -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) {

View File

@@ -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}>
<ClanProvider>
<Router root={Layout}>{routes}</Router>
</ClanProvider>
</QueryClientProvider>
</>
),

View File

@@ -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");
}
});

View 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;
},
};
});

View File

@@ -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: [],

View File

@@ -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);
}

View File

@@ -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 (
<>

View File

@@ -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>

View File

@@ -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", {

View File

@@ -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(

View File

@@ -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: {

View File

@@ -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");

View File

@@ -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}

View File

@@ -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"],
});
};

View File

@@ -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>

View File

@@ -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"

View File

@@ -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 (

View File

@@ -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");
}
}}

View 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,
};