Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions web/src/components/ApiKey/ApiKeyManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -261,40 +261,42 @@ async function copyKeyTemplate(key: ApiKeyFragment) {
role
}}</Badge>
</div>
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-green-700"><b>API Key:</b></span>
<div class="relative max-w-[300px] flex-1">
<Input
:model-value="showKey[key.id] ? key.key : '••••••••••••••••••••••••••••••••'"
class="w-full rounded px-2 py-1 pr-10 font-mono text-xs"
readonly
/>
<div class="flex flex-col gap-2">
<div class="flex items-center gap-2">
<span class="text-sm font-medium text-green-700"><b>API Key:</b></span>
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="copyKeyValue(key.key)"
>
<ClipboardDocumentIcon class="h-4 w-4" />
</Button>
Comment on lines +270 to +277
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add accessible names to icon-only copy and visibility buttons.

Line 270 and Line 291 use icon-only buttons without accessible names, reducing usability for assistive technologies.

♿ Suggested fix
                           <Button
                             variant="ghost"
                             size="icon"
                             class="h-8 w-8"
+                            aria-label="Copy API key to clipboard"
+                            title="Copy API key to clipboard"
                             `@click`="copyKeyValue(key.key)"
                           >
                             <ClipboardDocumentIcon class="h-4 w-4" />
                           </Button>
@@
                     <Button
                       variant="ghost"
                       size="icon"
                       class="absolute top-2 right-2 h-auto w-auto px-1 text-gray-500 hover:text-gray-700"
+                      :aria-label="showKey[key.id] ? 'Hide API key' : 'Show API key'"
+                      :title="showKey[key.id] ? 'Hide API key' : 'Show API key'"
                       `@click`="toggleShowKey(key.id)"
                     >

Also applies to: 291-298

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ApiKey/ApiKeyManager.vue` around lines 270 - 277, The
icon-only Button elements in ApiKeyManager.vue (the copy button that calls
copyKeyValue(key.key) and the visibility button that toggles key visibility)
lack accessible names; add clear accessible labels by adding aria-label
attributes (or visually hidden text) to those Button components (e.g.,
aria-label="Copy API key" for the ClipboardDocumentIcon button and
aria-label="Show API key" / "Hide API key" for the visibility toggle) and ensure
the toggle updates its label based on state so screen readers get the correct
action.

</TooltipTrigger>
<TooltipContent>
<p>{{ copied ? 'Copied!' : 'Copy to clipboard...' }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
Comment on lines +267 to +283
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, verify the file exists and check the actual code at the specified lines
fd "ApiKeyManager.vue" web

Repository: unraid/api

Length of output: 97


🏁 Script executed:

# Read the file to confirm the code snippet
cat -n web/src/components/ApiKey/ApiKeyManager.vue | sed -n '260,290p'

Repository: unraid/api

Length of output: 1789


🏁 Script executed:

# Search for TooltipTrigger imports and usage in the file
rg -n "TooltipTrigger|import.*Tooltip" web/src/components/ApiKey/ApiKeyManager.vue

Repository: unraid/api

Length of output: 257


🏁 Script executed:

# Search for TooltipTrigger component definition or package info
rg -n "TooltipTrigger" web/src/components --type=vue -A2 | head -50

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Check if as-child is used elsewhere with TooltipTrigger in the codebase
rg "TooltipTrigger.*as-child" web --type=vue

Repository: unraid/api

Length of output: 81


🏁 Script executed:

# Read around line 332 to see the context where as-child IS used
cat -n web/src/components/ApiKey/ApiKeyManager.vue | sed -n '325,345p'

Repository: unraid/api

Length of output: 1204


🏁 Script executed:

# Search for TooltipTrigger patterns without type flag
rg -n "TooltipTrigger" web/src/components/ApiKey/ApiKeyManager.vue -C2

Repository: unraid/api

Length of output: 1278


🏁 Script executed:

# Check for TooltipTrigger usage across the entire codebase
rg "TooltipTrigger(?!.*as-child)" web/src --multiline-dotall 2>/dev/null | head -40

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Find where TooltipTrigger component is defined or imported from
rg "from.*TooltipTrigger|import.*TooltipTrigger" web/src

Repository: unraid/api

Length of output: 190


🏁 Script executed:

# Find `@unraid/ui` location - is it in the repo or external?
fd -t f "@unraid/ui" . 2>/dev/null || find . -name "*unraid*" -type d 2>/dev/null | grep -v node_modules | head -20

Repository: unraid/api

Length of output: 36


🏁 Script executed:

# Search for all TooltipTrigger usages in the codebase
rg "TooltipTrigger" web/src --no-heading | sort | uniq -c

Repository: unraid/api

Length of output: 1567


🏁 Script executed:

# Get all TooltipTrigger context across the codebase
rg -B1 -A4 "TooltipTrigger" web/src 2>/dev/null

Repository: unraid/api

Length of output: 8458


🏁 Script executed:

# Check package.json for `@unraid/ui` info
fd "package.json" web -exec cat {} \; | grep -A5 -B5 "@unraid/ui" | head -20

Repository: unraid/api

Length of output: 200


Add as-child to TooltipTrigger for proper semantics.

Line 269 wraps a <Button> inside TooltipTrigger without the as-child prop. The rest of the codebase consistently uses as-child when wrapping interactive elements—see line 332 in the same file for the correct pattern. Without as-child, nested button elements can break focus behavior and keyboard accessibility.

🛠️ Suggested fix
-                        <TooltipTrigger>
+                        <TooltipTrigger as-child>
                           <Button
                             variant="ghost"
                             size="icon"
                             class="h-8 w-8"
                             `@click`="copyKeyValue(key.key)"
                           >
                             <ClipboardDocumentIcon class="h-4 w-4" />
                           </Button>
-                        </TooltipTrigger>
+                        </TooltipTrigger>
📝 Committable suggestion

‼️ 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.

Suggested change
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="copyKeyValue(key.key)"
>
<ClipboardDocumentIcon class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ copied ? 'Copied!' : 'Copy to clipboard...' }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger as-child>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
`@click`="copyKeyValue(key.key)"
>
<ClipboardDocumentIcon class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ copied ? 'Copied!' : 'Copy to clipboard...' }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/ApiKey/ApiKeyManager.vue` around lines 267 - 283, The
TooltipTrigger wrapping the Button should include the as-child prop to pass
semantics to the wrapped interactive element; update the TooltipTrigger
surrounding the Button (the one that calls copyKeyValue(key.key) and references
copied) to use as-child so the Button remains the real focusable element and
keyboard/focus behavior is preserved.

