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.
- package/README.md +1 -1
- package/dist/ApiKey.d.ts +1 -9
- package/dist/ApiKey.d.ts.map +1 -1
- package/dist/ApiKey.js +2 -3
- package/dist/ApiKey.js.map +1 -1
- package/dist/ApiKeys.d.ts +53 -27
- package/dist/ApiKeys.d.ts.map +1 -1
- package/dist/ApiKeys.js +32 -17
- package/dist/ApiKeys.js.map +1 -1
- package/dist/App.d.ts +2451 -1533
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +23 -9
- package/dist/App.js.map +1 -1
- package/dist/Assets.d.ts +10 -0
- package/dist/Assets.d.ts.map +1 -1
- package/dist/Assets.js +4 -0
- package/dist/Assets.js.map +1 -1
- package/dist/Auth.d.ts +2 -1
- package/dist/Auth.d.ts.map +1 -1
- package/dist/Auth.js +2 -1
- package/dist/Auth.js.map +1 -1
- package/dist/Scope.d.ts +40 -15
- package/dist/Scope.d.ts.map +1 -1
- package/dist/Scope.js +44 -13
- package/dist/Scope.js.map +1 -1
- package/dist/admin/App.d.ts +560 -9
- package/dist/admin/App.d.ts.map +1 -1
- package/dist/admin/App.js +38 -6
- package/dist/admin/App.js.map +1 -1
- package/dist/admin/apps/api-keys.d.ts +10 -24
- package/dist/admin/apps/api-keys.d.ts.map +1 -1
- package/dist/admin/apps/api-keys.js +10 -3
- package/dist/admin/apps/api-keys.js.map +1 -1
- package/dist/admin/apps/organizations.d.ts +833 -0
- package/dist/admin/apps/organizations.d.ts.map +1 -0
- package/dist/admin/apps/organizations.js +733 -0
- package/dist/admin/apps/organizations.js.map +1 -0
- package/dist/admin/apps/scopes.d.ts +8 -0
- package/dist/admin/apps/scopes.d.ts.map +1 -1
- package/dist/admin/ui.generated.d.ts +1 -1
- package/dist/admin/ui.generated.d.ts.map +1 -1
- package/dist/admin/ui.generated.js +53 -13
- package/dist/admin/ui.generated.js.map +1 -1
- package/dist/analytics/Analytics.d.ts +10 -0
- package/dist/analytics/Analytics.d.ts.map +1 -1
- package/dist/analytics/Analytics.js +8 -0
- package/dist/analytics/Analytics.js.map +1 -1
- package/dist/analytics/tables/requestEvents.d.ts +9 -0
- package/dist/analytics/tables/requestEvents.d.ts.map +1 -1
- package/dist/analytics/tables/requestEvents.js +14 -0
- package/dist/analytics/tables/requestEvents.js.map +1 -1
- package/dist/apps/data/App.d.ts +1354 -984
- package/dist/apps/data/App.d.ts.map +1 -1
- package/dist/apps/data/App.js +20 -3
- package/dist/apps/data/App.js.map +1 -1
- package/dist/apps/data/FundingRouteProviders.d.ts +94 -5
- package/dist/apps/data/FundingRouteProviders.d.ts.map +1 -1
- package/dist/apps/data/FundingRouteProviders.js +196 -5
- package/dist/apps/data/FundingRouteProviders.js.map +1 -1
- package/dist/apps/data/routes/activities.d.ts +38 -38
- package/dist/apps/data/routes/addresses.d.ts +24 -24
- package/dist/apps/data/routes/balances.d.ts +45 -45
- package/dist/apps/data/routes/blocks.d.ts +39 -39
- package/dist/apps/data/routes/coingecko.d.ts +176 -176
- package/dist/apps/data/routes/exchanges.d.ts +453 -453
- package/dist/apps/data/routes/fee-amm.d.ts +101 -101
- package/dist/apps/data/routes/funding.d.ts +730 -138
- package/dist/apps/data/routes/funding.d.ts.map +1 -1
- package/dist/apps/data/routes/funding.js +234 -0
- package/dist/apps/data/routes/funding.js.map +1 -1
- package/dist/apps/data/routes/indexer.d.ts +5 -5
- package/dist/apps/data/routes/rpc.d.ts +31 -31
- package/dist/apps/data/routes/verified-tokens.d.ts +81 -81
- package/dist/apps/management/App.d.ts +1156 -615
- package/dist/apps/management/App.d.ts.map +1 -1
- package/dist/apps/management/App.js +2 -2
- package/dist/apps/management/Billing.d.ts +3 -0
- package/dist/apps/management/Billing.d.ts.map +1 -1
- package/dist/apps/management/Billing.js +7 -2
- package/dist/apps/management/Billing.js.map +1 -1
- package/dist/apps/management/routes/api-keys.d.ts +106 -90
- package/dist/apps/management/routes/api-keys.d.ts.map +1 -1
- package/dist/apps/management/routes/api-keys.js +41 -30
- package/dist/apps/management/routes/api-keys.js.map +1 -1
- package/dist/apps/management/routes/billing.d.ts +278 -146
- package/dist/apps/management/routes/billing.d.ts.map +1 -1
- package/dist/apps/management/routes/billing.js +14 -14
- package/dist/apps/management/routes/billing.js.map +1 -1
- package/dist/apps/management/routes/invitations.d.ts +93 -20
- package/dist/apps/management/routes/invitations.d.ts.map +1 -1
- package/dist/apps/management/routes/invitations.js +12 -15
- package/dist/apps/management/routes/invitations.js.map +1 -1
- package/dist/apps/management/routes/me.d.ts +13 -13
- package/dist/apps/management/routes/members.d.ts +89 -18
- package/dist/apps/management/routes/members.d.ts.map +1 -1
- package/dist/apps/management/routes/members.js +10 -10
- package/dist/apps/management/routes/members.js.map +1 -1
- package/dist/apps/management/routes/orgs.d.ts +130 -70
- package/dist/apps/management/routes/orgs.d.ts.map +1 -1
- package/dist/apps/management/routes/orgs.js +19 -19
- package/dist/apps/management/routes/orgs.js.map +1 -1
- package/dist/apps/management/routes/projects.d.ts +137 -48
- package/dist/apps/management/routes/projects.d.ts.map +1 -1
- package/dist/apps/management/routes/projects.js +12 -13
- package/dist/apps/management/routes/projects.js.map +1 -1
- package/dist/apps/management/routes/scopes.d.ts +14 -28
- package/dist/apps/management/routes/scopes.d.ts.map +1 -1
- package/dist/apps/management/routes/scopes.js +6 -7
- package/dist/apps/management/routes/scopes.js.map +1 -1
- package/dist/apps/management/routes/usage.d.ts +63 -19
- package/dist/apps/management/routes/usage.d.ts.map +1 -1
- package/dist/apps/management/routes/usage.js +9 -10
- package/dist/apps/management/routes/usage.js.map +1 -1
- package/dist/apps/mcp.d.ts.map +1 -1
- package/dist/apps/mcp.js +35 -3
- package/dist/apps/mcp.js.map +1 -1
- package/dist/apps/relay/App.d.ts +26 -4
- package/dist/apps/relay/App.d.ts.map +1 -1
- package/dist/apps/relay/App.js +17 -14
- package/dist/apps/relay/App.js.map +1 -1
- package/dist/db/Db.d.ts +5 -3
- package/dist/db/Db.d.ts.map +1 -1
- package/dist/db/Db.js +52 -0
- package/dist/db/Db.js.map +1 -1
- package/dist/db/Schema.d.ts +13 -0
- package/dist/db/Schema.d.ts.map +1 -1
- package/dist/db/Schema.js +16 -1
- package/dist/db/Schema.js.map +1 -1
- package/dist/db/tables/adminAuditLogs.d.ts +27 -0
- package/dist/db/tables/adminAuditLogs.d.ts.map +1 -0
- package/dist/db/tables/adminAuditLogs.js +19 -0
- package/dist/db/tables/adminAuditLogs.js.map +1 -0
- package/dist/db/tables/invitations.d.ts +1 -1
- package/dist/db/tables/invitations.d.ts.map +1 -1
- package/dist/db/tables/memberships.d.ts +32 -4
- package/dist/db/tables/memberships.d.ts.map +1 -1
- package/dist/db/tables/memberships.js +31 -0
- package/dist/db/tables/memberships.js.map +1 -1
- package/dist/db/tables/organizations.d.ts +43 -1
- package/dist/db/tables/organizations.d.ts.map +1 -1
- package/dist/db/tables/organizations.js +103 -0
- package/dist/db/tables/organizations.js.map +1 -1
- package/dist/db/tables/projects.d.ts +25 -0
- package/dist/db/tables/projects.d.ts.map +1 -1
- package/dist/db/tables/projects.js +22 -0
- package/dist/db/tables/projects.js.map +1 -1
- package/dist/db/tables/webhookDeliveries.d.ts +26 -0
- package/dist/db/tables/webhookDeliveries.d.ts.map +1 -1
- package/dist/db/tables/webhookDeliveries.js +37 -0
- package/dist/db/tables/webhookDeliveries.js.map +1 -1
- package/dist/handlers/relay.d.ts +13 -10
- package/dist/handlers/relay.d.ts.map +1 -1
- package/dist/handlers/relay.js +46 -16
- package/dist/handlers/relay.js.map +1 -1
- package/dist/internal/Auth.d.ts +13 -18
- package/dist/internal/Auth.d.ts.map +1 -1
- package/dist/internal/Auth.js +78 -58
- package/dist/internal/Auth.js.map +1 -1
- package/dist/internal/Cursor.d.ts +1 -1
- package/dist/internal/Cursor.d.ts.map +1 -1
- package/dist/internal/Cursor.js +4 -0
- package/dist/internal/Cursor.js.map +1 -1
- package/dist/internal/EdgeCache.d.ts.map +1 -1
- package/dist/internal/EdgeCache.js +2 -0
- package/dist/internal/EdgeCache.js.map +1 -1
- package/dist/internal/FundingRoutes.d.ts +170 -0
- package/dist/internal/FundingRoutes.d.ts.map +1 -1
- package/dist/internal/FundingRoutes.js +68 -6
- package/dist/internal/FundingRoutes.js.map +1 -1
- package/dist/internal/Log.d.ts +16 -1
- package/dist/internal/Log.d.ts.map +1 -1
- package/dist/internal/Log.js +5 -3
- package/dist/internal/Log.js.map +1 -1
- package/package.json +2 -2
- package/src/ApiKey.ts +2 -3
- package/src/ApiKeys.ts +95 -17
- package/src/App.test.ts +44 -2
- package/src/App.ts +29 -9
- package/src/Assets.ts +8 -0
- package/src/Auth.test-d.ts +36 -0
- package/src/Auth.ts +20 -1
- package/src/Client.test-d.ts +64 -17
- package/src/Scope.test.ts +34 -0
- package/src/Scope.ts +53 -17
- package/src/admin/App.test.ts +33 -3
- package/src/admin/App.ts +49 -14
- package/src/admin/apps/api-keys.ts +10 -3
- package/src/admin/apps/organizations.test.ts +455 -0
- package/src/admin/apps/organizations.ts +953 -0
- package/src/admin/apps/scopes.test.ts +10 -0
- package/src/admin/ui/plugin.ts +1 -1
- package/src/admin/ui/src/components/MobileProfile.tsx +2 -0
- package/src/admin/ui/src/components/ThemeToggle.tsx +20 -0
- package/src/admin/ui/src/lib/api-keys.ts +36 -19
- package/src/admin/ui/src/lib/config.ts +2 -3
- package/src/admin/ui/src/lib/organizations.test.ts +89 -0
- package/src/admin/ui/src/lib/organizations.ts +262 -0
- package/src/admin/ui/src/lib/query-client.ts +3 -1
- package/src/admin/ui/src/lib/theme.ts +57 -0
- package/src/admin/ui/src/routeTree.gen.ts +47 -4
- package/src/admin/ui/src/routes/__root.tsx +18 -3
- package/src/admin/ui/src/routes/api-keys.tsx +52 -7
- package/src/admin/ui/src/routes/index.tsx +1 -1
- package/src/admin/ui/src/routes/organizations.tsx +158 -0
- package/src/admin/ui/src/routes/organizations_.$orgId.tsx +710 -0
- package/src/admin/ui/src/start.ts +6 -0
- package/src/admin/ui.generated.ts +63 -13
- package/src/analytics/Analytics.ts +25 -0
- package/src/analytics/tables/requestEvents.ts +33 -0
- package/src/apps/composition.test.ts +82 -2
- package/src/apps/data/App.ts +24 -3
- package/src/apps/data/FundingRouteProviders.test.ts +204 -0
- package/src/apps/data/FundingRouteProviders.ts +294 -9
- package/src/apps/data/routes/funding.test.ts +90 -23
- package/src/apps/data/routes/funding.ts +315 -0
- package/src/apps/data/routes/rpc.test.ts +33 -0
- package/src/apps/management/App.ts +2 -2
- package/src/apps/management/Billing.ts +10 -2
- package/src/apps/management/routes/api-keys.test.ts +137 -1
- package/src/apps/management/routes/api-keys.ts +63 -38
- package/src/apps/management/routes/billing.ts +14 -14
- package/src/apps/management/routes/invitations.test.ts +102 -1
- package/src/apps/management/routes/invitations.ts +13 -15
- package/src/apps/management/routes/members.ts +10 -10
- package/src/apps/management/routes/orgs.test.ts +78 -16
- package/src/apps/management/routes/orgs.ts +21 -20
- package/src/apps/management/routes/projects.ts +12 -13
- package/src/apps/management/routes/scopes.test.ts +16 -0
- package/src/apps/management/routes/scopes.ts +6 -7
- package/src/apps/management/routes/usage.ts +9 -14
- package/src/apps/mcp.test.ts +25 -1
- package/src/apps/mcp.ts +43 -3
- package/src/apps/relay/App.test.ts +23 -18
- package/src/apps/relay/App.ts +35 -14
- package/src/db/Db.ts +59 -3
- package/src/db/Schema.ts +20 -1
- package/src/db/tables/adminAuditLogs.ts +47 -0
- package/src/db/tables/invitations.ts +1 -1
- package/src/db/tables/memberships.ts +59 -3
- package/src/db/tables/organizations.ts +148 -1
- package/src/db/tables/projects.ts +47 -0
- package/src/db/tables/webhookDeliveries.ts +83 -0
- package/src/handlers/relay.test-d.ts +8 -0
- package/src/handlers/relay.test.ts +2 -2
- package/src/handlers/relay.ts +62 -22
- package/src/internal/Auth.test.ts +174 -7
- package/src/internal/Auth.ts +90 -62
- package/src/internal/Cursor.test.ts +10 -0
- package/src/internal/Cursor.ts +5 -1
- package/src/internal/EdgeCache.test.ts +11 -0
- package/src/internal/EdgeCache.ts +2 -0
- package/src/internal/FundingRoutes.test.ts +26 -0
- package/src/internal/FundingRoutes.ts +187 -18
- package/src/internal/Log.test.ts +41 -3
- 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
|
+
]
|