@hanzo/dashboard 0.2.0 → 0.2.2
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/LICENSE.md +21 -0
- package/README.md +245 -0
- package/package.json +38 -50
- package/src/analytics/index.ts +20 -0
- package/src/analytics/parts.tsx +91 -0
- package/src/analytics/product.tsx +64 -0
- package/src/analytics/source.ts +141 -0
- package/src/analytics/web.tsx +83 -0
- package/src/charts/Charts.tsx +470 -0
- package/src/dashboard.css +157 -0
- package/src/index.ts +119 -0
- package/src/landing/Hero.tsx +115 -0
- package/src/landing/Landing.tsx +47 -0
- package/src/landing/Metrics.tsx +134 -0
- package/src/landing/Rail.tsx +61 -0
- package/src/landing/Samples.tsx +78 -0
- package/src/landing/logic.ts +47 -0
- package/src/landing/parts.tsx +116 -0
- package/src/landing/types.ts +98 -0
- package/src/motion/hooks.ts +98 -0
- package/src/motion/motion.ts +76 -0
- package/src/overview/Overview.tsx +203 -0
- package/src/overview/config.ts +177 -0
- package/src/overview/logic.ts +204 -0
- package/src/overview/primitives.tsx +306 -0
- package/src/overview/superadmin.test.ts +36 -0
- package/src/overview/tiles.tsx +223 -0
- package/src/pipeline/Pipeline.tsx +178 -0
- package/src/pipeline/stages.ts +156 -0
- package/src/types.ts +12 -0
- package/readme.md +0 -57
- package/src/lib/api.ts +0 -155
- package/src/lib/components/ConfirmDialog.tsx +0 -53
- package/src/lib/components/TenantRow.tsx +0 -39
- package/src/lib/components/TenantStatusBadge.tsx +0 -33
- package/src/lib/index.ts +0 -30
- package/src/lib/nav.ts +0 -12
- package/src/lib/screens/NewTenantScreen.tsx +0 -137
- package/src/lib/screens/TenantsScreen.tsx +0 -229
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { Text, XStack } from '@hanzo/gui'
|
|
2
|
-
import type { TenantStatus } from '../api'
|
|
3
|
-
|
|
4
|
-
// Color map mirrors the BaseApp.Status.Phase semantics — we read the
|
|
5
|
-
// Status verbatim from the operator (see plugin/tenant/types.go), so
|
|
6
|
-
// any new value will fall through to the neutral default rather than
|
|
7
|
-
// silently rendering wrong. `as const` keeps the hex literals narrow so
|
|
8
|
-
// they satisfy the Gui `bg`/`color` token unions (a widened `string` won't).
|
|
9
|
-
const NEUTRAL = { bg: '#3f3f46', fg: '#fafafa' } as const // zinc 700 / 50
|
|
10
|
-
const COLORS = {
|
|
11
|
-
Pending: NEUTRAL,
|
|
12
|
-
Creating: { bg: '#1e40af', fg: '#dbeafe' }, // blue 800 / 100
|
|
13
|
-
Running: { bg: '#166534', fg: '#dcfce7' }, // green 800 / 100
|
|
14
|
-
Degraded: { bg: '#92400e', fg: '#fef3c7' }, // amber 800 / 100
|
|
15
|
-
Deleting: { bg: '#991b1b', fg: '#fee2e2' }, // red 800 / 100
|
|
16
|
-
} as const satisfies Record<TenantStatus, { bg: string; fg: string }>
|
|
17
|
-
|
|
18
|
-
export function TenantStatusBadge({ status }: { status: TenantStatus }) {
|
|
19
|
-
const c = COLORS[status] ?? NEUTRAL
|
|
20
|
-
return (
|
|
21
|
-
<XStack
|
|
22
|
-
bg={c.bg}
|
|
23
|
-
px={8}
|
|
24
|
-
py={2}
|
|
25
|
-
rounded={4}
|
|
26
|
-
items="center"
|
|
27
|
-
>
|
|
28
|
-
<Text fontSize={12} color={c.fg} fontWeight="600">
|
|
29
|
-
{status}
|
|
30
|
-
</Text>
|
|
31
|
-
</XStack>
|
|
32
|
-
)
|
|
33
|
-
}
|
package/src/lib/index.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
// @hanzo/superbase-dashboard — the Hanzo Base tenant-management screens.
|
|
2
|
-
//
|
|
3
|
-
// Host-agnostic surface consumed by BOTH the standalone dashboard (base.hanzo.ai,
|
|
4
|
-
// the superbase Go binary) and the embedded console module (console.hanzo.ai,
|
|
5
|
-
// console2). ONE source of screens; each host supplies a transport + navigation.
|
|
6
|
-
//
|
|
7
|
-
// import { createTenantsApi, TenantsScreen, NewTenantScreen } from '@hanzo/superbase-dashboard'
|
|
8
|
-
//
|
|
9
|
-
// See ./api (createTenantsApi) and ./nav (TenantsNav) for the two injection
|
|
10
|
-
// points. The screens themselves never touch a token, a router, or a fetch.
|
|
11
|
-
|
|
12
|
-
export { createTenantsApi, ApiError } from './api'
|
|
13
|
-
export type {
|
|
14
|
-
TenantsApi,
|
|
15
|
-
TenantsTransport,
|
|
16
|
-
TenantRecord,
|
|
17
|
-
TenantStatus,
|
|
18
|
-
TenantSpec,
|
|
19
|
-
CreateTenantInput,
|
|
20
|
-
UpdateTenantInput,
|
|
21
|
-
ListResponse,
|
|
22
|
-
} from './api'
|
|
23
|
-
export type { TenantsNav } from './nav'
|
|
24
|
-
|
|
25
|
-
export { TenantsScreen } from './screens/TenantsScreen'
|
|
26
|
-
export { NewTenantScreen } from './screens/NewTenantScreen'
|
|
27
|
-
|
|
28
|
-
export { TenantStatusBadge } from './components/TenantStatusBadge'
|
|
29
|
-
export { TenantRow } from './components/TenantRow'
|
|
30
|
-
export { ConfirmDialog } from './components/ConfirmDialog'
|
package/src/lib/nav.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// Host-agnostic navigation surface for the Base screens.
|
|
2
|
-
//
|
|
3
|
-
// The screens never import a router. Each host maps these intents to its own
|
|
4
|
-
// routing: the standalone app to react-router `navigate`, console2 to the Next
|
|
5
|
-
// `useRouter().push` under the `/base` product path. Tenant DETAIL is a
|
|
6
|
-
// client-state drawer inside TenantsScreen — not a route — so it needs no nav.
|
|
7
|
-
export interface TenantsNav {
|
|
8
|
-
/** Return to the tenants list, optionally re-selecting a tenant by id. */
|
|
9
|
-
toList(opts?: { selected?: string }): void
|
|
10
|
-
/** Open the new-tenant form. */
|
|
11
|
-
toNew(): void
|
|
12
|
-
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Button, Card, Input, Label, Text, XStack, YStack } from '@hanzo/gui'
|
|
3
|
-
import type { TenantsApi } from '../api'
|
|
4
|
-
import type { TenantsNav } from '../nav'
|
|
5
|
-
|
|
6
|
-
// Slug validation mirrors the Base collection constraint (lowercase
|
|
7
|
-
// kebab-case, must start with a letter, 2-31 chars). Keep this in
|
|
8
|
-
// sync with the slug regex in plugin/tenant/collection.go.
|
|
9
|
-
const SLUG_RE = /^[a-z][a-z0-9-]{1,30}$/
|
|
10
|
-
|
|
11
|
-
const REPLICAS = [3, 5, 7] as const
|
|
12
|
-
const STORAGE = ['10Gi', '25Gi', '50Gi'] as const
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* The create-tenant form. Host-agnostic: submits through the injected `api`,
|
|
16
|
-
* returns to the list (re-selecting the new tenant) or cancels via `nav`.
|
|
17
|
-
*/
|
|
18
|
-
export function NewTenantScreen({ api, nav }: { api: TenantsApi; nav: TenantsNav }) {
|
|
19
|
-
const [slug, setSlug] = useState('')
|
|
20
|
-
const [name, setName] = useState('')
|
|
21
|
-
const [replicas, setReplicas] = useState<(typeof REPLICAS)[number]>(3)
|
|
22
|
-
const [storage, setStorage] = useState<(typeof STORAGE)[number]>('10Gi')
|
|
23
|
-
const [submitting, setSubmitting] = useState(false)
|
|
24
|
-
const [error, setError] = useState<string | null>(null)
|
|
25
|
-
|
|
26
|
-
const slugError =
|
|
27
|
-
slug.length === 0 ? null :
|
|
28
|
-
SLUG_RE.test(slug) ? null :
|
|
29
|
-
'Lowercase letters, digits and dashes only; must start with a letter; 2-31 chars.'
|
|
30
|
-
|
|
31
|
-
const canSubmit = slug.length > 0 && !slugError && name.length > 0 && !submitting
|
|
32
|
-
|
|
33
|
-
const onSubmit = async () => {
|
|
34
|
-
if (!canSubmit) return
|
|
35
|
-
setSubmitting(true)
|
|
36
|
-
setError(null)
|
|
37
|
-
try {
|
|
38
|
-
const created = await api.create({ slug, name, spec: { replicas, storage } })
|
|
39
|
-
nav.toList({ selected: created.id })
|
|
40
|
-
} catch (e) {
|
|
41
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
42
|
-
setSubmitting(false)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
return (
|
|
47
|
-
<YStack flex={1} p={24} gap={16} maxW={560}>
|
|
48
|
-
<XStack items="center" gap={12}>
|
|
49
|
-
<Text fontSize={24} fontWeight="700" color="#f4f4f5" flex={1}>New tenant</Text>
|
|
50
|
-
<Button onPress={() => nav.toList()}>Cancel</Button>
|
|
51
|
-
</XStack>
|
|
52
|
-
|
|
53
|
-
<Card bg="#09090b" borderColor="#27272a" borderWidth={1} p={20} gap={16}>
|
|
54
|
-
<YStack gap={6}>
|
|
55
|
-
<Label htmlFor="slug">
|
|
56
|
-
<Text color="#d4d4d8" fontWeight="600">Slug</Text>
|
|
57
|
-
</Label>
|
|
58
|
-
<Input
|
|
59
|
-
id="slug"
|
|
60
|
-
value={slug}
|
|
61
|
-
onChangeText={(v) => setSlug(v.toLowerCase())}
|
|
62
|
-
placeholder="acme-prod"
|
|
63
|
-
autoCapitalize="none"
|
|
64
|
-
/>
|
|
65
|
-
{slugError && <Text color="#fca5a5" fontSize={12}>{slugError}</Text>}
|
|
66
|
-
{!slugError && slug.length > 0 && (
|
|
67
|
-
<Text color="#71717a" fontSize={12}>
|
|
68
|
-
Subdomain will be <Text color="#a1a1aa" fontWeight="600">{slug}.base.hanzo.ai</Text>
|
|
69
|
-
</Text>
|
|
70
|
-
)}
|
|
71
|
-
</YStack>
|
|
72
|
-
|
|
73
|
-
<YStack gap={6}>
|
|
74
|
-
<Label htmlFor="name">
|
|
75
|
-
<Text color="#d4d4d8" fontWeight="600">Display name</Text>
|
|
76
|
-
</Label>
|
|
77
|
-
<Input
|
|
78
|
-
id="name"
|
|
79
|
-
value={name}
|
|
80
|
-
onChangeText={setName}
|
|
81
|
-
placeholder="Acme Production"
|
|
82
|
-
/>
|
|
83
|
-
</YStack>
|
|
84
|
-
|
|
85
|
-
<YStack gap={6}>
|
|
86
|
-
<Text color="#d4d4d8" fontWeight="600">Replicas</Text>
|
|
87
|
-
<XStack gap={8}>
|
|
88
|
-
{REPLICAS.map((n) => (
|
|
89
|
-
<Button
|
|
90
|
-
key={n}
|
|
91
|
-
theme={replicas === n ? 'blue' : undefined}
|
|
92
|
-
onPress={() => setReplicas(n)}
|
|
93
|
-
>
|
|
94
|
-
{n}
|
|
95
|
-
</Button>
|
|
96
|
-
))}
|
|
97
|
-
</XStack>
|
|
98
|
-
<Text color="#71717a" fontSize={12}>
|
|
99
|
-
Odd-count replica set for the writer-election quorum.
|
|
100
|
-
</Text>
|
|
101
|
-
</YStack>
|
|
102
|
-
|
|
103
|
-
<YStack gap={6}>
|
|
104
|
-
<Text color="#d4d4d8" fontWeight="600">Storage</Text>
|
|
105
|
-
<XStack gap={8}>
|
|
106
|
-
{STORAGE.map((s) => (
|
|
107
|
-
<Button
|
|
108
|
-
key={s}
|
|
109
|
-
theme={storage === s ? 'blue' : undefined}
|
|
110
|
-
onPress={() => setStorage(s)}
|
|
111
|
-
>
|
|
112
|
-
{s}
|
|
113
|
-
</Button>
|
|
114
|
-
))}
|
|
115
|
-
</XStack>
|
|
116
|
-
<Text color="#71717a" fontSize={12}>PVC size per replica.</Text>
|
|
117
|
-
</YStack>
|
|
118
|
-
|
|
119
|
-
{error && (
|
|
120
|
-
<Card p={12} bg="#450a0a" borderColor="#7f1d1d" borderWidth={1}>
|
|
121
|
-
<Text color="#fecaca">{error}</Text>
|
|
122
|
-
</Card>
|
|
123
|
-
)}
|
|
124
|
-
|
|
125
|
-
<XStack justify="flex-end">
|
|
126
|
-
<Button
|
|
127
|
-
theme="blue"
|
|
128
|
-
disabled={!canSubmit}
|
|
129
|
-
onPress={onSubmit}
|
|
130
|
-
>
|
|
131
|
-
{submitting ? 'Creating…' : 'Create tenant'}
|
|
132
|
-
</Button>
|
|
133
|
-
</XStack>
|
|
134
|
-
</Card>
|
|
135
|
-
</YStack>
|
|
136
|
-
)
|
|
137
|
-
}
|
|
@@ -1,229 +0,0 @@
|
|
|
1
|
-
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
-
import { Button, Card, Text, XStack, YStack } from '@hanzo/gui'
|
|
3
|
-
import { TenantRow } from '../components/TenantRow'
|
|
4
|
-
import { TenantStatusBadge } from '../components/TenantStatusBadge'
|
|
5
|
-
import { ConfirmDialog } from '../components/ConfirmDialog'
|
|
6
|
-
import type { TenantsApi, TenantRecord } from '../api'
|
|
7
|
-
import type { TenantsNav } from '../nav'
|
|
8
|
-
|
|
9
|
-
const POLL_MS = 5000
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* The tenants list + detail drawer. Host-agnostic: data comes from the injected
|
|
13
|
-
* `api`, navigation to the new-tenant form from `nav`. Detail is a client-state
|
|
14
|
-
* drawer (not a route), so it works identically standalone and embedded.
|
|
15
|
-
*/
|
|
16
|
-
export function TenantsScreen({ api, nav }: { api: TenantsApi; nav: TenantsNav }) {
|
|
17
|
-
const [tenants, setTenants] = useState<TenantRecord[]>([])
|
|
18
|
-
const [loading, setLoading] = useState(false)
|
|
19
|
-
const [error, setError] = useState<string | null>(null)
|
|
20
|
-
const [selected, setSelected] = useState<TenantRecord | null>(null)
|
|
21
|
-
const [confirmDelete, setConfirmDelete] = useState(false)
|
|
22
|
-
|
|
23
|
-
const refresh = useCallback(async () => {
|
|
24
|
-
setLoading(true)
|
|
25
|
-
setError(null)
|
|
26
|
-
try {
|
|
27
|
-
const r = await api.list({ perPage: 200 })
|
|
28
|
-
setTenants(r.items)
|
|
29
|
-
// Re-resolve selected from refreshed list so drawer reflects latest status.
|
|
30
|
-
setSelected((s) => (s ? r.items.find((x) => x.id === s.id) ?? null : null))
|
|
31
|
-
} catch (e) {
|
|
32
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
33
|
-
} finally {
|
|
34
|
-
setLoading(false)
|
|
35
|
-
}
|
|
36
|
-
}, [api])
|
|
37
|
-
|
|
38
|
-
useEffect(() => {
|
|
39
|
-
refresh()
|
|
40
|
-
const id = setInterval(refresh, POLL_MS)
|
|
41
|
-
return () => clearInterval(id)
|
|
42
|
-
}, [refresh])
|
|
43
|
-
|
|
44
|
-
const onDelete = async () => {
|
|
45
|
-
if (!selected) return
|
|
46
|
-
try {
|
|
47
|
-
await api.remove(selected.id)
|
|
48
|
-
setConfirmDelete(false)
|
|
49
|
-
setSelected(null)
|
|
50
|
-
await refresh()
|
|
51
|
-
} catch (e) {
|
|
52
|
-
setError(e instanceof Error ? e.message : String(e))
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<XStack flex={1}>
|
|
58
|
-
<YStack flex={1} p={24} gap={16}>
|
|
59
|
-
<XStack items="center" gap={12}>
|
|
60
|
-
<Text fontSize={24} fontWeight="700" color="#f4f4f5" flex={1}>Tenants</Text>
|
|
61
|
-
<Button onPress={refresh} disabled={loading}>Refresh</Button>
|
|
62
|
-
<Button theme="blue" onPress={() => nav.toNew()}>New tenant</Button>
|
|
63
|
-
</XStack>
|
|
64
|
-
|
|
65
|
-
{error && (
|
|
66
|
-
<Card p={12} bg="#450a0a" borderColor="#7f1d1d" borderWidth={1}>
|
|
67
|
-
<Text color="#fecaca">{error}</Text>
|
|
68
|
-
</Card>
|
|
69
|
-
)}
|
|
70
|
-
|
|
71
|
-
<Card bg="#09090b" borderColor="#27272a" borderWidth={1} p={0}>
|
|
72
|
-
<XStack
|
|
73
|
-
px={16}
|
|
74
|
-
py={10}
|
|
75
|
-
borderBottomWidth={1}
|
|
76
|
-
borderBottomColor="#27272a"
|
|
77
|
-
bg="#0a0a0a"
|
|
78
|
-
gap={16}
|
|
79
|
-
>
|
|
80
|
-
<Text flex={1} color="#71717a" fontSize={12} fontWeight="600">SLUG</Text>
|
|
81
|
-
<Text flex={2} color="#71717a" fontSize={12} fontWeight="600">SUBDOMAIN</Text>
|
|
82
|
-
<Text width={120} color="#71717a" fontSize={12} fontWeight="600">STATUS</Text>
|
|
83
|
-
<Text flex={2} color="#71717a" fontSize={12} fontWeight="600">CURRENT WRITER</Text>
|
|
84
|
-
<Text width={80} color="#71717a" fontSize={12} fontWeight="600" style={{ textAlign: 'right' }}>TERM</Text>
|
|
85
|
-
</XStack>
|
|
86
|
-
{tenants.length === 0 ? (
|
|
87
|
-
<YStack p={32} items="center">
|
|
88
|
-
<Text color="#71717a">No tenants yet. Create one to get started.</Text>
|
|
89
|
-
</YStack>
|
|
90
|
-
) : (
|
|
91
|
-
tenants.map((t) => (
|
|
92
|
-
<TenantRow key={t.id} tenant={t} onClick={setSelected} />
|
|
93
|
-
))
|
|
94
|
-
)}
|
|
95
|
-
</Card>
|
|
96
|
-
</YStack>
|
|
97
|
-
|
|
98
|
-
{selected && (
|
|
99
|
-
<YStack
|
|
100
|
-
width={420}
|
|
101
|
-
bg="#0a0a0a"
|
|
102
|
-
borderLeftWidth={1}
|
|
103
|
-
borderLeftColor="#27272a"
|
|
104
|
-
p={24}
|
|
105
|
-
gap={16}
|
|
106
|
-
>
|
|
107
|
-
<XStack items="center" gap={12}>
|
|
108
|
-
<Text fontSize={20} fontWeight="700" color="#f4f4f5" flex={1}>{selected.slug}</Text>
|
|
109
|
-
<Button size="$2" onPress={() => setSelected(null)}>Close</Button>
|
|
110
|
-
</XStack>
|
|
111
|
-
<YStack gap={4}>
|
|
112
|
-
<Text color="#71717a" fontSize={12}>NAME</Text>
|
|
113
|
-
<Text color="#f4f4f5">{selected.name}</Text>
|
|
114
|
-
</YStack>
|
|
115
|
-
<YStack gap={4}>
|
|
116
|
-
<Text color="#71717a" fontSize={12}>STATUS</Text>
|
|
117
|
-
<XStack><TenantStatusBadge status={selected.status} /></XStack>
|
|
118
|
-
</YStack>
|
|
119
|
-
<YStack gap={4}>
|
|
120
|
-
<Text color="#71717a" fontSize={12}>SUBDOMAIN</Text>
|
|
121
|
-
<Text color="#f4f4f5">{selected.subdomain || '—'}</Text>
|
|
122
|
-
</YStack>
|
|
123
|
-
<YStack gap={4}>
|
|
124
|
-
<Text color="#71717a" fontSize={12}>NAMESPACE</Text>
|
|
125
|
-
<Text color="#f4f4f5">{selected.namespace}</Text>
|
|
126
|
-
</YStack>
|
|
127
|
-
<YStack gap={4}>
|
|
128
|
-
<Text color="#71717a" fontSize={12}>SPEC</Text>
|
|
129
|
-
<Text color="#f4f4f5">
|
|
130
|
-
{selected.spec.replicas} replicas · {selected.spec.storage}
|
|
131
|
-
</Text>
|
|
132
|
-
</YStack>
|
|
133
|
-
<YStack gap={4}>
|
|
134
|
-
<Text color="#71717a" fontSize={12}>CURRENT WRITER</Text>
|
|
135
|
-
<Text color="#f4f4f5">{selected.current_writer || '—'}</Text>
|
|
136
|
-
</YStack>
|
|
137
|
-
<YStack gap={4}>
|
|
138
|
-
<Text color="#71717a" fontSize={12}>TERM</Text>
|
|
139
|
-
<Text color="#f4f4f5">{selected.term}</Text>
|
|
140
|
-
</YStack>
|
|
141
|
-
<YStack gap={4}>
|
|
142
|
-
<Text color="#71717a" fontSize={12}>OWNER</Text>
|
|
143
|
-
<Text color="#f4f4f5">{selected.owner_iam_user || '—'}</Text>
|
|
144
|
-
</YStack>
|
|
145
|
-
|
|
146
|
-
{selected.status === 'Running' && selected.subdomain ? (
|
|
147
|
-
<DbAccess tenant={selected} />
|
|
148
|
-
) : (
|
|
149
|
-
<YStack gap={4}>
|
|
150
|
-
<Text color="#71717a" fontSize={12}>ENDPOINT</Text>
|
|
151
|
-
<Text color="#52525b">Available once the tenant is Running.</Text>
|
|
152
|
-
</YStack>
|
|
153
|
-
)}
|
|
154
|
-
|
|
155
|
-
<XStack flex={1} />
|
|
156
|
-
<Button theme="red" onPress={() => setConfirmDelete(true)}>Delete tenant</Button>
|
|
157
|
-
</YStack>
|
|
158
|
-
)}
|
|
159
|
-
|
|
160
|
-
<ConfirmDialog
|
|
161
|
-
open={confirmDelete}
|
|
162
|
-
title="Delete tenant?"
|
|
163
|
-
message={`This deletes the "${selected?.slug ?? ''}" tenant record. The operator will tear down the BaseApp and all associated state. This cannot be undone.`}
|
|
164
|
-
confirmLabel="Delete"
|
|
165
|
-
destructive
|
|
166
|
-
onConfirm={onDelete}
|
|
167
|
-
onCancel={() => setConfirmDelete(false)}
|
|
168
|
-
/>
|
|
169
|
-
</XStack>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// What makes a tenant "usable as a database backend": its live endpoints. A
|
|
174
|
-
// Running Base instance always serves the REST/realtime data API at `/v1`
|
|
175
|
-
// (collections, records, auth, files — usable from any app/SDK) and, when the
|
|
176
|
-
// image runs with BASE_ENABLE_ADMIN_UI=1, the visual admin (collections /
|
|
177
|
-
// records / auth / SQL) at `/_/`. We surface both straight off the tenant
|
|
178
|
-
// subdomain so a freshly-provisioned Base is immediately usable.
|
|
179
|
-
function DbAccess({ tenant }: { tenant: TenantRecord }) {
|
|
180
|
-
const [copied, setCopied] = useState(false)
|
|
181
|
-
const base = `https://${tenant.subdomain}`
|
|
182
|
-
const apiBase = `${base}/v1`
|
|
183
|
-
|
|
184
|
-
const open = (url: string) => {
|
|
185
|
-
if (typeof window !== 'undefined') window.open(url, '_blank', 'noopener,noreferrer')
|
|
186
|
-
}
|
|
187
|
-
const copy = async (text: string) => {
|
|
188
|
-
try {
|
|
189
|
-
await navigator.clipboard.writeText(text)
|
|
190
|
-
setCopied(true)
|
|
191
|
-
setTimeout(() => setCopied(false), 1200)
|
|
192
|
-
} catch {
|
|
193
|
-
/* clipboard unavailable (insecure context) — no-op */
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return (
|
|
198
|
-
<YStack gap={10} p={12} bg="#09090b" borderColor="#27272a" borderWidth={1} rounded={8}>
|
|
199
|
-
<Text color="#71717a" fontSize={12} fontWeight="600">USE THIS DATABASE</Text>
|
|
200
|
-
|
|
201
|
-
<YStack gap={4}>
|
|
202
|
-
<Text color="#71717a" fontSize={11}>ENDPOINT</Text>
|
|
203
|
-
<XStack items="center" gap={8}>
|
|
204
|
-
<Text color="#f4f4f5" flex={1} numberOfLines={1}>{base}</Text>
|
|
205
|
-
<Button size="$2" onPress={() => open(base)}>Open ↗</Button>
|
|
206
|
-
</XStack>
|
|
207
|
-
</YStack>
|
|
208
|
-
|
|
209
|
-
<YStack gap={4}>
|
|
210
|
-
<Text color="#71717a" fontSize={11}>ADMIN — collections · records · auth · SQL</Text>
|
|
211
|
-
<XStack items="center" gap={8}>
|
|
212
|
-
<Text color="#a1a1aa" flex={1} numberOfLines={1}>{base}/_/</Text>
|
|
213
|
-
<Button size="$2" theme="blue" onPress={() => open(`${base}/_/`)}>Open admin ↗</Button>
|
|
214
|
-
</XStack>
|
|
215
|
-
</YStack>
|
|
216
|
-
|
|
217
|
-
<YStack gap={4}>
|
|
218
|
-
<Text color="#71717a" fontSize={11}>REST API</Text>
|
|
219
|
-
<XStack items="center" gap={8}>
|
|
220
|
-
<Text color="#a1a1aa" flex={1} numberOfLines={1}>{apiBase}</Text>
|
|
221
|
-
<Button size="$2" onPress={() => copy(apiBase)}>{copied ? 'Copied' : 'Copy'}</Button>
|
|
222
|
-
</XStack>
|
|
223
|
-
<Text color="#52525b" fontSize={11}>
|
|
224
|
-
e.g. GET {apiBase}/collections/<name>/records
|
|
225
|
-
</Text>
|
|
226
|
-
</YStack>
|
|
227
|
-
</YStack>
|
|
228
|
-
)
|
|
229
|
-
}
|