From 2fc5a83a1aba394d7247d18bdac2168c5d2eccbd Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Thu, 21 Oct 2021 19:18:58 -0400 Subject: [PATCH 1/9] Add the ability for Thorium Nova to host plugin assets. Closes #64 --- server/src/init/httpServer.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/init/httpServer.ts b/server/src/init/httpServer.ts index b59ab57a..95204e48 100644 --- a/server/src/init/httpServer.ts +++ b/server/src/init/httpServer.ts @@ -4,6 +4,7 @@ import staticServe from "fastify-static"; import cors from "fastify-cors"; import path from "path"; import {promises as fs} from "fs"; +import {thoriumPath} from "../utils/appPaths"; export default function buildHTTPServer({ staticRoot = path.join(__dirname, "public"), @@ -30,6 +31,11 @@ export default function buildHTTPServer({ }); app.register(staticServe, {root: `${staticRoot}/assets`, prefix: "/assets"}); + app.register(staticServe, { + root: `${thoriumPath}/plugins`, + prefix: "/plugins", + decorateReply: false, + }); app.get("/*", async (_req, reply) => { // Return a slightly different index.html for headless servers. From 1b614d302fbb992edcda1eb9a3f10c008170fbfe Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Thu, 21 Oct 2021 19:37:20 -0400 Subject: [PATCH 2/9] Starting on the plugin editing UI --- client/src/App.tsx | 11 +++---- client/src/pages/Config/index.tsx | 26 +++++++++++++++ server/src/classes/Plugins/index.ts | 49 +++++++++++++++++++++++++++++ 3 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 client/src/pages/Config/index.tsx create mode 100644 server/src/classes/Plugins/index.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index dea884cb..6bdf43bd 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -9,7 +9,9 @@ import Credits from "./components/Credits"; import {WelcomeLogo} from "./components/WelcomeLogo"; import {WelcomeButtons} from "./components/WelcomeButtons"; import {FlightLobby} from "./components/FlightLobby"; -import {DocLayout, routes as docRoutes} from "./docs"; + +const DocLayout = lazy(() => import("./docs")); +const Config = lazy(() => import("./pages/Config")); const MainPage = () => { // const {netSend} = useThorium(); @@ -62,11 +64,8 @@ function AppRoutes() { /> } /> } /> - }> - {docRoutes.map(({path, component: Component = Fragment}) => ( - } /> - ))} - + }> + }> } /> ); diff --git a/client/src/pages/Config/index.tsx b/client/src/pages/Config/index.tsx new file mode 100644 index 00000000..19492bd4 --- /dev/null +++ b/client/src/pages/Config/index.tsx @@ -0,0 +1,26 @@ +import Menubar from "@thorium/ui/Menubar"; +import SearchableList from "@thorium/ui/SearchableList"; + +export default function Config() { + return ( +
+ +
+

Plugin Config

