Skip to content

Commit

Permalink
Merge pull request #9 from linomp/refactor/break-ui-into-components
Browse files Browse the repository at this point in the history
Refactor/break UI into components
  • Loading branch information
linomp authored Feb 23, 2024
2 parents b80ca2f + ce080f0 commit 8086f19
Show file tree
Hide file tree
Showing 46 changed files with 357 additions and 973 deletions.
5 changes: 5 additions & 0 deletions .github/workflows/PR_FE.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ jobs:
run: |
cd mvp/client/ui
npm install
- name: Run tests
run: |
cd mvp/client/ui
npm run test
- name: Build frontend
run: |
Expand Down
33 changes: 24 additions & 9 deletions mvp/client/ui/src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
<script lang="ts">
import { onMount } from "svelte";
import { Route, Router } from "svelte-navigator";
import { GameParametersService, OpenAPI } from "./api/generated/";
import HomePage from "src/pages/HomePage.svelte";
import { onMount } from "svelte";
import { GameParametersService } from "./api/generated/services/GameParametersService";
import { OpenAPI } from "./api/generated/core/OpenAPI";
import Spinner from "src/components/Spinner.svelte";
import { globalSettings } from "./stores/stores";
import { isUndefinedOrNull } from "./shared/utils";
OpenAPI.BASE = import.meta.env.VITE_API_BASE;
onMount(async () => {
try {
let globalSettings =
const result =
await GameParametersService.getParametersGameParametersGet();
console.log("API works!", globalSettings);
globalSettings.set(result);
} catch (error) {
console.error("Error fetching global settings:", error);
alert("Error fetching game settings. Please refresh the page.");
}
});
</script>
Expand All @@ -23,7 +25,20 @@
</svelte:head>

