Add simulator language selector
This commit is contained in:
@@ -30,58 +30,65 @@ type LogEntry = {
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
type Language = "en" | "fr";
|
||||
type Translation = {
|
||||
en: string;
|
||||
fr: string;
|
||||
};
|
||||
|
||||
const scenarios: Array<{ key: ScenarioKey; title: string; description: string }> = [
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
||||
const LANGUAGE_STORAGE_KEY = "plast-track-simulator-language";
|
||||
|
||||
const scenarios: Array<{ key: ScenarioKey; title: Translation; description: Translation }> = [
|
||||
{
|
||||
key: "power_on",
|
||||
title: "Power ON",
|
||||
description: "Simulate machine power available.",
|
||||
title: { en: "Power ON", fr: "Marche ON" },
|
||||
description: { en: "Simulate machine power available.", fr: "Simuler la presence alimentation machine." },
|
||||
},
|
||||
{
|
||||
key: "power_off",
|
||||
title: "Power OFF",
|
||||
description: "Close open downtime and move out of planning.",
|
||||
title: { en: "Power OFF", fr: "Arret OFF" },
|
||||
description: { en: "Close open downtime and move out of planning.", fr: "Fermer l'arret ouvert et sortir du planning." },
|
||||
},
|
||||
{
|
||||
key: "cycle_completed",
|
||||
title: "Single Cycle",
|
||||
description: "Publish one normalized completed cycle.",
|
||||
title: { en: "Single Cycle", fr: "Cycle simple" },
|
||||
description: { en: "Publish one normalized completed cycle.", fr: "Publier un cycle termine normalise." },
|
||||
},
|
||||
{
|
||||
key: "cycle_burst",
|
||||
title: "Cycle Burst",
|
||||
description: "Generate many cycles with the selected cycle time.",
|
||||
title: { en: "Cycle Burst", fr: "Rafale cycles" },
|
||||
description: { en: "Generate many cycles with the selected cycle time.", fr: "Generer plusieurs cycles avec le temps choisi." },
|
||||
},
|
||||
{
|
||||
key: "cycle_signal_edge",
|
||||
title: "Passive Edge",
|
||||
description: "Toggle cycle_signal using the machine configured edge.",
|
||||
title: { en: "Passive Edge", fr: "Front passif" },
|
||||
description: { en: "Toggle cycle_signal using the machine configured edge.", fr: "Basculer cycle_signal selon le front configure." },
|
||||
},
|
||||
{
|
||||
key: "debounce_noise",
|
||||
title: "Debounce Noise",
|
||||
description: "Send a too-fast edge that should be ignored.",
|
||||
title: { en: "Debounce Noise", fr: "Bruit anti-rebond" },
|
||||
description: { en: "Send a too-fast edge that should be ignored.", fr: "Envoyer un front trop rapide qui doit etre ignore." },
|
||||
},
|
||||
{
|
||||
key: "machine_stopped",
|
||||
title: "Open Stop",
|
||||
description: "Force an unqualified downtime if an OF is running.",
|
||||
title: { en: "Open Stop", fr: "Ouvrir arret" },
|
||||
description: { en: "Force an unqualified downtime if an OF is running.", fr: "Forcer un arret non qualifie si un OF est en cours." },
|
||||
},
|
||||
{
|
||||
key: "recover_with_cycle",
|
||||
title: "Recover",
|
||||
description: "Close downtime by producing a new cycle.",
|
||||
title: { en: "Recover", fr: "Reprise" },
|
||||
description: { en: "Close downtime by producing a new cycle.", fr: "Fermer l'arret avec un nouveau cycle." },
|
||||
},
|
||||
{
|
||||
key: "alarm_on",
|
||||
title: "Alarm ON",
|
||||
description: "Raise the general_alarm passive signal.",
|
||||
title: { en: "Alarm ON", fr: "Alarme ON" },
|
||||
description: { en: "Raise the general_alarm passive signal.", fr: "Activer le signal passif general_alarm." },
|
||||
},
|
||||
{
|
||||
key: "alarm_off",
|
||||
title: "Alarm OFF",
|
||||
description: "Clear the general_alarm passive signal.",
|
||||
title: { en: "Alarm OFF", fr: "Alarme OFF" },
|
||||
description: { en: "Clear the general_alarm passive signal.", fr: "Desactiver le signal passif general_alarm." },
|
||||
},
|
||||
];
|
||||
|
||||
@@ -113,6 +120,10 @@ function parseInputValue(value: string): boolean | number | string {
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [language, setLanguage] = useState<Language>(() => {
|
||||
const savedLanguage = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
||||
return savedLanguage === "en" || savedLanguage === "fr" ? savedLanguage : "fr";
|
||||
});
|
||||
const [machines, setMachines] = useState<Machine[]>([]);
|
||||
const [selectedMachine, setSelectedMachine] = useState("");
|
||||
const [cycleTimeSec, setCycleTimeSec] = useState(20);
|
||||
@@ -122,15 +133,27 @@ function App() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||
|
||||
function t(en: string, fr: string) {
|
||||
return language === "fr" ? fr : en;
|
||||
}
|
||||
|
||||
function tt(value: Translation) {
|
||||
return value[language];
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
apiRequest<Machine[]>("/api/machines")
|
||||
.then((items) => {
|
||||
setMachines(items);
|
||||
setSelectedMachine((current) => current || items[0]?.code || "");
|
||||
})
|
||||
.catch((error: Error) => addLog("Load machines", "error", error.message));
|
||||
.catch((error: Error) => addLog(t("Load machines", "Chargement machines"), "error", error.message));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(LANGUAGE_STORAGE_KEY, language);
|
||||
}, [language]);
|
||||
|
||||
function addLog(label: string, status: LogEntry["status"], details: string) {
|
||||
setLogs((current) => [
|
||||
{
|
||||
@@ -146,7 +169,7 @@ function App() {
|
||||
|
||||
async function runScenario(scenario: ScenarioKey) {
|
||||
if (!selectedMachine) {
|
||||
addLog("Scenario blocked", "error", "Select a machine first.");
|
||||
addLog(t("Scenario blocked", "Scenario bloque"), "error", t("Select a machine first.", "Selectionner une machine d'abord."));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -160,9 +183,10 @@ function App() {
|
||||
count: burstCount,
|
||||
}),
|
||||
});
|
||||
addLog(scenario, "ok", JSON.stringify(result, null, 2));
|
||||
const scenarioLabel = scenarios.find((item) => item.key === scenario)?.title;
|
||||
addLog(scenarioLabel ? tt(scenarioLabel) : scenario, "ok", JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
addLog(scenario, "error", error instanceof Error ? error.message : "Unknown error");
|
||||
addLog(scenario, "error", error instanceof Error ? error.message : t("Unknown error", "Erreur inconnue"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -171,7 +195,7 @@ function App() {
|
||||
async function sendRawInput(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
if (!selectedMachine || !rawInputName.trim()) {
|
||||
addLog("Raw input blocked", "error", "Machine and input name are required.");
|
||||
addLog(t("Raw input blocked", "Entree brute bloquee"), "error", t("Machine and input name are required.", "Machine et nom entree sont obligatoires."));
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
@@ -187,28 +211,40 @@ function App() {
|
||||
});
|
||||
addLog(`raw:${rawInputName}`, "ok", JSON.stringify(result, null, 2));
|
||||
} catch (error) {
|
||||
addLog("Raw input", "error", error instanceof Error ? error.message : "Unknown error");
|
||||
addLog(t("Raw input", "Entree brute"), "error", error instanceof Error ? error.message : t("Unknown error", "Erreur inconnue"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
const selectedMachineInfo = machines.find((machine) => machine.code === selectedMachine);
|
||||
const machinePowerLabel = selectedMachineInfo?.powered_on ? t("Powered ON", "Sous tension") : t("Powered OFF", "Hors tension");
|
||||
|
||||
return (
|
||||
<main className="shell">
|
||||
<section className="hero">
|
||||
<div>
|
||||
<p className="eyebrow">Separate test console</p>
|
||||
<h1>Passive Signal Simulator</h1>
|
||||
<p className="eyebrow">{t("Separate test console", "Console de test separee")}</p>
|
||||
<h1>{t("Passive Signal Simulator", "Simulateur signaux passifs")}</h1>
|
||||
<p className="hero__copy">
|
||||
Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without
|
||||
adding test controls to the production dashboard.
|
||||
{t(
|
||||
"Simulate cycles, power states, stops, alarms, signal edges, debounce noise, and raw digital inputs without adding test controls to the production dashboard.",
|
||||
"Simuler cycles, etats alimentation, arrets, alarmes, fronts signal, bruit anti-rebond et entrees digitales brutes sans ajouter de controles de test au tableau de production.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="connection-card">
|
||||
<span>Backend API</span>
|
||||
<strong>{API_BASE_URL}</strong>
|
||||
<div className="hero__side">
|
||||
<label className="language-select">
|
||||
<span>{t("Language", "Langue")}</span>
|
||||
<select value={language} onChange={(event) => setLanguage(event.target.value as Language)}>
|
||||
<option value="fr">Francais</option>
|
||||
<option value="en">English</option>
|
||||
</select>
|
||||
</label>
|
||||
<div className="connection-card">
|
||||
<span>{t("Backend API", "API backend")}</span>
|
||||
<strong>{API_BASE_URL}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -216,16 +252,16 @@ function App() {
|
||||
<article className="panel panel--machine">
|
||||
<div className="panel__header">
|
||||
<div>
|
||||
<p className="eyebrow">Target</p>
|
||||
<h2>Machine</h2>
|
||||
<p className="eyebrow">{t("Target", "Cible")}</p>
|
||||
<h2>{t("Machine", "Machine")}</h2>
|
||||
</div>
|
||||
<button className="ghost-button" type="button" onClick={() => window.location.reload()}>
|
||||
Reload
|
||||
{t("Reload", "Recharger")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label className="field">
|
||||
<span>Machine code</span>
|
||||
<span>{t("Machine code", "Code machine")}</span>
|
||||
<select value={selectedMachine} onChange={(event) => setSelectedMachine(event.target.value)}>
|
||||
{machines.map((machine) => (
|
||||
<option key={machine.code} value={machine.code}>
|
||||
@@ -240,16 +276,16 @@ function App() {
|
||||
<span className={`status-dot status-dot--${selectedMachineInfo.status}`} />
|
||||
<div>
|
||||
<strong>{selectedMachineInfo.status}</strong>
|
||||
<small>{selectedMachineInfo.powered_on ? "Powered ON" : "Powered OFF"}</small>
|
||||
<small>{machinePowerLabel}</small>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="empty">No machine returned by the backend.</p>
|
||||
<p className="empty">{t("No machine returned by the backend.", "Aucune machine retournee par le backend.")}</p>
|
||||
)}
|
||||
|
||||
<div className="numeric-grid">
|
||||
<label className="field">
|
||||
<span>Cycle time sec</span>
|
||||
<span>{t("Cycle time sec", "Temps cycle sec")}</span>
|
||||
<input
|
||||
min="1"
|
||||
step="0.1"
|
||||
@@ -259,7 +295,7 @@ function App() {
|
||||
/>
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Burst count</span>
|
||||
<span>{t("Burst count", "Nombre cycles")}</span>
|
||||
<input
|
||||
min="1"
|
||||
max="100"
|
||||
@@ -274,10 +310,10 @@ function App() {
|
||||
<article className="panel panel--scenarios">
|
||||
<div className="panel__header">
|
||||
<div>
|
||||
<p className="eyebrow">Cases</p>
|
||||
<h2>Scenario Launcher</h2>
|
||||
<p className="eyebrow">{t("Cases", "Cas")}</p>
|
||||
<h2>{t("Scenario Launcher", "Lanceur scenarios")}</h2>
|
||||
</div>
|
||||
{loading ? <span className="busy">Sending...</span> : null}
|
||||
{loading ? <span className="busy">{t("Sending...", "Envoi...")}</span> : null}
|
||||
</div>
|
||||
|
||||
<div className="scenario-grid">
|
||||
@@ -289,8 +325,8 @@ function App() {
|
||||
type="button"
|
||||
onClick={() => runScenario(scenario.key)}
|
||||
>
|
||||
<strong>{scenario.title}</strong>
|
||||
<span>{scenario.description}</span>
|
||||
<strong>{tt(scenario.title)}</strong>
|
||||
<span>{tt(scenario.description)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -299,21 +335,21 @@ function App() {
|
||||
<article className="panel">
|
||||
<div className="panel__header">
|
||||
<div>
|
||||
<p className="eyebrow">Manual</p>
|
||||
<h2>Raw Passive Input</h2>
|
||||
<p className="eyebrow">{t("Manual", "Manuel")}</p>
|
||||
<h2>{t("Raw Passive Input", "Entree passive brute")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form className="raw-form" onSubmit={sendRawInput}>
|
||||
<label className="field">
|
||||
<span>Input name</span>
|
||||
<span>{t("Input name", "Nom entree")}</span>
|
||||
<input value={rawInputName} onChange={(event) => setRawInputName(event.target.value)} />
|
||||
</label>
|
||||
<label className="field">
|
||||
<span>Input value</span>
|
||||
<span>{t("Input value", "Valeur entree")}</span>
|
||||
<input value={rawInputValue} onChange={(event) => setRawInputValue(event.target.value)} />
|
||||
</label>
|
||||
<button className="primary-button" disabled={loading || !selectedMachine} type="submit">
|
||||
Send raw input
|
||||
{t("Send raw input", "Envoyer entree brute")}
|
||||
</button>
|
||||
</form>
|
||||
</article>
|
||||
@@ -321,15 +357,15 @@ function App() {
|
||||
<article className="panel panel--log">
|
||||
<div className="panel__header">
|
||||
<div>
|
||||
<p className="eyebrow">Audit</p>
|
||||
<h2>Simulation Log</h2>
|
||||
<p className="eyebrow">{t("Audit", "Audit")}</p>
|
||||
<h2>{t("Simulation Log", "Journal simulation")}</h2>
|
||||
</div>
|
||||
<button className="ghost-button" type="button" onClick={() => setLogs([])}>
|
||||
Clear
|
||||
{t("Clear", "Effacer")}
|
||||
</button>
|
||||
</div>
|
||||
<div className="log-list">
|
||||
{logs.length === 0 ? <p className="empty">No simulator event sent yet.</p> : null}
|
||||
{logs.length === 0 ? <p className="empty">{t("No simulator event sent yet.", "Aucun evenement simulateur envoye.")}</p> : null}
|
||||
{logs.map((log) => (
|
||||
<details className={`log-entry log-entry--${log.status}`} key={log.id} open={log.status === "error"}>
|
||||
<summary>
|
||||
|
||||
@@ -96,6 +96,29 @@ h2 {
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
.hero__side {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.language-select {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
border: 1px solid rgba(10, 47, 90, 0.1);
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.86);
|
||||
box-shadow: 0 12px 30px rgba(10, 47, 90, 0.08);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.language-select span {
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.connection-card {
|
||||
min-width: 300px;
|
||||
padding: 20px;
|
||||
@@ -355,6 +378,10 @@ pre {
|
||||
.connection-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.hero__side {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
|
||||
Reference in New Issue
Block a user