Skip to content

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

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

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

Conversation

@omaroubari
Copy link

@omaroubari omaroubari commented Mar 23, 2026

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
databuddy.api.key.ux.mp4
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

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 8:07pm

@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.

@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: f9c5db44-bf1d-4d6a-b06e-8097701a3123

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.

@CLAassistant
Copy link

CLAassistant commented Mar 23, 2026

CLA assistant check
All committers have signed the CLA.

@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 tabs ("All", "Restricted", "Read data only") and swapping the website dropdown + trash-button pattern for a searchable Popover/Command multi-picker. It also introduces an animated sliding indicator to the default Tabs variant and polishes disabled-state styling on Button. Documentation is updated to match the new flow throughout.

Key issues found:

  • CommandInput inside CommandList (P1): The search input is nested inside the scrollable list container rather than being a direct child of Command. This breaks cmdk's filtering logic and causes the input to scroll with the list.
  • Tab switch discards restricted scope customisations (P2): assignGlobalScopes calls setGlobalScopes(DEFAULT_SCOPES) unconditionally when the user returns to the "Restricted" tab, erasing any manual checkbox selections they made before switching away.
  • Stale helper text (P2): The description "Read Data is included by default" no longer reflects reality — the new default is all scopes and the tab default is "All".
  • _data-[state=active] workaround in tabs.tsx (P2): Prefixing Tailwind variant classes with _ is a non-conventional way to disable them; a plain code comment or an explicit data-[state=inactive]: rule would be easier to understand and maintain.

Confidence Score: 3/5

  • Not safe to merge until the CommandInput placement bug is fixed — it breaks the website search UX in the key creation dialog.
  • One P1 structural bug (CommandInput inside CommandList) actively breaks the website search/filter functionality. The two P2 items (tab-reset UX regression and stale helper text) are not blocking but degrade the user experience. Once the P1 is resolved the PR is close to merge-ready.
  • apps/dashboard/components/organizations/api-key-create-dialog.tsx — Command structure bug and tab-reset logic. apps/dashboard/components/ui/tabs.tsx — non-standard class-disabling pattern.

Important Files Changed

Filename Overview
apps/dashboard/components/organizations/api-key-create-dialog.tsx Major refactor: replaces Select + per-website scope toggles with a Tabs preset picker and a searchable Popover/Command multi-select. Contains a structural bug (CommandInput inside CommandList), a UX regression (tab switching discards restricted scope customisations), and a stale description.
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 positioning an absolute indicator element. The approach of prefixing active-state classes with _ to disable them is non-conventional but intentional per inline comment.
apps/dashboard/components/ui/button.tsx Minor styling fixes for disabled states on destructive and outline variants — consistent with the design system conventions.
apps/docs/content/docs/api-keys.mdx Documentation updated to reflect the new three-preset flow and the behaviour change where website-restricted keys no longer inherit a default global read:data scope.
apps/docs/content/docs/api/authentication.mdx Updated creation steps to document the three preset options; accurate and aligned with the new UI flow.
apps/docs/content/docs/api/index.mdx One-line addition describing the default organisation-wide access; no issues.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([Open Create Dialog]) --> B[Enter Key Name]
    B --> C{Choose Preset Tab}
    C -->|All| D[globalScopes = ALL_SCOPES]
    C -->|Restricted| E[globalScopes = DEFAULT_SCOPES\nShow scope checkboxes]
    E --> E2[User toggles individual scopes]
    E2 --> F
    C -->|Read data only| F[globalScopes = DEFAULT_SCOPES]
    D --> G{Select Websites?}
    F --> G
    G -->|No websites selected| H[resources.global = globalScopes]
    G -->|One or more websites| I[resources[website:id] = globalScopes\nfor each selected website\nglobal key omitted]
    H --> J[mutation.mutate]
    I --> J
    J --> K([API Key Created — show secret])
Loading

Reviews (1): Last reviewed commit: "fix(dashboard): various bug fixes in api..." | Re-trigger Greptile

Comment on lines +101 to +115
const assignGlobalScopes = (value: string) => {
switch (value) {
case "all":
setGlobalScopes(ALL_SCOPES);
break;
case "restricted":
setGlobalScopes(DEFAULT_SCOPES);
break;
case "read-data":
setGlobalScopes(DEFAULT_SCOPES);
break;
default:
setGlobalScopes(DEFAULT_SCOPES);
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Switching tabs discards "Restricted" customizations

assignGlobalScopes resets scope state on every tab change. If a user is on the "Restricted" tab and customises their scopes (e.g., toggling on extra permissions), then accidentally clicks "All" and back to "Restricted", their work is silently discarded — setGlobalScopes(DEFAULT_SCOPES) is called again.

Consider preserving the restricted selection in a separate ref/state so it can be restored when the user returns to that tab, similar to how form field values are kept across focus changes.

Comment on lines 285 to 288
<p className="text-muted-foreground text-xs">
These permissions apply to all websites. Read Data is included
by default.
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Stale description contradicts new default

The helper text still says "Read Data is included by default", but the initial value of globalScopes is now ALL_SCOPES (all scopes) and the tabs default to "all". The description misleads users into thinking they are starting with a minimal permission set.

Suggested change
<p className="text-muted-foreground text-xs">
These permissions apply to all websites. Read Data is included
by default.
</p>
<p className="text-muted-foreground text-xs">
These permissions apply to all websites unless you restrict to
specific ones below.
</p>

Comment on lines +259 to +261
"focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 _data-[state=active]:border-accent-foreground/10",
"_data-[state=active]:bg-secondary-brightest [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
// disable trigger active styling so TabList default `absolute` indicator animation is visible
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Underscore-prefixed Tailwind classes are a brittle workaround

_data-[state=active]:border-accent-foreground/10 and _data-[state=active]:bg-secondary-brightest are not valid Tailwind utility classes — the leading _ makes them unrecognisable to the compiler, effectively commenting them out without actually removing them. While the inline comment explains the intent, this approach will confuse future contributors who don't know the convention, and any Tailwind purge / scan step that processes strings will include these as false positives.

A more idiomatic approach is to use data-[state=inactive]:... to set the desired state explicitly, or (if you need the classes to stay visible for documentation) leave the originals as a plain code comment above the line.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +374 to +403
<Command>
<CommandList>
<CommandEmpty>No websites found.</CommandEmpty>
<CommandInput placeholder="Search websites..." />
<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.

P1 CommandInput placed inside CommandList

CommandInput is nested inside CommandList, but the cmdk library (which the shadcn Command is built on) requires it to be a direct child of Command, outside the list. The list element is the scrollable container, so placing the input there causes the search box to scroll alongside the results and breaks the built-in filtering — CommandEmpty will not respond to input changes correctly.

The correct structure should be:

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

@vercel vercel bot temporarily deployed to Preview – databuddy-links March 23, 2026 20:07 Inactive
@omaroubari omaroubari changed the title feat(dashboard): streamline api key permission setup #3 feat(dashboard): streamline api key permission setup Mar 23, 2026
@omaroubari
Copy link
Author

Fixed a bunch of bugs. Ready for review.

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