Skip to content

Commit 3768699

Browse files
rameerezclaude
andcommitted
Refactor dashboard views with extracted partials
Extract reusable partials from key row for cleaner code: - _token_display.html.erb - masked/viewable token with show button - _key_badges.html.erb - type, environment, expiration badges - _key_status.html.erb - active/expired/revoked status badge - _key_actions.html.erb - edit/revoke action buttons - _empty_state.html.erb - empty state messaging Update existing partials: - _key_row.html.erb - now uses extracted partials - _keys_table.html.erb - simplified table structure - _publishable_keys.html.erb - improved section layout - _secret_keys.html.erb - improved section layout Add minimal JS for show/copy token functionality: - Event delegation for .btn-show-token and .btn-copy-token - Vanilla JS, no framework dependencies - Progressive enhancement (works without JS) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 8e62223 commit 3768699

10 files changed

Lines changed: 151 additions & 83 deletions
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<%# Partial for displaying empty state when no keys exist %>
2+
<%# Locals: message (optional) - Custom message to display %>
3+
4+
<% message ||= "You don't have any API keys yet!" %>
5+
6+
<div class="api-keys-empty-state" style="text-align: center; padding: 2em;">
7+
<h4><%= message %></h4>
8+
<p>Create your first API key to get started.</p>
9+
</div>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<%# Partial for displaying key action buttons (edit, revoke) %>
2+
<%# Locals: key (required) - The ApiKey record %>
3+
4+
<% if key.active? %>
5+
<%= link_to api_keys.edit_key_path(key), title: "Edit Key", class: "api-keys-action-edit" do %>
6+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
7+
<% end %>
8+
9+
<% if key.revocable? %>
10+
<%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", class: "api-keys-action-revoke", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
11+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
12+
<% end %>
13+
<% else %>
14+
<span title="This key cannot be revoked" class="api-keys-action-disabled" style="color: var(--api-keys-muted-color); cursor: help;">
15+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" clip-rule="evenodd"></path></svg>
16+
</span>
17+
<% end %>
18+
<% else %>
19+
&mdash;
20+
<% end %>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<%# Partial for displaying key type and environment badges %>
2+
<%# Locals: key (required) - The ApiKey record %>
3+
4+
<% if key.key_type.present? %>
5+
<% type_config = key.key_type_config %>
6+
<% is_publishable = type_config&.dig(:revocable) == false %>
7+
<span class="api-keys-badge api-keys-badge-type <%= is_publishable ? 'api-keys-badge-publishable' : 'api-keys-badge-secret' %>" style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: var(<%= is_publishable ? '--api-keys-badge-publishable-bg' : '--api-keys-badge-secret-bg' %>); color: var(<%= is_publishable ? '--api-keys-badge-publishable-color' : '--api-keys-badge-secret-color' %>); margin-left: 4px;">
8+
<%= key.key_type.humanize %>
9+
</span>
10+
<% end %>
11+
12+
<% if key.environment.present? %>
13+
<% is_live = key.environment == 'live' %>
14+
<span class="api-keys-badge api-keys-badge-env <%= is_live ? 'api-keys-badge-live' : 'api-keys-badge-test' %>" style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: var(<%= is_live ? '--api-keys-badge-live-bg' : '--api-keys-badge-test-bg' %>); color: var(<%= is_live ? '--api-keys-badge-live-color' : '--api-keys-badge-test-color' %>); margin-left: 4px;">
15+
<%= key.environment.upcase %>
16+
</span>
17+
<% end %>
Lines changed: 20 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
1-
<tr>
2-
<td>
3-
<%# Status indicator (no text originally, keeping it that way unless specified otherwise) %>
4-
<% if key.active? %>
5-
<span style="color: green;"></span>
6-
<% elsif key.revoked? %>
7-
<span style="color: orange;">[Revoked]</span>
8-
<% elsif key.expired? %>
9-
<span style="color: red;">[Expired]</span>
10-
<% end %>
1+
<%# Partial for displaying a single API key row in the table %>
2+
<%# Locals: key (required) - The ApiKey record %>
3+
4+
<tr class="api-keys-row <%= 'api-keys-row-inactive' if defined?(inactive) && inactive %>">
5+
<td class="api-keys-cell-name">
6+
<%= render partial: 'api_keys/keys/key_status', locals: { key: key } %>
117
<%= key.name.presence || (key.key_type.present? ? "#{key.key_type.humanize} key" : "API key") %>
12-
<%# Key type and environment badges %>
13-
<% if key.key_type.present? %>
14-
<% type_config = key.key_type_config %>
15-
<span style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: <%= type_config&.dig(:revocable) == false ? '#fef3cd' : '#e7f1ff' %>; color: <%= type_config&.dig(:revocable) == false ? '#856404' : '#004085' %>; margin-left: 4px;">
16-
<%= key.key_type.humanize %>
17-
</span>
18-
<% end %>
19-
<% if key.environment.present? %>
20-
<span style="display: inline-block; padding: 2px 6px; font-size: 0.75em; border-radius: 3px; background-color: <%= key.environment == 'live' ? '#d4edda' : '#f8d7da' %>; color: <%= key.environment == 'live' ? '#155724' : '#721c24' %>; margin-left: 4px;">
21-
<%= key.environment.upcase %>
22-
</span>
23-
<% end %>
24-
</td>
25-
<td class="api-keys-token-cell">
26-
<% if key.public_key_type? && key.viewable_token.present? %>
27-
<span class="token-masked"><code><%= key.masked_token %></code></span>
28-
<span class="token-full" style="display: none;"><code style="word-break: break-all;"><%= key.viewable_token %></code></span>
29-
<button type="button" class="btn-show-token" style="margin-left: 8px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Show full token">Show</button>
30-
<button type="button" class="btn-copy-token" style="display: none; margin-left: 4px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Copy to clipboard" data-token="<%= key.viewable_token %>">Copy</button>
31-
<% else %>
32-
<code><%= key.masked_token %></code>
33-
<% end %>
8+
<%= render partial: 'api_keys/keys/key_badges', locals: { key: key } %>
349
</td>
3510

