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",
|
"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",
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 { 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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
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 {
|
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" ];
|
||||||
|
|||||||
Reference in New Issue
Block a user