apache / apache/apisix-dashboard
Tracking: verified findings from a full frontend review (master @ 9979b31)
- Dominant language
- TypeScript
- Stars
- 1.2k
- Forks
- 617
- Avg merge
- 4d 18h
- Merged PRs (30d)
- 1
Description
This is the tracking issue for a full review of the rewritten dashboard (`master @ 9979b31`), done with static analysis cross-checked against a live APISIX instance. Every item below was verified by reading the code at the cited location and/or reproducing at runtime — items are grouped by theme, each with its pointer. Dedicated issues/PRs are linked where they exist; unlinked items are up for grabs (several are `good first issue` material).
## Already tracked separately
- [ ] #3414 — **CRITICAL** no-op Edit→Save deletes inline upstream health checks (silent data loss; runtime-reproduced)
- [ ] #3415 — **CRITICAL** 401 fabricated into a successful `{data:{}}` response (false success toasts, crashes)
- [ ] #3416 — secrets/private keys/tokens plaintext in view mode
- [ ] #3412 — Plugin Metadata page false "Key not found" toasts (root cause identified in the issue: skip list says `'500'`, Admin API returns `404`)
- [ ] #3296 — `zOneOf` inverted xor (dead code today; fix or remove)
- [ ] #3205 — no search on list pages. Verified scope: **all 12** list pages set `search={false}`; worse, 5 pages (consumers, services, plugin_configs, consumer_groups, credentials) declare `sorter: true` with no handler — the sort arrows render but do nothing. The plumbing already exists end-to-end (`pageSearchSchema` reserves `name`/`label`; the request interceptor already serializes Admin API `filter`) — only the input box is missing.
- [ ] #3322 — dark mode. Cost note: nothing in `src/` is theme-aware today; the expensive part is the antd+Mantine dual-theming — nearly free after UI-library consolidation (below).
## Error handling / resilience
- [ ] Network-level failure (Admin API unreachable/timeout) of any mutation gives **zero feedback**: the interceptor only handles `err.response` errors (`src/config/req.ts`), the shared `QueryClient` has no `MutationCache.onError` (`src/config/global.ts`), and 25 of 26 `useMutation` sites have no local `onError`. Fix in one place: `new QueryClient({ mutationCache: new MutationCache({ onError }) })` toasting on `!err.response`.
- [ ] No `defaultErrorComponent` on the router: 11 of 12 resources render loader/render failures as the dev-styled red stack dump with no retry (only stream_routes registers a custom one — and that one dereferences `err.response?.data.error_msg` without guarding `data`, the exact #3370 shape).
- [ ] `routes/detail.$id.tsx` has the app's only local mutation `onError` — it double-toasts on HTTP errors (interceptor already toasted) with hardcoded English strings.
- [ ] Plugin metadata hook classifies *any* failed GET as "not configured" → a real 500/network failure on a configured plugin offers the user an empty `{}` to save over their existing metadata (detailed in #3412 comment).
- [ ] `PluginEditorDrawer` fires `onSave` (async `mutateAsync` on the metadata page), then closes and resets **without awaiting** — a failed save loses the user's edits.
- [ ] `secrets/detail.$manager.$id.tsx` is the only non-suspense detail page: on fetch failure it renders a blank editable form (Save would PUT an empty object over the real secret) instead of an error state.
- [ ] Route `vars` invalid JSON: with a zod resolver attached, RHF ignores the Monaco field-level `validate` rule, so `JSON.parse` throws inside `mutationFn` → on `routes/add` the Add click does nothing, silently.
- [ ] `?page=abc` becomes `NaN` and is sent to the Admin API; duplicated `?page=` params throw a ZodError into the raw boundary (`src/types/schema/pageSearch.ts`).
- [ ] Settings modal admin-key field fires `queryClient.invalidateQueries()` + `refetchQueries()` **per keystroke** (`src/components/page/SettingsModal.tsx`) — captured an 18-request burst of identical list GETs; typing a 32-char key ≈ 32 full refetch storms. Apply on blur/debounce.
## Data-integrity (form ↔ API round-trip)
- [ ] `rmDoubleUnderscoreKeys` recurses into `null` (`typeof null === 'object'`) and throws — runs *before* the null-cleaner in `pipeProduce`, so a plugin config containing `null` crashes the submit (`src/utils/producer.ts`).
- [ ] Empty-value stripping inside surviving plugin configs: `produceRestoreEmptyPlugins` only restores whole-plugin entries; nested meaningful empties (`{"headers": {}}`, `[]`, `""`) are still silently removed (sibling of #3269/#3277). Inline route/service `upstream.discovery_args: {}` also still dropped (the #3376 merge-back fix covered the upstreams page only).
- [ ] Inline upstream `name`/`desc`/`labels` are unconditionally `delete`d on submit (`FormPartUpstream/util.ts`) while the form renders inputs for them — user input silently discarded; API-created values lost on any edit-save.
- [ ] Route `script`/`script_id` have no form control; with `shouldUnregister: true` they're dropped from the PUT body — a scripted route loses its script on any dashboard edit.
- [ ] `checks.active.http_request_headers` is rendered with the Labels widget (object) but typed `array` — existing values display empty, new input fails zod: field unusable in both directions (`FormSectionChecks.tsx`).
- [ ] `ssls.client.skip_mtls_uri_regex` rendered as a boolean Switch but typed `array` — same both-directions breakage (`FormPartSSL/index.tsx`).
- [ ] Labels widget splits on every `:` — a label value containing a colon errors on any subsequent edit of the tag set (`Labels.tsx`; split on first colon only).
- [ ] Object-form nodes conversion corrupts IPv6 hosts and invents `port: 1` / `priority: 0` (`FormItemNodes.tsx` `key.split(':')`).
- [ ] Stream routes: create path bypasses `pipeProduce` (only resource whose create/edit run different cleaning pipelines), and the reused plugins section offers a `plugin_config_id` input the resource schema doesn't have — typed values silently vanish.
- [ ] Consumer delete redirects to `/consumer_groups` instead of `/consumers` (`consumers/detail.$username/index.tsx` — copy-paste).
## UX / IA / i18n / a11y
- [ ] Dirty-form navigation is fully unguarded (no `useBlocker`/`beforeunload` anywhere): sidebar click, browser Back, or tab close silently discards edits; the 14 add pages have no Cancel at all, so navigation is their *only* exit. Conversely `useEditCancelGuard` always interrogates even with zero changes (its own comment documents why `isDirty` is untrustworthy under the `disabled`-toggling architecture — worth fixing at the root: mount the form only in edit mode).
- [ ] Language choice is never persisted or detected (`lng: 'en'` hardcoded; reload resets a zh/tr user to English — while the admin key *is* persisted); antd `ConfigProvider` is hardwired to `enUS` regardless of app language, so list pages stay English after switching; delete-confirm builds sentences by concatenation, producing garbled output in de/tr/es (trailing "¿?"); translation-progress indicator counts keys not values (all locales report 100% while tr has 98/232 English values) so it never displays.
- [ ] Accessibility: exactly **one** `aria-label` in all of `src/` — language/settings/burger buttons and plugin-card action buttons are nameless to screen readers; view-mode Monaco text is ~2.1:1 contrast (gray-5 on gray-0, `global.css`) on the primary read path; both drawers set `closeOnEscape={false}` (for the select drawer there's no state to protect); plugin cards are wrapped in `Combobox`/`role="option"` semantics with nested interactive buttons and no keyboard wiring. Zero a11y assertions in e2e.
- [ ] IA: flat 12-item nav with no grouping/icons; no breadcrumbs (4-level-deep credential pages orient by browser Back); one static `document.title` for every page (router `HeadContent` is already mounted — one `head` option per route away).
- [ ] Form TOC never refreshes when sections appear dynamically: `refreshTOC` builds a debounced function and discards it without calling (`FormSection/index.tsx:149-152`).
- [ ] Timestamps rendered as raw UTC ISO strings in list columns (`new Date(...).toISOString()`), inconsistent with detail views.
## Architecture / hygiene (the recurring-bug factories)
- [ ] **Dual UI library**: antd + pro-components exist only for `ProTable` on the 12 list pages (configured down to `search={false} options={false}`) + one `EditableProTable`; everything else is Mantine. Two token systems, two focus rings, two error styles on one screen; both frameworks load on the default landing route. Replace with a Mantine-native table.
- [ ] **MobX toolchain held hostage by one component**: `mobx-react-observer`'s SWC plugin wraps every component app-wide to serve a single `useLocalObservable` search filter in `PluginCardList.tsx` (replaceable by `useState`+`useMemo`); `mobx-persist-store` has **zero** imports — dead dependency.
- [ ] **Hand-written zod transcription of Admin API constraints** (~1,077 lines) is the root of the recurring "form rejects what the API accepts" class (#3146, #3147, #3362, #3376, #3395…). Cheapest structural guard: a CI contract test asserting the zod layer is *looser or equal* to the gateway's own schema; longer term, the JSON-Schema-driven form work (#3347/#3311/#2986).
- [ ] **Submit pipeline strips then restores**: `pipeProduce` deep-cleans all empty values then patches back known exceptions one incident at a time — every future meaningful-empty field is pre-broken. Invert to dirty-fields-based submission.
- [ ] `apis/` imports types from `components/` (inverted layering, e.g. `apis/ssls.ts`); one detail page re-declares an exported query-options helper.
- [ ] The repo's only unit test (`upstreams.test.ts`, guarding the #3376 fix) **never runs in CI** — no `test` script, no workflow invokes vitest.
## Test-suite gaps (would-be regression escapes)
- [ ] No round-trip invariant test (create → read → no-op edit-save → deep-diff) — the class that would have caught #3414
- [ ] No masking assertions outside the settings modal (F7 unguarded)
- [ ] Mutation-failure feedback pinned only for route-create; no update/delete/network-level cases
- [ ] `validation.zOneOf-single-field.spec.ts` only tests the case an inverted implementation also passes — needs both/neither quadrants
- [ ] No i18n key-parity unit test; the lang-switch integration spec self-skips if the switcher isn't found by accessible name
- [ ] Chromium-only; e2e backend tracks the moving `apache/apisix:dev` tag even for PR runs
- [ ] 15 `waitForTimeout` call sites; suite is parallel-unsafe locally (specs `deleteAll*` each other's fixtures; green in CI only because `workers: 1`)
I'll be sending a first batch of small PRs for the top error-handling items and #3412/#3296/#3414. Happy to split any checkbox above into its own issue on request.
Contributor guide
Research direction
Start by selecting one unlinked checkbox and reading its cited file or entry point, such as src/config/req.ts, src/config/global.ts, or upstreams.test.ts. Run the nearest existing test or reproduce the reported behavior against a live APISIX instance. Done means the selected finding is fixed and covered by an appropriate regression test.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 20/100