Skip to content

feat(dashboard): streamline api key permission setup#358

Closed
omaroubari wants to merge 7 commits intodatabuddy-analytics:mainfrom
omaroubari:dashboard/streamline-api-key-creation
Closed

feat(dashboard): streamline api key permission setup#358
omaroubari wants to merge 7 commits intodatabuddy-analytics:mainfrom
omaroubari:dashboard/streamline-api-key-creation

Conversation

@omaroubari
Copy link

Description

  • replace the API key dialog's manual selects with preset global permission tabs and a searchable website picker
  • default newly selected websites to the current global scopes to reduce repetitive permission setup
  • simplify API key resource submission so global access is only sent when no website-specific restrictions are chosen
  • handcrafted with minimal AI assistance
  • polished button and tabs components styling inline with established design system
Checklist
  • [ x ] My code follows the style guidelines of this project
  • [ x ] I have performed a self-review of my code
  • [ x ] I have commented my code, particularly in hard-to-understand areas
  • [ x ] I have made corresponding changes to the documentation
  • [ x ] My changes generate no new warnings
  • [ ] I have added tests that prove my fix is effective or that my feature works
  • [ x ] New and existing unit tests pass locally with my changes

@vercel
Copy link

vercel bot commented Mar 23, 2026

@omaroubari is attempting to deploy a commit to the Databuddy OSS Team on Vercel.

A member of the Team first needs to authorize it.

@vercel vercel bot temporarily deployed to Preview – databuddy-links March 23, 2026 16:58 Inactive
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 23, 2026

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c2c6172a-9acf-4d53-b58c-b59c86700bcb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link

vercel bot commented Mar 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
databuddy-links Skipped Skipped Mar 23, 2026 5:14pm

@CLAassistant
Copy link

CLAassistant commented Mar 23, 2026

CLA assistant check
All committers have signed the CLA.

@omaroubari omaroubari marked this pull request as draft March 23, 2026 17:01
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 23, 2026

Greptile Summary

This PR streamlines the API key creation dialog by replacing manual scope selects with three preset permission tabs (All / Restricted / Read data only), swapping the website add-button pattern for a combobox picker, and carrying the selected global scopes forward as the default for newly added websites. UI polish is also applied to button.tsx and tabs.tsx (animated sliding indicator for the default variant).

Key changes:

  • api-key-create-dialog.tsx: New Tabs-based global permission presets + Popover/Command website picker; onSubmit simplified so global is only set when no website restrictions are chosen.
  • tabs.tsx: Default variant gains an animated absolute-position indicator; active-state trigger background/border styles are neutralized via an _data-[state=active]: prefix trick so the indicator is visually authoritative.
  • button.tsx: Disabled-state border and text colors aligned across destructive and outline variants.
  • Docs updated to describe the new preset workflow and clarify that website-restricted keys do not inherit global read access.

Issues found:

  • aria-expanded={open} (line 358 in the dialog) is bound to the sheet's open prop instead of popoverOpen, permanently reporting true while the sheet is visible — breaks combobox accessibility.
  • Stale entry.scopes guard in onSubmit (line 170–174): the check uses a snapshot taken at add-time, but the value written is the live globalScopes. If the user empties all scopes in "Restricted" mode after adding websites, entries with a stale non-empty snapshot pass the guard and are submitted with an empty scope array.
  • Missing CommandInput: the picker is described as searchable and includes a CommandEmpty message, but no CommandInput is rendered — the filter never activates.
  • z-5 in tabs.tsx is not in Tailwind's default scale and will be silently ignored.

Confidence Score: 2/5

  • Not safe to merge without fixing the aria-expanded accessibility bug and the empty-scope submission edge case.
  • Two P1 logic bugs in the main dialog file — a wrong aria-expanded binding that breaks combobox accessibility, and an onSubmit guard that can silently produce API key submissions with empty website scopes — block a confident merge. The missing CommandInput means a feature described in both the PR description and the UI placeholder text ("No websites found") is not functional. These issues are straightforward fixes but materially affect correctness and accessibility.
  • apps/dashboard/components/organizations/api-key-create-dialog.tsx (two P1 logic bugs + missing CommandInput); apps/dashboard/components/ui/tabs.tsx (non-standard z-5 class)

