@tempoxyz/ds 0.3.10 → 0.3.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -6
- package/dist/docs/AGENTS.md +12 -0
- package/dist/docs/index.md +145 -0
- package/dist/docs/marketing/introduction/getting-started.md +241 -0
- package/dist/docs/product/components/accordion.md +489 -0
- package/dist/docs/product/components/address-card.md +147 -0
- package/dist/docs/product/components/agent-logo.md +194 -0
- package/dist/docs/product/components/alert-dialog.md +631 -0
- package/dist/docs/product/components/alert.md +448 -0
- package/dist/docs/product/components/amount-input.md +305 -0
- package/dist/docs/product/components/amount.md +794 -0
- package/dist/docs/product/components/app-shell.md +559 -0
- package/dist/docs/product/components/asset-chip.md +294 -0
- package/dist/docs/product/components/autocomplete.md +620 -0
- package/dist/docs/product/components/avatar.md +321 -0
- package/dist/docs/product/components/badge.md +339 -0
- package/dist/docs/product/components/badged-logo.md +116 -0
- package/dist/docs/product/components/banner.md +379 -0
- package/dist/docs/product/components/bar-chart.md +208 -0
- package/dist/docs/product/components/breadcrumbs.md +380 -0
- package/dist/docs/product/components/button-group.md +286 -0
- package/dist/docs/product/components/button.md +650 -0
- package/dist/docs/product/components/calendar.md +375 -0
- package/dist/docs/product/components/callout.md +96 -0
- package/dist/docs/product/components/checkbox-group.md +521 -0
- package/dist/docs/product/components/checkbox.md +467 -0
- package/dist/docs/product/components/choice-group.md +516 -0
- package/dist/docs/product/components/choicebox.md +641 -0
- package/dist/docs/product/components/code.md +100 -0
- package/dist/docs/product/components/combobox.md +939 -0
- package/dist/docs/product/components/container.md +257 -0
- package/dist/docs/product/components/copy-button.md +642 -0
- package/dist/docs/product/components/date-picker.md +514 -0
- package/dist/docs/product/components/dialog.md +1332 -0
- package/dist/docs/product/components/drawer.md +966 -0
- package/dist/docs/product/components/empty.md +340 -0
- package/dist/docs/product/components/field.md +570 -0
- package/dist/docs/product/components/fieldset.md +398 -0
- package/dist/docs/product/components/frame.md +434 -0
- package/dist/docs/product/components/gauge.md +299 -0
- package/dist/docs/product/components/get-started-visual.md +117 -0
- package/dist/docs/product/components/grid.md +288 -0
- package/dist/docs/product/components/hero-visual.md +155 -0
- package/dist/docs/product/components/hex.md +487 -0
- package/dist/docs/product/components/icon-tile.md +292 -0
- package/dist/docs/product/components/identicon.md +371 -0
- package/dist/docs/product/components/inline.md +233 -0
- package/dist/docs/product/components/inset.md +225 -0
- package/dist/docs/product/components/kbd.md +423 -0
- package/dist/docs/product/components/list-group.md +981 -0
- package/dist/docs/product/components/loading-dots.md +237 -0
- package/dist/docs/product/components/menu.md +934 -0
- package/dist/docs/product/components/native-select.md +204 -0
- package/dist/docs/product/components/network-logo.md +248 -0
- package/dist/docs/product/components/number-field.md +543 -0
- package/dist/docs/product/components/otp-field.md +616 -0
- package/dist/docs/product/components/pagination.md +516 -0
- package/dist/docs/product/components/popover.md +544 -0
- package/dist/docs/product/components/progress.md +388 -0
- package/dist/docs/product/components/qr.md +314 -0
- package/dist/docs/product/components/radio-group.md +528 -0
- package/dist/docs/product/components/secret-value.md +406 -0
- package/dist/docs/product/components/select.md +751 -0
- package/dist/docs/product/components/separator.md +379 -0
- package/dist/docs/product/components/sidebar-menu.md +922 -0
- package/dist/docs/product/components/skeleton.md +517 -0
- package/dist/docs/product/components/slider.md +539 -0
- package/dist/docs/product/components/snippet.md +424 -0
- package/dist/docs/product/components/spinner.md +216 -0
- package/dist/docs/product/components/split-bar.md +139 -0
- package/dist/docs/product/components/split-button.md +464 -0
- package/dist/docs/product/components/stack.md +299 -0
- package/dist/docs/product/components/status-chip.md +226 -0
- package/dist/docs/product/components/status-indicator.md +119 -0
- package/dist/docs/product/components/status-row.md +174 -0
- package/dist/docs/product/components/stepper.md +451 -0
- package/dist/docs/product/components/success-hero.md +157 -0
- package/dist/docs/product/components/surface.md +639 -0
- package/dist/docs/product/components/switch.md +472 -0
- package/dist/docs/product/components/table.md +967 -0
- package/dist/docs/product/components/tabs.md +627 -0
- package/dist/docs/product/components/tempo-logo.md +185 -0
- package/dist/docs/product/components/text-input.md +624 -0
- package/dist/docs/product/components/textarea.md +352 -0
- package/dist/docs/product/components/toast.md +535 -0
- package/dist/docs/product/components/token-logo.md +371 -0
- package/dist/docs/product/components/token.md +247 -0
- package/dist/docs/product/components/tooltip.md +490 -0
- package/dist/docs/product/components/verification-code.md +116 -0
- package/dist/docs/product/components/visual.md +249 -0
- package/dist/docs/product/components.md +203 -0
- package/dist/docs/product/foundations/accessibility.md +377 -0
- package/dist/docs/product/foundations/agents.md +150 -0
- package/dist/docs/product/foundations/color.md +522 -0
- package/dist/docs/product/foundations/content/tone.md +194 -0
- package/dist/docs/product/foundations/content/voice.md +194 -0
- package/dist/docs/product/foundations/content/writing-style.md +406 -0
- package/dist/docs/product/foundations/iconography.md +282 -0
- package/dist/docs/product/foundations/intent.md +360 -0
- package/dist/docs/product/foundations/layout.md +508 -0
- package/dist/docs/product/foundations/linter.md +496 -0
- package/dist/docs/product/foundations/motion.md +151 -0
- package/dist/docs/product/foundations/radius-and-padding.md +86 -0
- package/dist/docs/product/foundations/typography.md +266 -0
- package/dist/docs/product/introduction/faq.md +133 -0
- package/dist/docs/product/introduction/getting-started.md +377 -0
- package/dist/docs/product/patterns/blockchain-values.md +709 -0
- package/dist/docs/product/patterns/dashboards.md +638 -0
- package/dist/docs/product/patterns/destructive-actions.md +626 -0
- package/dist/docs/product/patterns/empty-states.md +612 -0
- package/dist/docs/product/patterns/errors-and-validation.md +808 -0
- package/dist/docs/product/patterns/feedback-and-status.md +518 -0
- package/dist/docs/product/patterns/forms.md +679 -0
- package/dist/docs/product/patterns/loading.md +630 -0
- package/dist/docs/product/patterns/navigation.md +397 -0
- package/dist/docs/product/patterns/payments.md +765 -0
- package/dist/docs/product/patterns/responsive-layout.md +997 -0
- package/dist/docs/product/patterns/tables-and-lists.md +964 -0
- package/dist/docs/product/products/tempo-console.md +671 -0
- package/dist/docs/product/products/tempo-performance.md +437 -0
- package/dist/docs/product/products/tempo-wallet.md +438 -0
- package/dist/marketing/index.d.ts +4 -0
- package/dist/marketing/index.d.ts.map +1 -0
- package/dist/marketing/index.js +4 -0
- package/dist/marketing/index.js.map +1 -0
- package/dist/marketing/index.js.zyzz.json +1 -0
- package/dist/marketing/vars.d.ts +907 -0
- package/dist/marketing/vars.d.ts.map +1 -0
- package/dist/marketing/vars.js +15 -0
- package/dist/marketing/vars.js.map +1 -0
- package/dist/marketing/vars.js.zyzz.json +1 -0
- package/dist/marketing/zyzz.config.d.ts +4512 -0
- package/dist/marketing/zyzz.config.d.ts.map +1 -0
- package/dist/marketing/zyzz.config.js +63 -0
- package/dist/marketing/zyzz.config.js.map +1 -0
- package/dist/marketing/zyzz.config.js.zyzz.json +1 -0
- package/dist/oxlint/index.d.ts +12 -1
- package/dist/oxlint/index.d.ts.map +1 -1
- package/dist/oxlint/index.js +39 -1
- package/dist/oxlint/index.js.map +1 -1
- package/dist/oxlint/internal/Docs.d.ts +9 -0
- package/dist/oxlint/internal/Docs.d.ts.map +1 -0
- package/dist/oxlint/internal/Docs.js +15 -0
- package/dist/oxlint/internal/Docs.js.map +1 -0
- package/dist/oxlint/internal/Imports.d.ts +1 -1
- package/dist/oxlint/internal/Imports.d.ts.map +1 -1
- package/dist/oxlint/internal/Imports.js +2 -0
- package/dist/oxlint/internal/Imports.js.map +1 -1
- package/dist/oxlint/internal/Styles.d.ts +40 -0
- package/dist/oxlint/internal/Styles.d.ts.map +1 -0
- package/dist/oxlint/internal/Styles.js +194 -0
- package/dist/oxlint/internal/Styles.js.map +1 -0
- package/dist/oxlint/internal/Tokens.d.ts +16 -0
- package/dist/oxlint/internal/Tokens.d.ts.map +1 -0
- package/dist/oxlint/internal/Tokens.js +39 -0
- package/dist/oxlint/internal/Tokens.js.map +1 -0
- package/dist/oxlint/rules/NoCrossSurface.d.ts +2 -2
- package/dist/oxlint/rules/NoCrossSurface.js +3 -3
- package/dist/oxlint/rules/NoCrossSurface.js.map +1 -1
- package/dist/oxlint/rules/NoCustomValues.d.ts +11 -0
- package/dist/oxlint/rules/NoCustomValues.d.ts.map +1 -0
- package/dist/oxlint/rules/NoCustomValues.js +150 -0
- package/dist/oxlint/rules/NoCustomValues.js.map +1 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts +11 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.d.ts.map +1 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.js +158 -0
- package/dist/oxlint/rules/NoSingleKeyShortcuts.js.map +1 -0
- package/dist/oxlint/rules/PreferComponent.d.ts +3 -2
- package/dist/oxlint/rules/PreferComponent.d.ts.map +1 -1
- package/dist/oxlint/rules/PreferComponent.js +11 -4
- package/dist/oxlint/rules/PreferComponent.js.map +1 -1
- package/dist/oxlint/rules/PreferLayout.d.ts +9 -0
- package/dist/oxlint/rules/PreferLayout.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferLayout.js +90 -0
- package/dist/oxlint/rules/PreferLayout.js.map +1 -0
- package/dist/oxlint/rules/PreferProgress.d.ts +9 -0
- package/dist/oxlint/rules/PreferProgress.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferProgress.js +43 -0
- package/dist/oxlint/rules/PreferProgress.js.map +1 -0
- package/dist/oxlint/rules/PreferSurface.d.ts +9 -0
- package/dist/oxlint/rules/PreferSurface.d.ts.map +1 -0
- package/dist/oxlint/rules/PreferSurface.js +55 -0
- package/dist/oxlint/rules/PreferSurface.js.map +1 -0
- package/dist/oxlint/rules/RequireAccessibleName.d.ts.map +1 -1
- package/dist/oxlint/rules/RequireAccessibleName.js +3 -1
- package/dist/oxlint/rules/RequireAccessibleName.js.map +1 -1
- package/dist/oxlint/rules/RequireScrollRegion.d.ts +11 -0
- package/dist/oxlint/rules/RequireScrollRegion.d.ts.map +1 -0
- package/dist/oxlint/rules/RequireScrollRegion.js +71 -0
- package/dist/oxlint/rules/RequireScrollRegion.js.map +1 -0
- package/dist/web/icons/index.js.zyzz.json +1 -0
- package/package.json +6 -1
- package/src/marketing/README.md +21 -0
- package/src/marketing/index.ts +3 -0
- package/src/marketing/vars.ts +751 -0
- package/src/marketing/zyzz.config.ts +376 -0
- package/src/oxlint/index.ts +40 -1
- package/src/oxlint/internal/Docs.ts +18 -0
- package/src/oxlint/internal/Imports.ts +2 -1
- package/src/oxlint/internal/Styles.ts +240 -0
- package/src/oxlint/internal/Tokens.ts +43 -0
- package/src/oxlint/rules/NoCrossSurface.ts +3 -3
- package/src/oxlint/rules/NoCustomValues.ts +170 -0
- package/src/oxlint/rules/NoSingleKeyShortcuts.ts +190 -0
- package/src/oxlint/rules/PreferComponent.ts +11 -4
- package/src/oxlint/rules/PreferLayout.ts +96 -0
- package/src/oxlint/rules/PreferProgress.ts +49 -0
- package/src/oxlint/rules/PreferSurface.ts +70 -0
- package/src/oxlint/rules/RequireAccessibleName.ts +3 -1
- package/src/oxlint/rules/RequireScrollRegion.ts +92 -0
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
# Tempo Console
|
|
2
|
+
|
|
3
|
+
Tempo Console is the web app for Tempo API projects, keys, usage, and billing.
|
|
4
|
+
|
|
5
|
+
> [!NOTE]
|
|
6
|
+
> The reference layouts on this page are compositions of library components and `style`, not shipped components. `@tempoxyz/ds` exports no `ApiKeysPage` or `CreateKeyForm`. Copy a layout into your app and adapt it.
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Tempo Console runs at [console.tempo.xyz](https://console.tempo.xyz) and is invite-only. You log in with an email address, a Google account, or a passkey, then choose an organization.
|
|
11
|
+
|
|
12
|
+
* **Organization:** Holds the team, projects, billing, and aggregate usage.
|
|
13
|
+
* **Project:** Holds one app or integration and its API keys.
|
|
14
|
+
* **Environment:** Separates production and sandbox keys, usage, and billing.
|
|
15
|
+
* **API Key:** A project-scoped credential for calling Tempo APIs.
|
|
16
|
+
|
|
17
|
+
Roles:
|
|
18
|
+
|
|
19
|
+
* **Developers:** Create keys, test in sandbox, and see project usage.
|
|
20
|
+
* **Owners:** Manage billing, change roles, and remove members.
|
|
21
|
+
* **Admins:** Invite members and review pending invitations.
|
|
22
|
+
|
|
23
|
+
## Surfaces
|
|
24
|
+
|
|
25
|
+
The console is a web app built with `@tempoxyz/ds`. No mobile app is documented.
|
|
26
|
+
|
|
27
|
+
The Tempo API exposes the same organization resources through management endpoints. Keys with the `management:read` and `management:write` scopes can call them.
|
|
28
|
+
|
|
29
|
+
## Key Screens
|
|
30
|
+
|
|
31
|
+
### API Keys
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { AlertDialog, AppShell, Banner, Button, ButtonGroup, Empty, Identicon, Menu, Select, SidebarMenu, style, Surface, Table } from '@tempoxyz/ds'
|
|
35
|
+
import {
|
|
36
|
+
Analytics,
|
|
37
|
+
CreditCard,
|
|
38
|
+
Folder,
|
|
39
|
+
Key,
|
|
40
|
+
LogOut,
|
|
41
|
+
MoreHorizontal,
|
|
42
|
+
Plus,
|
|
43
|
+
Sort,
|
|
44
|
+
Users,
|
|
45
|
+
} from '@tempoxyz/ds/icons'
|
|
46
|
+
import { useState } from 'react'
|
|
47
|
+
|
|
48
|
+
export const fill = true
|
|
49
|
+
type Environment = 'production' | 'sandbox'
|
|
50
|
+
|
|
51
|
+
const prefixes = { production: 'tempo:sk:', sandbox: 'tempo_sandbox:sk:' }
|
|
52
|
+
|
|
53
|
+
const projects = {
|
|
54
|
+
all: 'All projects',
|
|
55
|
+
'checkout-service': 'checkout-service',
|
|
56
|
+
'payouts-worker': 'payouts-worker',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const keys = [
|
|
60
|
+
{
|
|
61
|
+
created: 'Oct 14, 2026',
|
|
62
|
+
environment: 'production',
|
|
63
|
+
id: 'key_01',
|
|
64
|
+
last4: 'a91c',
|
|
65
|
+
lastUsed: '2 minutes ago',
|
|
66
|
+
name: 'checkout-service production',
|
|
67
|
+
project: 'checkout-service',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
created: 'Sep 30, 2026',
|
|
71
|
+
environment: 'production',
|
|
72
|
+
id: 'key_02',
|
|
73
|
+
last4: '07e1',
|
|
74
|
+
lastUsed: 'Yesterday',
|
|
75
|
+
name: 'payouts-worker production',
|
|
76
|
+
project: 'payouts-worker',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
created: 'Oct 18, 2026',
|
|
80
|
+
environment: 'sandbox',
|
|
81
|
+
id: 'key_03',
|
|
82
|
+
last4: '5a22',
|
|
83
|
+
lastUsed: '1 hour ago',
|
|
84
|
+
name: 'checkout-service CI',
|
|
85
|
+
project: 'checkout-service',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
created: 'Oct 19, 2026',
|
|
89
|
+
environment: 'sandbox',
|
|
90
|
+
id: 'key_04',
|
|
91
|
+
last4: 'c0d9',
|
|
92
|
+
lastUsed: 'Never',
|
|
93
|
+
name: 'jake local development',
|
|
94
|
+
project: 'payouts-worker',
|
|
95
|
+
},
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
export function Demo() {
|
|
99
|
+
const [environment, setEnvironment] = useState<Environment>('sandbox')
|
|
100
|
+
const [project, setProject] = useState<string | null>('all')
|
|
101
|
+
const [revoked, setRevoked] = useState<readonly string[]>([])
|
|
102
|
+
const [target, setTarget] = useState<(typeof keys)[number]>()
|
|
103
|
+
|
|
104
|
+
const visible = keys.filter(
|
|
105
|
+
(key) =>
|
|
106
|
+
key.environment === environment &&
|
|
107
|
+
!revoked.includes(key.id) &&
|
|
108
|
+
(project === 'all' || key.project === project),
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div {...styles.frame()}>
|
|
113
|
+
<AppShell.Root style={{ height: '100%', minHeight: 0, overflowY: 'auto' }}>
|
|
114
|
+
<AppShell.Sidebar aria-label="Console" style={{ height: '100%' }}>
|
|
115
|
+
<AppShell.SidebarHeader>
|
|
116
|
+
<Menu.Root>
|
|
117
|
+
<Menu.Trigger
|
|
118
|
+
aria-label="Organization and environment"
|
|
119
|
+
render={<SidebarMenu.Item render={<button type="button" />} />}
|
|
120
|
+
>
|
|
121
|
+
<Identicon
|
|
122
|
+
address="0x71C7656EC7ab88b098defB751B7401B5f6d8976F"
|
|
123
|
+
aria-hidden
|
|
124
|
+
size="32"
|
|
125
|
+
/>
|
|
126
|
+
<span {...styles.stack()}>
|
|
127
|
+
<span {...styles.name()}>Acme Payments</span>
|
|
128
|
+
<span {...styles.caption()}>
|
|
129
|
+
{environment === 'sandbox' ? 'Sandbox' : 'Production'}
|
|
130
|
+
</span>
|
|
131
|
+
</span>
|
|
132
|
+
<Sort />
|
|
133
|
+
</Menu.Trigger>
|
|
134
|
+
<Menu.Popup align="start">
|
|
135
|
+
<Menu.Group>
|
|
136
|
+
<Menu.GroupLabel>Environment</Menu.GroupLabel>
|
|
137
|
+
<Menu.RadioGroup
|
|
138
|
+
onValueChange={(value) => setEnvironment(value as Environment)}
|
|
139
|
+
value={environment}
|
|
140
|
+
>
|
|
141
|
+
<Menu.RadioItem value="production">Production</Menu.RadioItem>
|
|
142
|
+
<Menu.RadioItem value="sandbox">Sandbox</Menu.RadioItem>
|
|
143
|
+
</Menu.RadioGroup>
|
|
144
|
+
</Menu.Group>
|
|
145
|
+
</Menu.Popup>
|
|
146
|
+
</Menu.Root>
|
|
147
|
+
</AppShell.SidebarHeader>
|
|
148
|
+
<AppShell.SidebarBody>
|
|
149
|
+
<SidebarMenu.Root>
|
|
150
|
+
<SidebarMenu.Group aria-label="Organization">
|
|
151
|
+
<SidebarMenu.Item href="#projects">
|
|
152
|
+
<Folder />
|
|
153
|
+
Projects
|
|
154
|
+
</SidebarMenu.Item>
|
|
155
|
+
<SidebarMenu.Item active href="#api-keys">
|
|
156
|
+
<Key />
|
|
157
|
+
API Keys
|
|
158
|
+
</SidebarMenu.Item>
|
|
159
|
+
<SidebarMenu.Item href="#usage">
|
|
160
|
+
<Analytics />
|
|
161
|
+
Usage
|
|
162
|
+
</SidebarMenu.Item>
|
|
163
|
+
<SidebarMenu.Item href="#billing">
|
|
164
|
+
<CreditCard />
|
|
165
|
+
Billing
|
|
166
|
+
</SidebarMenu.Item>
|
|
167
|
+
<SidebarMenu.Item href="#team">
|
|
168
|
+
<Users />
|
|
169
|
+
Team
|
|
170
|
+
</SidebarMenu.Item>
|
|
171
|
+
</SidebarMenu.Group>
|
|
172
|
+
</SidebarMenu.Root>
|
|
173
|
+
</AppShell.SidebarBody>
|
|
174
|
+
<AppShell.SidebarFooter>
|
|
175
|
+
<Menu.Root>
|
|
176
|
+
<Menu.Trigger
|
|
177
|
+
aria-label="Account menu"
|
|
178
|
+
render={<SidebarMenu.Item render={<button type="button" />} />}
|
|
179
|
+
>
|
|
180
|
+
<Identicon
|
|
181
|
+
address="0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
|
182
|
+
aria-hidden
|
|
183
|
+
size="24"
|
|
184
|
+
/>
|
|
185
|
+
<span {...styles.grow()}>jake@acme.dev</span>
|
|
186
|
+
</Menu.Trigger>
|
|
187
|
+
<Menu.Popup side="top">
|
|
188
|
+
<Menu.Item>
|
|
189
|
+
<LogOut />
|
|
190
|
+
Log out
|
|
191
|
+
</Menu.Item>
|
|
192
|
+
</Menu.Popup>
|
|
193
|
+
</Menu.Root>
|
|
194
|
+
</AppShell.SidebarFooter>
|
|
195
|
+
</AppShell.Sidebar>
|
|
196
|
+
|
|
197
|
+
<AppShell.Main>
|
|
198
|
+
<AppShell.Header>
|
|
199
|
+
<AppShell.Title>API Keys</AppShell.Title>
|
|
200
|
+
<div {...styles.actions()}>
|
|
201
|
+
<Select.Root items={projects} onValueChange={setProject} value={project}>
|
|
202
|
+
<Select.Trigger aria-label="Project">
|
|
203
|
+
<Select.Value />
|
|
204
|
+
<Select.Icon />
|
|
205
|
+
</Select.Trigger>
|
|
206
|
+
<Select.Popup size="small">
|
|
207
|
+
{Object.entries(projects).map(([value, label]) => (
|
|
208
|
+
<Select.Item key={value} label={label} value={value}>
|
|
209
|
+
<Select.ItemText>{label}</Select.ItemText>
|
|
210
|
+
</Select.Item>
|
|
211
|
+
))}
|
|
212
|
+
</Select.Popup>
|
|
213
|
+
</Select.Root>
|
|
214
|
+
<Button size="small">
|
|
215
|
+
<Plus />
|
|
216
|
+
New key
|
|
217
|
+
</Button>
|
|
218
|
+
<AppShell.Trigger />
|
|
219
|
+
</div>
|
|
220
|
+
</AppShell.Header>
|
|
221
|
+
|
|
222
|
+
{environment === 'sandbox' && (
|
|
223
|
+
<Banner intent="warning" {...styles.banner()}>
|
|
224
|
+
<Banner.Content>
|
|
225
|
+
Sandbox. Keys here reach only non-mainnet chains, such as testnet.
|
|
226
|
+
</Banner.Content>
|
|
227
|
+
</Banner>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<Surface>
|
|
231
|
+
{visible.length === 0 ? (
|
|
232
|
+
<Empty.Root>
|
|
233
|
+
<Empty.Icon>
|
|
234
|
+
<Key />
|
|
235
|
+
</Empty.Icon>
|
|
236
|
+
<Empty.Title>No API keys</Empty.Title>
|
|
237
|
+
<Empty.Description>
|
|
238
|
+
Create a key to call Tempo APIs from this project.
|
|
239
|
+
</Empty.Description>
|
|
240
|
+
<Empty.Actions>
|
|
241
|
+
<Button size="small">New key</Button>
|
|
242
|
+
</Empty.Actions>
|
|
243
|
+
</Empty.Root>
|
|
244
|
+
) : (
|
|
245
|
+
<div {...styles.scroll()}>
|
|
246
|
+
<Table.Root>
|
|
247
|
+
<Table.Caption>
|
|
248
|
+
{environment === 'sandbox' ? 'Sandbox' : 'Production'} keys,{' '}
|
|
249
|
+
{project === 'all' ? 'all projects' : project}
|
|
250
|
+
</Table.Caption>
|
|
251
|
+
<Table.Head>
|
|
252
|
+
<Table.Row>
|
|
253
|
+
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
254
|
+
<Table.HeaderCell>Project</Table.HeaderCell>
|
|
255
|
+
<Table.HeaderCell>Key</Table.HeaderCell>
|
|
256
|
+
<Table.HeaderCell>Created</Table.HeaderCell>
|
|
257
|
+
<Table.HeaderCell>Last used</Table.HeaderCell>
|
|
258
|
+
<Table.HeaderCell justify="end">
|
|
259
|
+
<span {...styles.hidden()}>Actions</span>
|
|
260
|
+
</Table.HeaderCell>
|
|
261
|
+
</Table.Row>
|
|
262
|
+
</Table.Head>
|
|
263
|
+
<Table.Body>
|
|
264
|
+
{visible.map((key) => (
|
|
265
|
+
<Table.Row key={key.id}>
|
|
266
|
+
<Table.Cell>{key.name}</Table.Cell>
|
|
267
|
+
<Table.Cell>{key.project}</Table.Cell>
|
|
268
|
+
<Table.Cell mono>
|
|
269
|
+
{prefixes[environment]}…{key.last4}
|
|
270
|
+
</Table.Cell>
|
|
271
|
+
<Table.Cell>{key.created}</Table.Cell>
|
|
272
|
+
<Table.Cell>{key.lastUsed}</Table.Cell>
|
|
273
|
+
<Table.Cell justify="end">
|
|
274
|
+
<Menu.Root>
|
|
275
|
+
<Menu.Trigger
|
|
276
|
+
aria-label={`Actions for ${key.name}`}
|
|
277
|
+
render={<Button shape="icon" size="small" variant="tertiary" />}
|
|
278
|
+
>
|
|
279
|
+
<MoreHorizontal />
|
|
280
|
+
</Menu.Trigger>
|
|
281
|
+
<Menu.Popup align="end">
|
|
282
|
+
<Menu.Item intent="negative" onClick={() => setTarget(key)}>
|
|
283
|
+
Revoke key
|
|
284
|
+
</Menu.Item>
|
|
285
|
+
</Menu.Popup>
|
|
286
|
+
</Menu.Root>
|
|
287
|
+
</Table.Cell>
|
|
288
|
+
</Table.Row>
|
|
289
|
+
))}
|
|
290
|
+
</Table.Body>
|
|
291
|
+
</Table.Root>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
</Surface>
|
|
295
|
+
</AppShell.Main>
|
|
296
|
+
</AppShell.Root>
|
|
297
|
+
|
|
298
|
+
<AlertDialog.Root
|
|
299
|
+
onOpenChange={(open) => {
|
|
300
|
+
if (!open) setTarget(undefined)
|
|
301
|
+
}}
|
|
302
|
+
open={target !== undefined}
|
|
303
|
+
>
|
|
304
|
+
<AlertDialog.Popup>
|
|
305
|
+
<AlertDialog.Header>
|
|
306
|
+
<AlertDialog.Title>Revoke {target?.name}?</AlertDialog.Title>
|
|
307
|
+
<AlertDialog.Description>
|
|
308
|
+
Requests that use this key stop authenticating. You can't undo this.
|
|
309
|
+
</AlertDialog.Description>
|
|
310
|
+
</AlertDialog.Header>
|
|
311
|
+
<ButtonGroup>
|
|
312
|
+
<AlertDialog.Close render={<Button size="large" variant="secondary" />}>
|
|
313
|
+
Keep key
|
|
314
|
+
</AlertDialog.Close>
|
|
315
|
+
<AlertDialog.Close
|
|
316
|
+
onClick={() => {
|
|
317
|
+
if (target) setRevoked([...revoked, target.id])
|
|
318
|
+
}}
|
|
319
|
+
render={<Button size="large" />}
|
|
320
|
+
>
|
|
321
|
+
Revoke key
|
|
322
|
+
</AlertDialog.Close>
|
|
323
|
+
</ButtonGroup>
|
|
324
|
+
</AlertDialog.Popup>
|
|
325
|
+
</AlertDialog.Root>
|
|
326
|
+
</div>
|
|
327
|
+
)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
namespace styles {
|
|
331
|
+
export const actions = style({ alignItems: 'center', columnGap: '8', display: 'flex' })
|
|
332
|
+
|
|
333
|
+
export const banner = style({ marginBlockEnd: '16' })
|
|
334
|
+
|
|
335
|
+
export const caption = style({ color: 'content.secondary', margin: '0', typography: 'body.b3' })
|
|
336
|
+
|
|
337
|
+
// The transform makes the frame the containing block for the mobile drawer.
|
|
338
|
+
export const frame = style({
|
|
339
|
+
borderRadius: 'sm',
|
|
340
|
+
height: '36rem !custom',
|
|
341
|
+
overflow: 'hidden',
|
|
342
|
+
transform: 'translateZ(0) !custom',
|
|
343
|
+
selectors: {
|
|
344
|
+
// The maximized gallery preview shows the shell at full window height.
|
|
345
|
+
':root[data-maximized] &': { borderRadius: '0px !custom', height: '100vh !custom' },
|
|
346
|
+
},
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
export const grow = style({ flex: '1 !custom', minWidth: '0 !custom' })
|
|
350
|
+
|
|
351
|
+
// Visually hidden text still names the column for screen readers.
|
|
352
|
+
export const hidden = style({
|
|
353
|
+
clipPath: 'inset(50%) !custom',
|
|
354
|
+
height: '1px !custom',
|
|
355
|
+
overflow: 'hidden',
|
|
356
|
+
position: 'absolute',
|
|
357
|
+
whiteSpace: 'nowrap',
|
|
358
|
+
width: '1px !custom',
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
export const name = style({ margin: '0', typography: 'body.b2' })
|
|
362
|
+
|
|
363
|
+
// Cells keep one line and the table scrolls sideways, so key prefixes never break mid-token.
|
|
364
|
+
export const scroll = style({
|
|
365
|
+
overflowX: 'auto',
|
|
366
|
+
selectors: { '& :is(td, th)': { whiteSpace: 'nowrap' } },
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
export const stack = style({ display: 'flex', flex: '1 !custom', flexDirection: 'column' })
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Products/ConsoleKeys.tsx)
|
|
374
|
+
|
|
375
|
+
API Keys lists the keys of the selected environment, for the whole organization by default. To show the sandbox banner and keys, switch to sandbox in the organization menu.
|
|
376
|
+
|
|
377
|
+
* **Navigation (SidebarMenu):** The app shell holds Projects, API Keys, Usage, Billing, and Team.
|
|
378
|
+
* **Environment (Menu.RadioGroup):** The organization menu switches the environment. In sandbox, a warning banner stays below the header.
|
|
379
|
+
* **Project Filter (Select):** Filters keys by project, beside the New key button in the header.
|
|
380
|
+
* **Key Table (mono):** Lists the name, project, prefix and last four characters, creation date, and last use.
|
|
381
|
+
* **Revoke From the Row (negative):** The row menu holds Revoke key, which opens an alert dialog.
|
|
382
|
+
* **No Matching Keys (Empty):** Replaces the table with an empty state when no key matches the filters.
|
|
383
|
+
|
|
384
|
+
Patterns:
|
|
385
|
+
|
|
386
|
+
* [Tables and Lists](../patterns/tables-and-lists.md) for the key table, its filter, and row actions.
|
|
387
|
+
* [Destructive Actions](../patterns/destructive-actions.md) for the revoke confirmation.
|
|
388
|
+
* [Empty States](../patterns/empty-states.md) for a project or environment with no keys.
|
|
389
|
+
|
|
390
|
+
### Create Key
|
|
391
|
+
|
|
392
|
+
```tsx
|
|
393
|
+
import { Accordion, Alert, Badge, Button, Checkbox, CheckboxGroup, Field, ListGroup, SecretValue, Select, style, Surface, TextInput } from '@tempoxyz/ds'
|
|
394
|
+
import { useState } from 'react'
|
|
395
|
+
|
|
396
|
+
export const fill = true
|
|
397
|
+
const projects = { 'checkout-service': 'checkout-service', 'payouts-worker': 'payouts-worker' }
|
|
398
|
+
|
|
399
|
+
// The self-serve scopes and descriptions from the Tempo API scope catalog (`GET /v1/scopes`).
|
|
400
|
+
const scopes = [
|
|
401
|
+
['data:read', 'Read public chain data: tokens, transactions, transfers, blocks, and receipts.'],
|
|
402
|
+
['indexer:query', 'Run ad-hoc queries against the raw indexer passthrough.'],
|
|
403
|
+
['management:read', 'Read organization resources across projects and environments.'],
|
|
404
|
+
[
|
|
405
|
+
'management:write',
|
|
406
|
+
'Create, update, and delete organization resources across projects and environments.',
|
|
407
|
+
],
|
|
408
|
+
['mpp:write', 'Validate and broadcast MPP credentials through Tempo.'],
|
|
409
|
+
['routes:read', 'Read organization route transfers, deposit addresses, and deposits.'],
|
|
410
|
+
['rpc-relay:read', 'Fill and submit transactions through the relay.'],
|
|
411
|
+
['rpc-relay:sponsor', 'Sponsor transaction fees through the relay as the managed fee payer.'],
|
|
412
|
+
] as const
|
|
413
|
+
|
|
414
|
+
export function Demo() {
|
|
415
|
+
const [created, setCreated] = useState(false)
|
|
416
|
+
const [keyName, setKeyName] = useState('')
|
|
417
|
+
const [project, setProject] = useState<string | null>('checkout-service')
|
|
418
|
+
const [selected, setSelected] = useState<string[]>(['data:read', 'rpc-relay:read'])
|
|
419
|
+
const [submitted, setSubmitted] = useState(false)
|
|
420
|
+
|
|
421
|
+
const missing = submitted && keyName.trim() === ''
|
|
422
|
+
|
|
423
|
+
const reset = () => {
|
|
424
|
+
setCreated(false)
|
|
425
|
+
setKeyName('')
|
|
426
|
+
setSubmitted(false)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return (
|
|
430
|
+
<div {...styles.page()}>
|
|
431
|
+
<Surface {...styles.panel()}>
|
|
432
|
+
<Surface.Header>
|
|
433
|
+
<Surface.Title>{created ? 'Key created' : 'New key'}</Surface.Title>
|
|
434
|
+
<Badge size="small">Sandbox</Badge>
|
|
435
|
+
</Surface.Header>
|
|
436
|
+
|
|
437
|
+
{created ? (
|
|
438
|
+
<div {...styles.form()}>
|
|
439
|
+
<Alert intent="warning">
|
|
440
|
+
<Alert.Icon />
|
|
441
|
+
<Alert.Content>
|
|
442
|
+
Copy this key now and store it in a secret manager. You can't view it again.
|
|
443
|
+
</Alert.Content>
|
|
444
|
+
</Alert>
|
|
445
|
+
<SecretValue label="API key" value="tempo_sandbox:sk:demo_8QnV2kXbT4mL9wRz7cYp3fHs" />
|
|
446
|
+
<ListGroup.Root>
|
|
447
|
+
<ListGroup.Row>
|
|
448
|
+
<ListGroup.Label>Name</ListGroup.Label>
|
|
449
|
+
{keyName}
|
|
450
|
+
</ListGroup.Row>
|
|
451
|
+
<ListGroup.Row>
|
|
452
|
+
<ListGroup.Label>Project</ListGroup.Label>
|
|
453
|
+
{project}
|
|
454
|
+
</ListGroup.Row>
|
|
455
|
+
<ListGroup.Row>
|
|
456
|
+
<ListGroup.Label>Scopes</ListGroup.Label>
|
|
457
|
+
<span {...styles.scope()}>{selected.join(', ') || 'None'}</span>
|
|
458
|
+
</ListGroup.Row>
|
|
459
|
+
</ListGroup.Root>
|
|
460
|
+
<div {...styles.actions()}>
|
|
461
|
+
<Button onClick={reset}>Done</Button>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
) : (
|
|
465
|
+
<form
|
|
466
|
+
noValidate
|
|
467
|
+
onSubmit={(event) => {
|
|
468
|
+
event.preventDefault()
|
|
469
|
+
setSubmitted(true)
|
|
470
|
+
if (keyName.trim() !== '') setCreated(true)
|
|
471
|
+
}}
|
|
472
|
+
{...styles.form()}
|
|
473
|
+
>
|
|
474
|
+
<Field.Root>
|
|
475
|
+
<Field.Label>Project</Field.Label>
|
|
476
|
+
<Select.Root items={projects} onValueChange={setProject} value={project}>
|
|
477
|
+
<Select.Trigger>
|
|
478
|
+
<Select.Value placeholder="Select a project" />
|
|
479
|
+
<Select.Icon />
|
|
480
|
+
</Select.Trigger>
|
|
481
|
+
<Select.Popup>
|
|
482
|
+
{Object.entries(projects).map(([value, label]) => (
|
|
483
|
+
<Select.Item key={value} label={label} value={value}>
|
|
484
|
+
<Select.ItemText>{label}</Select.ItemText>
|
|
485
|
+
</Select.Item>
|
|
486
|
+
))}
|
|
487
|
+
</Select.Popup>
|
|
488
|
+
</Select.Root>
|
|
489
|
+
</Field.Root>
|
|
490
|
+
|
|
491
|
+
<Field.Root invalid={missing}>
|
|
492
|
+
<Field.Label>Name</Field.Label>
|
|
493
|
+
<TextInput.Group>
|
|
494
|
+
<TextInput.Control
|
|
495
|
+
autoComplete="off"
|
|
496
|
+
onChange={(event) => setKeyName(event.target.value)}
|
|
497
|
+
placeholder="checkout-service CI"
|
|
498
|
+
value={keyName}
|
|
499
|
+
/>
|
|
500
|
+
</TextInput.Group>
|
|
501
|
+
<Field.Description>
|
|
502
|
+
Name the deployment or owner, so you can find the key when you rotate it.
|
|
503
|
+
</Field.Description>
|
|
504
|
+
<Field.Error match={missing}>Enter a name for the key.</Field.Error>
|
|
505
|
+
</Field.Root>
|
|
506
|
+
|
|
507
|
+
<Accordion.Root>
|
|
508
|
+
<Accordion.Item value="scopes">
|
|
509
|
+
<Accordion.Trigger>
|
|
510
|
+
View scopes ({selected.length} of {scopes.length} selected)
|
|
511
|
+
</Accordion.Trigger>
|
|
512
|
+
<Accordion.Panel>
|
|
513
|
+
<CheckboxGroup aria-label="Scopes" onValueChange={setSelected} value={selected}>
|
|
514
|
+
{scopes.map(([scope, description]) => (
|
|
515
|
+
<Field.Root key={scope}>
|
|
516
|
+
<Field.Item>
|
|
517
|
+
<Checkbox value={scope} />
|
|
518
|
+
<Field.Label>
|
|
519
|
+
<span {...styles.scope()}>{scope}</span>
|
|
520
|
+
</Field.Label>
|
|
521
|
+
</Field.Item>
|
|
522
|
+
<Field.Description {...styles.hint()}>{description}</Field.Description>
|
|
523
|
+
</Field.Root>
|
|
524
|
+
))}
|
|
525
|
+
</CheckboxGroup>
|
|
526
|
+
</Accordion.Panel>
|
|
527
|
+
</Accordion.Item>
|
|
528
|
+
</Accordion.Root>
|
|
529
|
+
|
|
530
|
+
{selected.length === 0 && (
|
|
531
|
+
<Alert intent="warning">
|
|
532
|
+
<Alert.Icon />
|
|
533
|
+
<Alert.Content>A key with no scopes can't access scoped APIs.</Alert.Content>
|
|
534
|
+
</Alert>
|
|
535
|
+
)}
|
|
536
|
+
|
|
537
|
+
<div {...styles.actions()}>
|
|
538
|
+
<Button type="button" variant="secondary">
|
|
539
|
+
Cancel
|
|
540
|
+
</Button>
|
|
541
|
+
<Button type="submit">Create key</Button>
|
|
542
|
+
</div>
|
|
543
|
+
</form>
|
|
544
|
+
)}
|
|
545
|
+
</Surface>
|
|
546
|
+
</div>
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
namespace styles {
|
|
551
|
+
export const actions = style({
|
|
552
|
+
columnGap: '8',
|
|
553
|
+
display: 'flex',
|
|
554
|
+
justifyContent: 'flex-end',
|
|
555
|
+
'@media (max-width: 40rem)': {
|
|
556
|
+
flexDirection: 'column-reverse',
|
|
557
|
+
rowGap: '8',
|
|
558
|
+
},
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
export const form = style({ display: 'flex', flexDirection: 'column', rowGap: '24' })
|
|
562
|
+
|
|
563
|
+
// Aligns each scope's description with its label, past the 20px checkbox and the 16px gap.
|
|
564
|
+
export const hint = style({ paddingInlineStart: '36' })
|
|
565
|
+
|
|
566
|
+
export const page = style({
|
|
567
|
+
backgroundColor: 'background.primary',
|
|
568
|
+
display: 'flex',
|
|
569
|
+
justifyContent: 'center',
|
|
570
|
+
padding: '16',
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
export const panel = style({ maxWidth: '32rem !custom', width: '100% !custom' })
|
|
574
|
+
|
|
575
|
+
export const scope = style({ typography: 'mono.m1' })
|
|
576
|
+
}
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
[View example source](https://github.com/tempoxyz/ds/blob/main/docs/src/examples/demos/Products/ConsoleCreateKey.tsx)
|
|
580
|
+
|
|
581
|
+
New key asks for a project, a name, and scopes. The console shows the token only once, after creation, so the layout switches to a reveal state with the key masked.
|
|
582
|
+
|
|
583
|
+
* **Project and Name (Field):** A select picks the project. An input names the key and shows an error when it is empty.
|
|
584
|
+
* **Scopes (CheckboxGroup):** View scopes opens each scope, its identifier, and its description from the Tempo API catalog.
|
|
585
|
+
* **Warnings (warning):** With no scopes, an alert warns that the key can't reach scoped APIs. A badge names the environment.
|
|
586
|
+
* **Reveal Once (SecretValue):** After Create key, holds the masked token. Copy works without a reveal.
|
|
587
|
+
|
|
588
|
+
Patterns:
|
|
589
|
+
|
|
590
|
+
* [Forms](../patterns/forms.md) for labels, descriptions, and the submit action.
|
|
591
|
+
* [Errors and Validation](../patterns/errors-and-validation.md) for the missing name and the empty scope set.
|
|
592
|
+
|
|
593
|
+
### Webhooks
|
|
594
|
+
|
|
595
|
+
Webhook subscriptions deliver signed onchain events to an HTTPS URL, a Slack incoming webhook, or a Better Stack source. The Tempo docs describe only the API (`/v1/webhooks`), so this page has no webhook layout yet.
|
|
596
|
+
|
|
597
|
+
To build one, reuse these parts:
|
|
598
|
+
|
|
599
|
+
* **Subscriptions (Table):** Lists subscriptions with their event type, destination, and status.
|
|
600
|
+
* **Signing Secret (SecretValue):** Holds the `whsec_` signing secret, shown once at creation.
|
|
601
|
+
* **Deliveries (numeric):** Lists deliveries with attempt, response status, and response time.
|
|
602
|
+
|
|
603
|
+
## Content
|
|
604
|
+
|
|
605
|
+
### Show the Key's Identity, Never the Secret
|
|
606
|
+
|
|
607
|
+
After creation, identify a key by its prefix and last four characters, such as `tempo:sk:…a91c`. Show the full token once, masked in [SecretValue](../components/secret-value.md), and tell people to copy it into a secret manager now.
|
|
608
|
+
|
|
609
|
+
**Do:** Say what happens when the reveal state closes: You can't view this key again.
|
|
610
|
+
|
|
611
|
+
**Don't:** Offer to show an existing key again. The console can't reveal it, so a lost key needs a
|
|
612
|
+
replacement.
|
|
613
|
+
|
|
614
|
+
### Name the Environment Wherever It Changes Meaning
|
|
615
|
+
|
|
616
|
+
Sandbox and production keys differ in prefix and in the chains they reach. Show the environment on every screen that creates, lists, or bills for keys.
|
|
617
|
+
|
|
618
|
+
* **Production (tempo:sk:):** Reach any chain, including mainnet.
|
|
619
|
+
* **Sandbox (tempo_sandbox:sk:):** Reach non-mainnet chains only.
|
|
620
|
+
|
|
621
|
+
### Name Keys After a Deployment or Owner
|
|
622
|
+
|
|
623
|
+
Suggest names such as checkout-service production or checkout-service CI in the placeholder and description. A descriptive name identifies the integration that a revoke stops.
|
|
624
|
+
|
|
625
|
+
### Write Scopes as Identifiers
|
|
626
|
+
|
|
627
|
+
Show each scope as its identifier in monospace, such as `data:read` or `rpc-relay:sponsor`, with its catalog description. Scopes can't change on an existing key, so state this where people pick them.
|
|
628
|
+
|
|
629
|
+
### State Side Effects, Including Missing Ones
|
|
630
|
+
|
|
631
|
+
A revoke stops the requests that use the key. The removal of a team member does not revoke the member's keys. State both in the confirmation, so an owner checks API Keys next.
|
|
632
|
+
|
|
633
|
+
### Quote Error Codes Exactly
|
|
634
|
+
|
|
635
|
+
If a request fails, show the code in monospace and the fix beside it. For example, a sandbox key that targets mainnet returns `403 api_key_forbidden`. The fix is `chainId=testnet` or a production key.
|
|
636
|
+
|
|
637
|
+
## Open Gaps
|
|
638
|
+
|
|
639
|
+
The library has no part for these needs yet. Before you add one, propose it as an issue in the Design System project.
|
|
640
|
+
|
|
641
|
+
* **Charts:** Usage shows request volume, sponsorship, and traffic by route and key. No chart shows volume over time.
|
|
642
|
+
* **Table Tools:** Tables have no sorting, column filters, or selection. Build filters from selects.
|
|
643
|
+
* **Multi-Line Code:** Snippets hold one line. A test request with headers needs a multi-line code block.
|
|
644
|
+
* **Relative Time:** Nothing formats `lastUsedAt` as 2 minutes ago. Use `Intl.RelativeTimeFormat` in your app.
|
|
645
|
+
* **Brand Marks:** The icon set has no Google mark for the Continue with Google button.
|
|
646
|
+
|
|
647
|
+
## Related
|
|
648
|
+
|
|
649
|
+
[Tables and Lists](../patterns/tables-and-lists.md)
|
|
650
|
+
|
|
651
|
+
Covers key and member tables.
|
|
652
|
+
|
|
653
|
+
[Destructive Actions](../patterns/destructive-actions.md)
|
|
654
|
+
|
|
655
|
+
Covers revoking keys and removing members.
|
|
656
|
+
|
|
657
|
+
[AppShell](../components/app-shell.md)
|
|
658
|
+
|
|
659
|
+
Use for the console frame.
|
|
660
|
+
|
|
661
|
+
[SecretValue](../components/secret-value.md)
|
|
662
|
+
|
|
663
|
+
Use for one-time key reveal.
|
|
664
|
+
|
|
665
|
+
[Table](../components/table.md)
|
|
666
|
+
|
|
667
|
+
Use for key lists.
|
|
668
|
+
|
|
669
|
+
[Banner](../components/banner.md)
|
|
670
|
+
|
|
671
|
+
Use for the sandbox notice.
|