</div>
<div class="relative min-w-0">
<code
class="bg-background block w-full rounded border px-3 py-2 pr-10 font-mono text-xs leading-5 break-all whitespace-pre-wrap"
>
{{ showKey[key.id] ? key.key : '••••••••••••••••••••••••••••••••' }}
</code>
<Button
variant="ghost"
size="icon"
class="absolute inset-y-0 right-2 h-auto w-auto px-1 text-gray-500 hover:text-gray-700"
class="absolute top-2 right-2 h-auto w-auto px-1 text-gray-500 hover:text-gray-700"
@click="toggleShowKey(key.id)"
>
<component :is="showKey[key.id] ? EyeSlashIcon : EyeIcon" class="h-5 w-5" />
</Button>
</div>
<TooltipProvider>
<Tooltip :delay-duration="0">
<TooltipTrigger>
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
@click="copyKeyValue(key.key)"
>
<ClipboardDocumentIcon class="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{{ copied ? 'Copied!' : 'Copy to clipboard...' }}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
<div
Expand Down
18 changes: 9 additions & 9 deletions web/src/components/ApiKeyAuthorize.standalone.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia';

import { ClipboardDocumentIcon, EyeIcon, EyeSlashIcon } from '@heroicons/vue/24/outline';
import { Button, Input } from '@unraid/ui';
import { Button } from '@unraid/ui';

import ApiKeyCreate from '~/components/ApiKey/ApiKeyCreate.vue';
import { useAuthorizationLink } from '~/composables/useAuthorizationLink.js';
Expand Down Expand Up @@ -152,16 +152,16 @@ const returnToApp = () => {
<div>
<label class="text-muted-foreground mb-2 block text-sm font-medium">Generated API Key</label>
<div class="bg-secondary rounded-lg p-3">
<div class="mb-2 flex gap-2">
<div class="relative flex-1">
<Input
:model-value="showKey ? createdApiKey : '••••••••••••••••••••••••••••••••'"
class="bg-background pr-10 font-mono text-sm"
readonly
/>
<div class="mb-2 flex items-start gap-2">
<div class="relative min-w-0 flex-1">
<code
class="bg-background block w-full rounded border px-3 py-2 pr-10 font-mono text-sm leading-6 break-all whitespace-pre-wrap"
>
{{ showKey ? createdApiKey : '••••••••••••••••••••••••••••••••' }}
</code>
<button
type="button"
class="text-muted-foreground hover:text-foreground absolute inset-y-0 right-2 flex items-center px-1"
class="text-muted-foreground hover:text-foreground absolute top-2 right-2 flex items-center px-1"
@click="toggleShowKey"
>
<component :is="showKey ? EyeSlashIcon : EyeIcon" class="h-4 w-4" />
Expand Down
Loading