Important Files Changed

Filename Overview
apps/dashboard/components/organizations/api-key-create-dialog.tsx Major refactor replacing manual selects with preset permission tabs and a combobox website picker. Contains two P1 logic bugs: aria-expanded is bound to the wrong variable (open prop instead of popoverOpen), and the onSubmit guard uses a stale entry.scopes snapshot while assigning live globalScopes, which can produce empty-scope website entries. The advertised "searchable" picker also lacks a CommandInput.
apps/dashboard/components/ui/tabs.tsx Adds an animated sliding indicator to the default tab variant by wrapping the list in a relative div and disabling active-state trigger styles via an _data-[state=active]: prefix hack. The non-standard z-5 class will be silently ignored by Tailwind's default scale, potentially causing the indicator div to render above triggers.
apps/dashboard/components/ui/button.tsx Small polish: disabled-state border class added to destructive variant and disabled:text-muted-foreground applied to outline variant. Cosmetic-only, no logic concerns.
apps/docs/content/docs/api-keys.mdx Documentation updated to reflect the new preset tabs (All / Restricted / Read data only) and the clarified behavior that website-restricted keys do not inherit global read access. Accurate and consistent with the new UI.
apps/docs/content/docs/api/authentication.mdx Updated setup steps to describe the new preset picker UX instead of manual scope selection. No issues.
apps/docs/content/docs/api/index.mdx One-line clarification added about default organization-wide access when creating a key. No issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([User opens Create API Key sheet]) --> B[Enter key name]
    B --> C{Select global permission preset}
    C -->|All| D[globalScopes = ALL_SCOPES]
    C -->|Restricted| E[globalScopes = DEFAULT_SCOPES\nthen toggle individual scopes]
    C -->|Read data only| F[globalScopes = DEFAULT_SCOPES]
    D & E & F --> G{Add website restrictions?}
    G -->|No| H["resources.global = globalScopes\n(global access)"]
    G -->|Yes — open combobox| I[Select/deselect websites\nwebsiteAccess entries store snapshot of globalScopes]
    I --> J{Submit}
    J -->|websiteAccess.length > 0| K["resources[website:X] = globalScopes ⚠️\n(uses live globalScopes, not snapshot)"]
    J -->|websiteAccess.length = 0| H
    H & K --> L[mutation.mutate → API call]
    L --> M([API key created — show secret])
Loading

Reviews (1): Last reviewed commit: "docs(api-keys): documented permission pr..." | Re-trigger Greptile

@omaroubari omaroubari closed this Mar 23, 2026
<Popover onOpenChange={setPopoverOpen} open={popoverOpen}>
<PopoverTrigger asChild>
<Button
aria-expanded={open}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 aria-expanded uses sheet open prop instead of popoverOpen

The aria-expanded attribute on the combobox trigger is bound to the component's open prop (whether the sheet/drawer is open), not popoverOpen (whether the website picker combobox is open). This means aria-expanded will always be true while the sheet is visible, breaking accessibility for screen readers.

Suggested change
aria-expanded={open}
aria-expanded={popoverOpen}

