Repository: unkeyed/unkey
Pull request #3028: feat: suggested display of rootkey for filters
Author: login: MichaelUnkey
Created: 2025-03-24T19:36:29Z | Merged: 2025-03-25T18:28:37Z

## What does this PR do?

<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->

Fixes # (issue)

*If there is not an issue for this, please create one first. This is used to tracking purposes and also helps use understand why this PR exists*

<!-- If there isn't an issue for this PR, please re-review our Contributing Guide and create an issue -->

## Type of change

<!-- Please mark the relevant points by using [x] -->

- [ ] Bug fix (non-breaking change which fixes an issue)
- [ ] Chore (refactoring code, technical debt, workflow improvements)
- [x] Enhancement (small improvements)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] This change requires a documentation update

## How should this be tested?

<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->

- Test A
- Test B

## Checklist

<!-- We're starting to get more and more contributions. Please help us making this efficient for all of us and go through this checklist. Please tick off what you did  -->

### Required

- [ ] Filled out the "How to test" section in this PR
- [ ] Read [Contributing Guide](./CONTRIBUTING.md)
- [ ] Self-reviewed my own code
- [ ] Commented on my code in hard-to-understand areas
- [ ] Ran `pnpm build`
- [ ] Ran `pnpm fmt`
- [ ] Checked for warnings, there are none
- [ ] Removed all `console.logs`
- [ ] Merged the latest changes from main onto my branch with `git pull origin main`
- [ ] My changes don't cause any responsiveness issues

### Appreciated

- [ ] If a UI change was made: Added a screen recording or screenshots to this PR
- [ ] Updated the Unkey Docs if changes were necessary


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Enhanced the display of filter option labels in the dashboard, offering a more consistent and informative presentation for audit logs.
  - Introduced a simplified role management interface, allowing users to connect roles to API keys through checkboxes.

- **Documentation**
  - Updated user instructions for connecting roles to API keys, including new navigation steps and visual aids for clarity.
  - Revised instructions for accessing roles and permissions, streamlining the process and enhancing user understanding.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
index ecb12404a6..ef1f89b7b7 100644
--- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
+++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx
@@ -1,5 +1,4 @@
 import { type Interval, IntervalSelect } from "@/app/(app)/apis/[apiId]/select";
-import type { NestedPermissions } from "@/app/(app)/authorization/roles/[roleId]/tree";
 import { StackedColumnChart } from "@/components/dashboard/charts";
 import { PageContent } from "@/components/page-content";
 import { Badge } from "@/components/ui/badge";
@@ -226,36 +225,12 @@ export default async function APIKeyDetailPage(props: {
     }
   });
 
