@hanzo/build 0.2.10 → 0.2.12
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 +98 -25
- 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/coding.d.ts +31 -1
- package/lib/api/coding.js +50 -1
- 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/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/sessions.d.ts +32 -4
- package/lib/api/sessions.js +42 -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 +221 -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 +199 -0
- package/lib/customize/index.d.ts +6 -0
- package/lib/customize/index.js +52 -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 +127 -0
- package/lib/customize/ui.d.ts +110 -0
- package/lib/customize/ui.js +99 -0
- package/lib/desk.d.ts +4 -1
- package/lib/desk.js +3 -16
- 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 +4 -4
- package/lib/git.d.ts +4 -0
- package/lib/git.js +6 -2
- package/lib/host.d.ts +5 -0
- package/lib/landing.d.ts +10 -0
- package/lib/landing.js +30 -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 +184 -32
- 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/mcp.d.ts +0 -1
- package/lib/mcp.js +0 -28
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Integrations(): import("react").JSX.Element;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Integrations: your own GitHub connection, the GitHub accounts the
|
|
4
|
+
* organization has installed the platform's App on, and its Slack workspace.
|
|
5
|
+
*
|
|
6
|
+
* Connecting leaves this page once, for GitHub's or Slack's own consent screen.
|
|
7
|
+
* Anyone connects their own GitHub; installing the App and connecting Slack are
|
|
8
|
+
* an org admin's, which is the platform's rule.
|
|
9
|
+
*/
|
|
10
|
+
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { Github, Hash, Lock, Slack } from '@hanzogui/lucide-icons-2';
|
|
12
|
+
import { Button } from '@hanzo/ui';
|
|
13
|
+
import { useState } from 'react';
|
|
14
|
+
import { connect, connection, disconnect as unlinkGithub, installations } from '../api/github.js';
|
|
15
|
+
import { authorize, channels, disconnect, read } from '../api/provider.js';
|
|
16
|
+
import { useRead } from '../data.js';
|
|
17
|
+
import { useHost, useTarget } from '../host.js';
|
|
18
|
+
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
19
|
+
export function Integrations() {
|
|
20
|
+
const host = useHost();
|
|
21
|
+
const t = useTarget();
|
|
22
|
+
const signed = Boolean(host.person);
|
|
23
|
+
const mine = useRead(signed ? () => connection(t) : null, null, [t, signed]);
|
|
24
|
+
const installed = useRead(signed ? () => installations(t) : null, [], [t, signed]);
|
|
25
|
+
const slack = useRead(signed ? () => read(t, 'slack') : null, null, [t, signed]);
|
|
26
|
+
const joined = slack.value?.connected ?? false;
|
|
27
|
+
const rooms = useRead(signed && joined ? () => channels(t) : null, { channels: [], next: '' }, [t, signed, joined]);
|
|
28
|
+
const [working, setWorking] = useState(false);
|
|
29
|
+
const [note, setNote] = useState('');
|
|
30
|
+
if (!signed) {
|
|
31
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: "Integrations" }), _jsx(Soft, { children: "Sign in to see this organization\u2019s integrations." })] }));
|
|
32
|
+
}
|
|
33
|
+
const act = async (what, done, after) => {
|
|
34
|
+
setWorking(true);
|
|
35
|
+
setNote('');
|
|
36
|
+
try {
|
|
37
|
+
await what();
|
|
38
|
+
after();
|
|
39
|
+
setNote(done);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
setNote(e instanceof Error ? e.message : 'That did not work');
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
setWorking(false);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** Off to the provider's consent page; it is the one hop away from this page. */
|
|
49
|
+
const away = async (where) => {
|
|
50
|
+
setWorking(true);
|
|
51
|
+
setNote('');
|
|
52
|
+
try {
|
|
53
|
+
host.open(await where());
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
setNote(e instanceof Error ? e.message : 'The connection did not start');
|
|
57
|
+
setWorking(false);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const me = mine.value;
|
|
61
|
+
const s = slack.value;
|
|
62
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: "Integrations", detail: `What ${host.org ?? 'this organization'} and you have connected.` }), _jsxs(Group, { title: "GitHub", detail: "Your connection brings your repositories; the App brings an account\u2019s to the organization.", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Github, { size: 16 }), title: "Your GitHub", detail: mine.error
|
|
63
|
+
? mine.error.message
|
|
64
|
+
: !me
|
|
65
|
+
? 'Reading…'
|
|
66
|
+
: !me.configured
|
|
67
|
+
? 'This deployment cannot connect GitHub yet.'
|
|
68
|
+
: me.connected
|
|
69
|
+
? `Connected as @${me.login}`
|
|
70
|
+
: 'Not connected', trailing: me?.configured ? (me.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => unlinkGithub(t), 'Your GitHub is disconnected', mine.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => connect(t)), children: "Connect" }))) : null }), installed.error ? (_jsx(Row, { title: "App installations", detail: installed.error.message })) : (installed.value.map((i) => (_jsx(Row, { leading: _jsx(Github, { size: 16 }), title: i.login, detail: [i.type, i.grant === 'all' ? 'every repository' : i.grant === 'selected' ? 'selected repositories' : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: i.connected ? '$ink' : '$soft', children: i.connected ? 'Installed' : 'Removed on GitHub' }) }, i.login)))), !installed.error && !installed.loading && installed.value.length === 0 ? (_jsx(Row, { title: "No GitHub accounts installed", detail: "Install the App on a GitHub account to bring its repositories here." })) : null] }), host.admin ? (_jsx(XStack, { children: _jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'github')), children: "Install on a GitHub account" }) })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin installs the App on a GitHub account." }))] }), _jsxs(Group, { title: "Slack", detail: "Talk to Hanzo from your workspace\u2019s channels.", children: [_jsxs(Card, { children: [_jsx(Row, { first: true, leading: _jsx(Slack, { size: 16 }), title: s?.connected && s.account ? s.account : 'Slack', detail: slack.error
|
|
71
|
+
? slack.error.message
|
|
72
|
+
: !s
|
|
73
|
+
? 'Reading…'
|
|
74
|
+
: s.connected
|
|
75
|
+
? `Connected${s.since ? ` since ${day(s.since)}` : ''}`
|
|
76
|
+
: s.available
|
|
77
|
+
? 'Not connected'
|
|
78
|
+
: s.note || 'This deployment cannot connect Slack yet.', trailing: host.admin && s && (s.connected || s.available) ? (s.connected ? (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void act(() => disconnect(t, 'slack'), 'Slack is disconnected', slack.reload), children: "Disconnect" })) : (_jsx(Button, { size: "sm", variant: "outline", disabled: working, onPress: () => void away(() => authorize(t, 'slack')), children: "Connect" }))) : null }), joined && rooms.error ? _jsx(Row, { title: "Channels", detail: rooms.error.message }) : null, joined
|
|
79
|
+
? rooms.value.channels.map((c) => (_jsx(Row, { leading: c.private ? _jsx(Lock, { size: 14 }) : _jsx(Hash, { size: 14 }), title: c.name || c.id, trailing: _jsx(SizableText, { size: "$1", color: c.member ? '$ink' : '$soft', children: c.member ? 'Joined' : 'Not joined' }) }, c.id)))
|
|
80
|
+
: null] }), !host.admin ? (_jsx(SizableText, { size: "$1", color: "$soft", children: "An org admin connects the Slack workspace." })) : null] }), _jsx(Note, { children: note })] }));
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function Keys(): import("react").JSX.Element;
|
|
@@ -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;
|