Webview: add welcome workflow
This commit is contained in:
52
pkgs/webview-ui/app/package-lock.json
generated
52
pkgs/webview-ui/app/package-lock.json
generated
@@ -10,8 +10,10 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modular-forms/solid": "^0.21.0",
|
||||
"@solid-primitives/storage": "^3.7.1",
|
||||
"@tanstack/solid-query": "^5.44.0",
|
||||
"material-icons": "^1.13.12",
|
||||
"nanoid": "^5.0.7",
|
||||
"solid-js": "^1.8.11",
|
||||
"solid-toast": "^0.5.0"
|
||||
},
|
||||
@@ -1498,6 +1500,26 @@
|
||||
"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": {
|
||||
"version": "0.0.111",
|
||||
"resolved": "https://registry.npmjs.org/@solid-primitives/styles/-/styles-0.0.111.tgz",
|
||||
@@ -1515,7 +1537,6 @@
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@solid-primitives/utils/-/utils-6.2.3.tgz",
|
||||
"integrity": "sha512-CqAwKb2T5Vi72+rhebSsqNZ9o67buYRdEJrIFzRXz3U59QqezuuxPsyzTSVCacwS5Pf109VRsgCJQoxKRoECZQ==",
|
||||
"dev": true,
|
||||
"peerDependencies": {
|
||||
"solid-js": "^1.6.12"
|
||||
}
|
||||
@@ -4177,10 +4198,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"dev": true,
|
||||
"version": "5.0.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz",
|
||||
"integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -4188,10 +4208,10 @@
|
||||
}
|
||||
],
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/natural-compare": {
|
||||
@@ -4753,6 +4773,24 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"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": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
|
||||
@@ -39,8 +39,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@modular-forms/solid": "^0.21.0",
|
||||
"@solid-primitives/storage": "^3.7.1",
|
||||
"@tanstack/solid-query": "^5.44.0",
|
||||
"material-icons": "^1.13.12",
|
||||
"nanoid": "^5.0.7",
|
||||
"solid-js": "^1.8.11",
|
||||
"solid-toast": "^0.5.0"
|
||||
}
|
||||
|
||||
@@ -1,24 +1,35 @@
|
||||
import { createSignal, type Component } from "solid-js";
|
||||
import { MachineProvider } from "./Config";
|
||||
import { Layout } from "./layout/layout";
|
||||
import { Route, Router } from "./Routes";
|
||||
import { Toaster } from "solid-toast";
|
||||
import { effect } from "solid-js/web";
|
||||
import { makePersisted } from "@solid-primitives/storage";
|
||||
|
||||
// Some global state
|
||||
const [route, setRoute] = createSignal<Route>("machines");
|
||||
export { route, setRoute };
|
||||
|
||||
const [currClanURI, setCurrClanURI] = createSignal<string | null>(null);
|
||||
export { currClanURI, setCurrClanURI };
|
||||
const [activeURI, setActiveURI] = createSignal<string | null>(null);
|
||||
export { activeURI, setActiveURI };
|
||||
|
||||
const [clanList, setClanList] = makePersisted(createSignal<string[]>([]), {
|
||||
name: "clanList",
|
||||
storage: localStorage,
|
||||
});
|
||||
|
||||
export { clanList, setClanList };
|
||||
|
||||
const App: Component = () => {
|
||||
effect(() => {
|
||||
if (clanList().length === 0) {
|
||||
setRoute("welcome");
|
||||
}
|
||||
});
|
||||
return [
|
||||
<Toaster position="top-right" />,
|
||||
<MachineProvider>
|
||||
<Layout>
|
||||
<Router route={route} />
|
||||
</Layout>
|
||||
</MachineProvider>,
|
||||
<Layout>
|
||||
<Router route={route} />
|
||||
</Layout>,
|
||||
];
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { CreateClan } from "./routes/clan/view";
|
||||
import { HostList } from "./routes/hosts/view";
|
||||
import { BlockDevicesView } from "./routes/blockdevices/view";
|
||||
import { Flash } from "./routes/flash/view";
|
||||
import { Settings } from "./routes/settings";
|
||||
import { Welcome } from "./routes/welcome";
|
||||
|
||||
export type Route = keyof typeof routes;
|
||||
|
||||
@@ -39,6 +41,16 @@ export const routes = {
|
||||
label: "Colors",
|
||||
icon: "color_lens",
|
||||
},
|
||||
settings: {
|
||||
child: Settings,
|
||||
label: "Settings",
|
||||
icon: "settings",
|
||||
},
|
||||
welcome: {
|
||||
child: Welcome,
|
||||
label: "welcome",
|
||||
icon: "settings",
|
||||
},
|
||||
};
|
||||
|
||||
interface RouterProps {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Match, Show, Switch, createSignal } from "solid-js";
|
||||
import { ErrorData, SuccessData, pyApi } from "../api";
|
||||
import { currClanURI } from "../App";
|
||||
|
||||
type MachineDetails = SuccessData<"list_machines">["data"][string];
|
||||
|
||||
@@ -23,51 +22,51 @@ const [deploymentInfo, setDeploymentInfo] = createSignal<DeploymentInfo>({});
|
||||
|
||||
const [errors, setErrors] = createSignal<MachineErrors>({});
|
||||
|
||||
pyApi.show_machine_hardware_info.receive((r) => {
|
||||
const { op_key } = r;
|
||||
if (r.status === "error") {
|
||||
console.error(r.errors);
|
||||
if (op_key) {
|
||||
setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op_key) {
|
||||
setHwInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||
}
|
||||
});
|
||||
// pyApi.show_machine_hardware_info.receive((r) => {
|
||||
// const { op_key } = r;
|
||||
// if (r.status === "error") {
|
||||
// console.error(r.errors);
|
||||
// if (op_key) {
|
||||
// setHwInfo((d) => ({ ...d, [op_key]: { system: null } }));
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// if (op_key) {
|
||||
// setHwInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||
// }
|
||||
// });
|
||||
|
||||
pyApi.show_machine_deployment_target.receive((r) => {
|
||||
const { op_key } = r;
|
||||
if (r.status === "error") {
|
||||
console.error(r.errors);
|
||||
if (op_key) {
|
||||
setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (op_key) {
|
||||
setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||
}
|
||||
});
|
||||
// pyApi.show_machine_deployment_target.receive((r) => {
|
||||
// const { op_key } = r;
|
||||
// if (r.status === "error") {
|
||||
// console.error(r.errors);
|
||||
// if (op_key) {
|
||||
// setDeploymentInfo((d) => ({ ...d, [op_key]: null }));
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
// if (op_key) {
|
||||
// setDeploymentInfo((d) => ({ ...d, [op_key]: r.data }));
|
||||
// }
|
||||
// });
|
||||
|
||||
export const MachineListItem = (props: MachineListItemProps) => {
|
||||
const { name, info } = props;
|
||||
|
||||
const clan_dir = currClanURI();
|
||||
if (clan_dir) {
|
||||
pyApi.show_machine_hardware_info.dispatch({
|
||||
op_key: name,
|
||||
clan_dir,
|
||||
machine_name: name,
|
||||
});
|
||||
// const clan_dir = currClanURI();
|
||||
// if (clan_dir) {
|
||||
// pyApi.show_machine_hardware_info.dispatch({
|
||||
// op_key: name,
|
||||
// clan_dir,
|
||||
// machine_name: name,
|
||||
// });
|
||||
|
||||
pyApi.show_machine_deployment_target.dispatch({
|
||||
op_key: name,
|
||||
clan_dir,
|
||||
machine_name: name,
|
||||
});
|
||||
}
|
||||
// pyApi.show_machine_deployment_target.dispatch({
|
||||
// op_key: name,
|
||||
// clan_dir,
|
||||
// machine_name: name,
|
||||
// });
|
||||
// }
|
||||
|
||||
return (
|
||||
<li>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { currClanURI } from "../App";
|
||||
import { activeURI, setRoute } from "../App";
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
@@ -14,12 +14,12 @@ export const Header = () => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<a class="text-xl">{currClanURI() || "Clan"}</a>
|
||||
<a class="text-xl">{activeURI()}</a>
|
||||
</div>
|
||||
<div class="flex-none">
|
||||
<span class="tooltip tooltip-bottom" data-tip="Account">
|
||||
<button class="btn btn-square btn-ghost">
|
||||
<span class="material-icons">account_circle</span>
|
||||
<span class="tooltip tooltip-bottom" data-tip="Settings">
|
||||
<button class="link" onClick={() => setRoute("settings")}>
|
||||
<span class="material-icons">settings</span>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { Component, JSXElement } from "solid-js";
|
||||
import { Component, JSXElement, Show } from "solid-js";
|
||||
import { Header } from "./header";
|
||||
import { Sidebar } from "../Sidebar";
|
||||
import { route, setRoute } from "../App";
|
||||
import { effect } from "solid-js/web";
|
||||
|
||||
interface LayoutProps {
|
||||
children: JSXElement;
|
||||
}
|
||||
|
||||
export const Layout: Component<LayoutProps> = (props) => {
|
||||
effect(() => {
|
||||
console.log(route());
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div class="drawer bg-base-100 lg:drawer-open">
|
||||
@@ -17,11 +21,16 @@ export const Layout: Component<LayoutProps> = (props) => {
|
||||
class="drawer-toggle hidden"
|
||||
/>
|
||||
<div class="drawer-content">
|
||||
<Header />
|
||||
<Show when={route() !== "welcome"}>
|
||||
<Header />
|
||||
</Show>
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
<div class="drawer-side z-40">
|
||||
<div
|
||||
class="drawer-side z-40"
|
||||
classList={{ "!hidden": route() === "welcome" }}
|
||||
>
|
||||
<label
|
||||
for="toplevel-drawer"
|
||||
aria-label="close sidebar"
|
||||
|
||||
@@ -8,24 +8,24 @@ type DevicesModel = Extract<
|
||||
>["data"]["blockdevices"];
|
||||
|
||||
export const BlockDevicesView: Component = () => {
|
||||
const [devices, setServices] = createSignal<DevicesModel>();
|
||||
const [devices, setDevices] = createSignal<DevicesModel>();
|
||||
|
||||
pyApi.show_block_devices.receive((r) => {
|
||||
const { status } = r;
|
||||
if (status === "error") return console.error(r.errors);
|
||||
setServices(r.data.blockdevices);
|
||||
});
|
||||
// pyApi.show_block_devices.receive((r) => {
|
||||
// const { status } = r;
|
||||
// if (status === "error") return console.error(r.errors);
|
||||
// setServices(r.data.blockdevices);
|
||||
// });
|
||||
|
||||
createEffect(() => {
|
||||
if (route() === "blockdevices") pyApi.show_block_devices.dispatch({});
|
||||
});
|
||||
// createEffect(() => {
|
||||
// if (route() === "blockdevices") pyApi.show_block_devices.dispatch({});
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="tooltip tooltip-bottom" data-tip="Refresh">
|
||||
<button
|
||||
class="btn btn-ghost"
|
||||
onClick={() => pyApi.show_block_devices.dispatch({})}
|
||||
// onClick={() => pyApi.show_block_devices.dispatch({})}
|
||||
>
|
||||
<span class="material-icons ">refresh</span>
|
||||
</button>
|
||||
|
||||
@@ -15,31 +15,24 @@ import {
|
||||
custom,
|
||||
} from "@modular-forms/solid";
|
||||
import toast from "solid-toast";
|
||||
import { setCurrClanURI, setRoute } from "@/src/App";
|
||||
import { isValidHostname } from "@/util";
|
||||
import { setActiveURI, setRoute } from "@/src/App";
|
||||
|
||||
interface ClanDetailsProps {
|
||||
directory: string;
|
||||
}
|
||||
|
||||
interface ClanFormProps {
|
||||
actions: JSX.Element;
|
||||
}
|
||||
|
||||
type CreateForm = Meta & {
|
||||
template_url: string;
|
||||
};
|
||||
|
||||
export const ClanForm = (props: ClanFormProps) => {
|
||||
const { actions } = props;
|
||||
export const ClanForm = () => {
|
||||
const [formStore, { Form, Field }] = createForm<CreateForm>({
|
||||
initialValues: {
|
||||
template_url: "git+https://git.clan.lol/clan/clan-core#templates.minimal",
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<CreateForm> = (values, event) => {
|
||||
console.log("submit", values);
|
||||
const handleSubmit: SubmitHandler<CreateForm> = async (values, event) => {
|
||||
const { template_url, ...meta } = values;
|
||||
pyApi.open_file.dispatch({
|
||||
file_request: {
|
||||
@@ -49,55 +42,60 @@ export const ClanForm = (props: ClanFormProps) => {
|
||||
op_key: "create_clan",
|
||||
});
|
||||
|
||||
pyApi.open_file.receive((r) => {
|
||||
if (r.op_key !== "create_clan") {
|
||||
return;
|
||||
}
|
||||
if (r.status !== "success") {
|
||||
toast.error("Cannot select clan directory");
|
||||
return;
|
||||
}
|
||||
const target_dir = r?.data;
|
||||
if (!target_dir) {
|
||||
toast.error("Cannot select clan directory");
|
||||
return;
|
||||
}
|
||||
// await new Promise<void>((done) => {
|
||||
// pyApi.open_file.receive((r) => {
|
||||
// if (r.op_key !== "create_clan") {
|
||||
// done();
|
||||
// return;
|
||||
// }
|
||||
// if (r.status !== "success") {
|
||||
// toast.error("Cannot select clan directory");
|
||||
// done();
|
||||
// return;
|
||||
// }
|
||||
// const target_dir = r?.data;
|
||||
// if (!target_dir) {
|
||||
// toast.error("Cannot select clan directory");
|
||||
// done();
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (!isValidHostname(target_dir)) {
|
||||
toast.error(`Directory name must be valid URI: ${target_dir}`);
|
||||
return;
|
||||
}
|
||||
// console.log({ formStore });
|
||||
|
||||
toast.promise(
|
||||
new Promise<void>((resolve, reject) => {
|
||||
pyApi.create_clan.receive((r) => {
|
||||
if (r.status === "error") {
|
||||
reject();
|
||||
console.error(r.errors);
|
||||
}
|
||||
resolve();
|
||||
// Navigate to the new clan
|
||||
setCurrClanURI(target_dir);
|
||||
setRoute("machines");
|
||||
});
|
||||
// toast.promise(
|
||||
// new Promise<void>((resolve, reject) => {
|
||||
// pyApi.create_clan.receive((r) => {
|
||||
// done();
|
||||
// if (r.status === "error") {
|
||||
// reject();
|
||||
// console.error(r.errors);
|
||||
// return;
|
||||
// }
|
||||
// resolve();
|
||||
|
||||
pyApi.create_clan.dispatch({
|
||||
options: { directory: target_dir, meta, template_url },
|
||||
op_key: "create_clan",
|
||||
});
|
||||
}),
|
||||
{
|
||||
loading: "Creating clan...",
|
||||
success: "Clan Successfully Created",
|
||||
error: "Failed to create clan",
|
||||
}
|
||||
);
|
||||
});
|
||||
// // Navigate to the new clan
|
||||
// setCurrClanURI(target_dir);
|
||||
// setRoute("machines");
|
||||
// });
|
||||
|
||||
// pyApi.create_clan.dispatch({
|
||||
// options: { directory: target_dir, meta, template_url },
|
||||
// op_key: "create_clan",
|
||||
// });
|
||||
// }),
|
||||
// {
|
||||
// loading: "Creating clan...",
|
||||
// success: "Clan Successfully Created",
|
||||
// error: "Failed to create clan",
|
||||
// }
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="card card-normal">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Form onSubmit={handleSubmit} shouldActive>
|
||||
<Field name="icon">
|
||||
{(field, props) => (
|
||||
<>
|
||||
@@ -201,7 +199,17 @@ export const ClanForm = (props: ClanFormProps) => {
|
||||
</div>
|
||||
)}
|
||||
</Field>
|
||||
{actions}
|
||||
{
|
||||
<div class="card-actions justify-end">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
type="submit"
|
||||
disabled={formStore.submitting}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
@@ -212,71 +220,3 @@ type Meta = Extract<
|
||||
OperationResponse<"show_clan_meta">,
|
||||
{ status: "success" }
|
||||
>["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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import { pyApi } from "@/src/api";
|
||||
import { Match, Switch, createEffect, createSignal } from "solid-js";
|
||||
import toast from "solid-toast";
|
||||
import { ClanDetails, ClanForm } from "./clanDetails";
|
||||
import { ClanForm } from "./clanDetails";
|
||||
|
||||
export const CreateClan = () => {
|
||||
return (
|
||||
<div>
|
||||
<ClanForm
|
||||
actions={
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-primary" type="submit">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<ClanForm />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -24,16 +24,17 @@ type BlockDevices = Extract<
|
||||
OperationResponse<"show_block_devices">,
|
||||
{ status: "success" }
|
||||
>["data"]["blockdevices"];
|
||||
|
||||
export const Flash = () => {
|
||||
const [formStore, { Form, Field }] = createForm<FlashFormValues>({});
|
||||
|
||||
const [devices, setDevices] = createSignal<BlockDevices>([]);
|
||||
pyApi.show_block_devices.receive((r) => {
|
||||
console.log("block devices", r);
|
||||
if (r.status === "success") {
|
||||
setDevices(r.data.blockdevices);
|
||||
}
|
||||
});
|
||||
// pyApi.show_block_devices.receive((r) => {
|
||||
// console.log("block devices", r);
|
||||
// if (r.status === "success") {
|
||||
// setDevices(r.data.blockdevices);
|
||||
// }
|
||||
// });
|
||||
|
||||
const handleSubmit: SubmitHandler<FlashFormValues> = (values, event) => {
|
||||
// pyApi.open_file.dispatch({ file_request: { mode: "save" } });
|
||||
@@ -50,11 +51,11 @@ export const Flash = () => {
|
||||
console.log("submit", values);
|
||||
};
|
||||
|
||||
effect(() => {
|
||||
if (route() === "flash") {
|
||||
pyApi.show_block_devices.dispatch({});
|
||||
}
|
||||
});
|
||||
// effect(() => {
|
||||
// if (route() === "flash") {
|
||||
// pyApi.show_block_devices.dispatch({});
|
||||
// }
|
||||
// });
|
||||
return (
|
||||
<div class="">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
|
||||
@@ -16,15 +16,15 @@ type ServiceModel = Extract<
|
||||
export const HostList: Component = () => {
|
||||
const [services, setServices] = createSignal<ServiceModel>();
|
||||
|
||||
pyApi.show_mdns.receive((r) => {
|
||||
const { status } = r;
|
||||
if (status === "error") return console.error(r.errors);
|
||||
setServices(r.data.services);
|
||||
});
|
||||
// pyApi.show_mdns.receive((r) => {
|
||||
// const { status } = r;
|
||||
// if (status === "error") return console.error(r.errors);
|
||||
// setServices(r.data.services);
|
||||
// });
|
||||
|
||||
createEffect(() => {
|
||||
if (route() === "hosts") pyApi.show_mdns.dispatch({});
|
||||
});
|
||||
// createEffect(() => {
|
||||
// if (route() === "hosts") pyApi.show_mdns.dispatch({});
|
||||
// });
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -7,117 +7,86 @@ import {
|
||||
createSignal,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
import { useMachineContext } from "../../Config";
|
||||
import { route, setCurrClanURI } from "@/src/App";
|
||||
import { OperationResponse, pyApi } from "@/src/api";
|
||||
import { activeURI, route, setActiveURI } from "@/src/App";
|
||||
import { OperationResponse, callApi, pyApi } from "@/src/api";
|
||||
import toast from "solid-toast";
|
||||
import { MachineListItem } from "@/src/components/MachineListItem";
|
||||
|
||||
type FilesModel = Extract<
|
||||
OperationResponse<"get_directory">,
|
||||
{ status: "success" }
|
||||
>["data"]["files"];
|
||||
// type FilesModel = Extract<
|
||||
// OperationResponse<"get_directory">,
|
||||
// { status: "success" }
|
||||
// >["data"]["files"];
|
||||
|
||||
type ServiceModel = Extract<
|
||||
OperationResponse<"show_mdns">,
|
||||
{ status: "success" }
|
||||
>["data"]["services"];
|
||||
// type ServiceModel = Extract<
|
||||
// OperationResponse<"show_mdns">,
|
||||
// { status: "success" }
|
||||
// >["data"]["services"];
|
||||
|
||||
type MachinesModel = Extract<
|
||||
OperationResponse<"list_machines">,
|
||||
{ status: "success" }
|
||||
>["data"];
|
||||
|
||||
pyApi.open_file.receive((r) => {
|
||||
if (r.op_key === "open_clan") {
|
||||
console.log(r);
|
||||
if (r.status === "error") return console.error(r.errors);
|
||||
// pyApi.open_file.receive((r) => {
|
||||
// if (r.op_key === "open_clan") {
|
||||
// console.log(r);
|
||||
// if (r.status === "error") return console.error(r.errors);
|
||||
|
||||
if (r.data) {
|
||||
setCurrClanURI(r.data);
|
||||
}
|
||||
}
|
||||
});
|
||||
// if (r.data) {
|
||||
// setCurrClanURI(r.data);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
export const MachineListView: Component = () => {
|
||||
const [{ machines, loading }, { getMachines }] = useMachineContext();
|
||||
// const [files, setFiles] = createSignal<FilesModel>([]);
|
||||
|
||||
const [files, setFiles] = createSignal<FilesModel>([]);
|
||||
pyApi.get_directory.receive((r) => {
|
||||
const { status } = r;
|
||||
if (status === "error") return console.error(r.errors);
|
||||
setFiles(r.data.files);
|
||||
});
|
||||
// pyApi.get_directory.receive((r) => {
|
||||
// const { status } = r;
|
||||
// if (status === "error") return console.error(r.errors);
|
||||
// setFiles(r.data.files);
|
||||
// });
|
||||
|
||||
const [services, setServices] = createSignal<ServiceModel>();
|
||||
pyApi.show_mdns.receive((r) => {
|
||||
const { status } = r;
|
||||
if (status === "error") return console.error(r.errors);
|
||||
setServices(r.data.services);
|
||||
});
|
||||
// const [services, setServices] = createSignal<ServiceModel>();
|
||||
// pyApi.show_mdns.receive((r) => {
|
||||
// const { status } = r;
|
||||
// if (status === "error") return console.error(r.errors);
|
||||
// setServices(r.data.services);
|
||||
// });
|
||||
|
||||
createEffect(() => {
|
||||
console.log(files());
|
||||
});
|
||||
const [machines, setMachines] = createSignal<MachinesModel>({});
|
||||
const [loading, setLoading] = createSignal<boolean>(false);
|
||||
|
||||
const [data, setData] = createSignal<MachinesModel>({});
|
||||
createEffect(() => {
|
||||
if (route() === "machines") getMachines();
|
||||
});
|
||||
|
||||
const unpackedMachines = () => Object.entries(data());
|
||||
|
||||
createEffect(() => {
|
||||
const response = machines();
|
||||
if (response?.status === "success") {
|
||||
console.log(response.data);
|
||||
setData(response.data);
|
||||
toast.success("Machines loaded");
|
||||
const listMachines = async () => {
|
||||
const uri = activeURI();
|
||||
if (!uri) {
|
||||
return;
|
||||
}
|
||||
if (response?.status === "error") {
|
||||
setData({});
|
||||
console.error(response.errors);
|
||||
toast.error("Error loading machines");
|
||||
response.errors.forEach((error) =>
|
||||
toast.error(
|
||||
`${error.message}: ${error.description} From ${error.location}`
|
||||
)
|
||||
);
|
||||
setLoading(true);
|
||||
const response = await callApi("list_machines", {
|
||||
flake_url: uri,
|
||||
});
|
||||
setLoading(false);
|
||||
if (response.status === "success") {
|
||||
setMachines(response.data);
|
||||
}
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
if (route() === "machines") listMachines();
|
||||
});
|
||||
|
||||
const unpackedMachines = () => Object.entries(machines());
|
||||
|
||||
return (
|
||||
<div class="max-w-screen-lg">
|
||||
<div class="tooltip tooltip-bottom" data-tip="Open Clan">
|
||||
<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="Open Clan"></div>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
<Show when={services()}>
|
||||
{/* <Show when={services()}>
|
||||
{(services) => (
|
||||
<For each={Object.values(services())}>
|
||||
{(service) => (
|
||||
@@ -163,7 +132,7 @@ export const MachineListView: Component = () => {
|
||||
)}
|
||||
</For>
|
||||
)}
|
||||
</Show>
|
||||
</Show> */}
|
||||
<Switch>
|
||||
<Match when={loading()}>
|
||||
{/* Loading skeleton */}
|
||||
|
||||
98
pkgs/webview-ui/app/src/routes/settings/index.tsx
Normal file
98
pkgs/webview-ui/app/src/routes/settings/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
32
pkgs/webview-ui/app/src/routes/welcome/index.tsx
Normal file
32
pkgs/webview-ui/app/src/routes/welcome/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
npmDeps = pkgs.fetchNpmDeps {
|
||||
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.
|
||||
npmPackFlags = [ "--ignore-scripts" ];
|
||||
|
||||
Reference in New Issue
Block a user