+ +
+
+ +
+
hi
+
+
+
+ ); +} diff --git a/server/src/classes/Plugins/index.ts b/server/src/classes/Plugins/index.ts new file mode 100644 index 00000000..8fa2bb3e --- /dev/null +++ b/server/src/classes/Plugins/index.ts @@ -0,0 +1,49 @@ +import uniqid from "@thorium/uniqid"; +import {pubsub} from "server/src/utils/pubsub"; + +// export function publish(plugin: BasePlugin) { +// pubsub.publish("plugins", { +// id: plugin.id, +// }); +// pubsub.publish("plugin", { +// id: plugin.id, +// }); +// } + +export default class BasePlugin { + id: string; + name: string; + author: string; + description: string; + assetPath(asset: string) { + return asset ? `/plugins/${this.name}/${asset}` : ""; + } + get coverImage() { + return this.assetPath(this._coverImage); + } + set coverImage(value) { + this._coverImage = value; + } + _coverImage!: string; + + tags: string[]; + + constructor(params: Partial = {}) { + this.id = params.id || uniqid(); + this.name = params.name || "New Plugin"; + this.author = params.author || ""; + this.description = params.description || "A great plugin"; + this.coverImage = params._coverImage || ""; + this.tags = params.tags || []; + } + async writeFile(force?: boolean) {} + async removeFile(force?: boolean) {} + + save() { + this.writeFile(true); + } + serialize() { + const data = {...this}; + return data; + } +} From aaf2b7b8c9f63fa9c12174fdae26ca459d9edd82 Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Thu, 21 Oct 2021 20:26:00 -0400 Subject: [PATCH 3/9] Initial UI for the plugin configuration page. --- client/src/pages/Config/index.tsx | 141 ++++++++++++++++++++++++++++-- 1 file changed, 135 insertions(+), 6 deletions(-) diff --git a/client/src/pages/Config/index.tsx b/client/src/pages/Config/index.tsx index 19492bd4..07b4ced7 100644 --- a/client/src/pages/Config/index.tsx +++ b/client/src/pages/Config/index.tsx @@ -1,24 +1,153 @@ +import {usePrompt} from "@thorium/ui/AlertDialog"; +import Button from "@thorium/ui/Button"; +import InfoTip from "@thorium/ui/InfoTip"; +import Input from "@thorium/ui/Input"; import Menubar from "@thorium/ui/Menubar"; import SearchableList from "@thorium/ui/SearchableList"; +import TagInput from "@thorium/ui/TagInput"; +import UploadWell from "@thorium/ui/UploadWell"; +import {useState} from "react"; +import {FaEdit} from "react-icons/fa"; +import {NavLink, useNavigate, useParams} from "react-router-dom"; +let plugin: null | { + id: string; + name: string; + description: string; + tags: string[]; +} = null; +const setName = (params: any) => {}; +const setDescription = (params: any) => {}; +const setTags = (params: any) => {}; export default function Config() { + const [error, setError] = useState(false); + + const navigate = useNavigate(); + const params = useParams(); + const prompt = usePrompt(); return (
-
-

Plugin Config

+
+

Plugin Config

