tapimo 0.4.0 → 0.4.3

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.
Files changed (255) hide show
  1. package/README.md +1 -1
  2. package/dist/ApiKey.d.ts +1 -9
  3. package/dist/ApiKey.d.ts.map +1 -1
  4. package/dist/ApiKey.js +2 -3
  5. package/dist/ApiKey.js.map +1 -1
  6. package/dist/ApiKeys.d.ts +53 -27
  7. package/dist/ApiKeys.d.ts.map +1 -1
  8. package/dist/ApiKeys.js +32 -17
  9. package/dist/ApiKeys.js.map +1 -1
  10. package/dist/App.d.ts +2451 -1533
  11. package/dist/App.d.ts.map +1 -1
  12. package/dist/App.js +23 -9
  13. package/dist/App.js.map +1 -1
  14. package/dist/Assets.d.ts +10 -0
  15. package/dist/Assets.d.ts.map +1 -1
  16. package/dist/Assets.js +4 -0
  17. package/dist/Assets.js.map +1 -1
  18. package/dist/Auth.d.ts +2 -1
  19. package/dist/Auth.d.ts.map +1 -1
  20. package/dist/Auth.js +2 -1
  21. package/dist/Auth.js.map +1 -1
  22. package/dist/Scope.d.ts +40 -15
  23. package/dist/Scope.d.ts.map +1 -1
  24. package/dist/Scope.js +44 -13
  25. package/dist/Scope.js.map +1 -1
  26. package/dist/admin/App.d.ts +560 -9
  27. package/dist/admin/App.d.ts.map +1 -1
  28. package/dist/admin/App.js +38 -6
  29. package/dist/admin/App.js.map +1 -1
  30. package/dist/admin/apps/api-keys.d.ts +10 -24
  31. package/dist/admin/apps/api-keys.d.ts.map +1 -1
  32. package/dist/admin/apps/api-keys.js +10 -3
  33. package/dist/admin/apps/api-keys.js.map +1 -1
  34. package/dist/admin/apps/organizations.d.ts +833 -0
  35. package/dist/admin/apps/organizations.d.ts.map +1 -0
  36. package/dist/admin/apps/organizations.js +733 -0
  37. package/dist/admin/apps/organizations.js.map +1 -0
  38. package/dist/admin/apps/scopes.d.ts +8 -0
  39. package/dist/admin/apps/scopes.d.ts.map +1 -1
  40. package/dist/admin/ui.generated.d.ts +1 -1
  41. package/dist/admin/ui.generated.d.ts.map +1 -1
  42. package/dist/admin/ui.generated.js +53 -13
  43. package/dist/admin/ui.generated.js.map +1 -1
  44. package/dist/analytics/Analytics.d.ts +10 -0
  45. package/dist/analytics/Analytics.d.ts.map +1 -1
  46. package/dist/analytics/Analytics.js +8 -0
  47. package/dist/analytics/Analytics.js.map +1 -1
  48. package/dist/analytics/tables/requestEvents.d.ts +9 -0
  49. package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
  50. package/dist/analytics/tables/requestEvents.js +14 -0
  51. package/dist/analytics/tables/requestEvents.js.map +1 -1
  52. package/dist/apps/data/App.d.ts +1354 -984
  53. package/dist/apps/data/App.d.ts.map +1 -1
  54. package/dist/apps/data/App.js +20 -3
  55. package/dist/apps/data/App.js.map +1 -1
  56. package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
  57. package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
  58. package/dist/apps/data/FundingRouteProviders.js +196 -5
  59. package/dist/apps/data/FundingRouteProviders.js.map +1 -1
  60. package/dist/apps/data/routes/activities.d.ts +38 -38
  61. package/dist/apps/data/routes/addresses.d.ts +24 -24
  62. package/dist/apps/data/routes/balances.d.ts +45 -45
  63. package/dist/apps/data/routes/blocks.d.ts +39 -39
  64. package/dist/apps/data/routes/coingecko.d.ts +176 -176
  65. package/dist/apps/data/routes/exchanges.d.ts +453 -453
  66. package/dist/apps/data/routes/fee-amm.d.ts +101 -101
  67. package/dist/apps/data/routes/funding.d.ts +730 -138
  68. package/dist/apps/data/routes/funding.d.ts.map +1 -1
  69. package/dist/apps/data/routes/funding.js +234 -0
  70. package/dist/apps/data/routes/funding.js.map +1 -1
  71. package/dist/apps/data/routes/indexer.d.ts +5 -5
  72. package/dist/apps/data/routes/rpc.d.ts +31 -31
  73. package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
  74. package/dist/apps/management/App.d.ts +1156 -615
  75. package/dist/apps/management/App.d.ts.map +1 -1
  76. package/dist/apps/management/App.js +2 -2
  77. package/dist/apps/management/Billing.d.ts +3 -0
  78. package/dist/apps/management/Billing.d.ts.map +1 -1
  79. package/dist/apps/management/Billing.js +7 -2
  80. package/dist/apps/management/Billing.js.map +1 -1
  81. package/dist/apps/management/routes/api-keys.d.ts +106 -90
  82. package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
  83. package/dist/apps/management/routes/api-keys.js +41 -30
  84. package/dist/apps/management/routes/api-keys.js.map +1 -1
  85. package/dist/apps/management/routes/billing.d.ts +278 -146
  86. package/dist/apps/management/routes/billing.d.ts.map +1 -1
  87. package/dist/apps/management/routes/billing.js +14 -14
  88. package/dist/apps/management/routes/billing.js.map +1 -1
  89. package/dist/apps/management/routes/invitations.d.ts +93 -20
  90. package/dist/apps/management/routes/invitations.d.ts.map +1 -1
  91. package/dist/apps/management/routes/invitations.js +12 -15
  92. package/dist/apps/management/routes/invitations.js.map +1 -1
  93. package/dist/apps/management/routes/me.d.ts +13 -13
  94. package/dist/apps/management/routes/members.d.ts +89 -18
  95. package/dist/apps/management/routes/members.d.ts.map +1 -1
  96. package/dist/apps/management/routes/members.js +10 -10
  97. package/dist/apps/management/routes/members.js.map +1 -1
  98. package/dist/apps/management/routes/orgs.d.ts +130 -70
  99. package/dist/apps/management/routes/orgs.d.ts.map +1 -1
  100. package/dist/apps/management/routes/orgs.js +19 -19
  101. package/dist/apps/management/routes/orgs.js.map +1 -1
  102. package/dist/apps/management/routes/projects.d.ts +137 -48
  103. package/dist/apps/management/routes/projects.d.ts.map +1 -1
  104. package/dist/apps/management/routes/projects.js +12 -13
  105. package/dist/apps/management/routes/projects.js.map +1 -1
  106. package/dist/apps/management/routes/scopes.d.ts +14 -28
  107. package/dist/apps/management/routes/scopes.d.ts.map +1 -1
  108. package/dist/apps/management/routes/scopes.js +6 -7
  109. package/dist/apps/management/routes/scopes.js.map +1 -1
  110. package/dist/apps/management/routes/usage.d.ts +63 -19
  111. package/dist/apps/management/routes/usage.d.ts.map +1 -1
  112. package/dist/apps/management/routes/usage.js +9 -10
  113. package/dist/apps/management/routes/usage.js.map +1 -1
  114. package/dist/apps/mcp.d.ts.map +1 -1
  115. package/dist/apps/mcp.js +35 -3
  116. package/dist/apps/mcp.js.map +1 -1
  117. package/dist/apps/relay/App.d.ts +26 -4
  118. package/dist/apps/relay/App.d.ts.map +1 -1
  119. package/dist/apps/relay/App.js +17 -14
  120. package/dist/apps/relay/App.js.map +1 -1
  121. package/dist/db/Db.d.ts +5 -3
  122. package/dist/db/Db.d.ts.map +1 -1
  123. package/dist/db/Db.js +52 -0
  124. package/dist/db/Db.js.map +1 -1
  125. package/dist/db/Schema.d.ts +13 -0
  126. package/dist/db/Schema.d.ts.map +1 -1
  127. package/dist/db/Schema.js +16 -1
  128. package/dist/db/Schema.js.map +1 -1
  129. package/dist/db/tables/adminAuditLogs.d.ts +27 -0
  130. package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
  131. package/dist/db/tables/adminAuditLogs.js +19 -0
  132. package/dist/db/tables/adminAuditLogs.js.map +1 -0
  133. package/dist/db/tables/invitations.d.ts +1 -1
  134. package/dist/db/tables/invitations.d.ts.map +1 -1
  135. package/dist/db/tables/memberships.d.ts +32 -4
  136. package/dist/db/tables/memberships.d.ts.map +1 -1
  137. package/dist/db/tables/memberships.js +31 -0
  138. package/dist/db/tables/memberships.js.map +1 -1
  139. package/dist/db/tables/organizations.d.ts +43 -1
  140. package/dist/db/tables/organizations.d.ts.map +1 -1
  141. package/dist/db/tables/organizations.js +103 -0
  142. package/dist/db/tables/organizations.js.map +1 -1
  143. package/dist/db/tables/projects.d.ts +25 -0
  144. package/dist/db/tables/projects.d.ts.map +1 -1
  145. package/dist/db/tables/projects.js +22 -0
  146. package/dist/db/tables/projects.js.map +1 -1
  147. package/dist/db/tables/webhookDeliveries.d.ts +26 -0
  148. package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
  149. package/dist/db/tables/webhookDeliveries.js +37 -0
  150. package/dist/db/tables/webhookDeliveries.js.map +1 -1
  151. package/dist/handlers/relay.d.ts +13 -10
  152. package/dist/handlers/relay.d.ts.map +1 -1
  153. package/dist/handlers/relay.js +46 -16
  154. package/dist/handlers/relay.js.map +1 -1
  155. package/dist/internal/Auth.d.ts +13 -18
  156. package/dist/internal/Auth.d.ts.map +1 -1
  157. package/dist/internal/Auth.js +78 -58
  158. package/dist/internal/Auth.js.map +1 -1
  159. package/dist/internal/Cursor.d.ts +1 -1
  160. package/dist/internal/Cursor.d.ts.map +1 -1
  161. package/dist/internal/Cursor.js +4 -0
  162. package/dist/internal/Cursor.js.map +1 -1
  163. package/dist/internal/EdgeCache.d.ts.map +1 -1
  164. package/dist/internal/EdgeCache.js +2 -0
  165. package/dist/internal/EdgeCache.js.map +1 -1
  166. package/dist/internal/FundingRoutes.d.ts +170 -0
  167. package/dist/internal/FundingRoutes.d.ts.map +1 -1
  168. package/dist/internal/FundingRoutes.js +68 -6
  169. package/dist/internal/FundingRoutes.js.map +1 -1
  170. package/dist/internal/Log.d.ts +16 -1
  171. package/dist/internal/Log.d.ts.map +1 -1
  172. package/dist/internal/Log.js +5 -3
  173. package/dist/internal/Log.js.map +1 -1
  174. package/package.json +2 -2
  175. package/src/ApiKey.ts +2 -3
  176. package/src/ApiKeys.ts +95 -17
  177. package/src/App.test.ts +44 -2
  178. package/src/App.ts +29 -9
  179. package/src/Assets.ts +8 -0
  180. package/src/Auth.test-d.ts +36 -0
  181. package/src/Auth.ts +20 -1
  182. package/src/Client.test-d.ts +64 -17
  183. package/src/Scope.test.ts +34 -0
  184. package/src/Scope.ts +53 -17
  185. package/src/admin/App.test.ts +33 -3
  186. package/src/admin/App.ts +49 -14
  187. package/src/admin/apps/api-keys.ts +10 -3
  188. package/src/admin/apps/organizations.test.ts +455 -0
  189. package/src/admin/apps/organizations.ts +953 -0
  190. package/src/admin/apps/scopes.test.ts +10 -0
  191. package/src/admin/ui/plugin.ts +1 -1
  192. package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
  193. package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
  194. package/src/admin/ui/src/lib/api-keys.ts +36 -19
  195. package/src/admin/ui/src/lib/config.ts +2 -3
  196. package/src/admin/ui/src/lib/organizations.test.ts +89 -0
  197. package/src/admin/ui/src/lib/organizations.ts +262 -0
  198. package/src/admin/ui/src/lib/query-client.ts +3 -1
  199. package/src/admin/ui/src/lib/theme.ts +57 -0
  200. package/src/admin/ui/src/routeTree.gen.ts +47 -4
  201. package/src/admin/ui/src/routes/__root.tsx +18 -3
  202. package/src/admin/ui/src/routes/api-keys.tsx +52 -7
  203. package/src/admin/ui/src/routes/index.tsx +1 -1
  204. package/src/admin/ui/src/routes/organizations.tsx +158 -0
  205. package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
  206. package/src/admin/ui/src/start.ts +6 -0
  207. package/src/admin/ui.generated.ts +63 -13
  208. package/src/analytics/Analytics.ts +25 -0
  209. package/src/analytics/tables/requestEvents.ts +33 -0
  210. package/src/apps/composition.test.ts +82 -2
  211. package/src/apps/data/App.ts +24 -3
  212. package/src/apps/data/FundingRouteProviders.test.ts +204 -0
  213. package/src/apps/data/FundingRouteProviders.ts +294 -9
  214. package/src/apps/data/routes/funding.test.ts +90 -23
  215. package/src/apps/data/routes/funding.ts +315 -0
  216. package/src/apps/data/routes/rpc.test.ts +33 -0
  217. package/src/apps/management/App.ts +2 -2
  218. package/src/apps/management/Billing.ts +10 -2
  219. package/src/apps/management/routes/api-keys.test.ts +137 -1
  220. package/src/apps/management/routes/api-keys.ts +63 -38
  221. package/src/apps/management/routes/billing.ts +14 -14
  222. package/src/apps/management/routes/invitations.test.ts +102 -1
  223. package/src/apps/management/routes/invitations.ts +13 -15
  224. package/src/apps/management/routes/members.ts +10 -10
  225. package/src/apps/management/routes/orgs.test.ts +78 -16
  226. package/src/apps/management/routes/orgs.ts +21 -20
  227. package/src/apps/management/routes/projects.ts +12 -13
  228. package/src/apps/management/routes/scopes.test.ts +16 -0
  229. package/src/apps/management/routes/scopes.ts +6 -7
  230. package/src/apps/management/routes/usage.ts +9 -14
  231. package/src/apps/mcp.test.ts +25 -1
  232. package/src/apps/mcp.ts +43 -3
  233. package/src/apps/relay/App.test.ts +23 -18
  234. package/src/apps/relay/App.ts +35 -14
  235. package/src/db/Db.ts +59 -3
  236. package/src/db/Schema.ts +20 -1
  237. package/src/db/tables/adminAuditLogs.ts +47 -0
  238. package/src/db/tables/invitations.ts +1 -1
  239. package/src/db/tables/memberships.ts +59 -3
  240. package/src/db/tables/organizations.ts +148 -1
  241. package/src/db/tables/projects.ts +47 -0
  242. package/src/db/tables/webhookDeliveries.ts +83 -0
  243. package/src/handlers/relay.test-d.ts +8 -0
  244. package/src/handlers/relay.test.ts +2 -2
  245. package/src/handlers/relay.ts +62 -22
  246. package/src/internal/Auth.test.ts +174 -7
  247. package/src/internal/Auth.ts +90 -62
  248. package/src/internal/Cursor.test.ts +10 -0
  249. package/src/internal/Cursor.ts +5 -1
  250. package/src/internal/EdgeCache.test.ts +11 -0
  251. package/src/internal/EdgeCache.ts +2 -0
  252. package/src/internal/FundingRoutes.test.ts +26 -0
  253. package/src/internal/FundingRoutes.ts +187 -18
  254. package/src/internal/Log.test.ts +41 -3
  255. package/src/internal/Log.ts +23 -4