<Router primary={false}>
<div class="App min-h-screen flex flex-col">
<Route path="/" component={HomePage} />
</div>
{#if isUndefinedOrNull($globalSettings)}
<div class="spinner-container">
<Spinner />
</div>
{:else}
<div>
<Route path="/" component={HomePage} />
</div>
{/if}
</Router>

<style>
.spinner-container {
width: 5%;
margin: 0 auto;
}
</style>
7 changes: 7 additions & 0 deletions mvp/client/ui/src/components/GameOver.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
import { gameOverReason, gameSession } from "src/stores/stores";
</script>

<h3>Game Over</h3>
<pre>{JSON.stringify($gameSession, null, 2)}</pre>
<p>{$gameOverReason}</p>
99 changes: 99 additions & 0 deletions mvp/client/ui/src/components/MachineData.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script lang="ts">
import { isUndefinedOrNull } from "src/shared/utils";
import { PlayerActionsService } from "src/api/generated";
import {
dayInProgress,
gameOver,
gameSession,
globalSettings,
predictionPurchaseButtonDisabled,
sensorPurchaseButtonDisabled,
} from "src/stores/stores";
import Sensor from "src/components/Sensor.svelte";
$: {
sensorPurchaseButtonDisabled.set(
$dayInProgress ||
($gameSession?.available_funds ?? 0) <
($globalSettings?.sensor_cost ?? Infinity),
);
predictionPurchaseButtonDisabled.set(
$dayInProgress ||
($gameSession?.available_funds ?? 0) <
($globalSettings?.prediction_model_cost ?? Infinity),
);
}
const purchaseSensor = async (sensorName: string) => {
if ($gameOver) {
return;
}
try {
const result =
await PlayerActionsService.purchaseSensorPlayerActionsPurchasesSensorsPost(
sensorName,
$gameSession?.id!,
);
gameSession.set(result);
} catch (error: any) {
if (error.status === 400) {
alert("Not enough funds to buy the sensor!");
} else {
console.error("Error buying sensor:", error);
}
}
};
const purchaseRulPredictionModel = async () => {
if ($gameOver) {
return;
}
try {
const result =
await PlayerActionsService.purchasePredictionPlayerActionsPurchasesPredictionModelsPost(
"predicted_rul",
$gameSession?.id!,
);
gameSession.set(result);
} catch (error: any) {
if (error.status === 400) {
alert("Not enough funds to buy the prediction model!");
} else {
console.error("Error buying prediction model:", error);
}
}
};
</script>

{#if !isUndefinedOrNull($gameSession)}
<div class="machine-data">
<h3>Operational Parameters</h3>
{#each Object.entries($gameSession?.machine_state?.operational_parameters ?? {}) as [parameter, value]}
<Sensor
sensorCost={$globalSettings?.sensor_cost ?? 0}
sensorPurchaseButtonDisabled={$sensorPurchaseButtonDisabled}
{parameter}
{value}
{purchaseSensor}
/>
{/each}
<p>
{"Remaining Useful Life"}: {$gameSession?.machine_state?.predicted_rul
? `${$gameSession.machine_state?.predicted_rul} steps`
: "???"}
<span
hidden={!isUndefinedOrNull($gameSession?.machine_state?.predicted_rul)}
>
<button
disabled={$predictionPurchaseButtonDisabled}
on:click={() => purchaseRulPredictionModel()}
>
Buy (${$globalSettings?.prediction_model_cost})
</button>
</span>
</p>
</div>
{/if}
21 changes: 21 additions & 0 deletions mvp/client/ui/src/components/MachineView.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<script lang="ts">
import { dayInProgress, gameOver, gameSession } from "src/stores/stores";
import { isUndefinedOrNull } from "src/shared/utils";
import runningMachineSrc from "/img/healthy.gif";
const stoppedMachineSrc = new URL("/img/stopped.PNG", import.meta.url).href;
let stopAnimation = false;
$: {
stopAnimation = $gameOver || !$dayInProgress;
}
</script>

<img
class="machine-view"
src={stopAnimation ? stoppedMachineSrc : runningMachineSrc}
alt="..."
width="369"
height="276"
hidden={isUndefinedOrNull($gameSession)}
/>
26 changes: 26 additions & 0 deletions mvp/client/ui/src/components/Sensor.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<script lang="ts">
export let parameter: string;
export let value: number | null;
export let sensorPurchaseButtonDisabled: boolean;
export let purchaseSensor: (parameter: string) => void;
export let sensorCost: number;
const formatParameterName = (parameter: string) => {
return parameter
.split("_")
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ");
};
</script>

<p>
{formatParameterName(parameter)}: {value ?? "???"}
<span hidden={value != null}>
<button
disabled={sensorPurchaseButtonDisabled}
on:click={() => purchaseSensor(parameter)}
>
Buy (${sensorCost})
</button>
</span>
</p>
94 changes: 94 additions & 0 deletions mvp/client/ui/src/components/SessionData.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<script lang="ts">
import { PlayerActionsService, SessionsService } from "src/api/generated";
import { isUndefinedOrNull } from "src/shared/utils";
import {
dayInProgress,
gameOver,
gameSession,
globalSettings,
maintenanceButtonDisabled,
performedMaintenanceInThisTurn,
} from "src/stores/stores";
export let maintenanceCost: number;
export let fetchExistingSession: () => Promise<void>;
$: {
maintenanceButtonDisabled.set(
$performedMaintenanceInThisTurn ||
$dayInProgress ||
($gameSession?.available_funds ?? 0) <
($globalSettings?.maintenance_cost ?? Infinity),
);
}
const advanceToNextDay = async () => {
if (isUndefinedOrNull($gameSession) || $gameOver) {
return;
}
// TODO: migrate this polling strategy to a websocket connection
// start fetching machine health every second while the day is advancing
const intervalId = setInterval(fetchExistingSession, 500);
dayInProgress.set(true);
try {
let result = await SessionsService.advanceSessionsTurnsPut(
$gameSession?.id!,
);
gameSession.set(result);
} catch (error) {
console.error("Error advancing day:", error);
} finally {
await fetchExistingSession();
// stop fetching machine health until the player advances to next day again
clearInterval(intervalId);
dayInProgress.set(false);
performedMaintenanceInThisTurn.set(false);
}
};
const doMaintenance = async () => {
if ($gameOver || isUndefinedOrNull($gameSession)) {
return;
}
try {
const result =
await PlayerActionsService.doMaintenancePlayerActionsMaintenanceInterventionsPost(
$gameSession!.id,
);
gameSession.set(result);
performedMaintenanceInThisTurn.set(true);
} catch (error: any) {
if (error.status === 400) {
alert("Not enough funds to perform maintenance!");
} else {
console.error("Error performing maintenance:", error);
}
}
};
</script>

{#if !isUndefinedOrNull($gameSession)}
<div class="session-data">
<h3>Game Session Details</h3>
<p>Current Step: {$gameSession?.current_step}</p>
<p>Available Funds: {$gameSession?.available_funds}</p>
<div class="session-commands">
<button on:click={advanceToNextDay} disabled={$dayInProgress}>
Advance to next day
</button>
<button on:click={doMaintenance} disabled={$maintenanceButtonDisabled}>
Perform Maintenance (${maintenanceCost})
</button>
</div>
</div>
{/if}

<style>
.session-commands {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
</style>
32 changes: 32 additions & 0 deletions mvp/client/ui/src/components/Spinner.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<div class="spinner w-full text-center {$$props.class ?? 'h-11'}">
<svg
class="animate-spin"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="#9ca3af"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="12" y1="6" x2="12" y2="3"></line>
<line x1="6" y1="12" x2="3" y2="12"></line>
<line x1="7.75" y1="7.75" x2="5.6" y2="5.6"></line>
</svg>
</div>

<style>
.animate-spin {
@apply h-full;
animation: spin 1s infinite linear;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(359deg);
}
}
</style>
4 changes: 0 additions & 4 deletions mvp/client/ui/src/errorHandlers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { showToast, ToastType } from './stores/toasts'

export function jsErrorHandler(message: Event | string, source?: string, line?: number, column?: number, error?: Error) {
reportError({ message, source, line, column }, error)
alert((error?.name == 'SyntaxError' ? 'Your browser is probably too old, please update:' : 'Technical error occurred, please reload the page:') + '\n' + message)
Expand All @@ -13,8 +11,6 @@ export function handleUnhandledRejection(event: PromiseRejectionEvent) {
const e: Error & { statusCode: number } | undefined = event.reason
console.error(e)
if (e?.stack) return jsErrorHandler(e.message, undefined, undefined, undefined, e)
let message = e?.message ?? "Some Error"
showToast(message, { type: ToastType.ERROR })
}

export function initErrorHandlers() {
Expand Down
Loading

0 comments on commit 8086f19

Please sign in to comment.