11+
<td class="api-keys-cell-token api-keys-token-cell">
12+
<%= render partial: 'api_keys/keys/token_display', locals: { key: key } %>
13+
</td>
3614

37-
<td title="<%= key.created_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
15+
<td class="api-keys-cell-created" title="<%= key.created_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
3816
<%= time_ago_in_words(key.created_at) %> ago
3917
</td>
4018

41-
42-
<td>
19+
<td class="api-keys-cell-expires">
4320
<% if key.expires_at? %>
4421
<% if key.expired? %>
45-
<strong style="color: red;" title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
22+
<strong style="color: var(--api-keys-status-expired-color);" title="<%= key.expires_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
4623
Expired <%= time_ago_in_words(key.expires_at) %> ago
4724
</strong>
4825
<% else %>
@@ -55,46 +32,27 @@
5532
<% end %>
5633
</td>
5734

58-
59-
<td>
35+
<td class="api-keys-cell-last-used">
6036
<% if key.last_used_at? %>
6137
<span title="<%= key.last_used_at.strftime('%Y-%m-%d %H:%M:%S %Z') %>">
6238
<%= time_ago_in_words(key.last_used_at) %> ago
6339
</span>
64-
<%# TODO: Add relative time check (e.g., "within last 3 months") %>
6540
<% else %>
6641
<em>Never used</em>
6742
<% end %>
6843
</td>
6944

