aboutsummaryrefslogtreecommitdiff
path: root/server/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'server/frontend/src')
-rw-r--r--server/frontend/src/Analysis.tsx84
-rw-r--r--server/frontend/src/App.module.css0
-rw-r--r--server/frontend/src/App.tsx22
-rw-r--r--server/frontend/src/Test.tsx31
-rw-r--r--server/frontend/src/api-schema.d.ts236
-rw-r--r--server/frontend/src/api.ts13
-rw-r--r--server/frontend/src/index.css13
-rw-r--r--server/frontend/src/index.tsx23
8 files changed, 422 insertions, 0 deletions
diff --git a/server/frontend/src/Analysis.tsx b/server/frontend/src/Analysis.tsx
new file mode 100644
index 0000000..3bf9c13
--- /dev/null
+++ b/server/frontend/src/Analysis.tsx
@@ -0,0 +1,84 @@
+import { createAsync, useParams } from "@solidjs/router"
+import { client, getAnalysisList, paths } from "./api.ts";
+import { createSignal, For, onMount, Show, Suspense } from "solid-js";
+import { SolidApexCharts } from "solid-apexcharts";
+
+type AnalysisResult =
+ { status: 'not requested' }
+ | { status: 'loading' }
+ | { status: 'loaded', result: paths['/analysis/execute']['get']['responses'][200]['content']['application/json'] }
+
+export default function Analysis() {
+ const pathParams = useParams();
+ const analysisId = pathParams.id!;
+ let analysis = createAsync(() => getAnalysisList());
+ const analysisName = () => analysis()?.data?.find(it => it.id == analysisId)?.name
+ const [startTimestamp, setStartTimestamp] = createSignal(new Date().getTime() - 1000 * 60 * 60 * 24 * 30);
+ const [endTimestamp, setEndTimestamp] = createSignal(new Date().getTime());
+ const [analysisResult, setAnalysisResult] = createSignal<AnalysisResult>({ status: 'not requested' });
+ return <>
+ <h1><Suspense fallback="Name not loaded...">{analysisName()}</Suspense></h1>
+ <p>
+ <label>
+ Start:
+ <input type="date" value={new Date(startTimestamp()).toISOString().substring(0, 10)} onInput={it => setStartTimestamp(it.target.valueAsNumber)}></input>
+ </label>
+ <label>
+ End:
+ <input type="date" value={new Date(endTimestamp()).toISOString().substring(0, 10)} onInput={it => setEndTimestamp(it.target.valueAsNumber)}></input>
+ </label>
+ <button disabled={analysisResult().status === 'loading'} onClick={() => {
+ setAnalysisResult({ status: 'loading' });
+ (async () => {
+ const result = await client.GET('/analysis/execute', {
+ params: {
+ query: {
+ analysis: analysisId,
+ tEnd: endTimestamp(),
+ tStart: startTimestamp()
+ }
+ }
+ });
+ setAnalysisResult({
+ status: "loaded",
+ result: result.data!
+ });
+ })();
+ }}>
+ Refresh
+ </button>
+
+ <Show when={takeIf(analysisResult(), it => it.status == 'loaded')}>
+ {element =>
+ <For each={element().result.visualizations}>
+ {item =>
+ <div>
+ <SolidApexCharts
+ width={1200}
+ type="bar"
+ options={{
+ xaxis: {
+ type: 'numeric'
+ }
+ }}
+ series={[
+ {
+ name: item.label,
+ data: item.dataPoints.map(it => ([it.time, it.value]))
+ }
+ ]}
+ ></SolidApexCharts>
+ </div>
+ }
+ </For>}
+ </Show>
+ </p >
+ </>
+}
+
+function takeIf<T extends P, P>(
+ obj: P,
+ condition: (arg: P) => arg is T,
+): T | false {
+ return condition(obj) ? obj : false;
+} \ No newline at end of file
diff --git a/server/frontend/src/App.module.css b/server/frontend/src/App.module.css
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/server/frontend/src/App.module.css
diff --git a/server/frontend/src/App.tsx b/server/frontend/src/App.tsx
new file mode 100644
index 0000000..bdc1007
--- /dev/null
+++ b/server/frontend/src/App.tsx
@@ -0,0 +1,22 @@
+import { For, Suspense, type Component } from "solid-js";
+import { A, createAsync } from "@solidjs/router";
+import { client, getAnalysisList } from "./api.ts";
+
+const App: Component = () => {
+ let analysis = createAsync(() => getAnalysisList());
+ return (
+ <>
+ <Suspense fallback="Loading analysis...">
+ <ul>
+ <For each={analysis()?.data}>
+ {item =>
+ <li><A href={`/analysis/${item.id}`}>{item.name}</A></li>
+ }
+ </For>
+ </ul>
+ </Suspense>
+ </>
+ );
+};
+
+export default App;
diff --git a/server/frontend/src/Test.tsx b/server/frontend/src/Test.tsx
new file mode 100644
index 0000000..15d2f73
--- /dev/null
+++ b/server/frontend/src/Test.tsx
@@ -0,0 +1,31 @@
+import { A, createAsync } from "@solidjs/router";
+import { client } from "./api.js";
+import { For, Suspense } from "solid-js";
+
+export default function Test() {
+ let items = createAsync(() =>
+ client.GET("/item", {
+ params: {
+ query: {
+ itemId: ["HYPERION", "BAT_WAND"],
+ },
+ },
+ })
+ );
+ return (
+ <>
+ Test page <A href={"/"}>Back to main</A>
+ <hr />
+ <Suspense fallback={"Loading items..."}>
+ <p>Here are all Items:</p>
+ <For each={Object.entries(items()?.data || {})}>
+ {([id, name]) => (
+ <li>
+ <code>{id}</code>: {name}
+ </li>
+ )}
+ </For>
+ </Suspense>
+ </>
+ );
+}
diff --git a/server/frontend/src/api-schema.d.ts b/server/frontend/src/api-schema.d.ts
new file mode 100644
index 0000000..7ba1db4
--- /dev/null
+++ b/server/frontend/src/api-schema.d.ts
@@ -0,0 +1,236 @@
+/**
+ * This file was auto-generated by openapi-typescript.
+ * Do not make direct changes to the file.
+ */
+
+export interface paths {
+ "/profiles": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all profiles and players known to ledger */
+ get: operations["listProfiles"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analysis/execute": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Execute an analysis on a given timeframe */
+ get: operations["executeAnalysis"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/analysis/list": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** List all installed analysis */
+ get: operations["getAnalysis"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/item": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get item names for item ids */
+ get: operations["getItemNames"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/entries": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** Get all log entries */
+ get: operations["getLogEntries"];
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+}
+export type webhooks = Record<string, never>;
+export interface components {
+ schemas: never;
+ responses: never;
+ parameters: never;
+ requestBodies: never;
+ headers: never;
+ pathItems: never;
+}
+export type $defs = Record<string, never>;
+export interface operations {
+ listProfiles: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ playerId: string;
+ profileId: string;
+ }[];
+ };
+ };
+ };
+ };
+ executeAnalysis: {
+ parameters: {
+ query: {
+ /** @description An analysis id obtained from getAnalysis */
+ analysis: string;
+ /** @description The start of the timeframe to analyze */
+ tStart: number;
+ /** @description The end of the timeframe to analyze. Make sure to use the end of the day if you want the entire day included. */
+ tEnd: number;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ visualizations: {
+ label: string;
+ xLabel: string;
+ yLabel: string;
+ dataPoints: {
+ time: number;
+ value: number;
+ }[];
+ }[];
+ };
+ };
+ };
+ };
+ };
+ getAnalysis: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ name: string;
+ id: string;
+ }[];
+ };
+ };
+ };
+ };
+ getItemNames: {
+ parameters: {
+ query: {
+ itemId: string[];
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ [key: string]: string;
+ };
+ };
+ };
+ };
+ };
+ getLogEntries: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ /** @enum {string} */
+ type: "ACCESSORIES_SWAPPING" | "ALLOWANCE_GAIN" | "AUCTION_BOUGHT" | "AUCTION_LISTING_CHARGE" | "AUCTION_SOLD" | "AUTOMERCHANT_PROFIT_COLLECT" | "BANK_DEPOSIT" | "BANK_INTEREST" | "BANK_WITHDRAW" | "BASIC_REFORGE" | "BAZAAR_BUY_INSTANT" | "BAZAAR_BUY_ORDER" | "BAZAAR_SELL_INSTANT" | "BAZAAR_SELL_ORDER" | "BITS_PURSE_STATUS" | "BOOSTER_COOKIE_ATE" | "CAPSAICIN_EYEDROPS_USED" | "COMMUNITY_SHOP_BUY" | "CORPSE_DESECRATED" | "DIE_ROLLED" | "DRACONIC_SACRIFICE" | "DUNGEON_CHEST_OPEN" | "FORGED" | "GOD_POTION_DRANK" | "GOD_POTION_MIXIN_DRANK" | "GUMMY_POLAR_BEAR_ATE" | "KAT_TIMESKIP" | "KAT_UPGRADE" | "KISMET_REROLL" | "KUUDRA_CHEST_OPEN" | "NPC_BUY" | "NPC_SELL" | "PEST_REPELLENT_USED" | "VISITOR_BARGAIN" | "WYRM_EVOKED";
+ id: string;
+ items: {
+ itemId: string;
+ /** @enum {string} */
+ direction: "GAINED" | "TRANSFORM" | "SYNC" | "CATALYST" | "LOST";
+ amount: number;
+ }[];
+ }[];
+ };
+ };
+ };
+ };
+}
diff --git a/server/frontend/src/api.ts b/server/frontend/src/api.ts
new file mode 100644
index 0000000..8ab6272
--- /dev/null
+++ b/server/frontend/src/api.ts
@@ -0,0 +1,13 @@
+import createClient from "openapi-fetch";
+import type { paths } from "./api-schema.js";
+import { query } from "@solidjs/router";
+export { type paths };
+
+const apiRoot = import.meta.env.DEV ? "//localhost:8080/api" : "/api";
+
+export const client = createClient<paths>({ baseUrl: apiRoot });
+
+export const getAnalysisList = query(
+ () => client.GET("/analysis/list"),
+ "getAnalysisList"
+)
diff --git a/server/frontend/src/index.css b/server/frontend/src/index.css
new file mode 100644
index 0000000..4a1df4d
--- /dev/null
+++ b/server/frontend/src/index.css
@@ -0,0 +1,13 @@
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
+ "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
+ sans-serif;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+ font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
+ monospace;
+}
diff --git a/server/frontend/src/index.tsx b/server/frontend/src/index.tsx
new file mode 100644
index 0000000..610a78b
--- /dev/null
+++ b/server/frontend/src/index.tsx
@@ -0,0 +1,23 @@
+/* @refresh reload */
+import { render } from "solid-js/web";
+import 'solid-devtools';
+
+import "./index.css";
+import type { RouteDefinition } from "@solidjs/router";
+import { Router } from "@solidjs/router";
+import { lazy } from "solid-js";
+
+const root = document.getElementById("root");
+
+if (!(root instanceof HTMLElement)) {
+ throw new Error(
+ "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?"
+ );
+}
+const routes: Array<RouteDefinition> = [
+ { path: "/", component: lazy(() => import("./App.tsx")) },
+ { path: "/test/", component: lazy(() => import("./Test.tsx")) },
+ { path: "/analysis/:id", component: lazy(() => import("./Analysis.tsx")) },
+];
+
+render(() => <Router>{routes}</Router>, root!);