+ +
+
+ -
-
navigate(`/config/${id}`)} + renderItem={c => ( +
+
+ {c.name} +
+ {c.author} +
+
+ e.stopPropagation()} + > + + +
+ )} + /> +
+
+ setError(false)} + onBlur={(e: React.FocusEvent) => { + const target = e.target as HTMLInputElement; + plugin && target.value + ? setName({ + variables: {id: plugin.id, name: target.value}, + }) + : setError(true); + }} /> + setError(false)} + onBlur={(e: React.FocusEvent) => { + const target = e.target as HTMLInputElement; + plugin && target.value + ? setDescription({ + variables: {id: plugin.id, description: target.value}, + }) + : setError(true); + }} + /> + { + if (plugin?.tags.includes(tag) || !plugin) return; + setTags({ + variables: {id: plugin.id, tags: plugin.tags.concat(tag)}, + }); + }} + onRemove={tag => { + if (!plugin) return; + setTags({ + variables: { + id: plugin.id, + tags: plugin.tags.filter(t => t !== tag), + }, + }); + }} + /> +
+
+
-
hi
From 58430fbafcdc6770013bfcdb6e76f88719b41a00 Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Sat, 23 Oct 2021 07:57:54 -0400 Subject: [PATCH 4/9] Basic plugin definition. Closes #58 --- server/src/classes/Plugins/index.ts | 16 ++++++++-------- server/src/classes/ServerDataModel.ts | 3 +++ server/src/index.ts | 1 + server/src/systems/__test__/TimerSystem.test.ts | 2 +- server/src/utils/ecs/__test__/ecs.test.ts | 2 +- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/server/src/classes/Plugins/index.ts b/server/src/classes/Plugins/index.ts index 8fa2bb3e..0a18e977 100644 --- a/server/src/classes/Plugins/index.ts +++ b/server/src/classes/Plugins/index.ts @@ -1,14 +1,14 @@ import uniqid from "@thorium/uniqid"; import {pubsub} from "server/src/utils/pubsub"; -// export function publish(plugin: BasePlugin) { -// pubsub.publish("plugins", { -// id: plugin.id, -// }); -// pubsub.publish("plugin", { -// id: plugin.id, -// }); -// } +export function pluginPublish(plugin: BasePlugin) { + pubsub.publish("pluginsList", { + id: plugin.id, + }); + pubsub.publish("plugin", { + pluginId: plugin.id, + }); +} export default class BasePlugin { id: string; diff --git a/server/src/classes/ServerDataModel.ts b/server/src/classes/ServerDataModel.ts index b0ec4d7a..844b99f9 100644 --- a/server/src/classes/ServerDataModel.ts +++ b/server/src/classes/ServerDataModel.ts @@ -1,9 +1,11 @@ import {ServerClient} from "./Client"; +import BasePlugin from "./Plugins"; export class ServerDataModel { clients: Record; thoriumId: string; activeFlightName: string | null; + plugins: BasePlugin[] = []; constructor(params: ServerDataModel) { this.clients = Object.fromEntries( Object.entries(params.clients).map(([id, client]) => [ @@ -13,5 +15,6 @@ export class ServerDataModel { ); this.thoriumId = params.thoriumId; this.activeFlightName = params.activeFlightName; + this.plugins = params.plugins?.map(plugin => new BasePlugin(plugin)) || []; } } diff --git a/server/src/index.ts b/server/src/index.ts index 9c259e0b..6d0d3fa0 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -25,6 +25,7 @@ export async function startServer() { clients: {}, thoriumId: randomWords(3).join("-"), activeFlightName: null, + plugins: [], }, serialize: ({clients, ...data}) => ({ ...data, diff --git a/server/src/systems/__test__/TimerSystem.test.ts b/server/src/systems/__test__/TimerSystem.test.ts index b52d8daf..624936bd 100644 --- a/server/src/systems/__test__/TimerSystem.test.ts +++ b/server/src/systems/__test__/TimerSystem.test.ts @@ -1,7 +1,7 @@ import {ECS, Entity} from "server/src/utils/ecs"; import {TimerSystem} from "../TimerSystem"; -const server = {clients: {}, activeFlightName: "", thoriumId: ""}; +const server = {clients: {}, activeFlightName: "", thoriumId: "", plugins: []}; describe("TimerSystem", () => { let ecs: ECS; let timerSystem: TimerSystem; diff --git a/server/src/utils/ecs/__test__/ecs.test.ts b/server/src/utils/ecs/__test__/ecs.test.ts index 2ccdd868..30871c6d 100644 --- a/server/src/utils/ecs/__test__/ecs.test.ts +++ b/server/src/utils/ecs/__test__/ecs.test.ts @@ -2,7 +2,7 @@ import ECS from "../ecs"; import Entity from "../entity"; import System from "../system"; -const server = {clients: {}, activeFlightName: "", thoriumId: ""}; +const server = {clients: {}, activeFlightName: "", thoriumId: "", plugins: []}; describe("ECS", () => { it("should initialize", () => { let ecs = new ECS(server); From 12db783e191cabdf09a579a33be7cf560c53e705 Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Sat, 23 Oct 2021 08:13:07 -0400 Subject: [PATCH 5/9] Implement the NetRequest system This makes it possible to have clients request any data using parameters. It's basically a glorified real-time live data REST API over websockets, really. The cool thing about it is that it's implemented with React Suspense so to the developer, it behaves as if you were just calling a function. Closes #59 --- client/src/context/AppContext.tsx | 2 + client/src/context/useNetRequest.ts | 71 +++++++++++++++++ client/src/pages/Config/index.tsx | 24 +++--- client/src/utils/stableValueHash.ts | 66 ++++++++++++++++ server/src/classes/Client.ts | 118 ++++++++++++++++++++++++---- server/src/inputs/list.ts | 1 + server/src/inputs/plugins.ts | 13 +++ server/src/netRequests/index.ts | 22 ++++++ server/src/netRequests/list.ts | 1 + server/src/netRequests/plugins.ts | 15 ++++ server/src/utils/pubsub.ts | 15 ++-- server/src/utils/types.ts | 11 +++ 12 files changed, 330 insertions(+), 29 deletions(-) create mode 100644 client/src/context/useNetRequest.ts create mode 100644 client/src/utils/stableValueHash.ts create mode 100644 server/src/inputs/plugins.ts create mode 100644 server/src/netRequests/index.ts create mode 100644 server/src/netRequests/list.ts create mode 100644 server/src/netRequests/plugins.ts diff --git a/client/src/context/AppContext.tsx b/client/src/context/AppContext.tsx index 7790d91c..b74c0689 100644 --- a/client/src/context/AppContext.tsx +++ b/client/src/context/AppContext.tsx @@ -6,6 +6,7 @@ import useEasterEgg from "../hooks/useEasterEgg"; import {ErrorBoundary, FallbackProps} from "react-error-boundary"; import bg from "../images/background.jpg"; import {FaSpinner} from "react-icons/fa"; +import {NetRequestData} from "./useNetRequest"; const Fallback: React.FC = ({error}) => { return ( @@ -60,6 +61,7 @@ export default function AppContext({children}: {children: ReactNode}) { }> + {children} diff --git a/client/src/context/useNetRequest.ts b/client/src/context/useNetRequest.ts new file mode 100644 index 00000000..6a35f705 --- /dev/null +++ b/client/src/context/useNetRequest.ts @@ -0,0 +1,71 @@ +import {useEffect} from "react"; +import { + AllRequestNames, + AllRequestParams, + AllRequestReturns, +} from "server/src/netRequests"; +import {proxy, useSnapshot} from "valtio"; +import {NetResponseData} from "../hooks/useDataConnection"; +import {stableValueHash} from "../utils/stableValueHash"; +import {useThorium} from "./ThoriumContext"; +import {useErrorHandler} from "react-error-boundary"; +const netRequestProxy = proxy>({}); +const netRequestPromises: {[requestId: string]: (value: unknown) => void} = {}; + +export function NetRequestData() { + useNetRequestData(); + return null; +} +function useNetRequestData() { + const {socket} = useThorium(); + const handleError = useErrorHandler(); + + if (!socket) throw new Promise(() => {}); + useEffect(() => { + function handleNetRequestData(data: NetResponseData) { + try { + if (typeof data !== "object") { + throw new Error(`netResponse data must be an object. Got "${data}"`); + } + if ("error" in data) { + throw new Error(data.error); + } + if (!("requestId" in data && "response" in data)) { + const dataString = JSON.stringify(data, null, 2); + throw new Error( + `netResponse data must include a requestId and a response. Got ${dataString}` + ); + } + netRequestProxy[data.requestId] = data.response; + netRequestPromises[data.requestId]?.(null); + } catch (err) { + handleError(err); + } + } + socket.on("netRequestData", handleNetRequestData); + return () => { + socket.off("netRequestData", handleNetRequestData); + }; + }, [socket, handleError]); +} + +export function useNetRequest< + T extends AllRequestNames, + R extends AllRequestReturns[T] +>(requestName: T, params?: AllRequestParams[T]): R { + const requestId = stableValueHash({requestName, params}); + const data = useSnapshot(netRequestProxy); + const {socket} = useThorium(); + if (!socket) throw new Promise(() => {}); + + if (!data[requestId]) { + if (!netRequestPromises[requestId]) { + socket.send("netRequest", {requestName, params, requestId}); + } + throw new Promise(res => { + netRequestPromises[requestId] = res; + }); + } + + return data[requestId]; +} diff --git a/client/src/pages/Config/index.tsx b/client/src/pages/Config/index.tsx index 07b4ced7..5201f660 100644 --- a/client/src/pages/Config/index.tsx +++ b/client/src/pages/Config/index.tsx @@ -6,6 +6,8 @@ import Menubar from "@thorium/ui/Menubar"; import SearchableList from "@thorium/ui/SearchableList"; import TagInput from "@thorium/ui/TagInput"; import UploadWell from "@thorium/ui/UploadWell"; +import {useNetSend} from "client/src/context/ThoriumContext"; +import {useNetRequest} from "client/src/context/useNetRequest"; import {useState} from "react"; import {FaEdit} from "react-icons/fa"; import {NavLink, useNavigate, useParams} from "react-router-dom"; @@ -21,7 +23,8 @@ const setDescription = (params: any) => {}; const setTags = (params: any) => {}; export default function Config() { const [error, setError] = useState(false); - + const data = useNetRequest("pluginsList"); + const netSend = useNetSend(); const navigate = useNavigate(); const params = useParams(); const prompt = usePrompt(); @@ -37,19 +40,22 @@ export default function Config() { className="w-full btn-sm btn-success" onClick={async () => { const name = await prompt({header: "Enter plugin name"}); - console.log(name); + if (typeof name !== "string") return; + const id = await netSend("pluginCreate", {name}); + navigate(`/config/${id}/edit`); }} > New Plugin ({ + id: d.id, + name: d.name, + description: d.description, + tags: d.tags, + author: d.author, + }))} searchKeys={["name", "author", "tags"]} selectedItem={params.pluginId || null} setSelectedItem={id => navigate(`/config/${id}`)} @@ -71,7 +77,7 @@ export default function Config() { )} />
-
+
0) { + let thisPos = stack.indexOf(this); + ~thisPos ? stack.splice(thisPos + 1) : stack.push(this); + ~thisPos ? keys.splice(thisPos, Infinity, key) : keys.push(key); + if (~stack.indexOf(value)) value = cycleReplacer.call(this, key, value); + } else stack.push(value); + + return replacer == null ? value : replacer.call(this, key, value); + }; +} + +export function stableValueHash(value: any): string { + return JSON.stringify( + value, + serializer((_: unknown, val: any) => + isPlainObject(val) + ? Object.keys(val) + .sort() + .reduce((result, key) => { + result[key] = val[key]; + return result; + }, {} as any) + : val + ) + ); +} + +function hasObjectPrototype(o: any): boolean { + return Object.prototype.toString.call(o) === "[object Object]"; +} +// Copied from: https://github.com/jonschlinkert/is-plain-object +export function isPlainObject(o: any): o is Object { + if (!hasObjectPrototype(o)) { + return false; + } + + // If has modified constructor + const ctor = o.constructor; + if (typeof ctor === "undefined") { + return true; + } + + // If has modified prototype + const prot = ctor.prototype; + if (!hasObjectPrototype(prot)) { + return false; + } + + // If constructor does not have an Object-specific method + if (!prot.hasOwnProperty("isPrototypeOf")) { + return false; + } + + // Most likely a plain Object + return true; +} diff --git a/server/src/classes/Client.ts b/server/src/classes/Client.ts index ae02bbb4..6f0c4474 100644 --- a/server/src/classes/Client.ts +++ b/server/src/classes/Client.ts @@ -1,6 +1,5 @@ import {ServerChannel} from "@geckos.io/server"; import {OfflineStates, UnionToIntersection} from "../utils/types"; -import uniqid from "@thorium/uniqid"; import randomWords from "@thorium/random-words"; import {DataContext} from "../utils/DataContext"; import inputs, {AllInputNames} from "../inputs"; @@ -15,12 +14,17 @@ import {Entity} from "../utils/ecs"; import {SnapshotInterpolation} from "@geckos.io/snapshot-interpolation"; import {encode} from "@msgpack/msgpack"; import {SocketStream} from "fastify-websocket"; +import requests, {AllRequestNames} from "../netRequests"; class BaseClient { constructor(public id: string) {} } type NetSendData = {inputName: AllInputNames; params: any; requestId: string}; - +type NetRequestData = { + requestName: AllRequestNames; + params: any; + requestId: string; +}; const channels: Record = {}; const sockets: Record = {}; /** @@ -69,21 +73,32 @@ export class ServerClient extends BaseClient { this.clientContext = new DataContext(this.id, database); sockets[this.id] = connection; - await this.initNetSend(connection); - await this.initNetRequest(connection); + await this.initRequests(connection); await this.initSubscriptions(connection); } - private async initNetSend(socket: SocketStream) { + private async initRequests(socket: SocketStream) { if (!socket) throw new Error( "NetSend cannot be initialized before the socket is established." ); + const netRequestList: { + [requestId: string]: { + params: any; + requestName: AllRequestNames; + subscriptionId: number; + }; + } = {}; + socket.socket.on("close", () => { + for (let requestId in netRequestList) { + pubsub.unsubscribe(netRequestList[requestId]?.subscriptionId); + } + }); // Set up the whole netSend process for calling input functions socket.socket.on("message", async data => { try { - if (typeof data === "string") { - const messageData = JSON.parse(data); - if (messageData.type === "netSend") { + const messageData = JSON.parse(data.toString()); + switch (messageData.type) { + case "netSend": { const {inputName, params, requestId} = messageData.data as NetSendData; try { @@ -118,23 +133,96 @@ export class ServerClient extends BaseClient { }) ); } + break; + } + case "netRequest": { + const {requestName, params, requestId} = + messageData.data as NetRequestData; + + function handleNetRequestError(err: unknown) { + if (err === null) return; + let message = err; + if (err instanceof Error) { + message = err.message; + } + console.error(`Error in input ${requestName}: ${message}`); + if (err instanceof Error) console.error(err.stack); + socket.socket.send( + JSON.stringify({ + type: "netRequestData", + data: { + requestId: requestId, + error: message, + }, + }) + ); + pubsub.unsubscribe(netRequestList[requestId]?.subscriptionId); + delete netRequestList[requestId]; + } + + try { + const requestFunction = requests[requestName]; + // Create the subscription + const subscriptionId = await pubsub.subscribe( + requestName, + (payload: any, context: DataContext) => { + try { + const data = requestFunction(context, params, payload); + socket.socket.send( + JSON.stringify({ + type: "netRequestData", + data: { + requestId: requestId, + response: data, + }, + }) + ); + return data; + } catch (err) { + handleNetRequestError(err); + } + }, + this.clientContext + ); + netRequestList[requestId] = { + params, + requestName, + subscriptionId, + }; + // Collect and send the initial data + const response = + (await requestFunction(this.clientContext, params, null)) || {}; + + socket.socket.send( + JSON.stringify({ + type: "netRequestData", + data: { + requestId: requestId, + response, + }, + }) + ); + } catch (err) { + handleNetRequestError(err); + } + break; + } + case "netRequestEnd": { + const {requestId} = messageData.data as {requestId: string}; + pubsub.unsubscribe(netRequestList[requestId]?.subscriptionId); + delete netRequestList[requestId]; + break; } } } catch (err) { throw new Error( - `Client ${this.id} sent invalid NetSend data:${ + `Client ${this.id} sent invalid request data:${ typeof data === "object" ? JSON.stringify(data) : data }` ); } - if (typeof data === "object" && "inputName" in data) { - } else { - } }); } - private async initNetRequest(channel: SocketStream) { - // TODO: September 1, 2021 Set up net requests through the data channel - } get cards() { // TODO Aug 28, 2021 Populate this list with the dynamic list of cards assigned to the client. // Also, there needs to be some way to unsubscribe and re-subscribe whenever the client's diff --git a/server/src/inputs/list.ts b/server/src/inputs/list.ts index e691dc21..192534c8 100644 --- a/server/src/inputs/list.ts +++ b/server/src/inputs/list.ts @@ -2,3 +2,4 @@ export {clientInputs} from "./client"; export {flightInputs} from "./flight"; export {serverInputs} from "./server"; export {dotsInputs} from "./dots"; +export {pluginInputs} from "./plugins"; diff --git a/server/src/inputs/plugins.ts b/server/src/inputs/plugins.ts new file mode 100644 index 00000000..f91e5d96 --- /dev/null +++ b/server/src/inputs/plugins.ts @@ -0,0 +1,13 @@ +import BasePlugin from "../classes/Plugins"; +import {DataContext} from "../utils/DataContext"; +import {pubsub} from "../utils/pubsub"; + +export const pluginInputs = { + pluginCreate(context: DataContext, params: {name: string}) { + const plugin = new BasePlugin(params); + context.server.plugins.push(plugin); + pubsub.publish("pluginsList"); + pubsub.publish("plugin", {pluginId: plugin.id}); + return plugin.id; + }, +}; diff --git a/server/src/netRequests/index.ts b/server/src/netRequests/index.ts new file mode 100644 index 00000000..9ba5c200 --- /dev/null +++ b/server/src/netRequests/index.ts @@ -0,0 +1,22 @@ +import { + InputParams, + InputReturns, + RequestPublishParams, + UnionToIntersection, +} from "../utils/types"; +import * as allRequests from "./list"; + +export type AllRequests = UnionToIntersection< + typeof allRequests[keyof typeof allRequests] +>; +export type AllRequestNames = keyof AllRequests; +export type AllRequestParams = InputParams; +export type AllRequestPublishParams = RequestPublishParams; +export type AllRequestReturns = InputReturns; + +const flattenedRequests: AllRequests = Object.entries(allRequests).reduce( + (prev: any, [_, inputs]) => ({...prev, ...inputs}), + {} +); + +export default flattenedRequests; diff --git a/server/src/netRequests/list.ts b/server/src/netRequests/list.ts new file mode 100644 index 00000000..152065d8 --- /dev/null +++ b/server/src/netRequests/list.ts @@ -0,0 +1 @@ +export {pluginsRequest as allRequests} from "./plugins"; diff --git a/server/src/netRequests/plugins.ts b/server/src/netRequests/plugins.ts new file mode 100644 index 00000000..cf6430e6 --- /dev/null +++ b/server/src/netRequests/plugins.ts @@ -0,0 +1,15 @@ +import {DataContext} from "../utils/DataContext"; + +export const pluginsRequest = { + pluginsList(context: DataContext) { + return context.server.plugins; + }, + plugin( + context: DataContext, + params: {pluginId: string}, + publishParams: {pluginId: string} | null + ) { + if (publishParams && params.pluginId !== publishParams.pluginId) throw null; + return context.server.plugins.find(plugin => plugin.id === params.pluginId); + }, +}; diff --git a/server/src/utils/pubsub.ts b/server/src/utils/pubsub.ts index 13a17789..c5172dce 100644 --- a/server/src/utils/pubsub.ts +++ b/server/src/utils/pubsub.ts @@ -4,6 +4,11 @@ import { SubscriptionReturn, } from "client/src/utils/cardData"; import {EventEmitter} from "events"; +import { + AllRequestNames, + AllRequestPublishParams, + AllRequestReturns, +} from "../netRequests"; import {DataContext} from "./DataContext"; class PubSub { @@ -19,17 +24,17 @@ class PubSub { } public publish< - TriggerName extends SubscriptionNames, - Payload extends SubscriptionParams[TriggerName] + TriggerName extends SubscriptionNames | AllRequestNames, + Payload extends (SubscriptionParams & AllRequestPublishParams)[TriggerName] >(triggerName: TriggerName, payload?: Payload): Promise { this.ee.emit(triggerName, payload); return Promise.resolve(); } public subscribe< - TriggerName extends SubscriptionNames, - Payload extends SubscriptionParams[TriggerName], - Return extends SubscriptionReturn[TriggerName] + TriggerName extends SubscriptionNames | AllRequestNames, + Payload extends (SubscriptionParams & AllRequestPublishParams)[TriggerName], + Return extends (SubscriptionReturn & AllRequestReturns)[TriggerName] >( triggerName: TriggerName, onMessage: (payload: Payload, context: DataContext) => Return, diff --git a/server/src/utils/types.ts b/server/src/utils/types.ts index 1eaa24cc..2470f251 100644 --- a/server/src/utils/types.ts +++ b/server/src/utils/types.ts @@ -24,10 +24,21 @@ type SecondParam any> = Func extends ( ) => any ? R : never; +type ThirdParam any> = Func extends ( + first: any, + second: any, + third: infer R, + ...args: any +) => any + ? R + : never; type AnyFunc = (...args: any) => any; export type InputParams> = { [Property in keyof Inputs]: SecondParam; }; +export type RequestPublishParams> = { + [Property in keyof Requests]: ThirdParam; +}; export type InputReturns> = { [Property in keyof Inputs]: ReturnType; }; From 6484cdc16ed2aeaad51814eaad370db0f4870cf1 Mon Sep 17 00:00:00 2001 From: Alex Anderson Date: Sat, 23 Oct 2021 11:30:24 -0400 Subject: [PATCH 6/9] UPdate the properties of plugins. Closes #60 --- client/src/pages/Config/index.tsx | 94 ++++++++++++++++++++--------- server/src/classes/Plugins/index.ts | 8 +++ server/src/inputs/plugins.ts | 54 +++++++++++++++++ 3 files changed, 126 insertions(+), 30 deletions(-) diff --git a/client/src/pages/Config/index.tsx b/client/src/pages/Config/index.tsx index 5201f660..a34c6c65 100644 --- a/client/src/pages/Config/index.tsx +++ b/client/src/pages/Config/index.tsx @@ -1,4 +1,4 @@ -import {usePrompt} from "@thorium/ui/AlertDialog"; +import {useConfirm, usePrompt} from "@thorium/ui/AlertDialog"; import Button from "@thorium/ui/Button"; import InfoTip from "@thorium/ui/InfoTip"; import Input from "@thorium/ui/Input"; @@ -10,24 +10,18 @@ import {useNetSend} from "client/src/context/ThoriumContext"; import {useNetRequest} from "client/src/context/useNetRequest"; import {useState} from "react"; import {FaEdit} from "react-icons/fa"; -import {NavLink, useNavigate, useParams} from "react-router-dom"; +import {NavLink, useMatch, useNavigate, useParams} from "react-router-dom"; -let plugin: null | { - id: string; - name: string; - description: string; - tags: string[]; -} = null; -const setName = (params: any) => {}; -const setDescription = (params: any) => {}; -const setTags = (params: any) => {}; export default function Config() { const [error, setError] = useState(false); const data = useNetRequest("pluginsList"); const netSend = useNetSend(); const navigate = useNavigate(); - const params = useParams(); + const match = useMatch("/config/:pluginId/*"); + const params = match?.params || {}; const prompt = usePrompt(); + const confirm = useConfirm(); + const plugin = data.find(d => d.id === params.pluginId); return (
@@ -42,7 +36,7 @@ export default function Config() { const name = await prompt({header: "Enter plugin name"}); if (typeof name !== "string") return; const id = await netSend("pluginCreate", {name}); - navigate(`/config/${id}/edit`); + navigate(`/config/${id}`); }} > New Plugin @@ -79,54 +73,94 @@ export default function Config() {
setError(false)} onBlur={(e: React.FocusEvent) => { const target = e.target as HTMLInputElement; - plugin && target.value - ? setName({ - variables: {id: plugin.id, name: target.value}, + if (!plugin) return; + target.value + ? netSend("pluginSetName", { + name: target.value, + pluginId: plugin.id, }) : setError(true); }} /> setError(false)} + disabled={!plugin} onBlur={(e: React.FocusEvent) => { const target = e.target as HTMLInputElement; - plugin && target.value - ? setDescription({ - variables: {id: plugin.id, description: target.value}, - }) - : setError(true); + plugin && + netSend("pluginSetDescription", { + description: target.value, + pluginId: plugin.id, + }); }} /> { if (plugin?.tags.includes(tag) || !plugin) return; - setTags({ - variables: {id: plugin.id, tags: plugin.tags.concat(tag)}, + netSend("pluginSetTags", { + tags: [...plugin.tags, tag], + pluginId: plugin.id, }); }} onRemove={tag => { if (!plugin) return; - setTags({ - variables: { - id: plugin.id, - tags: plugin.tags.filter(t => t !== tag), - }, + netSend("pluginSetTags", { + tags: plugin.tags.filter(t => t !== tag), + pluginId: plugin.id, }); }} /> + +