70-
71-
<td>
45+
<td class="api-keys-cell-scopes">
7246
<% if key.scopes.present? %>
7347
<% key.scopes.each do |scope| %>
74-
<kbd class="tag is-small"><%= scope %></kbd>
48+
<kbd class="api-keys-scope-tag tag is-small"><%= scope %></kbd>
7549
<% end %>
7650
<% else %>
7751
&mdash;
7852
<% end %>
7953
</td>
80-
<td class="api-keys-action-buttons">
81-
<% if key.active? %>
82-
<%= link_to api_keys.edit_key_path(key), title: "Edit Key" do %>
83-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M16.793 2.793a3.121 3.121 0 1 1 4.414 4.414l-8.5 8.5A1 1 0 0 1 12 16H9a1 1 0 0 1-1-1v-3a1 1 0 0 1 .293-.707l8.5-8.5Zm3 1.414a1.121 1.121 0 0 0-1.586 0L10 12.414V14h1.586l8.207-8.207a1.121 1.121 0 0 0 0-1.586ZM6 5a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-4a1 1 0 1 1 2 0v4a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h4a1 1 0 1 1 0 2H6Z" clip-rule="evenodd"></path></svg>
84-
<% end %>
85-
<%# Only show revoke button for revocable keys %>
86-
<% if key.revocable? %>
87-
<%= button_to api_keys.revoke_key_path(key), title: "Revoke Key", data: { turbo_method: :post, turbo_confirm: "Are you sure you want to revoke this key? It will stop working immediately." } do %>
88-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M10.556 4a1 1 0 0 0-.97.751l-.292 1.14h5.421l-.293-1.14A1 1 0 0 0 13.453 4h-2.897Zm6.224 1.892-.421-1.639A3 3 0 0 0 13.453 2h-2.897A3 3 0 0 0 7.65 4.253l-.421 1.639H4a1 1 0 1 0 0 2h.1l1.215 11.425A3 3 0 0 0 8.3 22h7.4a3 3 0 0 0 2.984-2.683l1.214-11.425H20a1 1 0 1 0 0-2h-3.22Zm1.108 2H6.112l1.192 11.214A1 1 0 0 0 8.3 20h7.4a1 1 0 0 0 .995-.894l1.192-11.214ZM10 10a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Zm4 0a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-5a1 1 0 0 1 1-1Z" clip-rule="evenodd"></path></svg>
89-
<% end %>
90-
<% else %>
91-
<span title="This key cannot be revoked" style="color: #999; cursor: help;">
92-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" viewBox="0 0 24 24"><path fill-rule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" clip-rule="evenodd"></path></svg>
93-
</span>
94-
<% end %>
95-
<% else %>
96-
<%# No actions available for inactive/revoked/expired keys %>
97-
&mdash;
98-
<% end %>
54+
55+
<td class="api-keys-cell-actions api-keys-action-buttons">
56+
<%= render partial: 'api_keys/keys/key_actions', locals: { key: key } %>
9957
</td>
100-
</tr>
58+
</tr>
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<%# Partial for displaying key status indicator %>
2+
<%# Locals: key (required) - The ApiKey record %>
3+
4+
<% if key.active? %>
5+
<span class="api-keys-status api-keys-status-active" style="color: var(--api-keys-status-active-color);"></span>
6+
<% elsif key.revoked? %>
7+
<span class="api-keys-status api-keys-status-revoked" style="color: var(--api-keys-status-revoked-color);">[Revoked]</span>
8+
<% elsif key.expired? %>
9+
<span class="api-keys-status api-keys-status-expired" style="color: var(--api-keys-status-expired-color);">[Expired]</span>
10+
<% end %>

app/views/api_keys/keys/_keys_table.html.erb

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,7 @@
3838
</tbody>
3939
</table>
4040
<% else %>
41-
<div style="text-align: center; padding: 2em;">
42-
<h4>You don't have any API keys yet!</h4>
43-
<p>Create your first API key to get started.</p>
44-
<%# Consider adding a primary "Create Key" button here %>
45-
<%#= link_to "Create New API Key", new_key_path, class: "button primary" %>
46-
</div>
41+
<%= render partial: 'api_keys/keys/empty_state' %>
4742
<% end %>
4843
</div>
4944

app/views/api_keys/keys/_publishable_keys.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,7 @@
3434
</tbody>
3535
</table>
3636
<% else %>
37-
<div style="text-align: center; padding: 2em; color: var(--color-darkGrey, #777);">
38-
<p>No publishable keys yet.</p>
39-
</div>
37+
<%= render partial: 'api_keys/keys/empty_state', locals: { message: "No publishable keys yet." } %>
4038
<% end %>
4139
</div>
4240
</section>

app/views/api_keys/keys/_secret_keys.html.erb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,7 @@
3333
</tbody>
3434
</table>
3535
<% else %>
36-
<div style="text-align: center; padding: 2em; color: var(--color-darkGrey, #777);">
37-
<p>No secret keys yet.</p>
38-
</div>
36+
<%= render partial: 'api_keys/keys/empty_state', locals: { message: "No secret keys yet." } %>
3937
<% end %>
4038
</div>
4139
</section>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<%# Partial for displaying an API key token with optional show/copy functionality %>
2+
<%# Locals: key (required) - The ApiKey record %>
3+
4+
<% if key.public_key_type? && key.viewable_token.present? %>
5+
<span class="token-masked"><code><%= key.masked_token %></code></span>
6+
<span class="token-full" style="display: none;"><code style="word-break: break-all;"><%= key.viewable_token %></code></span>
7+
<button type="button" class="btn-show-token" style="margin-left: 8px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Show full token">Show</button>
8+
<button type="button" class="btn-copy-token" style="display: none; margin-left: 4px; font-size: 0.75em; padding: 2px 6px; cursor: pointer;" title="Copy to clipboard" data-token="<%= key.viewable_token %>">Copy</button>
9+
<% else %>
10+
<code><%= key.masked_token %></code>
11+
<% end %>