Comment on lines 170 to 174
for (const entry of websiteAccess) {
if (entry.resourceId && entry.scopes.length > 0) {
resources[`website:${entry.resourceId}`] = entry.scopes;
resources[`website:${entry.resourceId}`] = globalScopes;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale snapshot used as guard but live value assigned

entry.scopes is captured as a snapshot of globalScopes at the time a website was added, but in onSubmit it is only used as a guard (entry.scopes.length > 0) while the actual value written to resources is the current globalScopes. If the user later switches to the "Restricted" tab and de-selects all scopes (making globalScopes = []), the guard still passes (the snapshot is non-empty), but every website entry is written with an empty scope array. The API would receive website:X = [], silently creating a key with no permissions for the selected websites.

The condition should also verify the live value, or use entry.scopes as the authoritative value:

for (const entry of websiteAccess) {
    if (entry.resourceId && globalScopes.length > 0) {
        resources[`website:${entry.resourceId}`] = globalScopes;
    }
}

Comment on lines +373 to +401
<Command>
<CommandList>
<CommandEmpty>No websites found.</CommandEmpty>
<CommandGroup>
{(websites as Website[]).map((website) => (
<CommandItem
key={website.id}
onSelect={(currentValue) => {
isWebsiteSelected(currentValue)
? removeWebsite(currentValue)
: addWebsite(currentValue);
setPopoverOpen(false);
}}
value={website.id}
>
<TrashIcon size={14} />
</Button>
</div>
<div className="grid grid-cols-2 gap-1">
{SCOPE_OPTIONS.slice(0, 6).map((scope) => {
const isSelected = entry.scopes.includes(
scope.value
);
return (
<button
className={`flex items-center gap-2 rounded px-2 py-1.5 text-left text-xs ${
isSelected
? "bg-primary/20 text-foreground"
: "hover:bg-muted"
}`}
key={scope.value}
onClick={() =>
toggleWebsiteScope(
entry.resourceId ?? "",
scope.value
)
}
type="button"
>
<div
className={`flex size-3 shrink-0 items-center justify-center rounded-sm border ${
isSelected
? "border-primary bg-primary text-primary-foreground"
: "border-muted-foreground/30"
}`}
>
{isSelected && (
<CheckIcon size={8} weight="bold" />
)}
</div>
<span className="truncate">
{scope.label}
</span>
</button>
);
})}
</div>
</div>
);
})}
</div>
)}
{website.name || website.domain}
<CheckIcon
className={`ml-auto size-3.5 ${
isWebsiteSelected(website.id)
? "opacity-100"
: "opacity-0"
}`}
/>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No CommandInput — picker is not actually searchable

The PR description advertises a "searchable website picker", and the CommandEmpty ("No websites found.") implies filtering is expected. However, no CommandInput component is rendered inside Command, so users have no way to type a search query. The cmdk Command component filters items based on text typed into a CommandInput; without it, the filter never activates and CommandEmpty can never appear.

Consider adding a search input:

<Command>
    <CommandInput placeholder="Search websites..." />
    <CommandList>
        <CommandEmpty>No websites found.</CommandEmpty>
        ...
    </CommandList>
</Command>

<TabsPrimitive.Trigger
className={cn(
"inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1",
"z-5 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 z-5 is not a standard Tailwind utility

Tailwind's default z-index scale only includes z-0, z-10, z-20, z-30, z-40, z-50. z-5 will be silently ignored unless the project extends the scale with a custom config. The intent here is to ensure the trigger buttons stack above the z-0 sliding indicator <div>, but using an undefined utility class means there is no actual stacking guarantee.

Use z-10 (or another value from the configured scale) instead:

Suggested change
"z-5 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1",
"z-10 inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1",

Comment on lines +380 to +385
onSelect={(currentValue) => {
isWebsiteSelected(currentValue)
? removeWebsite(currentValue)
: addWebsite(currentValue);
setPopoverOpen(false);
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Popover closes after every selection, blocking multi-select

setPopoverOpen(false) is called unconditionally inside onSelect, so the dropdown closes immediately after adding or removing a website. Users who want to select multiple websites must reopen the combobox for each one. Keeping the popover open during multi-select (and closing it on blur/outside-click via Radix's default behavior) would be more ergonomic.

Consider only closing on removal (or not closing at all and relying on Radix's outside-click dismiss):

onSelect={(currentValue) => {
    if (isWebsiteSelected(currentValue)) {
        removeWebsite(currentValue);
    } else {
        addWebsite(currentValue);
    }
    // let Radix handle close-on-outside-click instead
}}

@omaroubari omaroubari reopened this Mar 23, 2026
@omaroubari omaroubari closed this Mar 23, 2026
@vercel vercel bot temporarily deployed to Preview – databuddy-links March 23, 2026 17:14 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants