Webview: add welcome workflow

This commit is contained in:
Johannes Kirschbauer
2024-07-11 16:34:55 +02:00
parent ac413a4d13
commit af4e843131
17 changed files with 412 additions and 379 deletions

View File

@@ -10,8 +10,10 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@modular-forms/solid": "^0.21.0", "@modular-forms/solid": "^0.21.0",
"@solid-primitives/storage": "^3.7.1",
"@tanstack/solid-query": "^5.44.0", "@tanstack/solid-query": "^5.44.0",
"material-icons": "^1.13.12", "material-icons": "^1.13.12",
"nanoid": "^5.0.7",
"solid-js": "^1.8.11", "solid-js": "^1.8.11",
"solid-toast": "^0.5.0" "solid-toast": "^0.5.0"
}, },
@@ -1498,6 +1500,26 @@
"solid-js": "^1.6.12" "solid-js": "^1.6.12"
} }
}, },
"node_modules/@solid-primitives/storage": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@solid-primitives/storage/-/storage-3.7.1.tgz",
"integrity": "sha512-tAmZKQg44RjDjrtWO/5hCOrktQspn/yVV0ySb7yKr7B3CVQlTQtldw3W8UetytJSD9podb9cplvvkq75fgpB1Q==",
"dependencies": {
"@solid-primitives/utils": "^6.2.3"
},
"peerDependencies": {
"@tauri-apps/plugin-store": "*",
"solid-js": "^1.6.12"
},
"peerDependenciesMeta": {
"@tauri-apps/plugin-store": {
"optional": true
},
"solid-start": {
"optional": true
}
}
},
"node_modules/@solid-primitives/styles": { "node_modules/@solid-primitives/styles": {
"version": "0.0.111", "version": "0.0.111",
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz", "resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz",
@@ -1515,7 +1537,6 @@
"version": "6.2.3", "version": "6.2.3",
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz", "resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
"integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==", "integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==",
"dev": true,
"peerDependencies": { "peerDependencies": {
"solid-js": "^1.6.12" "solid-js": "^1.6.12"
} }
@@ -4177,10 +4198,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "5.0.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -4188,10 +4208,10 @@
} }
], ],
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.js"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^18 || >=20"
} }
}, },
"node_modules/natural-compare": { "node_modules/natural-compare": {
@@ -4753,6 +4773,24 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"dev": true "dev": true
}, },
"node_modules/postcss/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",

View File

@@ -39,8 +39,10 @@
}, },
"dependencies": { "dependencies": {
"@modular-forms/solid": "^0.21.0", "@modular-forms/solid": "^0.21.0",
"@solid-primitives/storage": "^3.7.1",
"@tanstack/solid-query": "^5.44.0", "@tanstack/solid-query": "^5.44.0",
"material-icons": "^1.13.12", "material-icons": "^1.13.12",
"nanoid": "^5.0.7",
"solid-js": "^1.8.11", "solid-js": "^1.8.11",
"solid-toast": "^0.5.0" "solid-toast": "^0.5.0"
} }

View File

@@ -1,24 +1,35 @@
import { createSignal, type Component } from "solid-js"; import { createSignal, type Component } from "solid-js";
import { MachineProvider } from "./Config";
import { Layout } from "./layout/layout"; import { Layout } from "./layout/layout";
import { Route, Router } from "./Routes"; import { Route, Router } from "./Routes";
import { Toaster } from "solid-toast"; import { Toaster } from "solid-toast";
import { effect } from "solid-js/web";
import { makePersisted } from "@solid-primitives/storage";
// Some global state // Some global state
const [route, setRoute] = createSignal<Route>("machines"); const [route, setRoute] = createSignal<Route>("machines");
export { route, setRoute }; export { route, setRoute };
const [currClanURI, setCurrClanURI] = createSignal<string | null>(null); const [activeURI, setActiveURI] = createSignal<string | null>(null);
export { currClanURI, setCurrClanURI }; export { activeURI, setActiveURI };
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
name: "clanList",
storage: localStorage,
});
export { clanList, setClanList };
const App: Component = () => { const App: Component = () => {
effect(() => {
if (clanList().length === 0) {
setRoute("welcome");
}
});
return [ return [
<Toaster position="top-right" />, <Toaster position="top-right" />,
<MachineProvider> <Layout>
<Layout> <Router route={route} />
<Router route={route} /> </Layout>,
</Layout>
</MachineProvider>,
]; ];
}; };

