Webview: add welcome workflow

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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");
}
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}`
)
);
const listMachines = async () => {
const uri = activeURI();
if (!uri) {
return;
}
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 */}

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 {
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" ];