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
|
@@ -24,6 +24,16 @@ describe('GET /scopes', () => {
|
|
|
24
24
|
"scope": "indexer:query",
|
|
25
25
|
"selfServe": true,
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
"description": "Read organization resources across projects and environments.",
|
|
29
|
+
"scope": "management:read",
|
|
30
|
+
"selfServe": true,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"description": "Create, update, and delete organization resources across projects and environments.",
|
|
34
|
+
"scope": "management:write",
|
|
35
|
+
"selfServe": true,
|
|
36
|
+
},
|
|
27
37
|
{
|
|
28
38
|
"description": "Fill and submit transactions through the relay.",
|
|
29
39
|
"scope": "rpc-relay:read",
|
package/src/admin/ui/plugin.ts
CHANGED
|
@@ -23,7 +23,7 @@ const outFile = path.join(here, '..', 'ui.generated.ts')
|
|
|
23
23
|
// Total gzipped budget for the whole UI; the React + TanStack + Regen baseline
|
|
24
24
|
// is heavier than a hand-rolled SPA, so fail loudly before it bloats the
|
|
25
25
|
// package. Raise deliberately (and revisit code-splitting) if it trips.
|
|
26
|
-
const maxGzipBytes =
|
|
26
|
+
const maxGzipBytes = 405_000
|
|
27
27
|
|
|
28
28
|
const contentTypes: Record<string, string> = {
|
|
29
29
|
'.css': 'text/css; charset=utf-8',
|
|
@@ -3,6 +3,7 @@ import { Identicon } from 'regen-ui'
|
|
|
3
3
|
import { LogOut } from 'lucide-react'
|
|
4
4
|
|
|
5
5
|
import * as Config from '../lib/config'
|
|
6
|
+
import { ThemeToggle } from './ThemeToggle'
|
|
6
7
|
|
|
7
8
|
/** Mobile-only header profile: identicon + sign-out, shown when the sidebar is hidden. */
|
|
8
9
|
export function MobileProfile() {
|
|
@@ -12,6 +13,7 @@ export function MobileProfile() {
|
|
|
12
13
|
return (
|
|
13
14
|
<div className="flex items-center gap-2">
|
|
14
15
|
<Identicon address={identiconAddress(identity.email)} className="size-9 shrink-0" size={36} />
|
|
16
|
+
<ThemeToggle />
|
|
15
17
|
{config.auth.logoutUrl ? (
|
|
16
18
|
<a
|
|
17
19
|
aria-label="Sign out"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Moon, Sun } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
import * as Theme from '../lib/theme'
|
|
4
|
+
|
|
5
|
+
/** Button that switches between the light and dark color themes. */
|
|
6
|
+
export function ThemeToggle() {
|
|
7
|
+
const theme = Theme.use()
|
|
8
|
+
const label = theme === 'dark' ? 'Light mode' : 'Dark mode'
|
|
9
|
+
return (
|
|
10
|
+
<button
|
|
11
|
+
aria-label={label}
|
|
12
|
+
className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-button text-foreground-secondary hover:bg-surface-hover hover:text-foreground"
|
|
13
|
+
onClick={() => Theme.set(theme === 'dark' ? 'light' : 'dark')}
|
|
14
|
+
title={label}
|
|
15
|
+
type="button"
|
|
16
|
+
>
|
|
17
|
+
{theme === 'dark' ? <Sun className="size-[18px]" /> : <Moon className="size-[18px]" />}
|
|
18
|
+
</button>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -1,22 +1,49 @@
|
|
|
1
1
|
import * as ReactQuery from '@tanstack/react-query'
|
|
2
2
|
import type { InferRequestType, InferResponseType } from 'hono/client'
|
|
3
|
+
import * as z from 'zod/mini'
|
|
3
4
|
|
|
4
5
|
import { client } from './client'
|
|
5
6
|
|
|
6
7
|
/** A single API key record from the list endpoint. */
|
|
7
8
|
export type ApiKey = InferResponseType<(typeof client)['api-keys']['$get'], 200>['data'][number]
|
|
8
9
|
|
|
10
|
+
/** URL-backed API key filters. */
|
|
11
|
+
export const search = z.object({
|
|
12
|
+
orgId: z.catch(z.optional(z.string()), undefined),
|
|
13
|
+
projectId: z.catch(z.optional(z.string()), undefined),
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
/** API key list filters. */
|
|
17
|
+
export type Filters = z.output<typeof search>
|
|
18
|
+
|
|
9
19
|
/** List API keys. */
|
|
10
20
|
export namespace get {
|
|
11
|
-
export const
|
|
12
|
-
export function
|
|
13
|
-
return
|
|
21
|
+
export const baseKey = ['api-keys'] as const
|
|
22
|
+
export function key(filters: Filters = {}) {
|
|
23
|
+
return [...baseKey, filters.orgId ?? '', filters.projectId ?? ''] as const
|
|
14
24
|
}
|
|
15
|
-
export function
|
|
16
|
-
return
|
|
25
|
+
export function fn(filters: Filters = {}) {
|
|
26
|
+
return client['api-keys']
|
|
27
|
+
.$get({ query: filters.orgId ? { orgId: filters.orgId } : {} })
|
|
28
|
+
.then(async (response) => {
|
|
29
|
+
if (!response.ok) throw new Error('Failed to load API keys.')
|
|
30
|
+
const body = await response.json()
|
|
31
|
+
return {
|
|
32
|
+
data: filters.projectId
|
|
33
|
+
? body.data.filter((record) => record.projectId === filters.projectId)
|
|
34
|
+
: body.data,
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
export function options(filters: Filters = {}) {
|
|
39
|
+
return ReactQuery.queryOptions({
|
|
40
|
+
queryFn: () => fn(filters),
|
|
41
|
+
queryKey: key(filters),
|
|
42
|
+
staleTime: 30_000,
|
|
43
|
+
})
|
|
17
44
|
}
|
|
18
|
-
export function useQuery() {
|
|
19
|
-
return ReactQuery.useQuery(options())
|
|
45
|
+
export function useQuery(filters: Filters = {}) {
|
|
46
|
+
return ReactQuery.useQuery(options(filters))
|
|
20
47
|
}
|
|
21
48
|
}
|
|
22
49
|
|
|
@@ -33,12 +60,7 @@ export namespace provision {
|
|
|
33
60
|
const queryClient = ReactQuery.useQueryClient()
|
|
34
61
|
return ReactQuery.useMutation({
|
|
35
62
|
mutationFn: fn,
|
|
36
|
-
onSuccess: ({
|
|
37
|
-
queryClient.setQueryData(
|
|
38
|
-
get.key,
|
|
39
|
-
(previous: { data: ApiKey[] } | undefined) =>
|
|
40
|
-
previous && { data: [record, ...previous.data] },
|
|
41
|
-
),
|
|
63
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: get.baseKey }),
|
|
42
64
|
})
|
|
43
65
|
}
|
|
44
66
|
}
|
|
@@ -52,12 +74,7 @@ export namespace revoke {
|
|
|
52
74
|
const queryClient = ReactQuery.useQueryClient()
|
|
53
75
|
return ReactQuery.useMutation({
|
|
54
76
|
mutationFn: fn,
|
|
55
|
-
onSuccess: (
|
|
56
|
-
queryClient.setQueryData(
|
|
57
|
-
get.key,
|
|
58
|
-
(previous: { data: ApiKey[] } | undefined) =>
|
|
59
|
-
previous && { data: previous.data.filter((key) => key.id !== id) },
|
|
60
|
-
),
|
|
77
|
+
onSuccess: () => queryClient.invalidateQueries({ queryKey: get.baseKey }),
|
|
61
78
|
})
|
|
62
79
|
}
|
|
63
80
|
}
|
|
@@ -39,14 +39,13 @@ export async function getConfig(): Promise<Config> {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Bootstrap config query.
|
|
43
|
-
*
|
|
42
|
+
* Bootstrap config query. The query function reads the inlined value without a
|
|
43
|
+
* network round trip after the empty prerendered shell hydrates.
|
|
44
44
|
*/
|
|
45
45
|
export function options() {
|
|
46
46
|
return queryOptions({
|
|
47
47
|
queryKey: key,
|
|
48
48
|
queryFn: getConfig,
|
|
49
|
-
initialData: inlineConfig,
|
|
50
49
|
staleTime: Infinity,
|
|
51
50
|
})
|
|
52
51
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { QueryClient } from '@tanstack/react-query'
|
|
2
|
+
import { createMemoryHistory, createRouter } from '@tanstack/react-router'
|
|
3
|
+
import { describe, expect, it, vi } from 'vite-plus/test'
|
|
4
|
+
|
|
5
|
+
import { routeTree } from '../routeTree.gen'
|
|
6
|
+
import { Route as DetailRoute } from '../routes/organizations_.$orgId'
|
|
7
|
+
import { Route as ListRoute } from '../routes/organizations'
|
|
8
|
+
import * as Organizations from './organizations'
|
|
9
|
+
|
|
10
|
+
describe('schema.Search', () => {
|
|
11
|
+
it('normalizes valid lookup state', () => {
|
|
12
|
+
expect(Organizations.schema.Search.parse({ period: '24h', query: ' acme ' }))
|
|
13
|
+
.toMatchInlineSnapshot(`
|
|
14
|
+
{
|
|
15
|
+
"period": "24h",
|
|
16
|
+
"query": "acme",
|
|
17
|
+
}
|
|
18
|
+
`)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('drops lookup state below the API minimum', () => {
|
|
22
|
+
expect(Organizations.schema.Search.parse({ query: 'ab' })).toMatchInlineSnapshot(`
|
|
23
|
+
{
|
|
24
|
+
"query": undefined,
|
|
25
|
+
}
|
|
26
|
+
`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('drops an invalid usage period', () => {
|
|
30
|
+
expect(Organizations.schema.Search.parse({ period: 'forever' })).toMatchInlineSnapshot(`
|
|
31
|
+
{
|
|
32
|
+
"period": undefined,
|
|
33
|
+
}
|
|
34
|
+
`)
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe('searchParameters', () => {
|
|
39
|
+
it('preserves a lookup across list and detail navigation', () => {
|
|
40
|
+
expect(Organizations.searchParameters({ query: 'acme' })).toMatchInlineSnapshot(`
|
|
41
|
+
{
|
|
42
|
+
"query": "acme",
|
|
43
|
+
}
|
|
44
|
+
`)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('omits empty lookup state', () => {
|
|
48
|
+
expect(Organizations.searchParameters({})).toMatchInlineSnapshot(`{}`)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('organization routes', () => {
|
|
53
|
+
it('matches a direct detail URL without the list route as a parent', () => {
|
|
54
|
+
const router = createRouter({
|
|
55
|
+
context: { queryClient: new QueryClient() },
|
|
56
|
+
history: createMemoryHistory({
|
|
57
|
+
initialEntries: ['/organizations/org_example?query=acme'],
|
|
58
|
+
}),
|
|
59
|
+
routeTree,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
expect(router.matchRoutes('/organizations/org_example').map((match) => match.routeId))
|
|
63
|
+
.toMatchInlineSnapshot(`
|
|
64
|
+
[
|
|
65
|
+
"__root__",
|
|
66
|
+
"/organizations_/$orgId",
|
|
67
|
+
]
|
|
68
|
+
`)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('blocks only on the primary organization query', async () => {
|
|
72
|
+
const ensureQueryData = vi.fn().mockResolvedValue({})
|
|
73
|
+
const ensureInfiniteQueryData = vi.fn()
|
|
74
|
+
|
|
75
|
+
const loader = DetailRoute.options.loader as unknown as (options: unknown) => Promise<unknown>
|
|
76
|
+
await loader({
|
|
77
|
+
context: { queryClient: { ensureInfiniteQueryData, ensureQueryData } },
|
|
78
|
+
params: { orgId: 'org_example' },
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(ensureQueryData).toHaveBeenCalledOnce()
|
|
82
|
+
expect(ensureInfiniteQueryData).not.toHaveBeenCalled()
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('provides recoverable list and detail error surfaces', () => {
|
|
86
|
+
expect(ListRoute.options.errorComponent).toBeTypeOf('function')
|
|
87
|
+
expect(DetailRoute.options.errorComponent).toBeTypeOf('function')
|
|
88
|
+
})
|
|
89
|
+
})
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import * as ReactQuery from '@tanstack/react-query'
|
|
2
|
+
import type { InferResponseType } from 'hono/client'
|
|
3
|
+
import * as z from 'zod/mini'
|
|
4
|
+
|
|
5
|
+
import { client } from './client'
|
|
6
|
+
|
|
7
|
+
type ListResponse = InferResponseType<(typeof client)['organizations']['$get'], 200>
|
|
8
|
+
type DetailResponse = InferResponseType<(typeof client)['organizations'][':orgId']['$get'], 200>
|
|
9
|
+
type MembersResponse = InferResponseType<
|
|
10
|
+
(typeof client)['organizations'][':orgId']['members']['$get'],
|
|
11
|
+
200
|
|
12
|
+
>
|
|
13
|
+
type ProjectsResponse = InferResponseType<
|
|
14
|
+
(typeof client)['organizations'][':orgId']['projects']['$get'],
|
|
15
|
+
200
|
|
16
|
+
>
|
|
17
|
+
type ApiKeysResponse = InferResponseType<
|
|
18
|
+
(typeof client)['organizations'][':orgId']['api-keys']['$get'],
|
|
19
|
+
200
|
|
20
|
+
>
|
|
21
|
+
type SponsorshipResponse = InferResponseType<
|
|
22
|
+
(typeof client)['organizations'][':orgId']['sponsorship']['$get'],
|
|
23
|
+
200
|
|
24
|
+
>
|
|
25
|
+
type UsageResponse = InferResponseType<
|
|
26
|
+
(typeof client)['organizations'][':orgId']['usage']['$get'],
|
|
27
|
+
200
|
|
28
|
+
>
|
|
29
|
+
type WebhooksResponse = InferResponseType<
|
|
30
|
+
(typeof client)['organizations'][':orgId']['webhooks']['$get'],
|
|
31
|
+
200
|
|
32
|
+
>
|
|
33
|
+
|
|
34
|
+
/** An organization returned by the admin lookup API. */
|
|
35
|
+
export type Organization = ListResponse['data'][number]
|
|
36
|
+
|
|
37
|
+
/** Detailed organization information, including billing state. */
|
|
38
|
+
export type Detail = DetailResponse
|
|
39
|
+
|
|
40
|
+
/** An organization member with resolved identity. */
|
|
41
|
+
export type Member = MembersResponse['data'][number]
|
|
42
|
+
|
|
43
|
+
/** A project owned by an organization. */
|
|
44
|
+
export type Project = ProjectsResponse['data'][number]
|
|
45
|
+
|
|
46
|
+
/** Operational API key state for an organization. */
|
|
47
|
+
export type ApiKey = ApiKeysResponse['data'][number]
|
|
48
|
+
|
|
49
|
+
/** Recent organization sponsorship activity. */
|
|
50
|
+
export type Sponsorship = SponsorshipResponse
|
|
51
|
+
|
|
52
|
+
/** Recent organization API usage. */
|
|
53
|
+
export type Usage = UsageResponse
|
|
54
|
+
|
|
55
|
+
/** Organization webhook subscriptions and failures. */
|
|
56
|
+
export type Webhooks = WebhooksResponse
|
|
57
|
+
|
|
58
|
+
/** Schemas shared by organization lookup routes. */
|
|
59
|
+
export namespace schema {
|
|
60
|
+
/** Search parameters accepted by organization list and detail routes. */
|
|
61
|
+
export const Search = z.object({
|
|
62
|
+
period: z.catch(z.optional(z.enum(['24h', '7d', '30d'])), undefined),
|
|
63
|
+
query: z.catch(
|
|
64
|
+
z.optional(
|
|
65
|
+
z
|
|
66
|
+
.pipe(
|
|
67
|
+
z.string(),
|
|
68
|
+
z.transform((value) => value.trim()),
|
|
69
|
+
)
|
|
70
|
+
.check(z.minLength(3), z.maxLength(200)),
|
|
71
|
+
),
|
|
72
|
+
undefined,
|
|
73
|
+
),
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Builds canonical organization lookup search parameters. */
|
|
78
|
+
export function searchParameters(options: searchParameters.Options) {
|
|
79
|
+
return options.query ? { query: options.query } : {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export declare namespace searchParameters {
|
|
83
|
+
/** Organization lookup navigation state. */
|
|
84
|
+
type Options = { query?: string | undefined }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** List organizations, optionally filtered by id, name, or member email. */
|
|
88
|
+
export namespace list {
|
|
89
|
+
/** Organization lookup filters. */
|
|
90
|
+
export type Input = { query?: string | undefined }
|
|
91
|
+
|
|
92
|
+
export function key(options: Input) {
|
|
93
|
+
return ['organizations', 'list', options.query ?? ''] as const
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function fn(options: Input & { cursor?: string | undefined }) {
|
|
97
|
+
const response = await client.organizations.$get({
|
|
98
|
+
query: {
|
|
99
|
+
...(options.cursor ? { cursor: options.cursor } : {}),
|
|
100
|
+
limit: '25',
|
|
101
|
+
...(options.query ? { query: options.query } : {}),
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
if (!response.ok) throw new Error('Failed to load organizations.')
|
|
105
|
+
return (await response.json()) as ListResponse
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function options(options: Input) {
|
|
109
|
+
return ReactQuery.infiniteQueryOptions({
|
|
110
|
+
getNextPageParam: (page: ListResponse) => page.nextCursor,
|
|
111
|
+
initialPageParam: null as string | null,
|
|
112
|
+
queryFn: ({ pageParam }) => fn({ ...options, cursor: pageParam ?? undefined }),
|
|
113
|
+
queryKey: key(options),
|
|
114
|
+
staleTime: 30_000,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Get one organization by id. */
|
|
120
|
+
export namespace get {
|
|
121
|
+
export function key(orgId: string) {
|
|
122
|
+
return ['organizations', 'detail', orgId] as const
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function fn(orgId: string) {
|
|
126
|
+
const response = await client.organizations[':orgId'].$get({ param: { orgId } })
|
|
127
|
+
if (response.status === 404) throw new Error('Organization not found.')
|
|
128
|
+
if (!response.ok) throw new Error('Failed to load organization.')
|
|
129
|
+
return (await response.json()) as DetailResponse
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function options(orgId: string) {
|
|
133
|
+
return ReactQuery.queryOptions({
|
|
134
|
+
queryFn: () => fn(orgId),
|
|
135
|
+
queryKey: key(orgId),
|
|
136
|
+
staleTime: 30_000,
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** List members for one organization. */
|
|
142
|
+
export namespace members {
|
|
143
|
+
export function key(orgId: string) {
|
|
144
|
+
return ['organizations', orgId, 'members'] as const
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function fn(options: { cursor?: string | undefined; orgId: string }) {
|
|
148
|
+
const response = await client.organizations[':orgId'].members.$get({
|
|
149
|
+
param: { orgId: options.orgId },
|
|
150
|
+
query: { ...(options.cursor ? { cursor: options.cursor } : {}), limit: '25' },
|
|
151
|
+
})
|
|
152
|
+
if (!response.ok) throw new Error('Failed to load organization members.')
|
|
153
|
+
return (await response.json()) as MembersResponse
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function options(orgId: string) {
|
|
157
|
+
return ReactQuery.infiniteQueryOptions({
|
|
158
|
+
getNextPageParam: (page: MembersResponse) => page.nextCursor,
|
|
159
|
+
initialPageParam: null as string | null,
|
|
160
|
+
queryFn: ({ pageParam }) => fn({ cursor: pageParam ?? undefined, orgId }),
|
|
161
|
+
queryKey: key(orgId),
|
|
162
|
+
staleTime: 30_000,
|
|
163
|
+
})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** List projects for one organization. */
|
|
168
|
+
export namespace projects {
|
|
169
|
+
export function key(orgId: string) {
|
|
170
|
+
return ['organizations', orgId, 'projects'] as const
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function fn(options: { cursor?: string | undefined; orgId: string }) {
|
|
174
|
+
const response = await client.organizations[':orgId'].projects.$get({
|
|
175
|
+
param: { orgId: options.orgId },
|
|
176
|
+
query: { ...(options.cursor ? { cursor: options.cursor } : {}), limit: '25' },
|
|
177
|
+
})
|
|
178
|
+
if (!response.ok) throw new Error('Failed to load organization projects.')
|
|
179
|
+
return (await response.json()) as ProjectsResponse
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function options(orgId: string) {
|
|
183
|
+
return ReactQuery.infiniteQueryOptions({
|
|
184
|
+
getNextPageParam: (page: ProjectsResponse) => page.nextCursor,
|
|
185
|
+
initialPageParam: null as string | null,
|
|
186
|
+
queryFn: ({ pageParam }) => fn({ cursor: pageParam ?? undefined, orgId }),
|
|
187
|
+
queryKey: key(orgId),
|
|
188
|
+
staleTime: 30_000,
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** List operational API key state for one organization. */
|
|
194
|
+
export namespace apiKeys {
|
|
195
|
+
export function options(orgId: string) {
|
|
196
|
+
return ReactQuery.queryOptions({
|
|
197
|
+
queryFn: async () => {
|
|
198
|
+
const response = await client.organizations[':orgId']['api-keys'].$get({
|
|
199
|
+
param: { orgId },
|
|
200
|
+
})
|
|
201
|
+
if (!response.ok) throw new Error('Failed to load organization API keys.')
|
|
202
|
+
return (await response.json()) as ApiKeysResponse
|
|
203
|
+
},
|
|
204
|
+
queryKey: ['organizations', orgId, 'api-keys'],
|
|
205
|
+
staleTime: 30_000,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** Get recent sponsorship activity for one organization. */
|
|
211
|
+
export namespace sponsorship {
|
|
212
|
+
export function options(orgId: string) {
|
|
213
|
+
return ReactQuery.queryOptions({
|
|
214
|
+
queryFn: async () => {
|
|
215
|
+
const response = await client.organizations[':orgId'].sponsorship.$get({
|
|
216
|
+
param: { orgId },
|
|
217
|
+
})
|
|
218
|
+
if (!response.ok) throw new Error('Failed to load sponsorship activity.')
|
|
219
|
+
return (await response.json()) as SponsorshipResponse
|
|
220
|
+
},
|
|
221
|
+
queryKey: ['organizations', orgId, 'sponsorship'],
|
|
222
|
+
staleTime: 30_000,
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Get recent API usage for one organization. */
|
|
228
|
+
export namespace usage {
|
|
229
|
+
export type Period = '24h' | '30d' | '7d'
|
|
230
|
+
|
|
231
|
+
export function options(orgId: string, period: Period) {
|
|
232
|
+
return ReactQuery.queryOptions({
|
|
233
|
+
queryFn: async () => {
|
|
234
|
+
const response = await client.organizations[':orgId'].usage.$get({
|
|
235
|
+
param: { orgId },
|
|
236
|
+
query: { period },
|
|
237
|
+
})
|
|
238
|
+
if (!response.ok) throw new Error('Failed to load API usage.')
|
|
239
|
+
return (await response.json()) as UsageResponse
|
|
240
|
+
},
|
|
241
|
+
queryKey: ['organizations', orgId, 'usage', period],
|
|
242
|
+
staleTime: 30_000,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Get webhook health for one organization. */
|
|
248
|
+
export namespace webhooks {
|
|
249
|
+
export function options(orgId: string) {
|
|
250
|
+
return ReactQuery.queryOptions({
|
|
251
|
+
queryFn: async () => {
|
|
252
|
+
const response = await client.organizations[':orgId'].webhooks.$get({
|
|
253
|
+
param: { orgId },
|
|
254
|
+
})
|
|
255
|
+
if (!response.ok) throw new Error('Failed to load webhook health.')
|
|
256
|
+
return (await response.json()) as WebhooksResponse
|
|
257
|
+
},
|
|
258
|
+
queryKey: ['organizations', orgId, 'webhooks'],
|
|
259
|
+
staleTime: 30_000,
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -21,6 +21,8 @@ export const persistOptions: Omit<PersistQueryClientOptions, 'queryClient'> = {
|
|
|
21
21
|
persister,
|
|
22
22
|
dehydrateOptions: {
|
|
23
23
|
shouldDehydrateQuery: (query) =>
|
|
24
|
-
query.queryKey[0] !== 'config' &&
|
|
24
|
+
query.queryKey[0] !== 'config' &&
|
|
25
|
+
query.queryKey[0] !== 'organizations' &&
|
|
26
|
+
defaultShouldDehydrateQuery(query),
|
|
25
27
|
},
|
|
26
28
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react'
|
|
2
|
+
|
|
3
|
+
/** Admin UI color theme. */
|
|
4
|
+
export type Theme = 'dark' | 'light'
|
|
5
|
+
|
|
6
|
+
const storageKey = 'tempo-api-admin.theme'
|
|
7
|
+
|
|
8
|
+
/** Background colors matching Regen's background token in each theme. */
|
|
9
|
+
export const backgrounds = { dark: '#0c0c0c', light: '#fafafa' } as const
|
|
10
|
+
|
|
11
|
+
/** Applies the saved theme and browser chrome color before the first paint. */
|
|
12
|
+
export const bootstrap = `try {
|
|
13
|
+
const backgrounds = { dark: '${backgrounds.dark}', light: '${backgrounds.light}' }
|
|
14
|
+
const stored = localStorage.getItem('${storageKey}')
|
|
15
|
+
const pinned = stored === 'dark' || stored === 'light' ? stored : null
|
|
16
|
+
if (pinned) document.documentElement.dataset.theme = pinned
|
|
17
|
+
for (const scheme of ['light', 'dark']) {
|
|
18
|
+
const meta = document.createElement('meta')
|
|
19
|
+
meta.name = 'theme-color'
|
|
20
|
+
meta.media = '(prefers-color-scheme: ' + scheme + ')'
|
|
21
|
+
meta.content = backgrounds[pinned ?? scheme]
|
|
22
|
+
document.head.append(meta)
|
|
23
|
+
}
|
|
24
|
+
} catch {}`
|
|
25
|
+
|
|
26
|
+
/** Returns the saved theme, falling back to the operating system preference. */
|
|
27
|
+
export function get(): Theme {
|
|
28
|
+
const stored = localStorage.getItem(storageKey)
|
|
29
|
+
if (stored === 'dark' || stored === 'light') return stored
|
|
30
|
+
return matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Saves and applies the selected theme. */
|
|
34
|
+
export function set(theme: Theme) {
|
|
35
|
+
localStorage.setItem(storageKey, theme)
|
|
36
|
+
document.documentElement.dataset.theme = theme
|
|
37
|
+
for (const meta of document.querySelectorAll('meta[name="theme-color"]'))
|
|
38
|
+
meta.setAttribute('content', backgrounds[theme])
|
|
39
|
+
for (const listener of listeners) listener()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Reactively returns the effective theme. */
|
|
43
|
+
export function use(): Theme {
|
|
44
|
+
return useSyncExternalStore(subscribe, get, () => 'dark')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const listeners = new Set<() => void>()
|
|
48
|
+
|
|
49
|
+
function subscribe(listener: () => void) {
|
|
50
|
+
const media = matchMedia('(prefers-color-scheme: dark)')
|
|
51
|
+
listeners.add(listener)
|
|
52
|
+
media.addEventListener('change', listener)
|
|
53
|
+
return () => {
|
|
54
|
+
listeners.delete(listener)
|
|
55
|
+
media.removeEventListener('change', listener)
|
|
56
|
+
}
|
|
57
|
+
}
|