@@ -0,0 +1,710 @@
1
+ import {
2
+ type UseQueryResult,
3
+ useInfiniteQuery,
4
+ useQuery,
5
+ useSuspenseQuery,
6
+ } from '@tanstack/react-query'
7
+ import {
8
+ createFileRoute,
9
+ type ErrorComponentProps,
10
+ Link,
11
+ useNavigate,
12
+ useRouter,
13
+ } from '@tanstack/react-router'
14
+ import { Button, Rows, Select } from 'regen-ui'
15
+ import { ArrowLeft, RefreshCw } from 'lucide-react'
16
+
17
+ import { Card, Notice, Page } from '../components/Page'
18
+ import * as Organizations from '../lib/organizations'
19
+
20
+ export const Route = createFileRoute('/organizations_/$orgId')({
21
+ validateSearch: Organizations.schema.Search,
22
+ loader: ({ context, params }) =>
23
+ context.queryClient.ensureQueryData(Organizations.get.options(params.orgId)),
24
+ component: PageComponent,
25
+ errorComponent: OrganizationError,
26
+ })
27
+
28
+ function PageComponent() {
29
+ const { orgId } = Route.useParams()
30
+ const { period: period_search, query } = Route.useSearch()
31
+ const navigate = useNavigate({ from: Route.fullPath })
32
+ const period = period_search ?? '7d'
33
+ const organizationQuery = useSuspenseQuery(Organizations.get.options(orgId))
34
+ const organization = organizationQuery.data
35
+ const members = useInfiniteQuery(Organizations.members.options(orgId))
36
+ const projects = useInfiniteQuery(Organizations.projects.options(orgId))
37
+ const apiKeys = useQuery(Organizations.apiKeys.options(orgId))
38
+ const sponsorship = useQuery(Organizations.sponsorship.options(orgId))
39
+ const usage = useQuery(Organizations.usage.options(orgId, period))
40
+ const webhooks = useQuery(Organizations.webhooks.options(orgId))
41
+ const memberRecords = members.data?.pages.flatMap((page) => page.data) ?? []
42
+ const projectRecords = projects.data?.pages.flatMap((page) => page.data) ?? []
43
+ const usageLabel = usagePeriods[period].label
44
+ const operationalQueries = [
45
+ apiKeys,
46
+ members,
47
+ organizationQuery,
48
+ projects,
49
+ sponsorship,
50
+ usage,
51
+ webhooks,
52
+ ]
53
+ const updatedAt = oldestUpdatedAt(operationalQueries.map((result) => result.dataUpdatedAt))
54
+ const updateComplete = operationalQueries.every((result) => result.dataUpdatedAt > 0)
55
+ const updateError = operationalQueries.some((result) => result.error)
56
+ const refreshing = operationalQueries.some((result) => result.isFetching)
57
+
58
+ function refresh() {
59
+ void Promise.all([
60
+ apiKeys.refetch(),
61
+ members.refetch(),
62
+ organizationQuery.refetch(),
63
+ projects.refetch(),
64
+ sponsorship.refetch(),
65
+ usage.refetch(),
66
+ webhooks.refetch(),
67
+ ])
68
+ }
69
+
70
+ return (
71
+ <Page
72
+ title={organization.name}
73
+ description={organization.id}
74
+ action={
75
+ <Button
76
+ render={<Link search={Organizations.searchParameters({ query })} to="/organizations" />}
77
+ >
78
+ <ArrowLeft className="size-4" />
79
+ Organizations
80
+ </Button>
81
+ }
82
+ >
83
+ <Card className="grid grid-cols-2 gap-px bg-border md:grid-cols-3">
84
+ <dl className="contents">
85
+ <Summary label="Members">{organization.memberCount}</Summary>
86
+ <Summary label="Projects">{organization.projectCount}</Summary>
87
+ <Summary label="Owner user">{organization.userId ?? 'None'}</Summary>
88
+ <Summary label="Created by">{organization.createdBy ?? 'Unknown'}</Summary>
89
+ <Summary label="Created">{formatDate(organization.createdAt)}</Summary>
90
+ <Summary label="Updated">{formatDate(organization.updatedAt)}</Summary>
91
+ </dl>
92
+ </Card>
93
+
94
+ <section className="flex flex-col gap-3">
95
+ <div className="flex flex-wrap items-center justify-between gap-3">
96
+ <h2 className="text-heading-18 text-foreground">Operations</h2>
97
+ <div className="flex items-center gap-3">
98
+ <span aria-live="polite" className="text-label-12 text-foreground-secondary">
99
+ {refreshing && !updateComplete ? (
100
+ 'Updating…'
101
+ ) : updateError ? (
102
+ updatedAt ? (
103
+ <time
104
+ dateTime={new Date(updatedAt).toISOString()}
105
+ title={formatDateTime(updatedAt)}
106
+ >
107
+ Partially updated {formatTime(updatedAt)}
108
+ </time>
109
+ ) : (
110
+ 'Update unavailable'
111
+ )
112
+ ) : updatedAt && updateComplete ? (
113
+ <time
114
+ dateTime={new Date(updatedAt).toISOString()}
115
+ title={formatDateTime(updatedAt)}
116
+ >
117
+ Updated {formatTime(updatedAt)}
118
+ </time>
119
+ ) : (
120
+ 'Updating…'
121
+ )}
122
+ </span>
123
+ <Button loading={refreshing} onClick={refresh}>
124
+ <RefreshCw className="size-4" />
125
+ Refresh
126
+ </Button>
127
+ </div>
128
+ </div>
129
+ <Card className="grid grid-cols-2 gap-px bg-border lg:grid-cols-4">
130
+ <OperationalMetric
131
+ error={apiKeys.error}
132
+ label="Active API keys"
133
+ loading={apiKeys.isPending}
134
+ value={apiKeys.data?.data.length.toLocaleString()}
135
+ />
136
+ <OperationalMetric
137
+ error={usage.error}
138
+ label={`API requests, ${usageLabel}`}
139
+ loading={usage.isPending}
140
+ value={usage.data?.totals.requests.toLocaleString()}
141
+ />
142
+ <OperationalMetric
143
+ error={usage.error}
144
+ label={`Error rate, ${usageLabel}`}
145
+ loading={usage.isPending}
146
+ value={
147
+ usage.data
148
+ ? formatPercent(usage.data.totals.errors, usage.data.totals.requests)
149
+ : undefined
150
+ }
151
+ />
152
+ <OperationalMetric
153
+ error={webhooks.error}
154
+ label="Webhook failures, 7 days"
155
+ loading={webhooks.isPending}
156
+ value={webhooks.data?.failureCount.toLocaleString()}
157
+ />
158
+ </Card>
159
+ </section>
160
+
161
+ <div className="grid items-start gap-6 lg:grid-cols-2">
162
+ <UsageCard
163
+ onPeriodChange={(value) =>
164
+ void navigate({
165
+ params: { orgId },
166
+ replace: true,
167
+ search: { period: value, ...(query ? { query } : {}) },
168
+ to: '/organizations/$orgId',
169
+ })
170
+ }
171
+ period={period}
172
+ query={usage}
173
+ />
174
+ <ApiKeysCard orgId={orgId} query={apiKeys} />
175
+ </div>
176
+
177
+ <section className="flex flex-col gap-3">
178
+ <h2 className="text-heading-18 text-foreground">Billing</h2>
179
+ <div className="grid gap-3 md:grid-cols-2">
180
+ <BillingCard
181
+ environment="Production"
182
+ record={organization.billing.production}
183
+ sponsorship={sponsorship.data?.production}
184
+ />
185
+ <BillingCard
186
+ environment="Sandbox"
187
+ record={organization.billing.sandbox}
188
+ sponsorship={sponsorship.data?.sandbox}
189
+ />
190
+ </div>
191
+ {sponsorship.error ? (
192
+ <SectionError error={sponsorship.error} onRetry={() => sponsorship.refetch()} />
193
+ ) : null}
194
+ </section>
195
+
196
+ <WebhooksCard query={webhooks} />
197
+
198
+ <div className="grid items-start gap-6 lg:grid-cols-2">
199
+ <ResourceList
200
+ empty="No members."
201
+ error={members.error}
202
+ initialLoading={members.isPending}
203
+ loading={members.isFetchingNextPage}
204
+ more={members.hasNextPage}
205
+ onMore={() => members.fetchNextPage()}
206
+ onRetry={() =>
207
+ members.isFetchNextPageError ? members.fetchNextPage() : members.refetch()
208
+ }
209
+ title="Members"
210
+ >
211
+ {memberRecords.map((member) => (
212
+ <Rows.Row className="min-h-[60px] py-3" key={member.userId} style={{ height: 'auto' }}>
213
+ <span className="flex min-w-0 flex-1 flex-col gap-1">
214
+ <Link
215
+ className="truncate text-label-14 text-foreground hover:underline"
216
+ search={Organizations.searchParameters({ query: member.userId })}
217
+ to="/organizations"
218
+ >
219
+ {member.email ?? member.userId}
220
+ </Link>
221
+ <span className="truncate font-mono text-label-12 text-foreground-secondary">
222
+ {member.email ? member.userId : (member.address ?? 'No email or address')}
223
+ </span>
224
+ </span>
225
+ <span className="shrink-0 text-label-12 capitalize text-foreground-secondary">
226
+ {member.role}
227
+ </span>
228
+ </Rows.Row>
229
+ ))}
230
+ </ResourceList>
231
+
232
+ <ResourceList
233
+ empty="No projects."
234
+ error={projects.error}
235
+ initialLoading={projects.isPending}
236
+ loading={projects.isFetchingNextPage}
237
+ more={projects.hasNextPage}
238
+ onMore={() => projects.fetchNextPage()}
239
+ onRetry={() =>
240
+ projects.isFetchNextPageError ? projects.fetchNextPage() : projects.refetch()
241
+ }
242
+ title="Projects"
243
+ >
244
+ {projectRecords.map((project) => (
245
+ <Rows.Row className="min-h-[60px] py-3" key={project.id} style={{ height: 'auto' }}>
246
+ <span className="flex min-w-0 flex-1 flex-col gap-1">
247
+ <Link
248
+ className="truncate text-label-14 text-foreground hover:underline"
249
+ search={{ orgId, projectId: project.id }}
250
+ to="/api-keys"
251
+ >
252
+ {project.name}
253
+ </Link>
254
+ <span className="truncate font-mono text-label-12 text-foreground-secondary">
255
+ {project.id}
256
+ </span>
257
+ </span>
258
+ <span className="shrink-0 text-label-12 text-foreground-secondary">
259
+ {formatDate(project.createdAt)}
260
+ </span>
261
+ </Rows.Row>
262
+ ))}
263
+ </ResourceList>
264
+ </div>
265
+ </Page>
266
+ )
267
+ }
268
+
269
+ function OrganizationError(options: OrganizationError.Props) {
270
+ const router = useRouter()
271
+ const { query } = Route.useSearch()
272
+ return (
273
+ <Page
274
+ title="Organization unavailable"
275
+ action={
276
+ <Button
277
+ render={<Link search={Organizations.searchParameters({ query })} to="/organizations" />}
278
+ >
279
+ <ArrowLeft className="size-4" />
280
+ Organizations
281
+ </Button>
282
+ }
283
+ >
284
+ <Card className="flex flex-col items-center gap-4 px-6 py-12 text-center">
285
+ <p className="text-copy-14 text-negative">{options.error.message}</p>
286
+ <Button
287
+ onClick={() => {
288
+ options.reset()
289
+ void router.invalidate()
290
+ }}
291
+ >
292
+ Try again
293
+ </Button>
294
+ </Card>
295
+ </Page>
296
+ )
297
+ }
298
+
299
+ declare namespace OrganizationError {
300
+ type Props = ErrorComponentProps
301
+ }
302
+
303
+ function Summary(options: Summary.Props) {
304
+ return (
305
+ <div className="flex min-h-24 min-w-0 flex-col justify-between gap-3 bg-surface p-4">
306
+ <dt className="text-label-12 text-foreground-secondary">{options.label}</dt>
307
+ <dd className="truncate text-label-14 text-foreground">{options.children}</dd>
308
+ </div>
309
+ )
310
+ }
311
+
312
+ declare namespace Summary {
313
+ type Props = { children: React.ReactNode; label: string }
314
+ }
315
+
316
+ function OperationalMetric(options: OperationalMetric.Props) {
317
+ return (
318
+ <div className="flex min-h-28 flex-col justify-between gap-3 bg-surface p-4">
319
+ <span className="text-label-12 text-foreground-secondary">{options.label}</span>
320
+ <span
321
+ className={
322
+ options.error ? 'text-label-13 text-negative' : 'text-heading-24 text-foreground'
323
+ }
324
+ >
325
+ {options.error ? 'Unavailable' : options.loading ? '—' : (options.value ?? '0')}
326
+ </span>
327
+ </div>
328
+ )
329
+ }
330
+
331
+ declare namespace OperationalMetric {
332
+ type Props = {
333
+ error: Error | null
334
+ label: string
335
+ loading: boolean
336
+ value: string | undefined
337
+ }
338
+ }
339
+
340
+ function UsageCard(options: UsageCard.Props) {
341
+ const usage = options.query.data
342
+ return (
343
+ <section className="flex min-w-0 flex-col gap-3">
344
+ <div className="flex items-center justify-between gap-3">
345
+ <h2 className="text-heading-18 text-foreground">API usage</h2>
346
+ <Select
347
+ aria-label="API usage window"
348
+ items={usagePeriodItems}
349
+ onChange={(value) => options.onPeriodChange(value as Organizations.usage.Period)}
350
+ value={options.period}
351
+ />
352
+ </div>
353
+ <Card>
354
+ {options.query.isPending ? (
355
+ <Notice>Loading API usage…</Notice>
356
+ ) : options.query.error ? (
357
+ <SectionError error={options.query.error} onRetry={() => options.query.refetch()} />
358
+ ) : usage ? (
359
+ <div className="flex flex-col gap-5 p-4">
360
+ <dl className="grid grid-cols-3 gap-4">
361
+ <Detail label="Requests">{usage.totals.requests.toLocaleString()}</Detail>
362
+ <Detail label="Errors">{usage.totals.errors.toLocaleString()}</Detail>
363
+ <Detail label="Average latency">{usage.totals.averageDurationMs} ms</Detail>
364
+ </dl>
365
+ <div className="border-t border-border pt-4">
366
+ <p className="mb-2 text-label-12 text-foreground-secondary">Errors by code</p>
367
+ <div className="flex flex-wrap gap-2">
368
+ {usage.byError.length ? (
369
+ usage.byError.map((entry) => (
370
+ <span
371
+ className="border border-border px-2 py-1 text-label-12 text-foreground"
372
+ key={entry.code}
373
+ >
374
+ {entry.code}: {entry.requests.toLocaleString()}
375
+ </span>
376
+ ))
377
+ ) : (
378
+ <span className="text-label-13 text-foreground-secondary">No API errors.</span>
379
+ )}
380
+ </div>
381
+ </div>
382
+ <div className="border-t border-border pt-4">
383
+ <p className="mb-2 text-label-12 text-foreground-secondary">Busiest routes</p>
384
+ <div className="flex flex-col gap-2">
385
+ {usage.byRoute.slice(0, 5).map((entry) => (
386
+ <div
387
+ className="flex items-center justify-between gap-3 text-label-13"
388
+ key={entry.route}
389
+ >
390
+ <code className="truncate text-foreground">{entry.route}</code>
391
+ <span className="shrink-0 text-foreground-secondary">
392
+ {countLabel(entry.requests, 'request')}, {countLabel(entry.errors, 'error')}
393
+ </span>
394
+ </div>
395
+ ))}
396
+ </div>
397
+ </div>
398
+ </div>
399
+ ) : null}
400
+ </Card>
401
+ </section>
402
+ )
403
+ }
404
+
405
+ declare namespace UsageCard {
406
+ type Props = {
407
+ onPeriodChange: (period: Organizations.usage.Period) => void
408
+ period: Organizations.usage.Period
409
+ query: UseQueryResult<Organizations.Usage, Error>
410
+ }
411
+ }
412
+
413
+ function ApiKeysCard(options: ApiKeysCard.Props) {
414
+ const keys = options.query.data?.data ?? []
415
+ return (
416
+ <section className="flex min-w-0 flex-col gap-3">
417
+ <div className="flex items-center justify-between gap-3">
418
+ <h2 className="text-heading-18 text-foreground">Active API keys</h2>
419
+ <Link
420
+ className="text-label-13 text-foreground-secondary hover:text-foreground"
421
+ search={{ orgId: options.orgId }}
422
+ to="/api-keys"
423
+ >
424
+ View all
425
+ </Link>
426
+ </div>
427
+ <Card>
428
+ {options.query.isPending ? (
429
+ <Notice>Loading API keys…</Notice>
430
+ ) : options.query.error ? (
431
+ <SectionError error={options.query.error} onRetry={() => options.query.refetch()} />
432
+ ) : keys.length === 0 ? (
433
+ <Notice>No API keys.</Notice>
434
+ ) : (
435
+ <Rows variant="surface">
436
+ {keys.slice(0, 5).map((key) => (
437
+ <Rows.Row className="min-h-[64px] py-3" key={key.id} style={{ height: 'auto' }}>
438
+ <span className="flex min-w-0 flex-1 flex-col gap-1">
439
+ <span className="truncate text-label-14 text-foreground">
440
+ {key.name ?? key.id}
441
+ </span>
442
+ <span className="truncate text-label-12 text-foreground-secondary">
443
+ {key.environment} · {key.scopes.join(', ')}
444
+ </span>
445
+ <span className="truncate text-label-12 text-foreground-secondary">
446
+ Created by {key.createdBy ?? 'unknown'}
447
+ </span>
448
+ </span>
449
+ </Rows.Row>
450
+ ))}
451
+ </Rows>
452
+ )}
453
+ </Card>
454
+ </section>
455
+ )
456
+ }
457
+
458
+ declare namespace ApiKeysCard {
459
+ type Props = {
460
+ orgId: string
461
+ query: UseQueryResult<{ data: readonly Organizations.ApiKey[] }, Error>
462
+ }
463
+ }
464
+
465
+ function WebhooksCard(options: WebhooksCard.Props) {
466
+ const data = options.query.data
467
+ return (
468
+ <section className="flex min-w-0 flex-col gap-3">
469
+ <div className="flex items-end justify-between gap-3">
470
+ <h2 className="text-heading-18 text-foreground">Webhooks</h2>
471
+ {data ? (
472
+ <span className="text-label-12 text-foreground-secondary">
473
+ {countLabel(data.subscriptions.length, 'subscription')} ·{' '}
474
+ {countLabel(data.failureCount, 'failure')} in 7 days
475
+ </span>
476
+ ) : null}
477
+ </div>
478
+ <Card>
479
+ {options.query.isPending ? (
480
+ <Notice>Loading webhook health…</Notice>
481
+ ) : options.query.error ? (
482
+ <SectionError error={options.query.error} onRetry={() => options.query.refetch()} />
483
+ ) : !data || data.subscriptions.length === 0 ? (
484
+ <Notice>No webhooks.</Notice>
485
+ ) : (
486
+ <Rows variant="surface">
487
+ {data.subscriptions.map((subscription) => (
488
+ <Rows.Row
489
+ className="min-h-[64px] py-3"
490
+ key={subscription.id}
491
+ style={{ height: 'auto' }}
492
+ >
493
+ <span className="flex min-w-0 flex-1 flex-col gap-1">
494
+ <span className="truncate text-label-14 text-foreground">
495
+ {subscription.eventType}
496
+ </span>
497
+ <span className="truncate font-mono text-label-12 text-foreground-secondary">
498
+ {subscription.destination}
499
+ </span>
500
+ </span>
501
+ <span className="flex shrink-0 flex-col items-end gap-1 text-label-12 text-foreground-secondary">
502
+ <span className="capitalize">{subscription.status}</span>
503
+ <span className={subscription.failureCount ? 'text-negative' : undefined}>
504
+ {subscription.failureCount} failures
505
+ </span>
506
+ </span>
507
+ </Rows.Row>
508
+ ))}
509
+ </Rows>
510
+ )}
511
+ </Card>
512
+ {data?.failures.length ? (
513
+ <Card className="p-4">
514
+ <p className="mb-3 text-label-12 text-foreground-secondary">Latest delivery failures</p>
515
+ <div className="flex flex-col gap-3">
516
+ {data.failures.slice(0, 5).map((failure) => (
517
+ <div
518
+ className="flex items-start justify-between gap-4 text-label-13"
519
+ key={failure.id}
520
+ >
521
+ <span className="min-w-0 truncate text-negative">
522
+ {failure.responseStatus ?? 'Network'} · {failure.error ?? 'Delivery failed'}
523
+ </span>
524
+ <span className="shrink-0 text-foreground-secondary">
525
+ {formatDate(failure.createdAt)}
526
+ </span>
527
+ </div>
528
+ ))}
529
+ </div>
530
+ </Card>
531
+ ) : null}
532
+ </section>
533
+ )
534
+ }
535
+
536
+ declare namespace WebhooksCard {
537
+ type Props = { query: UseQueryResult<Organizations.Webhooks, Error> }
538
+ }
539
+
540
+ function SectionError(options: SectionError.Props) {
541
+ return (
542
+ <div className="flex items-center gap-3 p-4 text-copy-14 text-negative" role="alert">
543
+ <span>{options.error.message}</span>
544
+ <Button onClick={options.onRetry}>Retry</Button>
545
+ </div>
546
+ )
547
+ }
548
+
549
+ declare namespace SectionError {
550
+ type Props = { error: Error; onRetry: () => unknown }
551
+ }
552
+
553
+ function BillingCard(options: BillingCard.Props) {
554
+ const { settings, source } = options.record
555
+ const remaining =
556
+ settings?.spendLimit && options.sponsorship
557
+ ? Math.max(
558
+ Number(settings.spendLimit) - Number(options.sponsorship.committedSpend),
559
+ 0,
560
+ ).toString()
561
+ : undefined
562
+ return (
563
+ <Card className="flex flex-col gap-5 p-4">
564
+ <div className="flex items-center justify-between gap-3">
565
+ <h3 className="text-label-14 text-foreground">{options.environment}</h3>
566
+ <span className="text-label-12 capitalize text-foreground-secondary">
567
+ {source?.status ?? 'No billing source'}
568
+ </span>
569
+ </div>
570
+ <dl className="grid grid-cols-2 gap-4">
571
+ <Detail label="Stripe customer">{source?.stripeCustomerId ?? '—'}</Detail>
572
+ <Detail label="Spend period">
573
+ {options.sponsorship ? periodLabel(options.sponsorship.period) : '—'}
574
+ </Detail>
575
+ <Detail label="Spend limit">{formatMoney(settings?.spendLimit, settings?.currency)}</Detail>
576
+ <Detail label="Committed spend">
577
+ {options.sponsorship
578
+ ? formatMoney(options.sponsorship.committedSpend, options.sponsorship.currency)
579
+ : '—'}
580
+ </Detail>
581
+ <Detail label="Limit remaining">{formatMoney(remaining, settings?.currency)}</Detail>
582
+ <Detail label="Transaction limit">
583
+ {formatMoney(settings?.txFeeLimit, settings?.currency)}
584
+ </Detail>
585
+ </dl>
586
+ </Card>
587
+ )
588
+ }
589
+
590
+ declare namespace BillingCard {
591
+ type Props = {
592
+ environment: string
593
+ record: Organizations.Detail['billing']['production']
594
+ sponsorship?: Organizations.Sponsorship['production'] | undefined
595
+ }
596
+ }
597
+
598
+ function Detail(options: Detail.Props) {
599
+ return (
600
+ <div className="min-w-0">
601
+ <dt className="text-label-12 text-foreground-secondary">{options.label}</dt>
602
+ <dd className="mt-1 truncate text-label-13 text-foreground">{options.children}</dd>
603
+ </div>
604
+ )
605
+ }
606
+
607
+ declare namespace Detail {
608
+ type Props = { children: React.ReactNode; label: string }
609
+ }
610
+
611
+ function ResourceList(options: ResourceList.Props) {
612
+ const empty = Array.isArray(options.children) && options.children.length === 0
613
+ return (
614
+ <section className="flex min-w-0 flex-col gap-3">
615
+ <h2 className="text-heading-18 text-foreground">{options.title}</h2>
616
+ <Card>
617
+ {options.initialLoading ? (
618
+ <Notice>Loading {options.title.toLowerCase()}…</Notice>
619
+ ) : options.error && empty ? (
620
+ <div className="flex flex-col items-center gap-4 px-6 py-12 text-center" role="alert">
621
+ <p className="text-copy-14 text-negative">{options.error.message}</p>
622
+ <Button onClick={options.onRetry}>Retry</Button>
623
+ </div>
624
+ ) : empty ? (
625
+ <Notice>{options.empty}</Notice>
626
+ ) : (
627
+ <Rows variant="surface">{options.children}</Rows>
628
+ )}
629
+ </Card>
630
+ {options.error && !empty ? (
631
+ <div className="flex items-center gap-3 text-copy-14 text-negative" role="alert">
632
+ <span>{options.error.message}</span>
633
+ <Button onClick={options.onRetry}>Retry</Button>
634
+ </div>
635
+ ) : options.more ? (
636
+ <Button className="self-start" loading={options.loading} onClick={options.onMore}>
637
+ Load more
638
+ </Button>
639
+ ) : null}
640
+ </section>
641
+ )
642
+ }
643
+
644
+ declare namespace ResourceList {
645
+ type Props = {
646
+ children: React.ReactNode
647
+ empty: string
648
+ error: Error | null
649
+ initialLoading: boolean
650
+ loading: boolean
651
+ more: boolean
652
+ onMore: () => unknown
653
+ onRetry: () => unknown
654
+ title: string
655
+ }
656
+ }
657
+
658
+ function formatDate(value: string) {
659
+ return new Intl.DateTimeFormat('en', { dateStyle: 'medium' }).format(new Date(value))
660
+ }
661
+
662
+ function formatDateTime(value: number) {
663
+ return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'medium' }).format(value)
664
+ }
665
+
666
+ function formatTime(value: number) {
667
+ return new Intl.DateTimeFormat('en', { timeStyle: 'medium' }).format(value)
668
+ }
669
+
670
+ function formatMoney(value: string | null | undefined, currency: string | undefined) {
671
+ if (value === null || value === undefined) return '—'
672
+ return new Intl.NumberFormat('en', {
673
+ currency: currency?.toUpperCase() ?? 'USD',
674
+ style: 'currency',
675
+ }).format(Number(value))
676
+ }
677
+
678
+ function formatPercent(part: number, total: number) {
679
+ if (total === 0) return '0%'
680
+ return new Intl.NumberFormat('en', { maximumFractionDigits: 1, style: 'percent' }).format(
681
+ part / total,
682
+ )
683
+ }
684
+
685
+ function periodLabel(period: Organizations.Sponsorship['production']['period']) {
686
+ if (period === 'month') return 'Current month'
687
+ return period
688
+ }
689
+
690
+ function countLabel(count: number, label: string) {
691
+ return `${count.toLocaleString()} ${label}${count === 1 ? '' : 's'}`
692
+ }
693
+
694
+ function oldestUpdatedAt(values: readonly number[]) {
695
+ const updated = values.filter((value) => value > 0)
696
+ if (updated.length === 0) return undefined
697
+ return Math.min(...updated)
698
+ }
699
+
700
+ const usagePeriods = {
701
+ '24h': { label: '24 hours' },
702
+ '30d': { label: '30 days' },
703
+ '7d': { label: '7 days' },
704
+ } as const
705
+
706
+ const usagePeriodItems = [
707
+ { label: '24 hours', value: '24h' },
708
+ { label: '7 days', value: '7d' },
709
+ { label: '30 days', value: '30d' },
710
+ ]