-  const roleTee = key.workspace.roles.map((role) => {
-    const nested: NestedPermissions = {};
-    for (const permission of key.workspace.permissions) {
-      let n = nested;
-      const parts = permission.name.split(".");
-      for (let i = 0; i < parts.length; i++) {
-        const p = parts[i];
-        if (!(p in n)) {
-          n[p] = {
-            id: permission.id,
-            name: permission.name,
-            description: permission.description,
-            checked: role.permissions.some((p) => p.permissionId === permission.id),
-            part: p,
-            permissions: {},
-            path: parts.slice(0, i).join("."),
-          };
-        }
-        n = n[p].permissions;
-      }
-    }
-    const data = {
+  const rolesList = key.workspace.roles.map((role) => {
+    return {
       id: role.id,
       name: role.name,
-      description: role.description,
-      keyId: key.id,
-      active: key.roles.some((keyRole) => keyRole.roleId === role.id),
-      nestedPermissions: nested,
+      isActive: key.roles.some((keyRole) => keyRole.roleId === role.id),
     };
-    return data;
   });
 
   return (
@@ -372,7 +347,7 @@ export default async function APIKeyDetailPage(props: {
               </div>
               <RBACButtons permissions={key.workspace.permissions} />
             </div>
-            <PermissionList roles={roleTee} />
+            <PermissionList roles={rolesList} keyId={key.id} />
           </div>
         </div>
       </PageContent>
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx
index 7764394a1a..9c2fe2a77c 100644
--- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx
+++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx
@@ -1,83 +1,84 @@
 "use client";
-import { Tree } from "@/app/(app)/authorization/roles/[roleId]/tree";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { cn } from "@/lib/utils";
-import { Button } from "@unkey/ui";
-import { useEffect, useState } from "react";
-
-export type NestedPermission = {
-  id: string;
-  checked: boolean;
-  description: string | null;
-  name: string;
-  part: string;
-  path: string;
-  permissions: NestedPermissions;
-};
-export type NestedPermissions = Record<string, NestedPermission>;
+import { Checkbox } from "@/components/ui/checkbox";
+import { trpc } from "@/lib/trpc/client";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
 export type Role = {
   id: string;
   name: string;
-  keyId: string;
-  active: boolean;
-  description: string | null;
-  nestedPermissions: NestedPermissions;
+  isActive: boolean;
 };
+
 type PermissionTreeProps = {
   roles: Role[];
+  keyId: string;
 };
 
-export function PermissionList({ roles }: PermissionTreeProps) {
-  const [activeRoleId, setActiveRoleId] = useState<string | null>(
-    roles.length > 0 ? roles[0].id : null,
-  );
-  const [key, setKey] = useState(0);
-
-  // biome-ignore lint/correctness/useExhaustiveDependencies: Force rerender on role change
-  useEffect(() => {
-    // Update the key when activeRoleId changes to force a complete re-render
-    setKey((prev) => prev + 1);
-  }, [activeRoleId]);
+export function PermissionList({ roles, keyId }: PermissionTreeProps) {
+  const router = useRouter();
+  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
+    onMutate: () => {
+      toast.loading("Connecting role to key");
+    },
+    onSuccess: () => {
+      toast.dismiss();
+      toast.success("Role connected to key");
+      router.refresh();
+    },
+    onError: (error) => {
+      toast.dismiss();
+      toast.error(error.message);
+    },
+  });
 
-  const activeRole = roles.find((r) => r.id === activeRoleId);
+  const disconnectRole = trpc.rbac.disconnectRoleFromKey.useMutation({
+    onMutate: () => {
+      toast.loading("Disconnecting role from key");
+    },
+    onSuccess: () => {
+      toast.dismiss();
+      toast.success("Role disconnected from key");
+      router.refresh();
+    },
+    onError: (error) => {
+      toast.dismiss();
+      toast.error(error.message);
+    },
+  });
 
   return (
     <Card>
       <CardHeader className="pb-0">
         <div className="mb-2">
-          <CardTitle>Permissions & Roles</CardTitle>
-          <CardDescription>Connect roles with permissions to control access</CardDescription>
-        </div>
-        <div className="border-b border-gray-6">
-          <div className="flex flex-wrap -mb-px">
-            {roles.map((role) => (
-              <Button
-                variant="ghost"
-                key={role.id}
-                onClick={() => setActiveRoleId(role.id)}
-                className={cn(
-                  "rounded-none rounded-t inline-flex items-center px-4 py-2 text-sm font-medium border-b-2 transition-colors outline-none focus:ring-0",
-                  activeRoleId === role.id
-                    ? "border-primary text-primary"
-                    : "border-transparent text-gray-11 hover:text-gray-12 hover:border-gray-7",
-                )}
-              >
-                {role.name}
-                {role.active && <span className="ml-2 h-2 w-2 rounded-full bg-success-9" />}
-              </Button>
-            ))}
-          </div>
+          <CardTitle>Roles</CardTitle>
+          <CardDescription>Manage roles for this key</CardDescription>
         </div>
       </CardHeader>
 
       <CardContent className="pt-6">
-        {activeRole && activeRoleId && (
-          <Tree
-            key={`tree-${key}-${activeRoleId}`}
-            nestedPermissions={activeRole.nestedPermissions}
-            role={{ id: activeRoleId }}
-          />
-        )}
+        <div className="space-y-1">
+          {roles.map((role) => (
+            <div
+              key={role.id}
+              className="flex items-center space-x-2 p-2 rounded-lg hover:bg-gray-2"
+            >
+              <Checkbox
+                checked={role.isActive}
+                onCheckedChange={(checked) => {
+                  if (checked) {
+                    connectRole.mutate({ keyId: keyId, roleId: role.id });
+                  } else {
+                    disconnectRole.mutate({ keyId: keyId, roleId: role.id });
+                  }
+                }}
+              />
+              <div className="flex flex-col">
+                <span className="text-sm font-medium">{role.name}</span>
+              </div>
+            </div>
+          ))}
+        </div>
       </CardContent>
     </Card>
   );
diff --git a/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx b/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
index e722be8e5b..575673aa4c 100644
--- a/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
+++ b/apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
@@ -14,7 +14,7 @@ export const RootKeysFilter = ({
     <FilterCheckbox
       showScroll
       options={(rootKeys ?? []).map((rootKey, index) => ({
-        label: rootKey.name ?? "<EMPTY>",
+        label: getRootKeyLabel(rootKey), // Format the root key label when empty and contains no name.
         value: rootKey.id,
         checked: false,
         id: index,
@@ -32,3 +32,15 @@ export const RootKeysFilter = ({
     />
   );
 };
+
+// Format the root key label when rootkey contains no name. Splits and formats into 'unkey_1234...5678'
+// This is to avoid displaying the full id in the filter.
+const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
+  const id = rootKey.id;
+  const prefix = id.substring(0, id.indexOf("_") + 1);
+  const obfuscatedMiddle =
+    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
+  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
+  const lastFour = id.substring(id.length - 4);
+  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+};
diff --git a/apps/docs/apis/features/authorization/api-key-screen.png b/apps/docs/apis/features/authorization/api-key-screen.png
new file mode 100644
index 0000000000..b272058865
Binary files /dev/null and b/apps/docs/apis/features/authorization/api-key-screen.png differ
diff --git a/apps/docs/apis/features/authorization/api-keys-navigation.png b/apps/docs/apis/features/authorization/api-keys-navigation.png
new file mode 100644
index 0000000000..cf9497bff1
Binary files /dev/null and b/apps/docs/apis/features/authorization/api-keys-navigation.png differ
diff --git a/apps/docs/apis/features/authorization/example.mdx b/apps/docs/apis/features/authorization/example.mdx
index 0181bbb345..5fb84adfae 100644
--- a/apps/docs/apis/features/authorization/example.mdx
+++ b/apps/docs/apis/features/authorization/example.mdx
@@ -76,28 +76,33 @@ Now that we have permissions and roles in place, we can connect them to keys.
 <Tab title="Dashboard">
 
 1. In the sidebar, click on one of your APIs
-2. Then click on `Keys` in the tabs
+2. In the breakcrumb navigation on the top click Reqests and then keys 
+    <Frame>
+      <img src="/apis/features/authorization/api-keys-navigation.png" alt="Breadcrumb Navigation"/>
+    </Frame>
+  
 3. Select one of your existing keys by clicking on it
-4. Go to the `Permissions` tab
+4. Scroll down to the `Roles` section if not visible 
 
-You should now be on `/app/keys/key_auth_???/key_???/permissions`
+You should now be on `/app/keys/key_auth_???/key_???`
 
 <Frame>
-  <img src="/apis/features/authorization/connections.png" alt="Unconnected roles and permissions"/>
+  <img src="/apis/features/authorization/api-key-screen.png" alt="Unconnected roles and permissions"/>
   </Frame>
 
 
-  You can connect a role to your key by clicking on the checkbox in the graph.
+  You can connect a role to your key by clicking on the checkbox.
 
   Let's give this key the `dns.manager` and `read-only` roles.
+  A toast message should come up in the lower corner when the action has been completed.
 
 
 <Frame>
-  <img src="/apis/features/authorization/connections-connected.png" alt="Connected roles and permissions"/>
+  <img src="/apis/features/authorization/role-add-example.png" alt="Unconnected roles and permissions"/>
   </Frame>
 
-As you can see, now the key is connected to the following permissions:
-`domain.dns.create_record`, `domain.dns.read_record`, `domain.dns.update_record`, `domain.dns.delete_record`, `domain.create_domain`, `domain.read_domain`
+As you can see, now the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section:
+
 
 
 </Tab>
diff --git a/apps/docs/apis/features/authorization/role-add-example.png b/apps/docs/apis/features/authorization/role-add-example.png
new file mode 100644
index 0000000000..fba15ab4c1
Binary files /dev/null and b/apps/docs/apis/features/authorization/role-add-example.png differ
diff --git a/apps/docs/apis/features/authorization/roles-and-permissions.mdx b/apps/docs/apis/features/authorization/roles-and-permissions.mdx
index 51a65f5716..546bab3a96 100644
--- a/apps/docs/apis/features/authorization/roles-and-permissions.mdx
+++ b/apps/docs/apis/features/authorization/roles-and-permissions.mdx
@@ -90,28 +90,32 @@ A checked box means the role will grant the permission to keys.
 ## Connecting roles to keys
 
 1. In the sidebar, click on one of your APIs
-2. Then click on `Keys` in the tabs
+2. In the breakcrumb navigation on the top click Reqests and then keys 
+    <Frame>
+      <img src="/apis/features/authorization/api-keys-navigation.png" alt="Breadcrumb Navigation"/>
+    </Frame>
 3. Select one of your existing keys by clicking on it
-4. Go to the `Permissions` tab
+4. Scroll down to the `Roles` section if not visible 
 
-You should now be on `/app/keys/key_auth_???/key_???/permissions`
+You should now be on `/app/keys/key_auth_???/key_???`
 
 <Frame>
-  <img src="/apis/features/authorization/connections.png" alt="Unconnected roles and permissions"/>
+  <img src="/apis/features/authorization/api-key-screen.png" alt="Unconnected roles and permissions"/>
   </Frame>
 
 
-  You can connect a role to your key by clicking on the checkbox in the graph.
+  You can connect a role to your key by clicking on the checkbox.
 
   Let's give this key the `dns.manager` and `read-only` roles.
+  A toast message should come up in the lower corner when the action has been completed.
 
 
 <Frame>
-  <img src="/apis/features/authorization/connections-connected.png" alt="Connected roles and permissions"/>
+  <img src="/apis/features/authorization/role-add-example.png" alt="Unconnected roles and permissions"/>
   </Frame>
 
-As you can see, now the key is connected to the following permissions:
-`domain.dns.create_record`, `domain.dns.read_record`, `domain.dns.update_record`, `domain.dns.delete_record`, `domain.create_domain`, `domain.read_domain`
+As you can see, now the key now contains 2 `roles` and 5 `permissions` shown just above the Roles section:
+
 
 
 


Reviews:
user: login: ogzhanolguncu
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:49:35Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx (3)</summary><blockquote>

`39-42`: **Simplify the obfuscation logic.**

The current obfuscation logic is complex and may be confusing. The current calculation for `obfuscatedMiddle` checks if a substring has length > 0, but this will always be true if there are at least 5 characters after the underscore.


Consider simplifying the formatting logic:

```diff
-  const obfuscatedMiddle =
-    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
-  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
-  const lastFour = id.substring(id.length - 4);
-  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+  const underscoreIndex = id.indexOf("_");
+  const middlePartStart = underscoreIndex + 1;
+  const middlePartEnd = id.length - 4;
+  
+  // Show "..." only if there are characters between nextFour and lastFour
+  const shouldShowObfuscation = middlePartEnd - (middlePartStart + 4) > 0;
+  
+  const nextFour = id.substring(middlePartStart, Math.min(middlePartStart + 4, id.length));
+  const lastFour = id.substring(Math.max(id.length - 4, 0));
+  
+  return rootKey.name ?? prefix + nextFour + (shouldShowObfuscation ? "..." : "") + lastFour;
```

This approach more clearly expresses the intent and handles edge cases better.

---

`43-43`: **Consider handling empty string names.**

The current implementation uses the nullish coalescing operator (`??`), which will return the right-hand value only if `rootKey.name` is `null` or `undefined`. However, an empty string (`""`) is considered a valid value, which might not be what you want.


If you want to use the formatted ID when the name is an empty string as well:

```diff
-  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+  return rootKey.name && rootKey.name.trim() !== "" ? 
+    rootKey.name : 
+    prefix + nextFour + obfuscatedMiddle + lastFour;
```

---

`36-44`: **Add comments explaining the formatting logic.**

The function uses a specific formatting pattern for the root key IDs, but there are no comments explaining the intent or expected format of these IDs.


Consider adding explanatory comments:

```diff
const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
+  // If the root key has a name, use it; otherwise, format the ID
+  // Expected format for IDs: "prefix_randomstring" (e.g., "root_a1b2c3d4e5f6g7h8")
+  // We show: "prefix_abcd...wxyz" where abcd are the first 4 chars after prefix and wxyz are the last 4
  const id = rootKey.id;
  const prefix = id.substring(0, id.indexOf("_") + 1);
  const obfuscatedMiddle =
    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
  const lastFour = id.substring(id.length - 4);
  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3ef6b0279fb47276e8a13f961e736bd1c7d8cdb1 and 3e71949a6a194ec04ba530716e3a62823c5b6ce6.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx` (2 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (17)</summary>

* GitHub Check: Test Go API Local / Test (Shard 8/8)
* GitHub Check: Test Go API Local / Test (Shard 6/8)
* GitHub Check: Test Go API Local / Test (Shard 4/8)
* GitHub Check: Test Packages / Test ./packages/rbac
* GitHub Check: Test Packages / Test ./packages/hono
* GitHub Check: Test Go API Local / Test (Shard 1/8)
* GitHub Check: Test Go API Local / Test (Shard 3/8)
* GitHub Check: Test Packages / Test ./packages/cache
* GitHub Check: Test Go API Local / Test (Shard 2/8)
* GitHub Check: Test Packages / Test ./packages/api
* GitHub Check: Test Go API Local / Test (Shard 7/8)
* GitHub Check: Test Packages / Test ./internal/encryption
* GitHub Check: Test Packages / Test ./internal/clickhouse
* GitHub Check: Test Agent Local / test_agent_local
* GitHub Check: Test API / API Test Local
* GitHub Check: Build / Build
* GitHub Check: Analyze (javascript-typescript)

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx (1)</summary>

`17-17`: **Good addition of formatted labels.**

The change to use a dedicated function for formatting the labels improves consistency and readability. This allows for a more sophisticated presentation of root keys in the filter component.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T13:52:15Z
user: login: MichaelUnkey
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-25T13:54:56Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx (2)</summary><blockquote>

`60-81`: **Improved UI implementation with checkboxes, but missing loading states.**

The checkbox-based UI is a more intuitive way to manage role assignments compared to the previous tree structure. However, there's no visual indication or disabled state when a mutation is in progress.


Consider disabling checkboxes during mutation loading states to prevent multiple clicks:

```diff
<Checkbox
  checked={role.isActive}
+ disabled={connectRole.isLoading || disconnectRole.isLoading}
  onCheckedChange={(checked) => {
    if (checked) {
      connectRole.mutate({ keyId: keyId, roleId: role.id });
    } else {
      disconnectRole.mutate({ keyId: keyId, roleId: role.id });
    }
  }}
/>
```

---

`18-85`: **Consider using optimistic updates for a smoother UX.**

The current implementation refreshes the entire page after each role connection/disconnection, which may cause a noticeable flicker or delay in the UI.


Consider implementing optimistic updates to provide immediate feedback without waiting for the server response:

```diff
export function PermissionList({ roles, keyId }: PermissionTreeProps) {
  const router = useRouter();
+ const [optimisticRoles, setOptimisticRoles] = useState(roles);
  
  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
    onMutate: ({ roleId }) => {
      toast.loading("Connecting role to key");
+     // Apply optimistic update
+     setOptimisticRoles(prev => 
+       prev.map(role => role.id === roleId ? {...role, isActive: true} : role)
+     );
    },
    // ... rest of the implementation
  });
  
  // Apply similar changes to disconnectRole mutation
  
  return (
    // ... existing code
    <div className="space-y-1">
-      {roles.map((role) => (
+      {optimisticRoles.map((role) => (
        // ... existing render logic
      ))}
    </div>
  );
}
```

This would provide immediate feedback to users while the server operation completes in the background.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between e52f5ded837cf6270bb7c5aa8828f7b4e9087c55 and 1838a7bc56d4558ef4f0a5f4253b4c94e9ee23bd.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (14)</summary>

* GitHub Check: Analyze (javascript-typescript)
* GitHub Check: Test Go API Local / Test (Shard 6/8)
* GitHub Check: Test Go API Local / Test (Shard 7/8)
* GitHub Check: Test Go API Local / Test (Shard 5/8)
* GitHub Check: Test Packages / Test ./internal/hash
* GitHub Check: Test Go API Local / Test (Shard 3/8)
* GitHub Check: Test Packages / Test ./packages/rbac
* GitHub Check: Test Packages / Test ./packages/api
* GitHub Check: Test Packages / Test ./internal/resend
* GitHub Check: Test Packages / Test ./internal/clickhouse
* GitHub Check: Test API / API Test Local
* GitHub Check: Test Agent Local / test_agent_local
* GitHub Check: Build / Build
* GitHub Check: autofix

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx (4)</summary>

`3-6`: **Good use of necessary imports for the new implementation.**

The addition of Checkbox, trpc, useRouter, and toast imports aligns well with the component's new functionality for managing role connections through checkboxes with proper user feedback.

---

`10-10`: **Improved naming with isActive vs. active.**

Renaming from "active" to "isActive" follows better boolean naming conventions, making the code more readable and self-documenting.

---

`15-15`: **Necessary addition of keyId parameter.**

Adding the keyId property to PermissionTreeProps is essential for the role connection/disconnection mutations.

---

`54-56`: **Clear and concise UI text.**

The updated card title and description provide better context about the component's purpose.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T17:29:34Z
user: login: chronark
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T18:25:33Z

Review comments:
diff_hunk: @@ -32,3 +32,13 @@ export const RootKeysFilter = ({
     />
   );
 };
+
+const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
+  const id = rootKey.id;
+  const prefix = id.substring(0, id.indexOf("_") + 1);
+  const obfuscatedMiddle =
+    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
+  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
+  const lastFour = id.substring(id.length - 4);
+  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+};
path: apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
user: login: ogzhanolguncu
body: Can we add a comment to this? It's a bit hard to understand whats going on
created_at: 2025-03-25T13:49:35Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,3 +32,13 @@ export const RootKeysFilter = ({
     />
   );
 };
+
+const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
+  const id = rootKey.id;
+  const prefix = id.substring(0, id.indexOf("_") + 1);
+  const obfuscatedMiddle =
+    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
+  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
+  const lastFour = id.substring(id.length - 4);
+  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+};
path: apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Handle edge cases in the `getRootKeyLabel` function.**

The current implementation might have unexpected behavior if the root key ID doesn't contain an underscore character. The `indexOf()` method returns -1 when a character isn't found, which could lead to unexpected substring operations.


Consider adding a check for whether the ID contains an underscore and provide a fallback format:

```diff
const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
  const id = rootKey.id;
+  // If the root key has a name, use it
+  if (rootKey.name) return rootKey.name;
+  
+  // If the ID doesn't contain an underscore, return a simpler format
+  if (id.indexOf("_") === -1) {
+    return id.length > 8 ? 
+      `${id.substring(0, 4)}...${id.substring(id.length - 4)}` : 
+      id;
+  }
  
  const prefix = id.substring(0, id.indexOf("_") + 1);
  const obfuscatedMiddle =
    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
  const lastFour = id.substring(id.length - 4);
-  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+  return prefix + nextFour + obfuscatedMiddle + lastFour;
};
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
  const id = rootKey.id;
  // If the root key has a name, use it
  if (rootKey.name) return rootKey.name;
  
  // If the ID doesn't contain an underscore, return a simpler format
  if (id.indexOf("_") === -1) {
    return id.length > 8 ? 
      `${id.substring(0, 4)}...${id.substring(id.length - 4)}` : 
      id;
  }
  
  const prefix = id.substring(0, id.indexOf("_") + 1);
  const obfuscatedMiddle =
    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
  const lastFour = id.substring(id.length - 4);
  return prefix + nextFour + obfuscatedMiddle + lastFour;
};
`````

</details>

<!-- suggestion_end -->

---

_💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Consider adding unit tests for the formatting function.**

The `getRootKeyLabel` function is important for UI display and handles various edge cases. It would benefit from unit tests to verify correct behavior with different inputs.


Consider adding tests for the following scenarios:
1. Root key with a name
2. Root key with a null name
3. Root key ID with no underscore
4. Root key ID with an underscore but short length
5. Root key ID with expected format

Let me check if there's a test file for this component:

---

🏁 Script executed:

```shell
#!/bin/bash
# Look for existing test files for this component
fd "root-keys-filter.test|root-keys-filter.spec" --type f
```

Length of output: 59

---



</details>

**Action Required: Add Comprehensive Unit Tests for `getRootKeyLabel` Function**

The `getRootKeyLabel` function is critical for ensuring correct UI display by handling various edge cases. Our initial search for test files (using patterns like “root-keys-filter.test” or “root-keys-filter.spec”) returned no results, so it's unclear whether tests for this function exist. Please confirm manually if there are any tests covering this functionality. If not, add unit tests covering at least the following scenarios:

- **When `rootKey.name` is provided:** The function should return the provided name.
- **When `rootKey.name` is `null`:** The function should construct the label from the `id` following the formatting logic.
- **When the `id` contains no underscore:** Verify the function’s behavior with such input.
- **When the `id` includes an underscore but is shorter than expected:** Ensure the function handles insufficient substring lengths safely.
- **When the `id` follows the expected format:** Validate that the label is formatted correctly.

Please verify manually whether tests exist elsewhere in the repository and add these tests if they are missing.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T13:52:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -32,3 +32,13 @@ export const RootKeysFilter = ({
     />
   );
 };
+
+const getRootKeyLabel = (rootKey: { id: string; name: string | null }) => {
+  const id = rootKey.id;
+  const prefix = id.substring(0, id.indexOf("_") + 1);
+  const obfuscatedMiddle =
+    id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5).length > 0 ? "..." : "";
+  const nextFour = id.substring(id.indexOf("_") + 1, id.indexOf("_") + 5);
+  const lastFour = id.substring(id.length - 4);
+  return rootKey.name ?? prefix + nextFour + obfuscatedMiddle + lastFour;
+};
path: apps/dashboard/app/(app)/audit/components/controls/components/logs-filters/components/root-keys-filter.tsx
user: login: MichaelUnkey
body: Sure can
created_at: 2025-03-25T13:54:56Z
author_association: COLLABORATOR
diff_hunk: @@ -1,83 +1,84 @@
 "use client";
-import { Tree } from "@/app/(app)/authorization/roles/[roleId]/tree";
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { cn } from "@/lib/utils";
-import { Button } from "@unkey/ui";
-import { useEffect, useState } from "react";
-
-export type NestedPermission = {
-  id: string;
-  checked: boolean;
-  description: string | null;
-  name: string;
-  part: string;
-  path: string;
-  permissions: NestedPermissions;
-};
-export type NestedPermissions = Record<string, NestedPermission>;
+import { Checkbox } from "@/components/ui/checkbox";
+import { trpc } from "@/lib/trpc/client";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
 export type Role = {
   id: string;
   name: string;
-  keyId: string;
-  active: boolean;
-  description: string | null;
-  nestedPermissions: NestedPermissions;
+  isActive: boolean;
 };
+
 type PermissionTreeProps = {
   roles: Role[];
+  keyId: string;
 };
 
-export function PermissionList({ roles }: PermissionTreeProps) {
-  const [activeRoleId, setActiveRoleId] = useState<string | null>(
-    roles.length > 0 ? roles[0].id : null,
-  );
-  const [key, setKey] = useState(0);
-
-  // biome-ignore lint/correctness/useExhaustiveDependencies: Force rerender on role change
-  useEffect(() => {
-    // Update the key when activeRoleId changes to force a complete re-render
-    setKey((prev) => prev + 1);
-  }, [activeRoleId]);
+export function PermissionList({ roles, keyId }: PermissionTreeProps) {
+  const router = useRouter();
+  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
+    onMutate: () => {
+      toast.loading("Connecting role to key");
+    },
+    onSuccess: () => {
+      toast.dismiss();
+      toast.success("Role connected to key");
+      router.refresh();
+    },
+    onError: (error) => {
+      toast.dismiss();
+      toast.error(error.message);
+    },
+  });
 
-  const activeRole = roles.find((r) => r.id === activeRoleId);
+  const disconnectRole = trpc.rbac.disconnectRoleFromKey.useMutation({
+    onMutate: () => {
+      toast.loading("Disconnecting role from key");
+    },
+    onSuccess: () => {
+      toast.dismiss();
+      toast.success("Role disconnected from key");
+      router.refresh();
+    },
+    onError: (error) => {
+      toast.dismiss();
+      toast.error(error.message);
+    },
+  });
path: apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Well-implemented mutations with proper error handling.**

The implementation of connectRole and disconnectRole mutations with toast notifications for loading, success, and error states provides good user feedback. However, there's an issue with toast notifications not disappearing as mentioned in the PR comments.



The toast.dismiss() call might not be working as expected. Consider implementing a more reliable approach to ensure toast notifications disappear properly:

```diff
  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
    onMutate: () => {
-     toast.loading("Connecting role to key");
+     const toastId = toast.loading("Connecting role to key");
+     return { toastId };
    },
-   onSuccess: () => {
-     toast.dismiss();
+   onSuccess: (_, __, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
      toast.success("Role connected to key");
      router.refresh();
    },
-   onError: (error) => {
-     toast.dismiss();
+   onError: (error, _, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
      toast.error(error.message);
    },
  });
```

Apply a similar pattern to the disconnectRole mutation as well.

---



</details>

**Action Required: Update Toast Handling in Role Mutations**

The current implementation does not track individual toast IDs generated by `toast.loading`, which might be why notifications are not disappearing reliably. Please update both the `connectRole` and `disconnectRole` mutations to capture the toast ID in the `onMutate` callback, then dismiss the specific toast using the returned `context`. For example:

```diff
  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
    onMutate: () => {
-     toast.loading("Connecting role to key");
+     const toastId = toast.loading("Connecting role to key");
+     return { toastId };
    },
-   onSuccess: () => {
-     toast.dismiss();
-     toast.success("Role connected to key");
+   onSuccess: (_, __, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
+     toast.success("Role connected to key");
      router.refresh();
    },
-   onError: (error) => {
-     toast.dismiss();
-     toast.error(error.message);
+   onError: (error, _, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
+     toast.error(error.message);
    },
  });

  const disconnectRole = trpc.rbac.disconnectRoleFromKey.useMutation({
    onMutate: () => {
-     toast.loading("Disconnecting role from key");
+     const toastId = toast.loading("Disconnecting role from key");
+     return { toastId };
    },
-   onSuccess: () => {
-     toast.dismiss();
-     toast.success("Role disconnected from key");
+   onSuccess: (_, __, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
+     toast.success("Role disconnected from key");
      router.refresh();
    },
-   onError: (error) => {
-     toast.dismiss();
-     toast.error(error.message);
+   onError: (error, _, context) => {
+     if (context?.toastId) toast.dismiss(context.toastId);
+     toast.error(error.message);
    },
  });
```

Please apply this change to ensure that toast notifications reliably disappear during role connection and disconnection. 



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export function PermissionList({ roles, keyId }: PermissionTreeProps) {
  const router = useRouter();
  const connectRole = trpc.rbac.connectRoleToKey.useMutation({
    onMutate: () => {
      const toastId = toast.loading("Connecting role to key");
      return { toastId };
    },
    onSuccess: (_, __, context) => {
      if (context?.toastId) toast.dismiss(context.toastId);
      toast.success("Role connected to key");
      router.refresh();
    },
    onError: (error, _, context) => {
      if (context?.toastId) toast.dismiss(context.toastId);
      toast.error(error.message);
    },
  });

  const disconnectRole = trpc.rbac.disconnectRoleFromKey.useMutation({
    onMutate: () => {
      const toastId = toast.loading("Disconnecting role from key");
      return { toastId };
    },
    onSuccess: (_, __, context) => {
      if (context?.toastId) toast.dismiss(context.toastId);
      toast.success("Role disconnected from key");
      router.refresh();
    },
    onError: (error, _, context) => {
      if (context?.toastId) toast.dismiss(context.toastId);
      toast.error(error.message);
    },
  });
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-25T17:29:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-03-24T19:36:32Z
body: <p><a href="https://linear.app/unkey/issue/ENG-1688/a-root-key-filter-should-show-something-other-than-empty-if-it-does">ENG-1688 A `Root Key` filter should show something other than &lt; empty &gt; if it does not have a name</a></p>
author_association: NONE
user: login: changeset-bot[bot]
created_at: 2025-03-24T19:36:33Z
body: ###  ⚠️  No Changeset found

Latest commit: e52f5ded837cf6270bb7c5aa8828f7b4e9087c55

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/unkeyed/unkey/new/eng-1688-a-root-key-filter-should-show-something-other-than-empty-if?filename=.changeset/rotten-foxes-help.md&value=---%0A%22%40unkey%2Fdashboard%22%3A%20patch%0A%22%40unkey%2Fdocs%22%3A%20patch%0A---%0A%0Afeat%3A%20suggested%20display%20of%20rootkey%20for%20filters%0A)


author_association: NONE
user: login: vercel[bot]
created_at: 2025-03-24T19:36:35Z
body: [vc]: #uCgGm0AD4G/GWKg9hQgBR4NvxSVUAN4yZl/x4ALKxBA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJkYXNoYm9hcmQiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9kYXNoYm9hcmQiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdW5rZXkvZGFzaGJvYXJkL0FyY3U1bnhqQ0FIRTJHTmJxeTFMSDhDbVl3THgiLCJwcmV2aWV3VXJsIjoiZGFzaGJvYXJkLWdpdC1lbmctMTY4OC1hLXJvb3Qta2V5LWZpbHRlci1zaG91bGQtc2gtNWNkZjM3LXVua2V5LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoiZGFzaGJvYXJkLWdpdC1lbmctMTY4OC1hLXJvb3Qta2V5LWZpbHRlci1zaG91bGQtc2gtNWNkZjM3LXVua2V5LnZlcmNlbC5hcHAifX0seyJuYW1lIjoid3d3Iiwicm9vdERpcmVjdG9yeSI6ImFwcHMvd3d3IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6Ind3dy1naXQtZW5nLTE2ODgtYS1yb290LWtleS1maWx0ZXItc2hvdWxkLXNob3ctc29tLWFmNTBmNi11bmtleS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS91bmtleS93d3cvNHVFYk1XQ2hIZWtIYWJSc01paUF1WWNxalg0TCIsInByZXZpZXdVcmwiOiJ3d3ctZ2l0LWVuZy0xNjg4LWEtcm9vdC1rZXktZmlsdGVyLXNob3VsZC1zaG93LXNvbS1hZjUwZjYtdW5rZXkudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9LHsibmFtZSI6InBsYXkiLCJyb290RGlyZWN0b3J5IjoiYXBwcy9wbGF5IiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InBsYXktZ2l0LWVuZy0xNjg4LWEtcm9vdC1rZXktZmlsdGVyLXNob3VsZC1zaG93LXNvLTZiODUxYy11bmtleS52ZXJjZWwuYXBwIn0sImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS91bmtleS9wbGF5L0VxYlV4MVdRR1NCYlhSRGNYRFk2OWlKQlNxOEciLCJwcmV2aWV3VXJsIjoicGxheS1naXQtZW5nLTE2ODgtYS1yb290LWtleS1maWx0ZXItc2hvdWxkLXNob3ctc28tNmI4NTFjLXVua2V5LnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQifSx7Im5hbWUiOiJlbmdpbmVlcmluZyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL2VuZ2luZWVyaW5nIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6ImVuZ2luZWVyaW5nLWdpdC1lbmctMTY4OC1hLXJvb3Qta2V5LWZpbHRlci1zaG91bGQtMWIyZjg4LXVua2V5LnZlcmNlbC5hcHAifSwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Vua2V5L2VuZ2luZWVyaW5nLzEzY0JaSFR4MUw1cW1yTHNOczZqTHRyMlplVE0iLCJwcmV2aWV3VXJsIjoiZW5naW5lZXJpbmctZ2l0LWVuZy0xNjg4LWEtcm9vdC1rZXktZmlsdGVyLXNob3VsZC0xYjJmODgtdW5rZXkudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCJ9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **dashboard** | ✅ Ready ([Inspect](https://vercel.com/unkey/dashboard/Arcu5nxjCAHE2GNbqy1LH8CmYwLx)) | [Visit Preview](https://dashboard-git-eng-1688-a-root-key-filter-should-sh-5cdf37-unkey.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/dashboard-git-eng-1688-a-root-key-filter-should-sh-5cdf37-unkey.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 5:31pm |
| **engineering** | ✅ Ready ([Inspect](https://vercel.com/unkey/engineering/13cBZHTx1L5qmrLsNs6jLtr2ZeTM)) | [Visit Preview](https://engineering-git-eng-1688-a-root-key-filter-should-1b2f88-unkey.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/engineering-git-eng-1688-a-root-key-filter-should-1b2f88-unkey.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 5:31pm |
| **play** | ✅ Ready ([Inspect](https://vercel.com/unkey/play/EqbUx1WQGSBbXRDcXDY69iJBSq8G)) | [Visit Preview](https://play-git-eng-1688-a-root-key-filter-should-show-so-6b851c-unkey.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/play-git-eng-1688-a-root-key-filter-should-show-so-6b851c-unkey.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 5:31pm |
| **www** | ✅ Ready ([Inspect](https://vercel.com/unkey/www/4uEbMWChHekHabRsMiiAuYcqjX4L)) | [Visit Preview](https://www-git-eng-1688-a-root-key-filter-should-show-som-af50f6-unkey.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/www-git-eng-1688-a-root-key-filter-should-show-som-af50f6-unkey.vercel.app?via=pr-comment-feedback-link) | Mar 25, 2025 5:31pm |





author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-03-24T19:36:38Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough
The changes introduce a new function, `getRootKeyLabel`, in the root keys filter component of the Dashboard Audit application. This function formats labels for each root key by extracting specific parts of the key's ID and name. Additionally, a significant refactoring of the `APIKeyDetailPage` component simplifies the handling of roles and permissions by replacing a nested structure with a flat list. The `PermissionList` component has also been restructured to manage role connections through checkboxes, enhancing the UI and functionality. Documentation updates reflect these changes in user instructions.

## Changes

| File(s) | Change Summary |
|---------|----------------|
| `apps/dashboard/app/(app)/audit/.../logs-filters/components/root-keys-filter.tsx` | Added `getRootKeyLabel` function to format labels by extracting segments of the root key's ID, replacing the previous direct assignment of `rootKey.name` or a placeholder string. |
| `apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx` | Refactored role handling to a flat list `rolesList`, removing nested permissions structure and simplifying logic. Renamed variable from `roleTee` to `rolesList`. |
| `apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/permission-list.tsx` | Restructured `PermissionList` component, modified `Role` type, updated `PermissionTreeProps`, and changed function signature to include `keyId`. Removed local state management for active role. |
| `apps/docs/apis/features/authorization/example.mdx` | Updated instructions for connecting roles to API keys, added breadcrumb navigation image, and simplified references to permissions. |
| `apps/docs/apis/features/authorization/roles-and-permissions.mdx` | Similar updates to instructions for connecting roles to keys, added breadcrumb navigation image, and simplified references to permissions. |

## Suggested labels
`Feature`, `🕹️ oss.gg`, `:joystick: 150 points`, `hacktoberfest`

## Suggested reviewers
- mcstepp
- perkinsjr
- chronark
- ogzhanolguncu

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=unkeyed/unkey&utm_content=3028):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-24T19:36:39Z
body: Thank you for following the naming conventions for pull request titles! 🙏
<!-- Sticky Pull Request Commentpr-title-lint-error -->
author_association: CONTRIBUTOR
user: login: chronark
created_at: 2025-03-25T13:55:14Z
body: ignore that, we can guarantee they’ll have an underscore
author_association: COLLABORATOR
user: login: chronark
created_at: 2025-03-25T16:49:08Z
body: can we fix the toast not vanishing?
[CleanShot 2025-03-25 at 17.48.51@2x.png](https://files.slack.com/files-pri/T05T2KX8C3X-F08KBQZL7PD/cleanshot_2025-03-25_at_17.48.51_2x.png)
author_association: COLLABORATOR