app/views/layouts/api_keys/application.html.erb

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,67 @@
1212
</head>
1313
<body>
1414
<style>
15+
/*
16+
* API Keys CSS Variables
17+
* Override these in your host application to customize the dashboard appearance.
18+
* Example in your app's CSS:
19+
* :root {
20+
* --api-keys-primary-color: #your-brand-color;
21+
* --api-keys-danger-color: #your-danger-color;
22+
* }
23+
*/
24+
:root {
25+
/* Colors */
26+
--api-keys-primary-color: #007bff;
27+
--api-keys-danger-color: #c23539;
28+
--api-keys-success-color: #28a745;
29+
--api-keys-warning-color: #ffc107;
30+
--api-keys-muted-color: #6c757d;
31+
32+
/* Badge colors */
33+
--api-keys-badge-secret-bg: #e7f1ff;
34+
--api-keys-badge-secret-color: #004085;
35+
--api-keys-badge-publishable-bg: #fef3cd;
36+
--api-keys-badge-publishable-color: #856404;
37+
--api-keys-badge-live-bg: #d4edda;
38+
--api-keys-badge-live-color: #155724;
39+
--api-keys-badge-test-bg: #f8d7da;
40+
--api-keys-badge-test-color: #721c24;
41+
42+
/* Status colors */
43+
--api-keys-status-active-color: green;
44+
--api-keys-status-revoked-color: orange;
45+
--api-keys-status-expired-color: red;
46+
47+
/* Spacing */
48+
--api-keys-section-padding: 1.5em;
49+
--api-keys-section-margin: 2em;
50+
--api-keys-border-radius: 8px;
51+
52+
/* Typography */
53+
--api-keys-font-family: inherit;
54+
--api-keys-code-font-size: 0.8em;
55+
}
56+
1557
body {
16-
/* grid-template-columns: 1fr min(100rem, 90%) 1fr !important; */
58+
font-family: var(--api-keys-font-family);
1759
}
1860

1961
@media (prefers-color-scheme: dark) {
62+
:root {
63+
--api-keys-badge-secret-bg: #1a365d;
64+
--api-keys-badge-secret-color: #90cdf4;
65+
--api-keys-badge-publishable-bg: #744210;
66+
--api-keys-badge-publishable-color: #faf089;
67+
--api-keys-badge-live-bg: #22543d;
68+
--api-keys-badge-live-color: #9ae6b4;
69+
--api-keys-badge-test-bg: #742a2a;
70+
--api-keys-badge-test-color: #feb2b2;
71+
}
72+
2073
body {
21-
/* Define dark mode variables directly */
22-
--bg-color:rgb(14, 14, 14);
23-
--bg-secondary-color:rgb(34, 34, 34);
74+
--bg-color: rgb(14, 14, 14);
75+
--bg-secondary-color: rgb(34, 34, 34);
2476
--font-color: #f5f5f5;
2577
--color-grey: #ccc;
2678
--color-darkGrey: #777;
@@ -29,7 +81,7 @@
2981

3082
code, pre {
3183
color: var(--font-color);
32-
font-size: 0.8em;
84+
font-size: var(--api-keys-code-font-size);
3385
}
3486

3587
.api-keys-align-center {
@@ -52,7 +104,7 @@
52104
.api-keys-action-buttons button {
53105
background: none;
54106
padding: 0;
55-
color: #c23539
107+
color: var(--api-keys-danger-color);
56108
}
57109

58110
.api-keys-button-text {
@@ -79,10 +131,10 @@
79131

80132
/* API Keys sections styling */
81133
.api-keys-section {
82-
margin-top: 2em;
83-
padding: 1.5em;
134+
margin-top: var(--api-keys-section-margin);
135+
padding: var(--api-keys-section-padding);
84136
border: 1px solid var(--color-grey, #ccc);
85-
border-radius: 8px;
137+
border-radius: var(--api-keys-border-radius);
86138
background-color: var(--bg-secondary-color, #f9f9f9);
87139
}
88140

0 commit comments

Comments
 (0)