View File

@@ -1,67 +0,0 @@
import {
createSignal,
createContext,
useContext,
JSXElement,
createEffect,
} from "solid-js";
import { OperationResponse, pyApi } from "./api";
import { currClanURI } from "./App";
export const makeMachineContext = () => {
const [machines, setMachines] =
createSignal<OperationResponse<"list_machines">>();
const [loading, setLoading] = createSignal(false);
pyApi.list_machines.receive((machines) => {
setLoading(false);
setMachines(machines);
});
createEffect(() => {
console.log("The state is now", machines());
});
return [
{ loading, machines },
{
getMachines: () => {
const clan_dir = currClanURI();
if (clan_dir) {
setLoading(true);
pyApi.list_machines.dispatch({
debug: true,
flake_url: clan_dir,
});
}
// When the gtk function sends its data the loading state will be set to false
},
},
] as const;
// `as const` forces tuple type inference
};
type MachineContextType = ReturnType<typeof makeMachineContext>;
export const MachineContext = createContext<MachineContextType>([
{
loading: () => false,
// eslint-disable-next-line
machines: () => undefined,
},
{
// eslint-disable-next-line
getMachines: () => {},
},
]);
export const useMachineContext = () => useContext(MachineContext);
export function MachineProvider(props: { children: JSXElement }) {
return (
<MachineContext.Provider value={makeMachineContext()}>
{props.children}
</MachineContext.Provider>
);
}

View File

