1. Approvals tenantId hydration race
loadQueue used to fall back to 'show all rows' when tenantId was
null. Now early-returns [] until tenantId hydrates, and the effect
depends on tenantId so it reloads once auth completes.
2. Settings persona quick-switch threw 'Password required'
signIn() in Settings called loginAs(email) without options.method.
Now explicit { method: 'dev' } so the documented dev-login path
is used.
169 lines
6.6 KiB
TypeScript
169 lines
6.6 KiB
TypeScript
// Settings scene — identity, backend URL, polling cadence, theme.
|
|
import { useState } from "react";
|
|
import { useApp } from "../state/store";
|
|
import { api } from "../lib/api";
|
|
import { User, Refresh, Cog, Pulse, Layers, Check } from "../components/icons";
|
|
|
|
const COMMON_EMAILS = [
|
|
"ceo-head@flow-master.ai",
|
|
"hr-head@flow-master.ai",
|
|
"it-head@flow-master.ai",
|
|
];
|
|
|
|
export default function Settings() {
|
|
const userEmail = useApp((s) => s.userEmail);
|
|
const actor = useApp((s) => s.actor);
|
|
const userDisplayName = useApp((s) => s.userDisplayName);
|
|
const loginAs = useApp((s) => s.loginAs);
|
|
const pollEverySec = useApp((s) => s.pollEverySec);
|
|
const setPollEverySec = useApp((s) => s.setPollEverySec);
|
|
const theme = useApp((s) => s.theme);
|
|
const setTheme = useApp((s) => s.setTheme);
|
|
const consoleOpen = useApp((s) => s.consoleOpen);
|
|
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
|
|
const mode = useApp((s) => s.mode);
|
|
const setMode = useApp((s) => s.setMode);
|
|
const refreshLive = useApp((s) => s.refreshLive);
|
|
const pushToast = useApp((s) => s.pushToast);
|
|
|
|
const [emailInput, setEmailInput] = useState(userEmail);
|
|
const [signingIn, setSigningIn] = useState(false);
|
|
|
|
const signIn = async (email: string) => {
|
|
setSigningIn(true);
|
|
try {
|
|
await loginAs(email, undefined, { method: "dev" });
|
|
} finally {
|
|
setSigningIn(false);
|
|
}
|
|
};
|
|
|
|
const clearToken = () => {
|
|
api.clearToken();
|
|
pushToast("info", "Cleared bearer token. Next request will re-login.");
|
|
};
|
|
|
|
return (
|
|
<div className="settings">
|
|
<header className="studio-head">
|
|
<div>
|
|
<div className="mc-hero-eyebrow"><Cog size={12} /> Settings</div>
|
|
<h2 className="mc-hero-title">Identity, backend & UI</h2>
|
|
<div className="mc-hero-sub">Everything here is persisted to localStorage and survives reloads.</div>
|
|
</div>
|
|
</header>
|
|
|
|
<div className="studio-grid">
|
|
<section className="studio-panel">
|
|
<h3 className="panel-h"><User size={12} /> Identity</h3>
|
|
<div className="settings-id">
|
|
<div className="i-field">
|
|
<span>Signed in</span>
|
|
<span>{userDisplayName ?? userEmail.split("@")[0]}</span>
|
|
</div>
|
|
<div className="i-field">
|
|
<span>Email</span>
|
|
<span>{userEmail}</span>
|
|
</div>
|
|
<div className="i-field">
|
|
<span>Status</span>
|
|
<span>{actor?.user_id ? "active session" : "no session"}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<h4 className="i-h" style={{ marginTop: 16 }}>Sign in as</h4>
|
|
<div className="studio-rows">
|
|
<div className="studio-row">
|
|
<input
|
|
className="studio-input"
|
|
placeholder="someone@flow-master.ai"
|
|
value={emailInput}
|
|
onChange={(e) => setEmailInput(e.target.value)}
|
|
/>
|
|
<button className="btn btn-primary" onClick={() => signIn(emailInput)} disabled={signingIn || !emailInput.includes("@")}>
|
|
{signingIn ? <span className="spin" /> : <Check size={12} />} Sign in
|
|
</button>
|
|
</div>
|
|
<div className="quick-users">
|
|
{COMMON_EMAILS.map((e) => (
|
|
<button key={e} className="link-btn" onClick={() => { setEmailInput(e); signIn(e); }} disabled={signingIn}>
|
|
{e}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="settings-row" style={{ marginTop: 12 }}>
|
|
<button className="btn btn-secondary" onClick={() => {
|
|
api.clearToken();
|
|
sessionStorage.removeItem("fm.mc.token.v1");
|
|
pushToast("ok", "Signed out.");
|
|
window.location.href = "/";
|
|
}}>
|
|
Sign out
|
|
</button>
|
|
<button className="link-btn" onClick={clearToken}>
|
|
<Refresh size={12} /> Reset session
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="studio-panel">
|
|
<h3 className="panel-h"><Pulse size={12} /> Data mode & polling</h3>
|
|
<div className="i-field">
|
|
<span>Current mode</span>
|
|
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
|
|
</div>
|
|
<div className="settings-row">
|
|
<button className="btn btn-primary" onClick={() => setMode(mode === "live" ? "snapshot" : "live")} disabled={signingIn}>
|
|
<Refresh size={12} /> {mode === "live" ? "Switch to snapshot" : "Switch to live"}
|
|
</button>
|
|
{mode === "live" && (
|
|
<button className="btn btn-ghost" onClick={refreshLive}>
|
|
<Refresh size={12} /> Refresh now
|
|
</button>
|
|
)}
|
|
</div>
|
|
<label className="studio-field" style={{ marginTop: 12 }}>
|
|
<span>Auto-refresh every (seconds)</span>
|
|
<input
|
|
type="number"
|
|
className="studio-input mono"
|
|
min={2}
|
|
max={120}
|
|
value={pollEverySec}
|
|
onChange={(e) => setPollEverySec(Number(e.target.value) || 8)}
|
|
/>
|
|
</label>
|
|
<div className="settings-hint">Polling only runs in LIVE mode. Currently {mode === "live" ? "polling" : "paused"}.</div>
|
|
</section>
|
|
|
|
<section className="studio-panel">
|
|
<h3 className="panel-h"><Cog size={12} /> Appearance</h3>
|
|
<div className="settings-row">
|
|
<button className={`btn ${theme === "dark" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTheme("dark")}>
|
|
Dark
|
|
</button>
|
|
<button className={`btn ${theme === "light" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTheme("light")}>
|
|
Light
|
|
</button>
|
|
</div>
|
|
<label className="settings-toggle" style={{ marginTop: 12 }}>
|
|
<input type="checkbox" checked={consoleOpen} onChange={(e) => setConsoleOpen(e.target.checked)} />
|
|
<span>Show live API console by default</span>
|
|
</label>
|
|
</section>
|
|
|
|
<section className="studio-panel">
|
|
<h3 className="panel-h"><Layers size={12} /> About</h3>
|
|
<p className="settings-text">
|
|
FlowMaster operator cockpit. Live at{" "}
|
|
<a className="settings-link" href="https://canvas.flow-master.ai" target="_blank" rel="noreferrer">
|
|
canvas.flow-master.ai
|
|
</a>. Every action writes to EA2 — open the live console (above) to see the API trail.
|
|
</p>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|