File size: 8,574 Bytes
609fb78 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 | // ---------------------------------------------------------------------------
// connectors/OdooConfig.tsx β WAVE 32 items 6/11, contract C2, rulings R9/R10/R11.
//
// β WHAT THE OWNER FOUND WHEN THEY CLICKED "Manage keys" ON ODOO: a list of
// credentials. Not which server database this workspace reads, not which of the
// ten mirrored grids it wants, not how often they sync, and no way to stop. This
// panel is those four questions, on the card the question is asked from.
//
// ββ THE PANEL IS A PURE COMPONENT AND THE FETCHING LIVES ABOVE IT, which is not
// a style preference β it is what makes the ticket's `done-when` provable. The
// page fetches on mount and `renderToStaticMarkup` does not run effects, so a
// component that loads its own data is a spinner forever in a shot, and a ticket
// whose done-when is "an admin SEES the server database and a checklist" cannot
// be evidenced by a picture of a spinner. `ConnectorCard` owns the request;
// `OdooConfigPanel` renders whatever it is handed. Same reason `ConnectorCard`
// itself was lifted out of the page in wave 30.
//
// β NO CLIENT UNION OVER `syncEvery` OR THE GRID KEYS. Both are the server's
// vocabulary (`odoo_relational.SYNC_PRESETS`, `odoo_relational.TABLES`) and
// arrive as strings β a client union turns "the server added a preset" into "the
// client drops the option" (the wave-9 law).
// ---------------------------------------------------------------------------
/** One materialisable grid, exactly as `GET /admin/connectors/odoo/config` sends it. */
export interface OdooGrid {
key: string;
label: string;
enabled: boolean;
}
/** The config payload. Everything optional but `grids` is genuinely optional on the wire β
* an older server, or a tenant with no Odoo at all, simply says less. */
export interface OdooConfig {
applicable?: boolean;
/** `keychain` | `env` | `none` β WHERE the credential lives, read as a string. */
source?: string;
label?: string;
/** The masked hint the keychain computed at write. Never the secret. */
preview?: string;
serverDb?: string;
serverUrl?: string;
apiUser?: string;
/** False for the deployment ENVIRONMENT: editing it here would be editing the container. */
serverDbEditable?: boolean;
grids?: OdooGrid[];
syncEvery?: string;
syncOptions?: string[];
syncFloorSeconds?: number;
frozen?: boolean;
frozenAt?: string;
canDisconnect?: boolean;
/** What the server did with what it was sent β a clamp, a refusal, a rewrite (W30/R6). */
notes?: string[];
}
/**
* The cadence words, in the two places a person reads them. β The MAP is presentation; the
* OPTIONS come from the server (`syncOptions`), so a preset we have no phrase for still renders
* β as its own key rather than as nothing.
*/
const EVERY_LABEL: Record<string, string> = {
"30m": "Every 30 minutes",
"1h": "Every hour",
"4h": "Every 4 hours",
daily: "Once a day",
manual: "Only when I ask",
};
export const everyLabel = (k: string): string => EVERY_LABEL[k] || k;
/** What the connection is, in one line, for the header of the panel. */
export function sourceLine(cfg: OdooConfig): string {
if (cfg.frozen) return "Disconnected β these databases are frozen as static data";
if (cfg.source === "env") return "Connected using this deployment's own credentials";
if (cfg.source === "keychain") return "Connected using a key stored in this workspace";
return "Not connected";
}
export default function OdooConfigPanel({
cfg,
busy,
onToggleGrid,
onSyncEvery,
onServerDb,
onDisconnect,
onReconnect,
}: {
cfg: OdooConfig;
busy?: boolean;
onToggleGrid: (key: string, enabled: boolean) => void;
onSyncEvery: (every: string) => void;
onServerDb: (db: string) => void;
onDisconnect: () => void;
onReconnect: () => void;
}) {
const grids = cfg.grids ?? [];
const options = cfg.syncOptions ?? [];
const on = grids.filter((g) => g.enabled).length;
return (
<div className="conn-odoo">
<p className="conn-card-meta">{sourceLine(cfg)}</p>
{/* ββ the credential, as much of it as may be shown ββββββββββββββββββββββββββββββββββ */}
<div className="conn-odoo-row">
<span className="conn-odoo-k">Key</span>
<span className="conn-odoo-v">
{cfg.preview ? cfg.preview : "held on this deployment"}
{cfg.apiUser ? " Β· " + cfg.apiUser : ""}
</span>
</div>
{cfg.serverUrl ? (
<div className="conn-odoo-row">
<span className="conn-odoo-k">Server</span>
<span className="conn-odoo-v">{cfg.serverUrl}</span>
</div>
) : null}
<div className="conn-odoo-row">
<span className="conn-odoo-k">Database</span>
{cfg.serverDbEditable ? (
// β A `defaultValue` + blur, not a controlled input: this component is PURE and holds
// no state, and a controlled field with no owner is a box you cannot type in.
<input
className="conn-odoo-v"
defaultValue={cfg.serverDb ?? ""}
maxLength={80}
disabled={busy || cfg.frozen}
aria-label="Odoo server database"
onBlur={(e) => {
const next = e.target.value.trim();
if (next && next !== (cfg.serverDb ?? "")) onServerDb(next);
}}
/>
) : (
<span className="conn-odoo-v">
{cfg.serverDb || "β"}
{/* R6's second sentence: a thing you cannot change says WHY, not nothing. */}
<em> β set on this deployment, not in this workspace</em>
</span>
)}
</div>
{/* ββ R9's second reading: WHICH grids this workspace materialises βββββββββββββββββββ */}
<p className="conn-odoo-h">
Databases to build <span className="conn-odoo-count">{on} of {grids.length}</span>
</p>
<div className="conn-odoo-grids">
{grids.map((g) => (
<label className="conn-odoo-grid" key={g.key}>
<input
type="checkbox"
checked={g.enabled}
disabled={busy || cfg.frozen}
onChange={(e) => onToggleGrid(g.key, e.target.checked)}
/>
<span>{g.label}</span>
</label>
))}
</div>
{/* β SAID BEFORE IT IS DONE, not after. Unticking stops the NEXT sync from rebuilding a
database; it does not delete the one you already have. A checkbox that silently threw
away rows would be the worst control on this page. */}
<p className="conn-card-meta">
Unticking one stops it being rebuilt on the next sync. Nothing you already have is
deleted.
</p>
{/* ββ R11: the cadence, from the server's own preset list, floor included ββββββββββββ */}
<p className="conn-odoo-h">How often to sync</p>
<select
className="conn-odoo-every"
value={cfg.syncEvery ?? ""}
disabled={busy || cfg.frozen}
aria-label="How often to sync"
onChange={(e) => onSyncEvery(e.target.value)}
>
{options.map((o) => (
<option value={o} key={o}>
{everyLabel(o)}
</option>
))}
</select>
{/* ββ R10: off, and back on ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */}
{cfg.frozen ? (
<div className="conn-odoo-off">
<p className="conn-card-meta">
Disconnected{cfg.frozenAt ? " on " + cfg.frozenAt.slice(0, 10) : ""}. Every row and
every column you had is still here and still readable β nothing is being refreshed.
</p>
<button type="button" className="conn-card-go" disabled={busy} onClick={onReconnect}>
Reconnect
</button>
</div>
) : cfg.canDisconnect ? (
<button type="button" className="conn-odoo-danger" disabled={busy} onClick={onDisconnect}>
Disconnect Odoo
</button>
) : null}
{/* Whatever the server did with what it was sent β a clamp, a rewrite, a warning. */}
{(cfg.notes ?? []).length ? (
<ul className="conn-odoo-notes">
{(cfg.notes ?? []).map((n) => (
<li key={n}>{n}</li>
))}
</ul>
) : null}
</div>
);
}
|