@@ -5,6 +5,8 @@ import { CreateClan } from "./routes/clan/view";
import { HostList } from "./routes/hosts/view"; import { HostList } from "./routes/hosts/view";
import { BlockDevicesView } from "./routes/blockdevices/view"; import { BlockDevicesView } from "./routes/blockdevices/view";
import { Flash } from "./routes/flash/view"; import { Flash } from "./routes/flash/view";
import { Settings } from "./routes/settings";
import { Welcome } from "./routes/welcome";
export type Route = keyof typeof routes; export type Route = keyof typeof routes;
@@ -39,6 +41,16 @@ export const routes = {
label: "Colors", label: "Colors",
icon: "color_lens", icon: "color_lens",
}, },
settings: {
child: Settings,
label: "Settings",
icon: "settings",
},
welcome: {
child: Welcome,
label: "welcome",
icon: "settings",
},
}; };
interface RouterProps { interface RouterProps {

View File

@@ -1,6 +1,5 @@
import { Match, Show, Switch, createSignal } from "solid-js"; import { Match, Show, Switch, createSignal } from "solid-js";
import { ErrorData, SuccessData, pyApi } from "../api"; import { ErrorData, SuccessData, pyApi } from "../api";
import { currClanURI } from "../App";
type MachineDetails = SuccessData<"list_machines">["data"][string]; type MachineDetails = SuccessData<"list_machines">["data"][string];
@@ -23,51 +22,51 @@ const [deploymentInfo, setDeploymentInfo] = createSignal<DeploymentInfo>({});
const [errors, setErrors] = createSignal<MachineErrors>({}); const [errors, setErrors] = createSignal<MachineErrors>({});
pyApi.show_machine_hardware_info.receive((r) => { // pyApi.show_machine_hardware_info.receive((r) => {
const { op_key } = r; // const { op_key } = r;
if (r.status === "error") { // if (r.status === "error") {
console.error(r.errors); // console.error(r.errors);
if (op_key) { // if (op_key) {
setHwInfo((d) => ({ ...d, [op_key]: { system: null } })); // setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
} // }
return; // return;
} // }
if (op_key) { // if (op_key) {
setHwInfo((d) => ({ ...d, [op_key]: r.data })); // setHwInfo((d) => ({ ...d, [op_key]: r.data }));
} // }
}); // });
pyApi.show_machine_deployment_target.receive((r) => { // pyApi.show_machine_deployment_target.receive((r) => {
const { op_key } = r; // const { op_key } = r;
if (r.status === "error") { // if (r.status === "error") {
console.error(r.errors); // console.error(r.errors);
if (op_key) { // if (op_key) {
setDeploymentInfo((d) => ({ ...d, [op_key]: null })); // setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
} // }
return; // return;
} // }
if (op_key) { // if (op_key) {
setDeploymentInfo((d) => ({ ...d, [op_key]: r.data })); // setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
} // }
}); // });
export const MachineListItem = (props: MachineListItemProps) => { export const MachineListItem = (props: MachineListItemProps) => {
const { name, info } = props; const { name, info } = props;
const clan_dir = currClanURI(); // const clan_dir = currClanURI();
if (clan_dir) { // if (clan_dir) {
pyApi.show_machine_hardware_info.dispatch({ // pyApi.show_machine_hardware_info.dispatch({
op_key: name, // op_key: name,
clan_dir, // clan_dir,
machine_name: name, // machine_name: name,
}); // });
pyApi.show_machine_deployment_target.dispatch({ // pyApi.show_machine_deployment_target.dispatch({
op_key: name, // op_key: name,
clan_dir, // clan_dir,
machine_name: name, // machine_name: name,
}); // });
} // }
return ( return (
<li> <li>

View File

@@ -1,4 +1,4 @@
import { currClanURI } from "../App"; import { activeURI, setRoute } from "../App";
export const Header = () => { export const Header = () => {
return ( return (
@@ -14,12 +14,12 @@ export const Header = () => {
</span> </span>
</div> </div>
<div class="flex-1"> <div class="flex-1">
<a class="text-xl">{currClanURI() || "Clan"}</a> <a class="text-xl">{activeURI()}</a>
</div> </div>
<div class="flex-none"> <div class="flex-none">
<span class="tooltip tooltip-bottom" data-tip="Account"> <span class="tooltip tooltip-bottom" data-tip="Settings">
<button class="btn btn-square btn-ghost"> <button class="link" onClick={() => setRoute("settings")}>
<span class="material-icons">account_circle</span> <span class="material-icons">settings</span>
</button> </button>
</span> </span>
</div> </div>

View File

@@ -1,13 +1,17 @@
import { Component, JSXElement } from "solid-js"; import { Component, JSXElement, Show } from "solid-js";
import { Header } from "./header"; import { Header } from "./header";
import { Sidebar } from "../Sidebar"; import { Sidebar } from "../Sidebar";
import { route, setRoute } from "../App"; import { route, setRoute } from "../App";
import { effect } from "solid-js/web";
interface LayoutProps { interface LayoutProps {
children: JSXElement; children: JSXElement;
} }
export const Layout: Component<LayoutProps> = (props) => { export const Layout: Component<LayoutProps> = (props) => {
effect(() => {
console.log(route());
});
return ( return (
<> <>
<div class="drawer bg-base-100 lg:drawer-open"> <div class="drawer bg-base-100 lg:drawer-open">
@@ -17,11 +21,16 @@ export const Layout: Component<LayoutProps> = (props) => {
class="drawer-toggle hidden" class="drawer-toggle hidden"
/> />
<div class="drawer-content"> <div class="drawer-content">
<Header /> <Show when={route() !== "welcome"}>
<Header />
</Show>
{props.children} {props.children}
</div> </div>
<div class="drawer-side z-40"> <div
class="drawer-side z-40"
classList={{ "!hidden": route() === "welcome" }}
>
<label <label
for="toplevel-drawer" for="toplevel-drawer"
aria-label="close sidebar" aria-label="close sidebar"

View File

@@ -8,24 +8,24 @@ type DevicesModel = Extract<
>["data"]["blockdevices"]; >["data"]["blockdevices"];
export const BlockDevicesView: Component = () => { export const BlockDevicesView: Component = () => {
const [devices, setServices] = createSignal<DevicesModel>(); const [devices, setDevices] = createSignal<DevicesModel>();
pyApi.show_block_devices.receive((r) => { // pyApi.show_block_devices.receive((r) => {
const { status } = r; // const { status } = r;
if (status === "error") return console.error(r.errors); // if (status === "error") return console.error(r.errors);
setServices(r.data.blockdevices); // setServices(r.data.blockdevices);
}); // });
createEffect(() => { // createEffect(() => {
if (route() === "blockdevices") pyApi.show_block_devices.dispatch({}); // if (route() === "blockdevices") pyApi.show_block_devices.dispatch({});
}); // });
return ( return (
<div> <div>
<div class="tooltip tooltip-bottom" data-tip="Refresh"> <div class="tooltip tooltip-bottom" data-tip="Refresh">
<button <button
class="btn btn-ghost" class="btn btn-ghost"
onClick={() => pyApi.show_block_devices.dispatch({})} // onClick={() => pyApi.show_block_devices.dispatch({})}
> >
<span class="material-icons ">refresh</span> <span class="material-icons ">refresh</span>
</button> </button>

View File

@@ -15,31 +15,24 @@ import {
custom, custom,
} from "@modular-forms/solid"; } from "@modular-forms/solid";
import toast from "solid-toast"; import toast from "solid-toast";
import { setCurrClanURI, setRoute } from "@/src/App"; import { setActiveURI, setRoute } from "@/src/App";
import { isValidHostname } from "@/util";
interface ClanDetailsProps { interface ClanDetailsProps {
directory: string; directory: string;
} }
interface ClanFormProps {
actions: JSX.Element;
}
type CreateForm = Meta & { type CreateForm = Meta & {
template_url: string; template_url: string;
}; };
export const ClanForm = (props: ClanFormProps) => { export const ClanForm = () => {
const { actions } = props;
const [formStore, { Form, Field }] = createForm<CreateForm>({ const [formStore, { Form, Field }] = createForm<CreateForm>({
initialValues: { initialValues: {
template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal", template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal",
}, },
}); });
const handleSubmit: SubmitHandler<CreateForm> = (values, event) => { const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
console.log("submit", values);
const { template_url, ...meta } = values; const { template_url, ...meta } = values;
pyApi.open_file.dispatch({ pyApi.open_file.dispatch({
file_request: { file_request: {
@@ -49,55 +42,60 @@ export const ClanForm = (props: ClanFormProps) => {
op_key: "create_clan", op_key: "create_clan",
}); });
pyApi.open_file.receive((r) => { // await new Promise<void>((done) => {
if (r.op_key !== "create_clan") { // pyApi.open_file.receive((r) => {
return; // if (r.op_key !== "create_clan") {
} // done();
if (r.status !== "success") { // return;
toast.error("Cannot select clan directory"); // }
return; // if (r.status !== "success") {
} // toast.error("Cannot select clan directory");
const target_dir = r?.data; // done();
if (!target_dir) { // return;
toast.error("Cannot select clan directory"); // }
return; // const target_dir = r?.data;
} // if (!target_dir) {
// toast.error("Cannot select clan directory");
// done();
// return;
// }
if (!isValidHostname(target_dir)) { // console.log({ formStore });
toast.error(`Directory name must be valid URI: ${target_dir}`);
return;
}
toast.promise( // toast.promise(
new Promise<void>((resolve, reject) => { // new Promise<void>((resolve, reject) => {
pyApi.create_clan.receive((r) => { // pyApi.create_clan.receive((r) => {
if (r.status === "error") { // done();
reject(); // if (r.status === "error") {
console.error(r.errors); // reject();
} // console.error(r.errors);
resolve(); // return;
// Navigate to the new clan // }
setCurrClanURI(target_dir); // resolve();
setRoute("machines");
});
pyApi.create_clan.dispatch({ // // Navigate to the new clan
options: { directory: target_dir, meta, template_url }, // setCurrClanURI(target_dir);
op_key: "create_clan", // setRoute("machines");
}); // });
}),
{ // pyApi.create_clan.dispatch({
loading: "Creating clan...", // options: { directory: target_dir, meta, template_url },
success: "Clan Successfully Created", // op_key: "create_clan",
error: "Failed to create clan", // });
} // }),
); // {
}); // loading: "Creating clan...",
// success: "Clan Successfully Created",
// error: "Failed to create clan",
// }
// );
// });
// });
}; };
return ( return (
<div class="card card-normal"> <div class="card card-normal">
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit} shouldActive>
<Field name="icon"> <Field name="icon">
{(field, props) => ( {(field, props) => (
<> <>
@@ -201,7 +199,17 @@ export const ClanForm = (props: ClanFormProps) => {
</div> </div>
)} )}
</Field> </Field>
{actions} {
<div class="card-actions justify-end">
<button
class="btn btn-primary"
type="submit"
disabled={formStore.submitting}
>
Create
</button>
</div>
}
</div> </div>
</Form> </Form>
</div> </div>
@@ -212,71 +220,3 @@ type Meta = Extract<
OperationResponse<"show_clan_meta">, OperationResponse<"show_clan_meta">,
{ status: "success" } { status: "success" }
>["data"]; >["data"];
export const ClanDetails = (props: ClanDetailsProps) => {
const { directory } = props;
const [loading, setLoading] = createSignal(false);
const [errors, setErrors] = createSignal<
| Extract<
OperationResponse<"show_clan_meta">,
{ status: "error" }
>["errors"]
| null
>(null);
const [data, setData] = createSignal<Meta>();
const loadMeta = () => {
pyApi.show_clan_meta.dispatch({ uri: directory });
setLoading(true);
};
createEffect(() => {
loadMeta();
pyApi.show_clan_meta.receive((response) => {
setLoading(false);
if (response.status === "error") {
setErrors(response.errors);
return console.error(response.errors);
}
setData(response.data);
});
});
return (
<Switch fallback={"loading"}>
<Match when={loading()}>
<div>Loading</div>
</Match>
<Match when={data()}>
{(data) => {
const meta = data();
return (
<ClanForm
actions={
<div class="card-actions justify-between">
<button class="btn btn-link" onClick={() => loadMeta()}>
Refresh
</button>
<button class="btn btn-primary">Open</button>
</div>
}
/>
);
}}
</Match>
<Match when={errors()}>
<button class="btn btn-secondary" onClick={() => loadMeta()}>
Retry
</button>
<For each={errors()}>
{(item) => (
<div class="flex flex-col gap-3">
<span class="bg-red-400 text-white">{item.message}</span>
<span class="bg-red-400 text-white">{item.description}</span>
<span class="bg-red-400 text-white">{item.location}</span>
</div>
)}
</For>
</Match>
</Switch>
);
};

View File

@@ -1,20 +1,9 @@
import { pyApi } from "@/src/api"; import { ClanForm } from "./clanDetails";
import { Match, Switch, createEffect, createSignal } from "solid-js";
import toast from "solid-toast";
import { ClanDetails, ClanForm } from "./clanDetails";
export const CreateClan = () => { export const CreateClan = () => {
return ( return (
<div> <div>
<ClanForm <ClanForm />
actions={
<div class="card-actions justify-end">
<button class="btn btn-primary" type="submit">
Create
</button>
</div>
}
/>
</div> </div>
); );
}; };

View File

@@ -24,16 +24,17 @@ type BlockDevices = Extract<
OperationResponse<"show_block_devices">, OperationResponse<"show_block_devices">,
{ status: "success" } { status: "success" }
>["data"]["blockdevices"]; >["data"]["blockdevices"];
export const Flash = () => { export const Flash = () => {
const [formStore, { Form, Field }] = createForm<FlashFormValues>({}); const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
const [devices, setDevices] = createSignal<BlockDevices>([]); const [devices, setDevices] = createSignal<BlockDevices>([]);
pyApi.show_block_devices.receive((r) => { // pyApi.show_block_devices.receive((r) => {
console.log("block devices", r); // console.log("block devices", r);
if (r.status === "success") { // if (r.status === "success") {
setDevices(r.data.blockdevices); // setDevices(r.data.blockdevices);
} // }
}); // });
const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => { const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => {
// pyApi.open_file.dispatch({ file_request: { mode: "save" } }); // pyApi.open_file.dispatch({ file_request: { mode: "save" } });
@@ -50,11 +51,11 @@ export const Flash = () => {
console.log("submit", values); console.log("submit", values);
}; };
effect(() => { // effect(() => {
if (route() === "flash") { // if (route() === "flash") {
pyApi.show_block_devices.dispatch({}); // pyApi.show_block_devices.dispatch({});
} // }
}); // });
return ( return (
<div class=""> <div class="">
<Form onSubmit={handleSubmit}> <Form onSubmit={handleSubmit}>

View File

@@ -16,15 +16,15 @@ type ServiceModel = Extract<
export const HostList: Component = () => { export const HostList: Component = () => {
const [services, setServices] = createSignal<ServiceModel>(); const [services, setServices] = createSignal<ServiceModel>();
pyApi.show_mdns.receive((r) => { // pyApi.show_mdns.receive((r) => {
const { status } = r; // const { status } = r;
if (status === "error") return console.error(r.errors); // if (status === "error") return console.error(r.errors);
setServices(r.data.services); // setServices(r.data.services);
}); // });
createEffect(() => { // createEffect(() => {
if (route() === "hosts") pyApi.show_mdns.dispatch({}); // if (route() === "hosts") pyApi.show_mdns.dispatch({});
}); // });
return ( return (
<div> <div>

View File

@@ -7,117 +7,86 @@ import {
createSignal, createSignal,
type Component, type Component,
} from "solid-js"; } from "solid-js";
import { useMachineContext } from "../../Config"; import { activeURI, route, setActiveURI } from "@/src/App";
import { route, setCurrClanURI } from "@/src/App"; import { OperationResponse, callApi, pyApi } from "@/src/api";
import { OperationResponse, pyApi } from "@/src/api";
import toast from "solid-toast"; import toast from "solid-toast";
import { MachineListItem } from "@/src/components/MachineListItem"; import { MachineListItem } from "@/src/components/MachineListItem";
type FilesModel = Extract< // type FilesModel = Extract<
OperationResponse<"get_directory">, // OperationResponse<"get_directory">,
{ status: "success" } // { status: "success" }
>["data"]["files"]; // >["data"]["files"];
type ServiceModel = Extract< // type ServiceModel = Extract<
OperationResponse<"show_mdns">, // OperationResponse<"show_mdns">,
{ status: "success" } // { status: "success" }
>["data"]["services"]; // >["data"]["services"];
type MachinesModel = Extract< type MachinesModel = Extract<
OperationResponse<"list_machines">, OperationResponse<"list_machines">,
{ status: "success" } { status: "success" }
>["data"]; >["data"];
pyApi.open_file.receive((r) => { // pyApi.open_file.receive((r) => {
if (r.op_key === "open_clan") { // if (r.op_key === "open_clan") {
console.log(r); // console.log(r);
if (r.status === "error") return console.error(r.errors); // if (r.status === "error") return console.error(r.errors);
if (r.data) { // if (r.data) {
setCurrClanURI(r.data); // setCurrClanURI(r.data);
} // }
} // }
}); // });
export const MachineListView: Component = () => { export const MachineListView: Component = () => {
const [{ machines, loading }, { getMachines }] = useMachineContext(); // const [files, setFiles] = createSignal<FilesModel>([]);
const [files, setFiles] = createSignal<FilesModel>([]); // pyApi.get_directory.receive((r) => {
pyApi.get_directory.receive((r) => { // const { status } = r;
const { status } = r; // if (status === "error") return console.error(r.errors);
if (status === "error") return console.error(r.errors); // setFiles(r.data.files);
setFiles(r.data.files); // });
});
const [services, setServices] = createSignal<ServiceModel>(); // const [services, setServices] = createSignal<ServiceModel>();
pyApi.show_mdns.receive((r) => { // pyApi.show_mdns.receive((r) => {
const { status } = r; // const { status } = r;
if (status === "error") return console.error(r.errors); // if (status === "error") return console.error(r.errors);
setServices(r.data.services); // setServices(r.data.services);
}); // });
createEffect(() => { const [machines, setMachines] = createSignal<MachinesModel>({});
console.log(files()); const [loading, setLoading] = createSignal<boolean>(false);
});
const [data, setData] = createSignal<MachinesModel>({}); const listMachines = async () => {
createEffect(() => { const uri = activeURI();
if (route() === "machines") getMachines(); if (!uri) {
}); return;
const unpackedMachines = () => Object.entries(data());
createEffect(() => {
const response = machines();
if (response?.status === "success") {
console.log(response.data);
setData(response.data);
toast.success("Machines loaded");
} }
if (response?.status === "error") { setLoading(true);
setData({}); const response = await callApi("list_machines", {
console.error(response.errors); flake_url: uri,
toast.error("Error loading machines"); });
response.errors.forEach((error) => setLoading(false);
toast.error( if (response.status === "success") {
`${error.message}: ${error.description} From ${error.location}` setMachines(response.data);
)
);
} }
};
createEffect(() => {
if (route() === "machines") listMachines();
}); });
const unpackedMachines = () => Object.entries(machines());
return ( return (
<div class="max-w-screen-lg"> <div class="max-w-screen-lg">
<div class="tooltip tooltip-bottom" data-tip="Open Clan"> <div class="tooltip tooltip-bottom" data-tip="Open Clan"></div>
<button
class="btn btn-ghost"
onClick={() =>
pyApi.open_file.dispatch({
file_request: {
title: "Open Clan",
mode: "select_folder",
},
op_key: "open_clan",
})
}
>
<span class="material-icons ">folder_open</span>
</button>
</div>
<div class="tooltip tooltip-bottom" data-tip="Search install targets">
<button
class="btn btn-ghost"
onClick={() => pyApi.show_mdns.dispatch({})}
>
<span class="material-icons ">search</span>
</button>
</div>
<div class="tooltip tooltip-bottom" data-tip="Refresh"> <div class="tooltip tooltip-bottom" data-tip="Refresh">
<button class="btn btn-ghost" onClick={() => getMachines()}> <button class="btn btn-ghost" onClick={() => listMachines()}>
<span class="material-icons ">refresh</span> <span class="material-icons ">refresh</span>
</button> </button>
</div> </div>
<Show when={services()}> {/* <Show when={services()}>
{(services) => ( {(services) => (
<For each={Object.values(services())}> <For each={Object.values(services())}>
{(service) => ( {(service) => (
@@ -163,7 +132,7 @@ export const MachineListView: Component = () => {
)} )}
</For> </For>
)} )}
</Show> </Show> */}
<Switch> <Switch>
<Match when={loading()}> <Match when={loading()}>
{/* Loading skeleton */} {/* Loading skeleton */}

View File

@@ -0,0 +1,98 @@
import { callApi } from "@/src/api";
import {
SubmitHandler,
createForm,
required,
setValue,
} from "@modular-forms/solid";
import { activeURI, setClanList, setActiveURI, setRoute } from "@/src/App";
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
type SettingsForm = {
base_dir: string | null;
};
export const registerClan = async () => {
try {
const loc = await callApi("open_file", {
file_request: { mode: "select_folder" },
});
console.log(loc);
if (loc.status === "success" && loc.data) {
// @ts-expect-error: data is a string
setClanList((s) => [...s, loc.data]);
setRoute((r) => {
if (r === "welcome") return "machines";
return r;
});
return loc.data;
}
} catch (e) {
//
}
};
export const Settings = () => {
const [formStore, { Form, Field }] = createForm<SettingsForm>({
initialValues: {
base_dir: activeURI(),
},
});
const handleSubmit: SubmitHandler<SettingsForm> = async (values, event) => {
//
};
return (
<div class="card card-normal">
<Form onSubmit={handleSubmit} shouldActive>
<div class="card-body">
<Field name="base_dir" validate={[required("Clan URI is required")]}>
{(field, props) => (
<label class="form-control w-full">
<div class="label">
<span class="label-text block after:ml-0.5 after:text-primary">
Directory
</span>
</div>
<div class="stats shadow">
<div class="stat">
<div class="stat-figure text-primary">
<span class="material-icons">inventory</span>
</div>
<div class="stat-title">Clan URI</div>
<div
class="stat-value"
classList={{ "text-slate-500": !field.value }}
>
{field.value || "Not set"}
<button
class="btn btn-ghost mx-4"
onClick={async () => {
const location = await registerClan();
if (location) {
setActiveURI(location);
setValue(formStore, "base_dir", location);
}
}}
>
<span class="material-icons">edit</span>
</button>
</div>
<div class="stat-desc">Where the clan source resides</div>
</div>
</div>
<div class="label">
{field.error && (
<span class="label-text-alt">{field.error}</span>
)}
</div>
</label>
)}
</Field>
</div>
</Form>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { setActiveURI, setRoute } from "@/src/App";
import { registerClan } from "../settings";
export const Welcome = () => {
return (
<div class="hero min-h-screen">
<div class="hero-content mb-32 text-center">
<div class="max-w-md">
<h1 class="text-5xl font-bold">Welcome to Clan</h1>
<p class="py-6">Own the services you use.</p>
<div class="flex flex-col items-start gap-2">
<button
class="btn btn-primary w-full"
onClick={() => setRoute("createClan")}
>
Build your own
</button>
<button
class="link w-full text-right text-primary"
onClick={async () => {
const uri = await registerClan();
if (uri) setActiveURI(uri);
}}
>
Or select folder
</button>
</div>
</div>
</div>
</div>
);
};

View File

@@ -16,7 +16,7 @@
npmDeps = pkgs.fetchNpmDeps { npmDeps = pkgs.fetchNpmDeps {
src = ./app; src = ./app;
hash = "sha256-3LjcHh+jCuarh9XmS+mOv7xaGgAHxf3L7fWnxxmxUGQ="; hash = "sha256-U8FwGL0FelUZwa8NjitfsFNDSofUPbp+nHrypeDj2Po=";
}; };
# The prepack script runs the build script, which we'd rather do in the build phase. # The prepack script runs the build script, which we'd rather do in the build phase.
npmPackFlags = [ "--ignore-scripts" ]; npmPackFlags = [ "--ignore-scripts" ];