-
-
Notifications
You must be signed in to change notification settings - Fork 73
refactor(dashboard): modular styles, TypeScript foundation, shared editor shells #613
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
338a315
9925f14
b057902
7258088
b73c4da
4d71af9
ecc2e8c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
This file was deleted.
Large diffs are not rendered by default.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| // Shared request lifecycle for the admin CRUD stores (budgets, rate limits, | ||
| // MCP servers, guardrails, auth keys, provider credentials, workflows, ...). | ||
| // Every store repeats the same guard ladder around getJSON/sendJSON; these | ||
| // helpers encode it once, in the one correct order: | ||
| // | ||
| // 1. stale — the response belongs to a previous API key: the caller must | ||
| // return without touching ANY state (CONVENTIONS.md), so it is | ||
| // checked before everything else, including 503. | ||
| // 2. unavailable — the feature is disabled on the gateway (503, sometimes | ||
| // 404): clear the list and flip the page's "available" flag. | ||
| // 3. error — 401 stays silent (the global auth dialog owns it); other | ||
| // failures produce a human-readable message. | ||
| // | ||
| // The helpers never touch store state themselves — each store applies the | ||
| // returned outcome to its own $state fields, so public store APIs stay put. | ||
| // | ||
| // Outcomes: loadAdminList resolves to { status, items, error, result } and | ||
| // sendAdminMutation to { status, error, result }, where status is one of | ||
| // "ok" | "stale" | "unavailable" | "error", `error` is the message for the | ||
| // page/form error slot (empty unless status === "error", except the | ||
| // unavailable mutation message), and `result` is the raw getJSON/sendJSON | ||
| // envelope (null when the request itself threw). | ||
|
|
||
| import { getJSON, isAbortError, sendJSON } from "./client.js"; | ||
| import { errorPayloadMessage } from "./errors.js"; | ||
|
|
||
| // loadAdminList GETs an admin collection and reduces the response to an | ||
| // outcome the store can apply in a few lines. Options: | ||
| // label — console/error label ("budgets") | ||
| // errorFallback — inline error fallback (default "Unable to load {label}.") | ||
| // unavailableStatuses — statuses meaning "feature disabled" (default [503]) | ||
| // normalize — maps the raw payload to rows (default as-is array, else []) | ||
| // options — extra fetch options (signal, ...) | ||
| export async function loadAdminList( | ||
| path, | ||
| { | ||
| label, | ||
| errorFallback = `Unable to load ${label}.`, | ||
| unavailableStatuses = [503], | ||
| normalize = (data) => (Array.isArray(data) ? data : []), | ||
| options, | ||
| }, | ||
| ) { | ||
| let result; | ||
| try { | ||
| result = await getJSON(path, { ...(options || {}), label }); | ||
| } catch (e) { | ||
| // Aborted requests stay silent: the caller's signal.aborted guard is | ||
| // what decides whether the outcome is applied at all. | ||
| if (!isAbortError(e)) { | ||
| console.error(`Failed to fetch ${label}:`, e); | ||
| } | ||
| return { status: "error", items: [], error: errorFallback, result: null }; | ||
| } | ||
| if (result.stale) { | ||
| return { status: "stale", items: [], error: "", result }; | ||
| } | ||
| if (unavailableStatuses.includes(result.status)) { | ||
| return { status: "unavailable", items: [], error: "", result }; | ||
| } | ||
| if (!result.ok) { | ||
| const error = | ||
| result.status === 401 | ||
| ? "" | ||
| : errorPayloadMessage(result.data, errorFallback); | ||
| return { status: "error", items: [], error, result }; | ||
| } | ||
| return { status: "ok", items: normalize(result.data), error: "", result }; | ||
| } | ||
|
|
||
| // sendAdminMutation wraps sendJSON (PUT/POST/DELETE) with the same ladder. | ||
| // Flash messages, closing forms, and refetching stay in the store — only the | ||
| // guard boilerplate lives here. Options: | ||
| // label — console/error label ("save budget") | ||
| // errorFallback — form error fallback (default "Unable to {label}.") | ||
| // unavailableStatuses — statuses meaning "feature disabled" (default [503]) | ||
| // unavailableMessage — form error text for the unavailable outcome | ||
| // options — extra fetch options | ||
| export async function sendAdminMutation( | ||
| path, | ||
| method, | ||
| body, | ||
| { | ||
| label, | ||
| errorFallback = `Unable to ${label}.`, | ||
| unavailableStatuses = [503], | ||
| unavailableMessage = "This feature is unavailable on the gateway.", | ||
| options, | ||
| }, | ||
| ) { | ||
| let result; | ||
| try { | ||
| result = await sendJSON(path, method, body, { ...(options || {}), label }); | ||
| } catch (e) { | ||
| if (!isAbortError(e)) { | ||
| console.error(`Failed to ${label}:`, e); | ||
| } | ||
| return { status: "error", error: errorFallback, result: null }; | ||
| } | ||
| if (result.stale) { | ||
| return { status: "stale", error: "", result }; | ||
| } | ||
| if (unavailableStatuses.includes(result.status)) { | ||
| return { status: "unavailable", error: unavailableMessage, result }; | ||
| } | ||
| if (!result.ok) { | ||
| const error = | ||
| result.status === 401 | ||
| ? "Authentication required." | ||
| : errorPayloadMessage(result.data, errorFallback); | ||
| return { status: "error", error, result }; | ||
| } | ||
| return { status: "ok", error: "", result }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| <script> | ||
| // Enabled/Disabled slide toggle shared by editors (MCP server, provider | ||
| // credential, failover rule, virtual model) and list rows. Renders the | ||
| // .alias-toggle track/thumb pair from the global sheet. | ||
| // | ||
| // Props: | ||
| // enabled — current state (parent flips it in onclick) | ||
| // label — what is being toggled, for the aria-label ("MCP server") | ||
| // disabled — read-only (config-managed rows) | ||
| // restricted — the "enabled but user-path-scoped" amber state | ||
| // onclick — toggle handler | ||
| // text — visible caption override; defaults to Enabled/Disabled | ||
| let { | ||
| enabled = false, | ||
| label = "", | ||
| disabled = false, | ||
| restricted = false, | ||
| onclick, | ||
| text, | ||
| } = $props(); | ||
| </script> | ||
|
|
||
| <button | ||
| type="button" | ||
| class="alias-toggle" | ||
| class:enabled | ||
| class:restricted | ||
| {disabled} | ||
| aria-label={(enabled ? "Disable " : "Enable ") + label} | ||
| {onclick} | ||
| > | ||
| <span class="alias-toggle-track"><span class="alias-toggle-thumb"></span></span> | ||
| <span>{text ?? (enabled ? "Enabled" : "Disabled")}</span> | ||
| </button> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| <!-- GoModel hexagon logo mark. Renders a bare inline SVG that fills its | ||
| container and inherits currentColor — the parent owns sizing/color | ||
| (Sidebar's `.sidebar-logo` wrapper styles it via `:global(svg)`). --> | ||
| <svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M15 3L25.39 9L25.39 21L15 27L4.61 21L4.61 9Z" stroke="currentColor" stroke-width="2" fill="none"/> | ||
| <circle cx="15" cy="15" r="4" fill="currentColor" opacity="0.3"/> | ||
| <path d="M15 9.5L15 6M15 20.5L15 24M19.76 12.25L22.79 10.5M10.24 17.75L7.21 19.5M19.76 17.75L22.79 19.5M10.24 12.25L7.21 10.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> | ||
| </svg> | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| <script> | ||
| // FormField — the label + control wrapper every editor form-grid cell uses. | ||
| // The control itself stays in the caller (inputs need page-specific | ||
| // bindings, autofocus flags, and datalists), so this only standardizes the | ||
| // .form-field / .form-field-label shell around it. | ||
| // | ||
| // Props: id (the control's id, wired to the label), label, children. | ||
| let { id, label = "", children } = $props(); | ||
| </script> | ||
|
|
||
| <div class="form-field"> | ||
| <label class="form-field-label" for={id}>{label}</label> | ||
| {@render children?.()} | ||
| </div> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| <script> | ||
| // EditorDialog — the shared editor-modal shell used by every create/edit | ||
| // dialog: Modal wrapper, editor header (title + close button), the form | ||
| // element, the error banner, and the footer actions row. | ||
| // | ||
| // Props: | ||
| // open — bind to the store's formOpen flag | ||
| // title — heading text ("Edit Budget"); also the default aria-label | ||
| // ariaLabel — accessible dialog name when it should differ from title | ||
| // error — form-level error text (rendered above the actions row) | ||
| // submitting — disables the submit button and swaps its label | ||
| // submitLabel / submittingLabel / submitIcon — submit button content | ||
| // cancel — set false to drop the Cancel button (default true) | ||
| // dialogClass — extra classes on the dialog shell next to .model-editor | ||
| // novalidate — skip native form validation (hidden-control traps) | ||
| // canClose — extra guard consulted on Escape/backdrop close, for | ||
| // editors that stack another dialog on top | ||
| // onclose — close the editor (called for Cancel/close/Escape) | ||
| // onsubmit — submit the form (preventDefault is already handled) | ||
| // Snippets: | ||
| // header — replaces the default <h3>{title}</h3> block | ||
| // headerHint — optional extra content under the default title | ||
| // children — the form fields | ||
| // extraActions — extra footer buttons between Cancel and submit | ||
| // | ||
| // Escape/backdrop closes are ignored while the auth dialog is on top, so | ||
| // a 401 never silently discards the form underneath it. | ||
| import Modal from "$lib/components/atoms/Modal.svelte"; | ||
| import DialogCloseButton from "$lib/components/atoms/DialogCloseButton.svelte"; | ||
| import Icon from "$lib/components/atoms/Icon.svelte"; | ||
| import { auth } from "$lib/stores/auth.svelte.js"; | ||
|
|
||
| let { | ||
| open = false, | ||
| title = "", | ||
| ariaLabel = "", | ||
| error = "", | ||
| submitting = false, | ||
| submitLabel = "Save", | ||
| submittingLabel = "Saving...", | ||
| submitIcon = "save", | ||
| cancel = true, | ||
| dialogClass = "", | ||
| novalidate = false, | ||
| canClose = () => true, | ||
| onclose, | ||
| onsubmit, | ||
| header, | ||
| headerHint, | ||
| children, | ||
| extraActions, | ||
| } = $props(); | ||
|
|
||
| function onModalClose() { | ||
| if (auth.dialogOpen) return; | ||
| if (!canClose()) return; | ||
| onclose?.(); | ||
| } | ||
| </script> | ||
|
|
||
| <Modal {open} variant="editor" onclose={onModalClose}> | ||
| <div | ||
| class={"model-editor" + (dialogClass ? " " + dialogClass : "")} | ||
| role="dialog" | ||
| aria-modal="true" | ||
| aria-label={ariaLabel || title} | ||
| > | ||
| <form | ||
| class="form" | ||
| {novalidate} | ||
| onsubmit={(event) => { | ||
| event.preventDefault(); | ||
| onsubmit?.(); | ||
| }} | ||
| > | ||
| <div class="editor-header"> | ||
| <div> | ||
| {#if header} | ||
| {@render header()} | ||
| {:else} | ||
| <h3>{title}</h3> | ||
| {#if headerHint} | ||
| {@render headerHint()} | ||
| {/if} | ||
| {/if} | ||
| </div> | ||
| <DialogCloseButton | ||
| label={"Close " + (ariaLabel || title).toLowerCase()} | ||
| onclick={() => onclose?.()} | ||
| /> | ||
| </div> | ||
|
|
||
| {@render children?.()} | ||
|
|
||
| {#if error} | ||
| <p class="form-error" role="alert" aria-live="assertive">{error}</p> | ||
| {/if} | ||
| <div class="form-actions"> | ||
| {#if cancel} | ||
| <button type="button" class="btn" onclick={() => onclose?.()}> | ||
| Cancel | ||
| </button> | ||
| {/if} | ||
| {#if extraActions} | ||
| {@render extraActions()} | ||
| {/if} | ||
| <button | ||
| type="submit" | ||
| class="btn btn-primary btn-with-icon" | ||
| disabled={submitting} | ||
| > | ||
| <Icon name={submitIcon} class="form-action-icon" /> | ||
| <span>{submitting ? submittingLabel : submitLabel}</span> | ||
| </button> | ||
|
Comment on lines
+107
to
+114
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Separate "in flight" from "not submittable".
♻️ Proposed refactor submitting = false,
+ submitDisabled = false,
submitLabel = "Save", <button
type="submit"
class="btn btn-primary btn-with-icon"
- disabled={submitting}
+ disabled={submitting || submitDisabled}
>Callers then pass e.g. 🤖 Prompt for AI Agents |
||
| </div> | ||
| </form> | ||
| </div> | ||
| </Modal> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Mark the decorative logo as hidden from assistive tech.
The SVG has no accessible name and no
aria-hidden, so screen readers may announce it as an unlabeled graphic next to the product name.♿ Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents