feat: enhance UI/UX with JSON editing, improved forms, and better table views#3292
feat: enhance UI/UX with JSON editing, improved forms, and better table views#3292calycekr wants to merge 5 commits intoapache:masterfrom
Conversation
- Add Form/JSON mode toggle for all Add pages via ?mode=json search param - Add Form/JSON mode toggle for all Detail pages with edit capability - Add TableActionMenu component with kebab dropdown for list pages - Add FormEditDrawer and JSONEditDrawer for inline editing from lists - Add PreviewJSONButton to preview JSON output in Form mode - Simplify JSON templates to contain only required fields - Improve button layout: Preview JSON on left, Cancel/Save on right - Add resource-specific name placeholders (my-route-name, my-service-name, etc.) New components: - FormEditDrawer, JSONEditDrawer, JSONEditorView - PreviewJSONModal, SafeTabSwitch, TableActionMenu
- Add toolbar search with debounced input across all list pages - Enable ProTable options (reload, density, column settings) - Add new columns: Methods, Hosts, Status for Routes; Type, Expiration for SSLs; Group ID, Plugins for Consumers; Type, Nodes for Upstreams - Add ellipsis for long text columns to prevent overflow - Fix tooltip overflow causing layout shift via CSS - Reduce PageHeader spacing for tighter layout - Extract RouteList and StreamRouteList as reusable components - Add useTableSearch custom hook for code reuse - Fix RespStreamRouteItem type referencing wrong schema - Add validity_start/validity_end to SSL schema - Add id, uri search params to pageSearchSchema - Improve detail page button layout (Preview JSON left, Cancel/Save right) - Add translations for table.methods, table.hosts in all locales
The onSuccess callback from page components returns a Promise from queryClient.invalidateQueries(). Without awaiting it, the drawer closes before the list query invalidation completes, causing the table to not refresh after saving.
There was a problem hiding this comment.
Pull request overview
This PR significantly enhances the APISIX Dashboard's UI/UX by introducing dual-mode editing (Form/JSON), inline table editing via drawers, improved search functionality, and better table views with additional informative columns. The changes affect list pages, add pages, and introduce several reusable components and hooks for consistent behavior across resources.
Changes:
- Added JSON editing mode for all resource pages alongside the traditional form view
- Introduced inline editing drawers (Form and JSON) accessible from table action menus for quick modifications
- Enhanced table views with search, additional columns (methods, hosts, status, plugins, etc.), and better visual indicators
- Created reusable components and hooks for consistent state management and UI patterns
Reviewed changes
Copilot reviewed 82 out of 82 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| src/utils/route-transformer.ts | Route data transformation utilities for API/Form/JSON conversions |
| src/utils/json-transformer.ts | Generic JSON transformation utilities |
| src/hooks/useDirtyState.ts | Hook for tracking unsaved changes with deep comparison |
| src/hooks/useTableSearch.ts | Reusable table search with debouncing |
| src/hooks/useUnifiedRouteState.ts | Route-specific unified state management |
| src/components/page/FormEditDrawer.tsx | Form editing in side drawer |
| src/components/page/JSONEditDrawer.tsx | JSON editing in side drawer |
| src/components/page/JSONEditorView.tsx | Monaco-based JSON editor component |
| src/components/page/PreviewJSONModal.tsx | JSON preview modal for form data |
| src/components/page/SafeTabSwitch.tsx | Tab switching with unsaved changes warning |
| src/components/page/TableActionMenu.tsx | Kebab menu for table row actions |
| src/components/page/RouteList.tsx | Reusable route list component |
| src/components/page/StreamRouteList.tsx | Reusable stream route list component |
| src/components/page/ToAddPageBtn.tsx | Enhanced with dropdown for Form/JSON selection |
| src/routes/**/*.tsx | Multiple routes updated with Form/JSON mode support and inline editing |
| src/types/schema/*.ts | Schema updates for SSL expiration, StreamRoute type fix, pageSearch extensions |
| src/locales/**/common.json | Translation additions for new UI features |
| src/styles/global.css | CSS fixes for tooltips and editor styling |
| src/components/form-slice/**/*.tsx | Form improvements with descriptions and better styling |
| e2e/tests/**/*.spec.ts | Minor formatting improvements |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Set sensible defaults for upstream node: port=80, weight=1 - Remove placeholder from Service ID field Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Previously, upstream nodes were only synced to react-hook-form when clicking outside the table (useClickOutside). This caused data loss when users edited nodes and clicked Save without clicking elsewhere.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 83 out of 83 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /** | ||
| * Validate JSON syntax without parsing | ||
| * | ||
| * @param jsonString - The JSON string to validate | ||
| * @returns Object with isValid flag and optional error message | ||
| */ | ||
| export const validateJSONSyntax = ( | ||
| jsonString: string | ||
| ): { isValid: boolean; error?: string } => { | ||
| try { | ||
| JSON.parse(jsonString); | ||
| return { isValid: true }; |
There was a problem hiding this comment.
The comment says “Validate JSON syntax without parsing”, but the implementation uses JSON.parse, which does parse the JSON. Either adjust the comment (e.g., “validate by attempting to parse”) or switch to a true syntax-only validator if that’s the intent.
| // Update original when initialOriginal changes (e.g., after data fetch) | ||
| if (initialOriginal !== undefined && !initializedRef.current) { | ||
| initializedRef.current = true; | ||
| setOriginal(initialOriginal); | ||
| setCurrent(initialOriginal); | ||
| } |
There was a problem hiding this comment.
useDirtyState is calling setOriginal/setCurrent during render when initialOriginal arrives. This can trigger React warnings (“Cannot update a component while rendering…”) and can lead to render loops. Move this initialization into a useEffect that runs when initialOriginal changes (and reset initializedRef appropriately if you need to handle refetches).
| // Parse vars string back to array | ||
| if (cloned.vars && typeof cloned.vars === 'string') { | ||
| try { | ||
| cloned.vars = JSON.parse(cloned.vars as string); | ||
| } catch { | ||
| // Keep as-is if parse fails (will be caught by validation) | ||
| cloned.vars = []; | ||
| } |
There was a problem hiding this comment.
On JSON parse failure for vars, this silently sets vars to an empty array. Because the form schema defines vars as an optional string (no JSON validation), invalid user input will be dropped and an empty vars will be saved without any error. Prefer surfacing the parse error (e.g., keep the original string and let validation fail, or throw/return an error so the UI can block save and show feedback).
| req | ||
| .delete(deleteApi) | ||
| .then(() => onDeleteSuccess?.()) | ||
| .then(() => { | ||
| notifications.show({ | ||
| message: t('info.delete.success', { name: resourceName }), | ||
| color: 'green', | ||
| }); | ||
| queryClient.invalidateQueries(); | ||
| }), |
There was a problem hiding this comment.
queryClient.invalidateQueries() without a queryKey will invalidate all queries after every delete, which can cause unnecessary refetching and UI churn. Consider scoping invalidation to the relevant resource/list query keys (or accept a queryKey/invalidateKeys prop) and rely on onDeleteSuccess for the local list refresh.
| <Menu shadow="md" width={200}> | ||
| <Menu.Target> | ||
| <Button | ||
| leftSection={<IconPlus />} | ||
| rightSection={<IconChevronDown />} | ||
| size="compact-sm" | ||
| variant="gradient" | ||
| > | ||
| {label} | ||
| </Button> | ||
| </Menu.Target> |
There was a problem hiding this comment.
The new add button requires two clicks to reach the default (form) flow because clicking the button only opens the menu. This is a behavior change from the previous single-click navigation and is likely to break existing e2e flows and user muscle memory. Consider a split-button approach (primary click navigates to form; chevron opens the menu) or make the main button click default to form and keep JSON as the secondary menu option.
| {t('form.view.editWithForm')} | ||
| </Menu.Item> | ||
| <Menu.Item leftSection={<IconCode />} onClick={handleJsonClick}> | ||
| {t('form.view.editWithJSON')} |
There was a problem hiding this comment.
ToAddPageDropdown/ToDetailPageDropdown use i18n keys like form.view.createWithForm, createWithJSON, editWithForm, and editWithJSON. These keys were added in en/common.json but are currently missing from other locale files updated in this PR (e.g., de, es, tr, zh), which will cause raw key strings to show in those languages. Add the missing translations (or provide a fallback strategy).
| {t('form.view.editWithForm')} | |
| </Menu.Item> | |
| <Menu.Item leftSection={<IconCode />} onClick={handleJsonClick}> | |
| {t('form.view.editWithJSON')} | |
| {t('form.view.editWithForm', 'Edit with form')} | |
| </Menu.Item> | |
| <Menu.Item leftSection={<IconCode />} onClick={handleJsonClick}> | |
| {t('form.view.editWithJSON', 'Edit with JSON')} |
Why submit this pull request?
What changes will this PR take into?
This PR enhances the dashboard's UI/UX for better operator experience.
Screenshots
1. JSON View for Advanced Users
2. Form View Improvements
3. Table View Improvements
New Components
FormEditDrawerJSONEditDrawerJSONEditorViewPreviewJSONModalSafeTabSwitchTableActionMenuRouteListStreamRouteListToAddPageBtnNew Hooks
useDirtyStateuseTableSearchuseUnifiedResourceStateuseUnifiedRouteStateNew Utils
json-transformer.tsroute-transformer.tsRelated issues
N/A (improvement based on user experience)
Checklist: