feat: Add API key creation and management support in dashboard#269
feat: Add API key creation and management support in dashboard#269prajjwalkumar17 merged 42 commits intomainfrom
Conversation
jagan-jaya
commented
Apr 28, 2026
- Add ApiKeysPage component for managing API keys (create, list, revoke)
- Add sidebar navigation link to API Keys page
- Add Cypress tests for API keys UI flows
- Add Cypress commands for API key operations (create, list, revoke, use)
- Update App.tsx to include API keys route
- Add .mintlify-dev.log to gitignore
- Build Decision Engine once from PR source and share as artifact - Run 10 Cypress spec files in parallel across matrix jobs - Each job starts isolated stack with API + deps - Upload screenshots on failure with 7-day retention
Add explicit permissions block to address CodeQL security findings: - contents: read (for checkout and file operations) - actions: read (for artifact upload/download)
- Change health check from /health to /health/ready for proper readiness - Add both website/package-lock.json and root package-lock.json to cache - Configure preview.proxy in vite.config.ts for API routing - Change VITE_API_BASE_URL to VITE_API_BASE_PATH for CI builds
- Use docker/build-push-action@v5 with GHA cache - Enable BuildKit layer caching via cache-from/cache-to - Optimize Dockerfile.postgres for dependency caching - Cache Cargo dependencies before copying source code
Project doesn't use workspace structure with crates/ directory
- Remove double compilation (dummy + real build) - Remove CARGO_INCREMENTAL=0 to enable incremental builds - Docker layer caching via GHA is sufficient optimization
…juspay/decision-engine into feature/add-cypress-ci-workflow
- Add ApiKeysPage component for managing API keys (create, list, revoke) - Add sidebar navigation link to API Keys page - Add Cypress tests for API keys UI flows - Add Cypress commands for API key operations (create, list, revoke, use) - Update App.tsx to include API keys route - Add .mintlify-dev.log to gitignore
…juspay/decision-engine into feature/api-key-dashboard-support
…m/juspay/decision-engine into feature/api-key-dashboard-support
There was a problem hiding this comment.
Pull request overview
Adds dashboard UI + test/CI coverage for creating and managing API keys, and improves the Euclid rule builder UX by introducing a searchable dropdown for condition key/value selection.
Changes:
- Add
ApiKeysPagewith create/list/revoke flows and wire it into the app route + sidebar. - Introduce
SearchableSelectand replace condition-row<select>controls in the Euclid rules builder. - Add Cypress UI tests + commands for API key workflows, and add a dedicated GitHub Actions Cypress workflow.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| website/src/pages/ApiKeysPage.tsx | New dashboard page for API key creation, listing, and revocation. |
| website/src/components/ui/SearchableSelect.tsx | New reusable searchable dropdown component. |
| website/src/components/pages/EuclidRulesPage.tsx | Switch condition key/value selects to SearchableSelect. |
| website/src/components/layout/Sidebar.tsx | Add sidebar navigation link to API Keys page. |
| website/src/App.tsx | Register /api-keys route. |
| cypress/support/commands.js | Add Cypress commands for API key lifecycle + using API keys for requests. |
| cypress/e2e/ui/api-keys-page.cy.js | Add UI coverage for API keys page flows. |
| cypress/e2e/ui/volume-split-page.cy.js | Adjust assertions around rule creation success. |
| cypress/e2e/ui/euclid-rules-volume-split.cy.js | Make routing-keys loading synchronization explicit. |
| cypress/e2e/ui/euclid-rules-volume-split-priority.cy.js | Make routing-keys loading synchronization explicit. |
| cypress/e2e/ui/euclid-rules-nested-branches.cy.js | Make routing-keys loading synchronization explicit (but selectors need updating). |
| cypress/e2e/ui/euclid-rules-enum-operators.cy.js | Make routing-keys loading synchronization explicit (but selectors need updating). |
| cypress/e2e/ui/euclid-rules-e2e.cy.js | Make routing-keys loading synchronization explicit (but selectors need updating). |
| cypress/e2e/ui/analytics-page.cy.js | Strengthen assertion to require visibility. |
| .gitignore | Ignore .mintlify-dev.log. |
| .github/workflows/cypress.yml | Add CI workflow to run API + UI Cypress suites. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const { data: keys, mutate } = useSWR<ApiKeyListItem[]>( | ||
| merchantId ? `/api-key/list/${merchantId}` : null, | ||
| fetcher, | ||
| ) |
There was a problem hiding this comment.
useSWR result isn't handling the list fetch error/loading state. With keys undefined (initial load or network error), the UI currently shows "No active API keys" which is misleading and hides failures. Consider using const { data, error, isLoading } = useSWR(...) and rendering a loading indicator and/or ErrorMessage for the list request error separately from revoke/create errors.
| const { data: keys, mutate } = useSWR<ApiKeyListItem[]>( | |
| merchantId ? `/api-key/list/${merchantId}` : null, | |
| fetcher, | |
| ) | |
| const { | |
| data: keys, | |
| error: listError, | |
| isLoading: isKeysLoading, | |
| mutate, | |
| } = useSWR<ApiKeyListItem[]>( | |
| merchantId ? `/api-key/list/${merchantId}` : null, | |
| fetcher, | |
| ) | |
| const hasKeysLoadError = Boolean(listError) | |
| const listErrorMessage = | |
| listError instanceof Error ? listError.message : 'Failed to load API keys' | |
| const hasLoadedKeys = !isKeysLoading && !hasKeysLoadError && Array.isArray(keys) |
| <input | ||
| value={description} | ||
| onChange={(e) => setDescription(e.target.value)} | ||
| onKeyDown={(e) => e.key === 'Enter' && handleCreate()} | ||
| placeholder="Description (optional)" | ||
| className="flex-1 rounded-lg border border-slate-200 bg-transparent px-3 py-1.5 text-sm focus:outline-none focus:ring-1 focus:ring-brand-500 dark:border-[#222226]" | ||
| /> | ||
| <Button onClick={handleCreate} disabled={creating || !merchantId}> | ||
| {creating ? 'Creating…' : 'Create API Key'} |
There was a problem hiding this comment.
onKeyDown triggers handleCreate() on Enter even when a create is already in progress. Because handleCreate() doesn't guard on creating, pressing Enter repeatedly (or holding it) can fire multiple POSTs while the button is disabled. Add a creating guard inside handleCreate and/or check !creating && merchantId before calling it from onKeyDown.
| <Button | ||
| size="sm" | ||
| variant="danger" | ||
| disabled={revokingId === key.key_id} |
There was a problem hiding this comment.
revokingId tracks only a single key, but the UI only disables the matching row. If a user clicks "Revoke" on a second key while the first request is still in flight, the first row becomes enabled again and multiple revokes can run concurrently with confusing UI state. Consider tracking a Set of revoking IDs or disabling all revoke buttons while any revoke is in progress.
| disabled={revokingId === key.key_id} | |
| disabled={Boolean(revokingId)} |
| <button | ||
| type="button" | ||
| disabled={disabled} | ||
| onClick={() => setOpen(o => !o)} | ||
| className="cond-select flex items-center gap-1 pr-2" | ||
| style={{ backgroundImage: 'none', display: 'flex', alignItems: 'center' }} | ||
| > | ||
| <span className="truncate max-w-[10rem]">{selectedLabel || <span className="text-slate-400">select...</span>}</span> | ||
| <ChevronDown | ||
| size={11} | ||
| className={`shrink-0 text-slate-400 transition-transform duration-150 ${open ? 'rotate-180' : ''}`} | ||
| /> | ||
| </button> |
There was a problem hiding this comment.
This custom select replaces a native <select> but doesn't expose listbox semantics (e.g., aria-haspopup="listbox", aria-expanded, roles for options) and has limited keyboard support (no ArrowUp/ArrowDown/Enter to navigate/select). This is an accessibility regression compared to the native control; consider implementing proper ARIA roles + keyboard interactions, or rendering a visually-customized native <select> for a11y.
| <option key={k} value={k}>{toLabel(k)}</option> | ||
| ))} | ||
| </select> | ||
| options={Object.keys(routingKeys).map((k) => ({ value: k, label: toLabel(k) }))} |
There was a problem hiding this comment.
Swapping the condition LHS/value controls from native <select> to SearchableSelect changes the DOM (it's now a <button class="cond-select"> + popup), but many Cypress specs still use cy.get('select.cond-select').select(...). Those tests will fail once this lands; either update the Cypress helpers/selectors to work with the new component (ideally via a stable data-testid) or add a compatibility layer (e.g., hidden native <select>).
| options={Object.keys(routingKeys).map((k) => ({ value: k, label: toLabel(k) }))} | |
| options={Object.keys(routingKeys).map((k) => ({ value: k, label: toLabel(k) }))} | |
| aria-label="Condition field" | |
| data-testid="condition-lhs-select" | |
| className="cond-select min-w-[9.5rem]" |
| const selectedLabel = options.find(o => o.value === value)?.label ?? value | ||
|
|
||
| const filtered = (query | ||
| ? options.filter(o => | ||
| o.label.toLowerCase().includes(query.toLowerCase()) || | ||
| o.value.toLowerCase().includes(query.toLowerCase()) | ||
| ) | ||
| : options | ||
| ).slice().sort((a, b) => a.label.localeCompare(b.label)) |
There was a problem hiding this comment.
filtered does a full filter + sort() on every render. With large option sets (e.g., routing keys) this can add noticeable overhead because the component re-renders on each keystroke/open toggle. Consider memoizing the derived list with useMemo (keyed on options + query) and only sorting when needed.