Files
traefik-llmgateway/webui/src/lib/load-state.ts
T
lukaszraczylo cf308624af feat(webui): make the dashboard responsive and add loading and empty states
Every tab row scrolls sideways instead of being clipped by its card.
On phones this hid tabs, such as the Config change helper's last forms
at 360-390px. The login button has a larger touch target, and long
names are truncated with a tooltip.

Loading and empty states:
- Skeletons show only while a selection has no data yet, never over
  data already on screen during a refresh.
- Empty states name the next step to take.
- Error states offer a Retry button that shows progress, and each card
  keeps its own error.
- Toasts confirm copies and key generation, and report background
  failures once per streak through polite and assertive live regions.

Other changes:
- The access matrix switches between Models, MCP servers and Agents,
  and says "none configured" only once its data has loaded.
- Home ranks the top 5 users by cost, requests or tokens. It requests
  only the needed metrics and warns before a range exceeds the
  server's key cap.
- MCP and agent deep links accept a bare target name.

admin_assets_gen.go is regenerated.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
2026-09-23 23:02:22 +01:00

44 lines
2.4 KiB
TypeScript

/**
* LoadState is the four states any data-driven view in this panel
* renders in place of its real content — the single vocabulary
* EmptyState.vue/ErrorState.vue/the Skeleton* components all key off of,
* so every page decides "skeleton vs error vs empty vs ready" the same
* way instead of each growing its own ad-hoc v-if chain.
*/
export type LoadState = 'skeleton' | 'error' | 'empty' | 'ready'
/** LoadStateInput is loadState's own three signals — every store in this panel already exposes something that maps onto these (a `loading`/`lastUpdated` flag, a data field's null-ness or length, an `error` string). */
export interface LoadStateInput {
/** Whether a fetch for the CURRENT selection is in flight (or has never yet completed) — a background refresh of data already on screen does not count (see hasData's own precedence below). */
loading: boolean
/** Whether there is currently displayable data for the CURRENT selection — false right after a filter/selection change clears the previous selection's data, not just on a cold first load. */
hasData: boolean
/** The store's current error message, '' when there is none. */
error: string
}
/**
* loadState decides which of the four LoadState values a view should
* render, from the three signals above. Precedence, highest first:
*
* 1. 'ready' whenever `hasData` is true — even alongside a non-empty
* `error` (a background poll just failed but the PREVIOUS successful
* result is still on screen): states-plan.md item 1's own rule is
* that a background refresh must never replace rendered data with a
* skeleton, and a background failure must never blank it into an
* error state either — that case surfaces via a toast instead
* (stores/toasts.ts), not by this helper switching states.
* 2. 'error' when there is no data and `error` is set — the first fetch
* for this selection failed outright, nothing to show underneath it.
* 3. 'skeleton' when there is no data, no error, and `loading` is true —
* first load, or a filter/selection change that cleared the previous
* selection's own data and kicked off a fresh fetch.
* 4. 'empty' otherwise — not loading, no error, genuinely no data.
*/
export function loadState({ loading, hasData, error }: LoadStateInput): LoadState {
if (hasData) return 'ready'
if (error) return 'error'
if (loading) return 'skeleton'
return 'empty'
}