@hanzo/build 0.2.9 → 0.2.11
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 +126 -23
- package/lib/api/agents.d.ts +80 -0
- package/lib/api/agents.js +148 -0
- package/lib/api/billing.d.ts +205 -0
- package/lib/api/billing.js +300 -0
- package/lib/api/call.d.ts +6 -0
- package/lib/api/call.js +11 -0
- package/lib/api/capabilities.d.ts +25 -0
- package/lib/api/capabilities.js +40 -0
- package/lib/api/changes.d.ts +56 -0
- package/lib/api/changes.js +63 -0
- package/lib/api/coding.d.ts +42 -4
- package/lib/api/coding.js +62 -3
- package/lib/api/connectors.d.ts +90 -0
- package/lib/api/connectors.js +109 -0
- package/lib/api/consent.d.ts +22 -0
- package/lib/api/consent.js +28 -0
- package/lib/api/environment.d.ts +57 -0
- package/lib/api/environment.js +85 -0
- package/lib/api/git.d.ts +6 -2
- package/lib/api/git.js +13 -5
- package/lib/api/github.d.ts +13 -0
- package/lib/api/github.js +9 -0
- package/lib/api/harness.d.ts +44 -0
- package/lib/api/harness.js +348 -0
- package/lib/api/keys.d.ts +41 -0
- package/lib/api/keys.js +55 -0
- package/lib/api/machines.d.ts +56 -0
- package/lib/api/machines.js +84 -0
- package/lib/api/members.d.ts +47 -0
- package/lib/api/members.js +88 -0
- package/lib/api/memory.d.ts +25 -0
- package/lib/api/memory.js +34 -0
- package/lib/api/places.d.ts +2 -2
- package/lib/api/places.js +3 -26
- package/lib/api/plugins.d.ts +55 -0
- package/lib/api/plugins.js +62 -0
- package/lib/api/pref.d.ts +49 -0
- package/lib/api/pref.js +68 -0
- package/lib/api/profile.d.ts +21 -0
- package/lib/api/profile.js +40 -0
- package/lib/api/projects.d.ts +19 -2
- package/lib/api/projects.js +29 -4
- package/lib/api/provider.d.ts +42 -0
- package/lib/api/provider.js +58 -0
- package/lib/api/sandbox.d.ts +30 -0
- package/lib/api/sandbox.js +49 -0
- package/lib/api/sessions.d.ts +39 -4
- package/lib/api/sessions.js +58 -6
- package/lib/api/skills.d.ts +70 -0
- package/lib/api/skills.js +90 -0
- package/lib/api/tools.d.ts +34 -0
- package/lib/api/tools.js +37 -0
- package/lib/api/turn.d.ts +84 -16
- package/lib/api/turn.js +224 -4
- package/lib/api/webhooks.d.ts +60 -0
- package/lib/api/webhooks.js +81 -0
- package/lib/ask.d.ts +18 -0
- package/lib/ask.js +69 -0
- package/lib/builder.js +39 -19
- package/lib/customize/agents.d.ts +2 -0
- package/lib/customize/agents.js +125 -0
- package/lib/customize/connectors.d.ts +2 -0
- package/lib/customize/connectors.js +197 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +50 -0
- package/lib/customize/plugins.d.ts +2 -0
- package/lib/customize/plugins.js +93 -0
- package/lib/customize/skills.d.ts +2 -0
- package/lib/customize/skills.js +125 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +8 -1
- package/lib/desk.js +29 -35
- package/lib/door.d.ts +7 -0
- package/lib/door.js +89 -0
- package/lib/environment.d.ts +15 -0
- package/lib/environment.js +132 -0
- package/lib/files.d.ts +14 -0
- package/lib/files.js +111 -0
- package/lib/find.d.ts +7 -0
- package/lib/find.js +77 -0
- package/lib/foot.d.ts +16 -0
- package/lib/foot.js +36 -0
- package/lib/forge.js +8 -4
- package/lib/git.d.ts +9 -0
- package/lib/git.js +74 -0
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +47 -14
- package/lib/markdown.d.ts +49 -0
- package/lib/markdown.js +123 -0
- package/lib/plans.d.ts +1 -0
- package/lib/plans.js +113 -0
- package/lib/prefs.d.ts +30 -0
- package/lib/prefs.js +83 -0
- package/lib/prose.d.ts +4 -0
- package/lib/prose.js +53 -0
- package/lib/route.d.ts +16 -1
- package/lib/route.js +32 -1
- package/lib/run.js +207 -28
- package/lib/section.js +2 -2
- package/lib/settings/account.d.ts +5 -0
- package/lib/settings/account.js +111 -0
- package/lib/settings/billing.d.ts +1 -0
- package/lib/settings/billing.js +104 -0
- package/lib/settings/capabilities.d.ts +1 -0
- package/lib/settings/capabilities.js +55 -0
- package/lib/settings/card.d.ts +31 -0
- package/lib/settings/card.js +105 -0
- package/lib/settings/code.d.ts +1 -0
- package/lib/settings/code.js +51 -0
- package/lib/settings/environments.d.ts +1 -0
- package/lib/settings/environments.js +53 -0
- package/lib/settings/general.d.ts +1 -0
- package/lib/settings/general.js +44 -0
- package/lib/settings/index.d.ts +4 -0
- package/lib/settings/index.js +22 -0
- package/lib/settings/integrations.d.ts +1 -0
- package/lib/settings/integrations.js +81 -0
- package/lib/settings/keys.d.ts +1 -0
- package/lib/settings/keys.js +102 -0
- package/lib/settings/machines.d.ts +1 -0
- package/lib/settings/machines.js +118 -0
- package/lib/settings/members.d.ts +1 -0
- package/lib/settings/members.js +67 -0
- package/lib/settings/memory.d.ts +1 -0
- package/lib/settings/memory.js +56 -0
- package/lib/settings/notifications.d.ts +1 -0
- package/lib/settings/notifications.js +97 -0
- package/lib/settings/privacy.d.ts +1 -0
- package/lib/settings/privacy.js +63 -0
- package/lib/settings/sections.d.ts +16 -0
- package/lib/settings/sections.js +32 -0
- package/lib/settings/ui.d.ts +49 -0
- package/lib/settings/ui.js +47 -0
- package/lib/settings/usage.d.ts +1 -0
- package/lib/settings/usage.js +114 -0
- package/lib/shelf.d.ts +6 -0
- package/lib/shelf.js +86 -16
- package/lib/switch.js +1 -2
- package/lib/transcript.d.ts +12 -0
- package/lib/transcript.js +56 -0
- package/lib/voice.d.ts +3 -1
- package/lib/voice.js +10 -4
- package/package.json +1 -1
- package/lib/account.d.ts +0 -14
- package/lib/account.js +0 -38
- package/lib/e2e/site.spec.d.ts +0 -1
- package/lib/e2e/site.spec.js +0 -125
- package/lib/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* API keys: your own keys for calling the Hanzo API from code — one secret key
|
|
4
|
+
* for a server, one publishable key for a page's source. Creating one ends the
|
|
5
|
+
* one before it, and a secret key is shown once, when it is made. A limit
|
|
6
|
+
* narrows what a key reaches; it never reaches further than you do.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
10
|
+
import { CopyButton } from '@hanzo/ui/product';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { keys, KINDS, limits, mint, revoke } from '../api/keys.js';
|
|
13
|
+
import { Confirm } from '../ask.js';
|
|
14
|
+
import { useRead } from '../data.js';
|
|
15
|
+
import { useHost, useTarget } from '../host.js';
|
|
16
|
+
import { Card, Field, Heading, Note, Once, Row, Soft } from './ui.js';
|
|
17
|
+
const NAME = { secret: 'Secret key', publishable: 'Publishable key' };
|
|
18
|
+
const FOR = {
|
|
19
|
+
secret: 'For a server. It acts as you, so it never goes in a page.',
|
|
20
|
+
publishable: 'Safe in a page’s source.',
|
|
21
|
+
};
|
|
22
|
+
/** The line under a key: what it is for, how it starts, what it reaches, when it was made. */
|
|
23
|
+
function about(type, k) {
|
|
24
|
+
if (!k)
|
|
25
|
+
return `${FOR[type]} None yet.`;
|
|
26
|
+
const when = k.created ? new Date(k.created) : null;
|
|
27
|
+
return [
|
|
28
|
+
type === 'secret' && k.prefix ? `${k.prefix}…` : '',
|
|
29
|
+
k.limit.length ? `Reaches ${k.limit.join(', ')}` : 'Unrestricted',
|
|
30
|
+
when && !Number.isNaN(when.getTime()) ? `Created ${when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' })}` : '',
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join(' · ');
|
|
34
|
+
}
|
|
35
|
+
export function Keys() {
|
|
36
|
+
const host = useHost();
|
|
37
|
+
const t = useTarget();
|
|
38
|
+
const signed = Boolean(host.person);
|
|
39
|
+
const list = useRead(signed ? () => keys(t) : null, [], [t, signed]);
|
|
40
|
+
const [making, setMaking] = useState(null);
|
|
41
|
+
const [revoking, setRevoking] = useState(null);
|
|
42
|
+
const [note, setNote] = useState('');
|
|
43
|
+
if (!signed) {
|
|
44
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys" }), _jsx(Soft, { children: "Sign in to see your API keys." })] }));
|
|
45
|
+
}
|
|
46
|
+
const held = (type) => list.value.find((k) => k.type === type);
|
|
47
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "API keys", detail: "Your own keys for calling api.hanzo.ai from code. You hold one of each; creating one ends the one before it." }), list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading your keys\u2026" })) : (_jsx(Card, { children: KINDS.map((type, i) => {
|
|
48
|
+
const k = held(type);
|
|
49
|
+
return (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: true, title: NAME[type], detail: about(type, k), trailing: _jsxs(XStack, { gap: "$1.5", shrink: 0, children: [k ? (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": `Revoke the ${type} key`, onPress: () => setRevoking(type), children: "Revoke" })) : null, _jsx(Button, { size: "sm", variant: "outline", "aria-label": `${k ? 'Rotate' : 'Create'} the ${type} key`, onPress: () => setMaking(type), children: k ? 'Rotate' : 'Create' })] }) }), type === 'publishable' && k?.key ? (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", pb: "$2.5", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$1", color: "$soft", numberOfLines: 1, "aria-label": "Publishable key", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: "Copy publishable key" })] })) : null] }, type));
|
|
50
|
+
}) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, rotating: making ? Boolean(held(making)) : false, onClose: () => setMaking(null), onMade: (m) => {
|
|
51
|
+
setNote(`${NAME[m.type]} created`);
|
|
52
|
+
list.reload();
|
|
53
|
+
} }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: `Revoke your ${revoking ?? 'secret'} key?`, says: "It stops working within a minute, and anything using it is refused until it has a new one.", act: "Revoke", run: async () => {
|
|
54
|
+
if (!revoking)
|
|
55
|
+
return;
|
|
56
|
+
await revoke(t, revoking);
|
|
57
|
+
setNote(`${NAME[revoking]} revoked`);
|
|
58
|
+
list.reload();
|
|
59
|
+
} })] }));
|
|
60
|
+
}
|
|
61
|
+
/** Creating or rotating one key: an optional limit, then the key itself, once. */
|
|
62
|
+
function Making({ type, rotating, onClose, onMade }) {
|
|
63
|
+
const t = useTarget();
|
|
64
|
+
const [limit, setLimit] = useState('');
|
|
65
|
+
const [made, setMade] = useState(null);
|
|
66
|
+
const [working, setWorking] = useState(false);
|
|
67
|
+
const [note, setNote] = useState('');
|
|
68
|
+
const close = () => {
|
|
69
|
+
if (working)
|
|
70
|
+
return;
|
|
71
|
+
setLimit('');
|
|
72
|
+
setMade(null);
|
|
73
|
+
setNote('');
|
|
74
|
+
onClose();
|
|
75
|
+
};
|
|
76
|
+
const go = async () => {
|
|
77
|
+
if (!type)
|
|
78
|
+
return;
|
|
79
|
+
setNote('');
|
|
80
|
+
let list;
|
|
81
|
+
try {
|
|
82
|
+
list = limits(limit);
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
setNote(e instanceof Error ? e.message : 'That is not a limit');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
setWorking(true);
|
|
89
|
+
try {
|
|
90
|
+
const m = await mint(t, type, list);
|
|
91
|
+
setMade(m);
|
|
92
|
+
onMade(m);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
setWorking(false);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? `Your new ${made.type} key` : `${rotating ? 'Rotate' : 'Create'} your ${type ?? ''} key` }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: "Secret key" })) : (_jsxs(XStack, { items: "center", gap: "$2", px: "$3", py: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)', wordBreak: 'break-all' }, children: made.key }), _jsx(CopyButton, { value: made.key, label: "Copy publishable key" })] })), made.limit.length ? (_jsxs(SizableText, { size: "$1", color: "$soft", children: ["It reaches ", made.limit.join(', '), " and nothing else."] })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: "Done" }) })] })) : (_jsxs(YStack, { gap: "$3", children: [rotating ? (_jsxs(SizableText, { size: "$2", color: "$soft", children: ["The ", type, " key you have now stops working."] })) : null, _jsx(Field, { label: "Limit", hint: "Optional. What it may reach, as kind:name \u2014 model:zen5, project:acme, product:commerce. Empty reaches whatever you do.", children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": "Limit", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(), children: rotating ? 'Rotate' : 'Create' })] })] }))] }) }));
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Machines(): import("react").JSX.Element;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Machines: the organization's own computers a run can be sent to, beside the
|
|
4
|
+
* Hanzo sandbox. `hanzo link` run on a machine registers it under its hostname
|
|
5
|
+
* and keeps it online while it runs; one can also be registered here by hand.
|
|
6
|
+
* The member who linked a machine, or an org admin, renames it, drains it,
|
|
7
|
+
* mints its claim key or removes it.
|
|
8
|
+
*/
|
|
9
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
10
|
+
import { MoreHorizontal, Plus } from '@hanzogui/lucide-icons-2';
|
|
11
|
+
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
12
|
+
import { CopyButton } from '@hanzo/ui/product';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { add, change, KINDS, key, machines, remove } from '../api/machines.js';
|
|
15
|
+
import { Confirm, Rename } from '../ask.js';
|
|
16
|
+
import { useRead } from '../data.js';
|
|
17
|
+
import { useHost, useTarget } from '../host.js';
|
|
18
|
+
import { Card, Field, Group, Heading, Note, Once, Row, Soft } from './ui.js';
|
|
19
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
20
|
+
/** What `hanzo link` needs, in the order it is typed on the machine. */
|
|
21
|
+
const LINK = ['hanzo login', 'hanzo link'].join('\n');
|
|
22
|
+
/** The line under a machine: what it is, where, how big, and what it is doing. */
|
|
23
|
+
function about(m) {
|
|
24
|
+
const load = m.running ? `${m.running} running` : m.sessions ? `${m.sessions} runs` : '';
|
|
25
|
+
return [m.status || 'unknown', m.kind, m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
|
|
26
|
+
}
|
|
27
|
+
function Dot({ status }) {
|
|
28
|
+
const on = status === 'online';
|
|
29
|
+
return (_jsx(YStack, { width: 8, height: 8, rounded: 999, shrink: 0, bg: on ? '$green10' : 'transparent', borderWidth: on ? 0 : 1, borderColor: status === 'draining' ? '$yellow10' : '$soft', "aria-hidden": true }));
|
|
30
|
+
}
|
|
31
|
+
export function Machines() {
|
|
32
|
+
const host = useHost();
|
|
33
|
+
const t = useTarget();
|
|
34
|
+
const signed = Boolean(host.person);
|
|
35
|
+
const list = useRead(signed ? () => machines(t) : null, [], [t, signed]);
|
|
36
|
+
const [adding, setAdding] = useState(false);
|
|
37
|
+
const [renaming, setRenaming] = useState(null);
|
|
38
|
+
const [removing, setRemoving] = useState(null);
|
|
39
|
+
const [keying, setKeying] = useState(null);
|
|
40
|
+
const [minted, setMinted] = useState(null);
|
|
41
|
+
const [note, setNote] = useState('');
|
|
42
|
+
if (!signed) {
|
|
43
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Machines" }), _jsx(Soft, { children: "Sign in to see your organization\u2019s machines." })] }));
|
|
44
|
+
}
|
|
45
|
+
const drain = async (m, status) => {
|
|
46
|
+
setNote('');
|
|
47
|
+
try {
|
|
48
|
+
await change(t, m.id, { status });
|
|
49
|
+
setNote(status === 'draining' ? `${m.label} takes no new runs` : `${m.label} takes runs again`);
|
|
50
|
+
list.reload();
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Machines", detail: "Your organization\u2019s own computers. A run sent to one runs there instead of in the Hanzo sandbox." }), _jsxs(Group, { title: "Link a machine", detail: "Run these on it. It registers under its hostname, and stays online while hanzo link runs.", children: [_jsxs(XStack, { items: "flex-start", gap: "$2", px: "$3", py: "$2.5", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsx(SizableText, { flex: 1, minW: 0, size: "$2", color: "$ink", "aria-label": "Link commands", style: { ...mono, whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: "Copy the commands" })] }), _jsx(XStack, { children: _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Register one by hand"] }) })] }), _jsx(Group, { title: "Linked", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: "Reading machines\u2026" })) : list.value.length === 0 ? (_jsx(Soft, { children: "No machine is linked yet." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Dot, { status: m.status }), title: m.label, detail: about(m), trailing: _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": `Actions for ${m.label}`, px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
57
|
+
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
|
|
58
|
+
m.status === 'draining'
|
|
59
|
+
? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
|
|
60
|
+
: { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
|
|
61
|
+
{ key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
|
|
62
|
+
{ type: 'separator' },
|
|
63
|
+
{ key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
|
|
64
|
+
] }) }, m.id))) })) }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
|
|
65
|
+
setNote(`${m.label} is registered`);
|
|
66
|
+
list.reload();
|
|
67
|
+
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: "Rename machine", name: renaming?.label ?? '', run: async (label) => {
|
|
68
|
+
if (!renaming)
|
|
69
|
+
return;
|
|
70
|
+
await change(t, renaming.id, { label });
|
|
71
|
+
setNote(`Renamed to ${label}`);
|
|
72
|
+
list.reload();
|
|
73
|
+
} }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: `Remove ${removing?.label ?? 'this machine'}?`, says: "Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.", act: "Remove", run: async () => {
|
|
74
|
+
if (!removing)
|
|
75
|
+
return;
|
|
76
|
+
await remove(t, removing.id);
|
|
77
|
+
setNote(`${removing.label} is removed`);
|
|
78
|
+
list.reload();
|
|
79
|
+
} }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: `Mint a claim key for ${keying?.label ?? 'this machine'}?`, says: "A key it had before stops working, so a runner using it stops getting runs until it has the new one. The new key is shown once.", act: "Mint key", run: async () => {
|
|
80
|
+
if (!keying)
|
|
81
|
+
return;
|
|
82
|
+
const k = await key(t, keying.id);
|
|
83
|
+
setMinted({ machine: keying, key: k });
|
|
84
|
+
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsxs(DialogTitle, { children: ["Claim key for ", minted?.machine.label ?? ''] }), minted ? (_jsx(Once, { value: minted.key, label: "Claim key", children: _jsxs(SizableText, { size: "$1", color: "$soft", children: ["A runner on this machine sends it as X-Target-Key when it claims the runs sent here, at POST /v1/agent/targets/", minted.machine.id, "/claim."] }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: "Done" }) })] }) })] }));
|
|
85
|
+
}
|
|
86
|
+
/** Registering a machine by hand: its name, what it is, and the hostname its runs report. */
|
|
87
|
+
function Adding({ open, onOpenChange, onAdded }) {
|
|
88
|
+
const t = useTarget();
|
|
89
|
+
const [label, setLabel] = useState('');
|
|
90
|
+
const [kind, setKind] = useState('machine');
|
|
91
|
+
const [hostname, setHostname] = useState('');
|
|
92
|
+
const [working, setWorking] = useState(false);
|
|
93
|
+
const [note, setNote] = useState('');
|
|
94
|
+
const close = () => {
|
|
95
|
+
if (working)
|
|
96
|
+
return;
|
|
97
|
+
setLabel('');
|
|
98
|
+
setKind('machine');
|
|
99
|
+
setHostname('');
|
|
100
|
+
setNote('');
|
|
101
|
+
onOpenChange(false);
|
|
102
|
+
};
|
|
103
|
+
const save = async () => {
|
|
104
|
+
setWorking(true);
|
|
105
|
+
setNote('');
|
|
106
|
+
try {
|
|
107
|
+
const m = await add(t, { label, kind, host: hostname });
|
|
108
|
+
setWorking(false);
|
|
109
|
+
onAdded(m);
|
|
110
|
+
close();
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
114
|
+
setWorking(false);
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 440, showCloseButton: false, children: [_jsx(DialogTitle, { children: "Register a machine" }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: "Name", children: _jsx(Input, { value: label, onChangeText: setLabel, placeholder: "workshop", "aria-label": "Machine name", autoFocus: true }) }), _jsx(Field, { label: "Kind", children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": "Kind", children: KINDS.map((k) => (_jsx(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": k, onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', children: _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: k }) }, k))) }) }), _jsx(Field, { label: "Hostname", hint: "Optional. What its runs report. hanzo link, signed in as you on a machine with this hostname, takes this row over.", children: _jsx(Input, { value: hostname, onChangeText: setHostname, placeholder: "gpu-01", "aria-label": "Hostname", autoCapitalize: "none" }) }), note ? (_jsx(SizableText, { size: "$1", color: "$soft", role: "status", children: note })) : null, _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", disabled: working, onPress: close, children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: "Register" })] })] })] }) }));
|
|
118
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Members(): import("react").JSX.Element;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Members: who is in the organization and in what role, and — for an org admin —
|
|
4
|
+
* who has been invited, inviting someone by email, and withdrawing an invitation.
|
|
5
|
+
* IAM holds both lists and decides who may read and write them.
|
|
6
|
+
*/
|
|
7
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
8
|
+
import { UserPlus } from '@hanzogui/lucide-icons-2';
|
|
9
|
+
import { Button, Input } from '@hanzo/ui';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { EMAIL, invitations, invite, revoke, roster } from '../api/members.js';
|
|
12
|
+
import { useRead } from '../data.js';
|
|
13
|
+
import { useHost, useTarget } from '../host.js';
|
|
14
|
+
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
15
|
+
const ROLE = { owner: 'Owner', admin: 'Admin', member: 'Member' };
|
|
16
|
+
function Initial({ name }) {
|
|
17
|
+
return (_jsx(YStack, { width: 28, height: 28, rounded: 999, bg: "$raised", items: "center", justify: "center", shrink: 0, children: _jsx(SizableText, { size: "$2", color: "$ink", children: (name || '?').charAt(0).toUpperCase() }) }));
|
|
18
|
+
}
|
|
19
|
+
export function Members() {
|
|
20
|
+
const host = useHost();
|
|
21
|
+
const t = useTarget();
|
|
22
|
+
const org = host.org ?? '';
|
|
23
|
+
const signed = Boolean(host.person && org);
|
|
24
|
+
const people = useRead(signed ? () => roster(t, org) : null, [], [t, org, signed]);
|
|
25
|
+
const asked = useRead(signed && host.admin ? () => invitations(t, org) : null, [], [t, org, signed, host.admin]);
|
|
26
|
+
const [email, setEmail] = useState('');
|
|
27
|
+
const [working, setWorking] = useState(false);
|
|
28
|
+
const [note, setNote] = useState('');
|
|
29
|
+
if (!signed) {
|
|
30
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Members" }), _jsx(Soft, { children: host.person ? 'Choose an organization to see its members.' : 'Sign in to see this organization’s members.' })] }));
|
|
31
|
+
}
|
|
32
|
+
const act = async (what, done, after) => {
|
|
33
|
+
setWorking(true);
|
|
34
|
+
setNote('');
|
|
35
|
+
try {
|
|
36
|
+
await what();
|
|
37
|
+
after();
|
|
38
|
+
setNote(done);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
setWorking(false);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const send = () => {
|
|
48
|
+
const address = email.trim();
|
|
49
|
+
if (!EMAIL.test(address)) {
|
|
50
|
+
setNote(`${address || 'That'} is not an email address`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
void act(() => invite(t, org, address), `${address} is invited. IAM sends no email: share the code with them.`, () => {
|
|
54
|
+
setEmail('');
|
|
55
|
+
asked.reload();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const open = asked.value.filter((i) => i.state === 'Active' && i.used < i.seats);
|
|
59
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Members", detail: `The people who can act in ${org}, and the role each one holds.` }), _jsx(Group, { title: "Members", detail: people.value.length ? `${people.value.length} in ${org}` : undefined, children: people.error ? (_jsx(Soft, { children: people.error.message })) : people.loading && !people.value.length ? (_jsx(Soft, { children: "Reading members\u2026" })) : people.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No one is on this organization\u2019s roster yet." }) })) : (_jsx(Card, { children: people.value.map((m, i) => (_jsx(Row, { first: i === 0, leading: _jsx(Initial, { name: m.name }), title: m.name, detail: [m.user, m.since ? `since ${day(m.since)}` : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: m.role === 'member' ? '$soft' : '$ink', children: ROLE[m.role] }) }, m.user))) })) }), host.admin ? (_jsxs(Group, { title: "Invitations", detail: "An invitation is a one-seat code for one address, redeemed when that person signs up.", children: [_jsxs(Card, { children: [_jsxs(XStack, { gap: "$2", items: "center", px: "$3", py: "$3", flexWrap: "wrap", children: [_jsx(YStack, { flex: 1, minW: 200, children: _jsx(Input, { value: email, onChangeText: setEmail, placeholder: "name@company.com", "aria-label": "Email to invite", inputMode: "email", autoCapitalize: "none" }) }), _jsxs(Button, { size: "sm", disabled: working || !email.trim(), onPress: send, children: [_jsx(UserPlus, { size: 14 }), " Invite"] })] }), asked.error ? (_jsx(YStack, { borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Soft, { children: asked.error.message }) })) : null, asked.value.map((i) => (_jsx(Row, { title: i.email || i.name, mono: !i.email, detail: [
|
|
60
|
+
i.code ? `Code ${i.code}` : '',
|
|
61
|
+
`${i.used} of ${i.seats} joined`,
|
|
62
|
+
i.state !== 'Active' ? i.state.toLowerCase() || 'not redeemable' : '',
|
|
63
|
+
day(i.created) ? `invited ${day(i.created)}` : '',
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.join(' · '), trailing: _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label": `Revoke the invitation for ${i.email || i.name}`, onPress: () => void act(() => revoke(t, i), 'The invitation is withdrawn', asked.reload), children: "Revoke" }) }, i.name)))] }), !asked.loading && !asked.error && asked.value.length > 0 && open.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Every invitation has been used or stopped." })) : null] })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin invites people and sees the open invitations." })), _jsx(Note, { children: note })] }));
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Memory(): import("react").JSX.Element;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Memory: what Hanzo remembers about the person in this organization, newest
|
|
4
|
+
* first. Each can be forgotten, and anything said in the box is remembered as
|
|
5
|
+
* written. The memories are the person's own; nobody else in the organization
|
|
6
|
+
* reads them.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Trash2 } from '@hanzogui/lucide-icons-2';
|
|
10
|
+
import { Button, Input } from '@hanzo/ui';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { forget, memories, remember } from '../api/memory.js';
|
|
13
|
+
import { useRead } from '../data.js';
|
|
14
|
+
import { useHost, useTarget } from '../host.js';
|
|
15
|
+
import { Card, Group, Heading, Note, Soft } from './ui.js';
|
|
16
|
+
/** A platform time as a date, or '' when it is not one. */
|
|
17
|
+
const day = (at) => {
|
|
18
|
+
const d = new Date(at);
|
|
19
|
+
return Number.isNaN(d.getTime()) ? '' : d.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' });
|
|
20
|
+
};
|
|
21
|
+
export function Memory() {
|
|
22
|
+
const host = useHost();
|
|
23
|
+
const t = useTarget();
|
|
24
|
+
const signed = Boolean(host.person);
|
|
25
|
+
const list = useRead(signed ? () => memories(t) : null, [], [t, signed]);
|
|
26
|
+
const [said, setSaid] = useState('');
|
|
27
|
+
const [busy, setBusy] = useState('');
|
|
28
|
+
const [note, setNote] = useState('');
|
|
29
|
+
if (!signed) {
|
|
30
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Memory" }), _jsx(Soft, { children: "Sign in to see what Hanzo remembers." })] }));
|
|
31
|
+
}
|
|
32
|
+
const act = async (what, run, done) => {
|
|
33
|
+
setBusy(what);
|
|
34
|
+
setNote('');
|
|
35
|
+
try {
|
|
36
|
+
await run();
|
|
37
|
+
list.reload();
|
|
38
|
+
setNote(done);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
setBusy('');
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const keep = () => {
|
|
50
|
+
const words = said.trim();
|
|
51
|
+
if (!words)
|
|
52
|
+
return;
|
|
53
|
+
void act('remember', () => remember(t, words), 'Remembered').then((took) => took && setSaid(''));
|
|
54
|
+
};
|
|
55
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Memory", detail: "What Hanzo remembers about you here, to carry from one run to the next. Only you see it." }), _jsx(Group, { title: "Tell Hanzo what to remember or change", children: _jsxs(XStack, { gap: "$2", items: "center", children: [_jsx(YStack, { flex: 1, minW: 0, children: _jsx(Input, { value: said, onChangeText: setSaid, onSubmitEditing: keep, "aria-label": "Tell Hanzo what to remember or change", placeholder: "I prefer small pull requests, one change each." }) }), _jsx(Button, { size: "sm", disabled: !said.trim() || busy !== '', onPress: keep, children: "Remember" })] }) }), _jsx(Group, { title: "What Hanzo remembers", detail: list.value.length ? `${list.value.length} ${list.value.length === 1 ? 'memory' : 'memories'}, newest first.` : undefined, children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Soft, { children: "Reading memories\u2026" })) : !list.value.length ? (_jsx(Soft, { children: "Nothing yet. What you tell Hanzo to remember shows here." })) : (_jsx(Card, { children: list.value.map((m, i) => (_jsxs(XStack, { items: "flex-start", gap: "$3", px: "$3", py: "$2.5", borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsxs(YStack, { flex: 1, minW: 0, gap: "$0.5", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: m.content }), _jsx(SizableText, { size: "$1", color: "$soft", children: [m.kind, day(m.updated)].filter(Boolean).join(' · ') })] }), _jsx(Button, { size: "icon-sm", variant: "ghost", "aria-label": `Forget ${m.content.slice(0, 60)}`, disabled: busy !== '', onPress: () => void act(m.id, () => forget(t, m.id), 'Forgotten'), children: _jsx(Trash2, { size: 14 }) })] }, m.id))) })) }), _jsx(Note, { children: note })] }));
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Notifications(): import("react").JSX.Element;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Notifications: the organization's webhooks. Each is an https address that
|
|
4
|
+
* every matching event is POSTed to, signed with a secret shown once, when the
|
|
5
|
+
* webhook is added. A test sends one signed event now; the log says what each
|
|
6
|
+
* delivery got back.
|
|
7
|
+
*/
|
|
8
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
9
|
+
import { Copy, Plus, Webhook, X } from '@hanzogui/lucide-icons-2';
|
|
10
|
+
import { Button, Input } from '@hanzo/ui';
|
|
11
|
+
import { useState } from 'react';
|
|
12
|
+
import { add, deliveries, endpoints, patterns, refuse, remove, test } from '../api/webhooks.js';
|
|
13
|
+
import { useRead } from '../data.js';
|
|
14
|
+
import { useHost, useTarget } from '../host.js';
|
|
15
|
+
import { Card, day, Field, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
16
|
+
const mono = { fontFamily: 'var(--f-mono, ui-monospace, monospace)' };
|
|
17
|
+
export function Notifications() {
|
|
18
|
+
const host = useHost();
|
|
19
|
+
const t = useTarget();
|
|
20
|
+
const signed = Boolean(host.person);
|
|
21
|
+
const list = useRead(signed ? () => endpoints(t) : null, [], [t, signed]);
|
|
22
|
+
const [adding, setAdding] = useState(false);
|
|
23
|
+
const [url, setUrl] = useState('');
|
|
24
|
+
const [events, setEvents] = useState('');
|
|
25
|
+
const [description, setDescription] = useState('');
|
|
26
|
+
const [secret, setSecret] = useState('');
|
|
27
|
+
const [log, setLog] = useState(null);
|
|
28
|
+
const [working, setWorking] = useState(false);
|
|
29
|
+
const [note, setNote] = useState('');
|
|
30
|
+
if (!signed) {
|
|
31
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Notifications" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s webhooks." })] }));
|
|
32
|
+
}
|
|
33
|
+
const act = async (what) => {
|
|
34
|
+
setWorking(true);
|
|
35
|
+
setNote('');
|
|
36
|
+
try {
|
|
37
|
+
setNote(await what());
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
setWorking(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const create = () => {
|
|
47
|
+
const draft = { url, events: patterns(events), description };
|
|
48
|
+
const why = refuse(draft);
|
|
49
|
+
if (why) {
|
|
50
|
+
setNote(why);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
void act(async () => {
|
|
54
|
+
const e = await add(t, draft);
|
|
55
|
+
setSecret(e.secret);
|
|
56
|
+
setAdding(false);
|
|
57
|
+
setUrl('');
|
|
58
|
+
setEvents('');
|
|
59
|
+
setDescription('');
|
|
60
|
+
list.reload();
|
|
61
|
+
return `${e.url} is added`;
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const trial = (e) => void act(async () => {
|
|
65
|
+
const r = await test(t, e.id);
|
|
66
|
+
if (log?.id === e.id)
|
|
67
|
+
setLog({ id: e.id, rows: await deliveries(t, e.id) });
|
|
68
|
+
return r.delivered
|
|
69
|
+
? `The test reached ${e.url}: ${r.status} in ${r.ms} ms`
|
|
70
|
+
: `The test did not reach ${e.url}: ${r.error || (r.status ? `it answered ${r.status}` : 'no answer')}`;
|
|
71
|
+
});
|
|
72
|
+
const drop = (e) => void act(async () => {
|
|
73
|
+
await remove(t, e.id);
|
|
74
|
+
if (log?.id === e.id)
|
|
75
|
+
setLog(null);
|
|
76
|
+
list.reload();
|
|
77
|
+
return `${e.url} is deleted`;
|
|
78
|
+
});
|
|
79
|
+
const show = (e) => {
|
|
80
|
+
if (log?.id === e.id) {
|
|
81
|
+
setLog(null);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
void act(async () => {
|
|
85
|
+
setLog({ id: e.id, rows: await deliveries(t, e.id) });
|
|
86
|
+
return '';
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Notifications", detail: `Webhooks: events in ${host.org ?? 'this organization'}, POSTed and signed to an address you run.`, action: adding || !host.admin ? undefined : (_jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " Add webhook"] })) }), adding ? (_jsxs(YStack, { gap: "$3", p: "$4", borderWidth: 1, borderColor: "$borderColor", rounded: "$3", children: [_jsx(Field, { label: "Endpoint URL", hint: "https only.", children: _jsx(Input, { value: url, onChangeText: setUrl, placeholder: "https://example.com/hooks/hanzo", "aria-label": "Endpoint URL", autoCapitalize: "none", inputMode: "url" }) }), _jsx(Field, { label: "Events", hint: "Subject patterns, comma separated, such as commerce.order.> \u2014 leave empty for every event.", children: _jsx(Input, { value: events, onChangeText: setEvents, placeholder: "commerce.order.>, event.>", "aria-label": "Events", autoCapitalize: "none" }) }), _jsx(Field, { label: "Description", children: _jsx(Input, { value: description, onChangeText: setDescription, placeholder: "What this endpoint is for", "aria-label": "Description" }) }), _jsxs(XStack, { gap: "$2", justify: "flex-end", children: [_jsx(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(false), children: "Cancel" }), _jsx(Button, { size: "sm", disabled: working || !url.trim(), onPress: create, children: "Add webhook" })] })] })) : null, secret ? (_jsxs(YStack, { gap: "$2", p: "$4", borderWidth: 1, borderColor: "$ink", rounded: "$3", children: [_jsx(SizableText, { size: "$2", color: "$ink", children: "Signing secret" }), _jsx(SizableText, { size: "$1", color: "$soft", children: "Shown this once. Keep it where your endpoint checks each delivery\u2019s signature." }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { flex: 1, size: "$2", color: "$ink", style: { ...mono, wordBreak: 'break-all' }, "aria-label": "Signing secret", children: secret }), _jsxs(Button, { size: "sm", variant: "outline", onPress: () => void navigator.clipboard?.writeText(secret).then(() => setNote('The secret is copied')), children: [_jsx(Copy, { size: 14 }), " Copy"] }), _jsx(XStack, { render: "button", "aria-label": "Hide the secret", p: "$1", onPress: () => setSecret(''), children: _jsx(X, { size: 14 }) })] })] })) : null, _jsx(Group, { title: "Webhooks", children: list.error ? (_jsx(Soft, { children: list.error.message })) : list.loading && !list.value.length ? (_jsx(Soft, { children: "Reading webhooks\u2026" })) : list.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: "No webhooks yet." }) })) : (_jsx(Card, { children: list.value.map((e, i) => (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: true, mono: true, leading: _jsx(Webhook, { size: 16 }), title: e.url, detail: [
|
|
90
|
+
e.description,
|
|
91
|
+
e.events.length ? e.events.join(', ') : 'every event',
|
|
92
|
+
e.status === 'disabled' ? 'disabled' : '',
|
|
93
|
+
`${e.deliveries} delivered, ${e.failures} failed this week`,
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.join(' · '), trailing: host.admin ? (_jsx(XStack, { render: "button", "aria-label": `Delete ${e.url}`, p: "$1", onPress: () => drop(e), children: _jsx(X, { size: 14 }) })) : undefined }), _jsxs(XStack, { gap: "$2", px: "$3", pb: "$2.5", flexWrap: "wrap", children: [host.admin ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, "aria-label": `Send a test to ${e.url}`, onPress: () => trial(e), children: "Send test" })) : null, _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label": `Deliveries to ${e.url}`, onPress: () => show(e), children: log?.id === e.id ? 'Hide deliveries' : 'Deliveries' })] }), log?.id === e.id ? (_jsx(YStack, { px: "$3", pb: "$3", gap: "$1.5", children: log.rows.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "Nothing delivered yet." })) : (log.rows.map((a, n) => (_jsxs(XStack, { gap: "$3", items: "baseline", children: [_jsx(SizableText, { size: "$1", color: a.status === 'ok' ? '$ink' : '$soft', width: 64, children: a.status === 'ok' ? 'OK' : a.status === 'failed' ? 'Failed' : 'Retrying' }), _jsx(SizableText, { flex: 1, size: "$1", color: "$soft", numberOfLines: 1, style: mono, children: `${a.subject}${a.code ? ` → ${a.code}` : ''}${a.error ? ` · ${a.error}` : ''}` }), _jsx(SizableText, { size: "$1", color: "$soft", "$max-sm": { display: 'none' }, children: day(a.at) })] }, `${a.at}-${n}`)))) })) : null] }, e.id))) })) }), _jsx(Note, { children: note })] }));
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Privacy(): import("react").JSX.Element;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Privacy: the two data-sharing answers IAM records for the person — whether
|
|
4
|
+
* Hanzo may train on their data, and whether anonymous usage is counted — and
|
|
5
|
+
* who can see the organization's projects: a public one is in the community
|
|
6
|
+
* catalogue with its source readable by anyone, until it is made private.
|
|
7
|
+
*/
|
|
8
|
+
import { YStack } from '@hanzo/gui';
|
|
9
|
+
import { Button, Switch } from '@hanzo/ui';
|
|
10
|
+
import { useState } from 'react';
|
|
11
|
+
import { consent, setConsent } from '../api/consent.js';
|
|
12
|
+
import { change, projects } from '../api/projects.js';
|
|
13
|
+
import { useRead } from '../data.js';
|
|
14
|
+
import { useHost, useTarget } from '../host.js';
|
|
15
|
+
import { Card, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
16
|
+
export function Privacy() {
|
|
17
|
+
const host = useHost();
|
|
18
|
+
const t = useTarget();
|
|
19
|
+
const signed = Boolean(host.person);
|
|
20
|
+
const answers = useRead(signed ? () => consent(t) : null, null, [t, signed]);
|
|
21
|
+
const published = useRead(signed ? () => projects(t) : null, [], [t, signed]);
|
|
22
|
+
const [shown, setShown] = useState(null);
|
|
23
|
+
const [busy, setBusy] = useState('');
|
|
24
|
+
const [note, setNote] = useState('');
|
|
25
|
+
if (!signed) {
|
|
26
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Privacy" }), _jsx(Soft, { children: "Sign in to see your privacy settings." })] }));
|
|
27
|
+
}
|
|
28
|
+
const now = shown ?? answers.value;
|
|
29
|
+
const answer = async (change) => {
|
|
30
|
+
setBusy('consent');
|
|
31
|
+
setNote('');
|
|
32
|
+
try {
|
|
33
|
+
setShown(await setConsent(t, change));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
setNote(e instanceof Error ? e.message : 'That was not saved');
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
setBusy('');
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const flip = async (p) => {
|
|
43
|
+
setBusy(p.slug);
|
|
44
|
+
setNote('');
|
|
45
|
+
try {
|
|
46
|
+
const saved = await change(t, p.slug, { visibility: p.visibility === 'private' ? 'public' : 'private' });
|
|
47
|
+
published.reload();
|
|
48
|
+
setNote(`${saved.name} is ${saved.visibility || 'saved'}`);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
setNote(e instanceof Error ? e.message : 'That was not saved');
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
setBusy('');
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const training = now?.training ?? '';
|
|
58
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Privacy", detail: "What Hanzo may do with your data, and what your organization shows the world." }), _jsx(Group, { title: "Your data", detail: "Recorded on your Hanzo account and honoured by every Hanzo surface.", children: answers.error && !now ? (_jsx(Soft, { children: answers.error.message })) : !now ? (_jsx(Soft, { children: "Reading your answers\u2026" })) : (_jsxs(Card, { children: [_jsx(Row, { first: true, title: "Help improve Hanzo models", detail: training === 'granted'
|
|
59
|
+
? 'Hanzo may train on your prompts and runs.'
|
|
60
|
+
: training === 'refused'
|
|
61
|
+
? 'Hanzo does not train on your data.'
|
|
62
|
+
: 'Not answered yet. Until you say yes, Hanzo does not train on your data.', trailing: _jsx(Switch, { "aria-label": "Help improve Hanzo models", checked: training === 'granted', disabled: busy === 'consent', onCheckedChange: (on) => void answer({ training: on ? 'granted' : 'refused' }) }) }), _jsx(Row, { title: "Usage insights", detail: "Anonymous counts of how the product is used. No prompts, code or answers.", trailing: _jsx(Switch, { "aria-label": "Usage insights", checked: now.insights, disabled: busy === 'consent', onCheckedChange: (on) => void answer({ insights: on }) }) })] })) }), _jsx(Group, { title: "Projects", detail: "A public project is listed in the community catalogue, and anyone can read its source. Keeping one private needs a paid plan.", children: published.error ? (_jsx(Soft, { children: published.error.message })) : published.loading && !published.value.length ? (_jsx(Soft, { children: "Reading projects\u2026" })) : !published.value.length ? (_jsx(Soft, { children: "Your organization has no projects yet." })) : (_jsx(Card, { children: published.value.map((p, i) => (_jsx(Row, { first: i === 0, title: p.name, detail: p.visibility === 'private' ? 'Private' : p.visibility === 'public' ? 'Public' : 'Visibility not reported', trailing: p.visibility ? (_jsx(Button, { size: "sm", variant: "outline", disabled: busy === p.slug, onPress: () => void flip(p), children: p.visibility === 'private' ? 'Make public' : 'Make private' })) : null }, p.slug))) })) }), _jsx(Note, { children: note })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sections of Settings, in the order the page lists them. A section is one
|
|
3
|
+
* file under settings/ and one entry here; the route grammar names every
|
|
4
|
+
* section there can be (route.ts SECTIONS), and this lists the ones built.
|
|
5
|
+
*/
|
|
6
|
+
import type { ReactNode } from 'react';
|
|
7
|
+
import type { Section } from '../route.ts';
|
|
8
|
+
export type Group = 'Settings' | 'Code' | 'Organization';
|
|
9
|
+
export declare const GROUPS: readonly Group[];
|
|
10
|
+
export interface Entry {
|
|
11
|
+
id: Section;
|
|
12
|
+
label: string;
|
|
13
|
+
group: Group;
|
|
14
|
+
body: () => ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare const ENTRIES: Entry[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Account } from './account.js';
|
|
3
|
+
import { Billing } from './billing.js';
|
|
4
|
+
import { Capabilities } from './capabilities.js';
|
|
5
|
+
import { Code } from './code.js';
|
|
6
|
+
import { Environments } from './environments.js';
|
|
7
|
+
import { General } from './general.js';
|
|
8
|
+
import { Integrations } from './integrations.js';
|
|
9
|
+
import { Keys } from './keys.js';
|
|
10
|
+
import { Machines } from './machines.js';
|
|
11
|
+
import { Members } from './members.js';
|
|
12
|
+
import { Memory } from './memory.js';
|
|
13
|
+
import { Notifications } from './notifications.js';
|
|
14
|
+
import { Privacy } from './privacy.js';
|
|
15
|
+
import { Usage } from './usage.js';
|
|
16
|
+
export const GROUPS = ['Settings', 'Code', 'Organization'];
|
|
17
|
+
export const ENTRIES = [
|
|
18
|
+
{ id: 'general', label: 'General', group: 'Settings', body: () => _jsx(General, {}) },
|
|
19
|
+
{ id: 'account', label: 'Account', group: 'Settings', body: () => _jsx(Account, {}) },
|
|
20
|
+
{ id: 'privacy', label: 'Privacy', group: 'Settings', body: () => _jsx(Privacy, {}) },
|
|
21
|
+
{ id: 'billing', label: 'Billing', group: 'Settings', body: () => _jsx(Billing, {}) },
|
|
22
|
+
{ id: 'usage', label: 'Usage', group: 'Settings', body: () => _jsx(Usage, {}) },
|
|
23
|
+
{ id: 'capabilities', label: 'Capabilities', group: 'Settings', body: () => _jsx(Capabilities, {}) },
|
|
24
|
+
{ id: 'memory', label: 'Memory', group: 'Settings', body: () => _jsx(Memory, {}) },
|
|
25
|
+
{ id: 'code', label: 'Code', group: 'Code', body: () => _jsx(Code, {}) },
|
|
26
|
+
{ id: 'environments', label: 'Environments', group: 'Code', body: () => _jsx(Environments, {}) },
|
|
27
|
+
{ id: 'machines', label: 'Machines', group: 'Code', body: () => _jsx(Machines, {}) },
|
|
28
|
+
{ id: 'keys', label: 'API keys', group: 'Code', body: () => _jsx(Keys, {}) },
|
|
29
|
+
{ id: 'members', label: 'Members', group: 'Organization', body: () => _jsx(Members, {}) },
|
|
30
|
+
{ id: 'integrations', label: 'Integrations', group: 'Organization', body: () => _jsx(Integrations, {}) },
|
|
31
|
+
{ id: 'notifications', label: 'Notifications', group: 'Organization', body: () => _jsx(Notifications, {}) },
|
|
32
|
+
];
|