-
-
Notifications
You must be signed in to change notification settings - Fork 75
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
Merged
Merged
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
338a315
refactor(dashboard): split dashboard.css into ordered style modules
SantiagoDePolonia 9925f14
refactor(dashboard): convert the $lib foundation to TypeScript
SantiagoDePolonia b057902
refactor(dashboard): extract shared EditorDialog/FormField/EnabledTog…
SantiagoDePolonia 7258088
refactor(dashboard): route admin CRUD stores through a shared request…
SantiagoDePolonia b73c4da
refactor(dashboard): split oversized components along scope-safe seams
SantiagoDePolonia 4d71af9
docs(dashboard): record TS foundation, style modules, and shared shel…
SantiagoDePolonia ecc2e8c
revert(dashboard): back out the TypeScript foundation migration
SantiagoDePolonia 2ad779e
fix(dashboard): address review findings on the refactoring PR
SantiagoDePolonia e419cbc
fix(dashboard): keep cached API-key rows through network-level load f…
SantiagoDePolonia 6ba88c3
refactor(dashboard): drop mutually-cancelling audit-pane spacing rules
SantiagoDePolonia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
67 changes: 0 additions & 67 deletions
67
internal/admin/dashboard/static/dist/assets/index-BOEWOpVo.js
This file was deleted.
Oops, something went wrong.
67 changes: 67 additions & 0 deletions
67
internal/admin/dashboard/static/dist/assets/index-D4Oy5e9B.js
Large diffs are not rendered by default.
Oops, something went wrong.
2 changes: 1 addition & 1 deletion
2
...ard/static/dist/assets/index-BDVc7dxb.css → ...ard/static/dist/assets/index-DhDJqJDv.css
Large diffs are not rendered by default.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 }; | ||
| } |
34 changes: 34 additions & 0 deletions
34
web/dashboard/src/lib/components/atoms/EnabledToggle.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" aria-hidden="true" focusable="false"> | ||
| <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> |
14 changes: 14 additions & 0 deletions
14
web/dashboard/src/lib/components/molecules/FormField.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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> |
122 changes: 122 additions & 0 deletions
122
web/dashboard/src/lib/components/organisms/EditorDialog.svelte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| <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 — request in flight: disables the submit button and swaps | ||
| // its label to submittingLabel | ||
| // submitDisabled — disables submit without the label swap (read-only | ||
| // config-managed rows, a pending delete, ...) | ||
| // 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, | ||
| submitDisabled = 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 || submitDisabled} | ||
| > | ||
| <Icon name={submitIcon} class="form-action-icon" /> | ||
| <span>{submitting ? submittingLabel : submitLabel}</span> | ||
| </button> | ||
| </div> | ||
| </form> | ||
| </div> | ||
| </Modal> | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.