@hanzo/build 0.2.80 → 0.2.81
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 +13 -0
- package/lib/activity.d.ts +3 -7
- package/lib/activity.js +18 -5
- package/lib/api/agents.d.ts +1 -20
- package/lib/api/agents.js +16 -8
- package/lib/api/auto.d.ts +3 -23
- package/lib/api/auto.js +39 -22
- package/lib/api/billing.d.ts +0 -35
- package/lib/api/billing.js +6 -5
- package/lib/api/call.d.ts +16 -8
- package/lib/api/call.js +32 -1
- package/lib/api/capabilities.d.ts +0 -11
- package/lib/api/capabilities.js +2 -1
- package/lib/api/codebases.d.ts +0 -10
- package/lib/api/codebases.js +3 -2
- package/lib/api/coding.d.ts +3 -32
- package/lib/api/coding.js +13 -7
- package/lib/api/connectors.d.ts +1 -17
- package/lib/api/connectors.js +5 -4
- package/lib/api/environment.d.ts +6 -20
- package/lib/api/environment.js +22 -19
- package/lib/api/github.d.ts +0 -22
- package/lib/api/github.js +3 -2
- package/lib/api/keys.d.ts +0 -14
- package/lib/api/keys.js +4 -3
- package/lib/api/library.d.ts +3 -17
- package/lib/api/library.js +23 -22
- package/lib/api/machines.d.ts +0 -15
- package/lib/api/machines.js +6 -5
- package/lib/api/mcp.d.ts +0 -9
- package/lib/api/mcp.js +3 -2
- package/lib/api/members.d.ts +0 -15
- package/lib/api/members.js +4 -3
- package/lib/api/memory.d.ts +0 -11
- package/lib/api/memory.js +2 -1
- package/lib/api/places.d.ts +1 -9
- package/lib/api/places.js +12 -1
- package/lib/api/plugins.d.ts +1 -16
- package/lib/api/plugins.js +4 -3
- package/lib/api/profile.d.ts +0 -10
- package/lib/api/profile.js +5 -4
- package/lib/api/projects.d.ts +0 -14
- package/lib/api/projects.js +2 -1
- package/lib/api/provider.d.ts +2 -14
- package/lib/api/provider.js +12 -10
- package/lib/api/sandbox.d.ts +0 -17
- package/lib/api/sandbox.js +5 -4
- package/lib/api/sessions.d.ts +0 -17
- package/lib/api/sessions.js +5 -4
- package/lib/api/skills.d.ts +1 -15
- package/lib/api/skills.js +5 -4
- package/lib/api/studio.d.ts +0 -14
- package/lib/api/studio.js +6 -5
- package/lib/api/turn.d.ts +6 -28
- package/lib/api/turn.js +105 -49
- package/lib/api/verdict.d.ts +0 -9
- package/lib/api/verdict.js +2 -1
- package/lib/api/webhooks.d.ts +1 -14
- package/lib/api/webhooks.js +5 -4
- package/lib/ask.js +7 -4
- package/lib/automations.d.ts +5 -4
- package/lib/automations.js +95 -74
- package/lib/back.js +10 -4
- package/lib/builder.js +7 -5
- package/lib/carry.js +20 -16
- package/lib/customize/agents.js +78 -31
- package/lib/customize/connectors.js +62 -48
- package/lib/customize/index.js +43 -10
- package/lib/customize/plugins.js +34 -22
- package/lib/customize/say.d.ts +13 -1
- package/lib/customize/say.js +15 -21
- package/lib/customize/skills.js +31 -22
- package/lib/customize/ui.d.ts +5 -9
- package/lib/customize/ui.js +26 -16
- package/lib/data.d.ts +1 -1
- package/lib/data.js +3 -2
- package/lib/desk.js +45 -33
- package/lib/door.js +35 -10
- package/lib/environment.js +25 -19
- package/lib/files.js +43 -34
- package/lib/find.js +12 -9
- package/lib/foot.js +7 -4
- package/lib/forge.js +61 -40
- package/lib/gear.d.ts +2 -1
- package/lib/gear.js +6 -4
- package/lib/git.js +29 -19
- package/lib/landing.d.ts +5 -5
- package/lib/landing.js +52 -44
- package/lib/library.js +73 -51
- package/lib/meter.d.ts +1 -1
- package/lib/meter.js +14 -5
- package/lib/mic.js +12 -5
- package/lib/mind.d.ts +1 -0
- package/lib/mind.js +5 -3
- package/lib/nav.d.ts +6 -5
- package/lib/nav.js +37 -30
- package/lib/panel.d.ts +2 -1
- package/lib/panel.js +52 -36
- package/lib/plan.d.ts +4 -20
- package/lib/plan.js +26 -10
- package/lib/plans.js +49 -30
- package/lib/plugins/design.js +3 -2
- package/lib/plugins/index.d.ts +23 -4
- package/lib/plugins/index.js +11 -26
- package/lib/plugins/pdf.js +2 -1
- package/lib/plugins/project.js +6 -3
- package/lib/plugins/research.js +3 -2
- package/lib/plugins/sketch.js +6 -3
- package/lib/plugins/slides.js +3 -2
- package/lib/plugins/template.js +6 -3
- package/lib/plus.js +25 -13
- package/lib/prefs.d.ts +0 -14
- package/lib/prefs.js +2 -1
- package/lib/project.js +69 -62
- package/lib/prompt.d.ts +12 -11
- package/lib/prompt.js +38 -20
- package/lib/publish.js +17 -12
- package/lib/reply.js +6 -3
- package/lib/run.js +72 -54
- package/lib/say.d.ts +2 -0
- package/lib/say.js +7 -0
- package/lib/scheduled.js +54 -43
- package/lib/section.d.ts +1 -0
- package/lib/section.js +8 -2
- package/lib/settings/account.d.ts +1 -0
- package/lib/settings/account.js +31 -21
- package/lib/settings/billing.js +55 -28
- package/lib/settings/capabilities.js +26 -15
- package/lib/settings/card.js +19 -12
- package/lib/settings/code.js +22 -11
- package/lib/settings/credits.d.ts +4 -4
- package/lib/settings/credits.js +34 -21
- package/lib/settings/environments.js +21 -14
- package/lib/settings/general.js +26 -14
- package/lib/settings/index.js +6 -3
- package/lib/settings/integrations.js +42 -28
- package/lib/settings/keys.js +37 -19
- package/lib/settings/list.d.ts +0 -5
- package/lib/settings/list.js +24 -15
- package/lib/settings/machines.d.ts +3 -0
- package/lib/settings/machines.js +63 -38
- package/lib/settings/members.js +23 -14
- package/lib/settings/memory.js +12 -6
- package/lib/settings/notifications.js +32 -19
- package/lib/settings/plan.d.ts +1 -1
- package/lib/settings/plan.js +40 -17
- package/lib/settings/privacy.js +26 -13
- package/lib/settings/ui.d.ts +3 -2
- package/lib/settings/ui.js +9 -3
- package/lib/settings/usage.js +7 -3
- package/lib/shelf.js +18 -15
- package/lib/side.d.ts +1 -26
- package/lib/side.js +11 -5
- package/lib/standing.d.ts +2 -10
- package/lib/standing.js +5 -3
- package/lib/suggest.d.ts +8 -28
- package/lib/suggest.js +39 -4
- package/lib/suggestions.js +14 -5
- package/lib/summary.d.ts +1 -1
- package/lib/summary.js +4 -1
- package/lib/sync.js +22 -12
- package/lib/tools.d.ts +4 -3
- package/lib/tools.js +10 -7
- package/lib/transcript.js +10 -8
- package/package.json +4 -2
package/lib/settings/keys.js
CHANGED
|
@@ -5,55 +5,69 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* own credential: creating one never touches another, and revoking one stops only
|
|
6
6
|
* that one. A secret key is shown once, when it is made. A limit narrows what a
|
|
7
7
|
* key reaches; it never reaches further than you do.
|
|
8
|
+
*
|
|
9
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a key's name, its
|
|
10
|
+
* prefix and its kind:name limits are drawn as given.
|
|
8
11
|
*/
|
|
9
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
13
|
+
import { phrase } from '@hanzo/i18n';
|
|
14
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
10
15
|
import { Button, Dialog, DialogContent, DialogTitle, Input } from '@hanzo/ui';
|
|
11
16
|
import { CopyButton } from '@hanzo/ui/product';
|
|
12
17
|
import { useState } from 'react';
|
|
18
|
+
import { told } from '../api/call.js';
|
|
13
19
|
import { keys, KINDS, limits, mint, revoke } from '../api/keys.js';
|
|
14
20
|
import { Confirm } from '../ask.js';
|
|
15
21
|
import { useRead } from '../data.js';
|
|
16
22
|
import { useHost, useTarget } from '../host.js';
|
|
17
23
|
import { Card, Field, Heading, Note, Once, Row, Soft } from './ui.js';
|
|
18
|
-
|
|
24
|
+
/** Each kind's words, whole, so a language says the kind its own way: its name, its create control and label, and the dialog's titles. */
|
|
25
|
+
const NAME = { secret: phrase('Secret key'), publishable: phrase('Publishable key') };
|
|
26
|
+
const CREATE = { secret: phrase('Create secret key'), publishable: phrase('Create publishable key') };
|
|
27
|
+
const CREATE_A = { secret: phrase('Create a secret key'), publishable: phrase('Create a publishable key') };
|
|
28
|
+
const NEW = { secret: phrase('Your new secret key'), publishable: phrase('Your new publishable key') };
|
|
29
|
+
/** The statuses IAM names besides active, as words; one it does not is drawn as given. */
|
|
30
|
+
const STATUS = { expired: phrase('Expired'), revoked: phrase('Revoked'), disabled: phrase('Disabled') };
|
|
19
31
|
/** The line under a key: its head, what it reaches, when it was made, and where it stands. */
|
|
20
|
-
function about(k) {
|
|
32
|
+
function about(k, t) {
|
|
21
33
|
const when = k.created ? new Date(k.created) : null;
|
|
22
34
|
return [
|
|
23
35
|
k.type === 'secret' && k.prefix ? `${k.prefix}…` : '',
|
|
24
|
-
k.type === 'publishable' ? 'Publishable' : '',
|
|
25
|
-
k.limit.length ?
|
|
26
|
-
when && !Number.isNaN(when.getTime()) ?
|
|
27
|
-
k.status !== 'active' ? k.status[0].toUpperCase() + k.status.slice(1) : '',
|
|
36
|
+
k.type === 'publishable' ? t('Publishable') : '',
|
|
37
|
+
k.limit.length ? t('Reaches {limits}', { limits: k.limit.join(', ') }) : t('Unrestricted'),
|
|
38
|
+
when && !Number.isNaN(when.getTime()) ? t('Created {date}', { date: when.toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric' }) }) : '',
|
|
39
|
+
k.status !== 'active' ? (STATUS[k.status] ? t(STATUS[k.status]) : k.status[0].toUpperCase() + k.status.slice(1)) : '',
|
|
28
40
|
]
|
|
29
41
|
.filter(Boolean)
|
|
30
42
|
.join(' · ');
|
|
31
43
|
}
|
|
32
44
|
export function Keys() {
|
|
45
|
+
const { t } = useI18n();
|
|
33
46
|
const host = useHost();
|
|
34
|
-
const
|
|
47
|
+
const target = useTarget();
|
|
35
48
|
const signed = Boolean(host.person);
|
|
36
|
-
const list = useRead(signed ? () => keys(
|
|
49
|
+
const list = useRead(signed ? () => keys(target) : null, [], [target, signed]);
|
|
37
50
|
const [making, setMaking] = useState(null);
|
|
38
51
|
const [revoking, setRevoking] = useState(null);
|
|
39
52
|
const [note, setNote] = useState('');
|
|
40
53
|
if (!signed) {
|
|
41
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
54
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('API keys') }), _jsx(Soft, { children: t('Sign in to see your API keys.') })] }));
|
|
42
55
|
}
|
|
43
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
44
|
-
setNote(
|
|
56
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('API keys'), detail: t('Your keys for calling {api} from code. Make one for each app; revoking one stops only that one.', { api: 'api.hanzo.ai' }) }), _jsx(XStack, { gap: "$2", children: KINDS.map((type) => (_jsx(Button, { size: "sm", variant: "outline", "aria-label": t(CREATE_A[type]), onPress: () => setMaking(type), children: t(CREATE[type]) }, type))) }), list.error ? (_jsx(Soft, { children: told(list.error, t) })) : list.loading && list.value.length === 0 ? (_jsx(Soft, { children: t('Reading your keys…') })) : list.value.length === 0 ? (_jsx(Soft, { children: t('No keys yet.') })) : (_jsx(Card, { children: list.value.map((k, i) => (_jsxs(YStack, { borderTopWidth: i ? 1 : 0, borderColor: "$borderColor", opacity: k.status === 'active' ? 1 : 0.55, children: [_jsx(Row, { first: true, title: k.name || t(NAME[k.type]), detail: about(k, t), trailing: k.status === 'revoked' ? null : (_jsx(Button, { size: "sm", variant: "ghost", "aria-label": t('Revoke {name}', { name: k.name || k.id }), onPress: () => setRevoking(k), children: t('Revoke') })) }), k.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": t('Publishable key'), style: { fontFamily: 'var(--f-mono, ui-monospace, monospace)' }, children: k.key }), _jsx(CopyButton, { value: k.key, label: t('Copy publishable key') })] })) : null] }, k.id))) })), _jsx(Note, { children: note }), _jsx(Making, { type: making, onClose: () => setMaking(null), onMade: (m) => {
|
|
57
|
+
setNote(t('{name} created', { name: m.name || t(NAME[m.type]) }));
|
|
45
58
|
list.reload();
|
|
46
|
-
} }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title:
|
|
59
|
+
} }), _jsx(Confirm, { open: revoking !== null, onOpenChange: (o) => !o && setRevoking(null), title: revoking?.name ? t('Revoke {name}?', { name: revoking.name }) : t('Revoke this key?'), says: t('It stops working within a minute, and anything using it is refused. Your other keys keep working.'), act: t('Revoke'),
|
|
47
60
|
// It acts only while it is open, which is while a key is chosen.
|
|
48
61
|
run: async () => {
|
|
49
|
-
await revoke(
|
|
50
|
-
setNote(
|
|
62
|
+
await revoke(target, revoking.id);
|
|
63
|
+
setNote(t('{name} revoked', { name: revoking.name || t(NAME[revoking.type]) }));
|
|
51
64
|
list.reload();
|
|
52
65
|
} })] }));
|
|
53
66
|
}
|
|
54
67
|
/** Creating one key: a name and an optional limit, then the key itself, once. */
|
|
55
68
|
function Making({ type, onClose, onMade }) {
|
|
56
|
-
const t =
|
|
69
|
+
const { t } = useI18n();
|
|
70
|
+
const target = useTarget();
|
|
57
71
|
const [name, setName] = useState('');
|
|
58
72
|
const [limit, setLimit] = useState('');
|
|
59
73
|
const [made, setMade] = useState(null);
|
|
@@ -76,21 +90,25 @@ function Making({ type, onClose, onMade }) {
|
|
|
76
90
|
list = limits(limit);
|
|
77
91
|
}
|
|
78
92
|
catch (e) {
|
|
79
|
-
setNote(e
|
|
93
|
+
setNote(told(e, t));
|
|
80
94
|
return;
|
|
81
95
|
}
|
|
82
96
|
setWorking(true);
|
|
83
97
|
try {
|
|
84
|
-
const m = await mint(
|
|
98
|
+
const m = await mint(target, type, name, list);
|
|
85
99
|
setMade(m);
|
|
86
100
|
onMade(m);
|
|
87
101
|
}
|
|
88
102
|
catch (e) {
|
|
89
|
-
setNote(e
|
|
103
|
+
setNote(told(e, t));
|
|
90
104
|
}
|
|
91
105
|
finally {
|
|
92
106
|
setWorking(false);
|
|
93
107
|
}
|
|
94
108
|
};
|
|
95
|
-
return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ?
|
|
109
|
+
return (_jsx(Dialog, { open: type !== null, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: made ? t(NEW[made.type]) : type ? t(CREATE_A[type]) : '' }), made ? (_jsxs(YStack, { gap: "$3", children: [made.type === 'secret' ? (_jsx(Once, { value: made.key, label: t('Secret key'), copy: t('Copy 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: t('Copy publishable key') })] })), made.limit.length ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('It reaches {limits} and nothing else.', { limits: made.limit.join(', ') }) })) : null, _jsx(XStack, { justify: "flex-end", children: _jsx(Button, { size: "sm", onPress: close, children: t('Done') }) })] })) : (_jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: t('Name'), hint: t('What it is for, so you can tell it from your other keys.'), children: _jsx(Input, { value: name, onChangeText: setName, placeholder: t('production'), "aria-label": t('Name') }) }), _jsx(Field, { label: t('Limit'), hint: t('Optional. What it may reach, as {form} — {examples}, {read} for read-only. Empty reaches whatever you do.', {
|
|
110
|
+
form: 'kind:name',
|
|
111
|
+
examples: 'model:zen5, project:acme, product:train',
|
|
112
|
+
read: 'read:*',
|
|
113
|
+
}), children: _jsx(Input, { value: limit, onChangeText: setLimit, placeholder: "model:zen5, project:acme", "aria-label": t('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: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void go(type), children: t('Create') })] })] }))] }) }));
|
|
96
114
|
}
|
package/lib/settings/list.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The sections of Settings, in the order the page lists them, by name and part
|
|
3
|
-
* alone: what a sidebar lists without loading the sections themselves
|
|
4
|
-
* (sections.tsx pairs each with its body).
|
|
5
|
-
*/
|
|
6
1
|
import type { Section } from '../route.ts';
|
|
7
2
|
export type Group = 'Settings' | 'Code' | 'Organization';
|
|
8
3
|
export declare const GROUPS: readonly Group[];
|
package/lib/settings/list.js
CHANGED
|
@@ -1,19 +1,28 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The sections of Settings, in the order the page lists them, by name and part
|
|
3
|
+
* alone: what a sidebar lists without loading the sections themselves
|
|
4
|
+
* (sections.tsx pairs each with its body).
|
|
5
|
+
*
|
|
6
|
+
* A part's id is its name, and every name is English, marked (`phrase`): a
|
|
7
|
+
* list draws each through `t`.
|
|
8
|
+
*/
|
|
9
|
+
import { phrase } from '@hanzo/i18n';
|
|
10
|
+
export const GROUPS = [phrase('Settings'), phrase('Code'), phrase('Organization')];
|
|
2
11
|
export const LIST = [
|
|
3
|
-
{ id: 'general', label: 'General', group: 'Settings' },
|
|
4
|
-
{ id: 'account', label: 'Account', group: 'Settings' },
|
|
5
|
-
{ id: 'privacy', label: 'Privacy', group: 'Settings' },
|
|
6
|
-
{ id: 'billing', label: 'Billing', group: 'Settings' },
|
|
7
|
-
{ id: 'usage', label: 'Usage', group: 'Settings' },
|
|
8
|
-
{ id: 'capabilities', label: 'Capabilities', group: 'Settings' },
|
|
9
|
-
{ id: 'memory', label: 'Memory', group: 'Settings' },
|
|
10
|
-
{ id: 'code', label: 'Code', group: 'Code' },
|
|
11
|
-
{ id: 'environments', label: 'Environments', group: 'Code' },
|
|
12
|
-
{ id: 'machines', label: 'Machines', group: 'Code' },
|
|
13
|
-
{ id: 'keys', label: 'API keys', group: 'Code' },
|
|
14
|
-
{ id: 'members', label: 'Members', group: 'Organization' },
|
|
15
|
-
{ id: 'integrations', label: 'Integrations', group: 'Organization' },
|
|
16
|
-
{ id: 'notifications', label: 'Notifications', group: 'Organization' },
|
|
12
|
+
{ id: 'general', label: phrase('General'), group: 'Settings' },
|
|
13
|
+
{ id: 'account', label: phrase('Account'), group: 'Settings' },
|
|
14
|
+
{ id: 'privacy', label: phrase('Privacy'), group: 'Settings' },
|
|
15
|
+
{ id: 'billing', label: phrase('Billing'), group: 'Settings' },
|
|
16
|
+
{ id: 'usage', label: phrase('Usage'), group: 'Settings' },
|
|
17
|
+
{ id: 'capabilities', label: phrase('Capabilities'), group: 'Settings' },
|
|
18
|
+
{ id: 'memory', label: phrase('Memory'), group: 'Settings' },
|
|
19
|
+
{ id: 'code', label: phrase('Code'), group: 'Code' },
|
|
20
|
+
{ id: 'environments', label: phrase('Environments'), group: 'Code' },
|
|
21
|
+
{ id: 'machines', label: phrase('Machines'), group: 'Code' },
|
|
22
|
+
{ id: 'keys', label: phrase('API keys'), group: 'Code' },
|
|
23
|
+
{ id: 'members', label: phrase('Members'), group: 'Organization' },
|
|
24
|
+
{ id: 'integrations', label: phrase('Integrations'), group: 'Organization' },
|
|
25
|
+
{ id: 'notifications', label: phrase('Notifications'), group: 'Organization' },
|
|
17
26
|
];
|
|
18
27
|
/** A part's label as a list draws it: none for the first part, or where the part's first row already says it (Code). */
|
|
19
28
|
export const heading = (g) => (g === 'Settings' || LIST.find((e) => e.group === g)?.label === g ? '' : g);
|
package/lib/settings/machines.js
CHANGED
|
@@ -5,12 +5,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
5
5
|
* and keeps it online while it runs; one can also be registered here by hand.
|
|
6
6
|
* The member who linked a machine, or an org admin, renames it, drains it,
|
|
7
7
|
* mints its claim key or removes it.
|
|
8
|
+
*
|
|
9
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a machine's name,
|
|
10
|
+
* hostname, id and hardware, and the commands to type, are drawn as given.
|
|
8
11
|
*/
|
|
9
12
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
13
|
+
import { phrase } from '@hanzo/i18n';
|
|
14
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
10
15
|
import { Activity, ChevronDown, ChevronUp, Cloud, Cpu, HardDrive, Laptop, MoreHorizontal, Plus, Server, Terminal, Zap, } from '@hanzogui/lucide-icons-2';
|
|
11
16
|
import { Button, Dialog, DialogContent, DialogTitle, DropdownMenu, Input } from '@hanzo/ui';
|
|
12
17
|
import { CopyButton } from '@hanzo/ui/product';
|
|
13
18
|
import { useEffect, useState } from 'react';
|
|
19
|
+
import { told } from '../api/call.js';
|
|
14
20
|
import { add, change, formatBytes, formatPercent, formatRelative, key, KINDS, machines, remove, state, } from '../api/machines.js';
|
|
15
21
|
import { Confirm, Rename } from '../ask.js';
|
|
16
22
|
import { useRead } from '../data.js';
|
|
@@ -23,10 +29,23 @@ const LINK = ['hanzo login', 'hanzo link'].join('\n');
|
|
|
23
29
|
const BEAT = 30_000;
|
|
24
30
|
/** The list before its first answer; every answer is a new array, even an empty one. */
|
|
25
31
|
const UNREAD = [];
|
|
32
|
+
/** A machine's status in a line, the platform's word or `not seen yet`, said in the reader's language; one it does not name, as given. */
|
|
33
|
+
const STATES = {
|
|
34
|
+
online: phrase('online'),
|
|
35
|
+
offline: phrase('offline'),
|
|
36
|
+
draining: phrase('draining'),
|
|
37
|
+
'not seen yet': phrase('not seen yet'),
|
|
38
|
+
'': phrase('unknown'),
|
|
39
|
+
};
|
|
40
|
+
/** What a machine is, as the platform names its kinds, said in the reader's language; one it does not name, as given. */
|
|
41
|
+
const KIND = { laptop: phrase('laptop'), gpu: phrase('gpu'), cloud: phrase('cloud'), cluster: phrase('cluster'), machine: phrase('machine') };
|
|
42
|
+
/** A status as a line says it. Code's place list says it the same way. */
|
|
43
|
+
export const said = (status, t) => (status in STATES ? t(STATES[status]) : status);
|
|
44
|
+
const kindOf = (kind, t) => (KIND[kind] ? t(KIND[kind]) : kind);
|
|
26
45
|
/** The line under a machine: what it is, where, how big, and what it is doing. */
|
|
27
|
-
function about(m) {
|
|
28
|
-
const load = m.running ?
|
|
29
|
-
return [state(m)
|
|
46
|
+
function about(m, t) {
|
|
47
|
+
const load = m.running ? t('{n} running', { n: m.running }) : m.sessions ? t('{n} runs', { n: m.sessions }) : '';
|
|
48
|
+
return [said(state(m), t), kindOf(m.kind, t), m.host && m.host !== m.label ? m.host : '', m.capacity, load].filter(Boolean).join(' · ');
|
|
30
49
|
}
|
|
31
50
|
function Dot({ m }) {
|
|
32
51
|
const on = state(m) === 'online';
|
|
@@ -47,6 +66,7 @@ function KindIcon({ kind, size = 16 }) {
|
|
|
47
66
|
}
|
|
48
67
|
}
|
|
49
68
|
function StatusBadge({ m }) {
|
|
69
|
+
const { t } = useI18n();
|
|
50
70
|
const s = state(m);
|
|
51
71
|
if (!s)
|
|
52
72
|
return null;
|
|
@@ -54,27 +74,28 @@ function StatusBadge({ m }) {
|
|
|
54
74
|
let text = s;
|
|
55
75
|
if (s === 'online') {
|
|
56
76
|
color = '$green10';
|
|
57
|
-
text = 'Online';
|
|
77
|
+
text = t('Online');
|
|
58
78
|
}
|
|
59
79
|
else if (s === 'draining') {
|
|
60
80
|
color = '$yellow10';
|
|
61
|
-
text = 'Draining';
|
|
81
|
+
text = t('Draining');
|
|
62
82
|
}
|
|
63
83
|
else if (s === 'not seen yet') {
|
|
64
84
|
color = '$blue10';
|
|
65
|
-
text = 'Not seen yet';
|
|
85
|
+
text = t('Not seen yet');
|
|
66
86
|
}
|
|
67
87
|
else if (s === 'offline') {
|
|
68
88
|
color = '$soft';
|
|
69
|
-
text = 'Offline';
|
|
89
|
+
text = t('Offline');
|
|
70
90
|
}
|
|
71
91
|
return (_jsxs(XStack, { px: "$2", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: color, items: "center", gap: "$1.5", children: [_jsx(Dot, { m: m }), _jsx(SizableText, { size: "$1", color: color, style: { fontWeight: 500 }, children: text })] }));
|
|
72
92
|
}
|
|
73
93
|
export function Machines() {
|
|
94
|
+
const { t } = useI18n();
|
|
74
95
|
const host = useHost();
|
|
75
|
-
const
|
|
96
|
+
const target = useTarget();
|
|
76
97
|
const signed = Boolean(host.person);
|
|
77
|
-
const list = useRead(signed ? () => machines(
|
|
98
|
+
const list = useRead(signed ? () => machines(target) : null, UNREAD, [target, signed]);
|
|
78
99
|
const [adding, setAdding] = useState(false);
|
|
79
100
|
const [renaming, setRenaming] = useState(null);
|
|
80
101
|
const [removing, setRemoving] = useState(null);
|
|
@@ -92,53 +113,57 @@ export function Machines() {
|
|
|
92
113
|
return () => clearInterval(id);
|
|
93
114
|
}, [again, signed]);
|
|
94
115
|
if (!signed) {
|
|
95
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
116
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Machines') }), _jsx(Soft, { children: t('Sign in to see your organization’s machines.') })] }));
|
|
96
117
|
}
|
|
97
118
|
const drain = async (m, status) => {
|
|
98
119
|
setNote('');
|
|
99
120
|
try {
|
|
100
|
-
await change(
|
|
101
|
-
setNote(status === 'draining' ?
|
|
121
|
+
await change(target, m.id, { status });
|
|
122
|
+
setNote(status === 'draining' ? t('{name} takes no new runs', { name: m.label }) : t('{name} takes runs again', { name: m.label }));
|
|
102
123
|
list.reload();
|
|
103
124
|
}
|
|
104
125
|
catch (e) {
|
|
105
|
-
setNote(e
|
|
126
|
+
setNote(told(e, t));
|
|
106
127
|
}
|
|
107
128
|
};
|
|
108
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
129
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Machines'), detail: t('Your organization’s own computers. A run sent to one runs there instead of in the Hanzo sandbox.') }), _jsxs(Group, { title: t('Link a machine'), detail: t('Run these on it. It registers under its hostname, and stays online while {command} runs.', { command: 'hanzo link' }), 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": t('Link commands'), 'data-i18n-ltr': '', fontFamily: "$mono", "$platform-web": { whiteSpace: 'pre-wrap' }, children: LINK }), _jsx(CopyButton, { value: LINK, label: t('Copy the commands') })] }), _jsxs(XStack, { items: "center", justify: "space-between", flexWrap: "wrap", gap: "$2", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Register one by hand')] }), _jsxs(Button, { size: "sm", variant: "ghost", onPress: () => setShowAdvancedLink(!showAdvancedLink), children: [showAdvancedLink ? _jsx(ChevronUp, { size: 14 }) : _jsx(ChevronDown, { size: 14 }), showAdvancedLink ? t('Hide options') : t('More connection options')] })] }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Supported: macOS, Linux, Windows, NVIDIA CUDA GPUs, Cloud VMs') })] }), showAdvancedLink ? (_jsxs(YStack, { gap: "$2.5", p: "$3", rounded: "$3", borderWidth: 1, borderColor: "$borderColor", bg: "$raised", children: [_jsxs(YStack, { gap: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: t('1. Background daemon (system service)') }), _jsx(SizableText, { size: "$1", color: "$soft", children: t('Run worker continuously in the background without keeping a terminal open:') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: "hanzo code --serve" }), _jsx(CopyButton, { value: "hanzo code --serve", label: t('Copy daemon command') })] })] }), _jsxs(YStack, { gap: "$1", pt: "$1", children: [_jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: t('2. Install CLI from shell') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2.5", py: "$1.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: "curl -fsSL https://hanzo.sh | sh" }), _jsx(CopyButton, { value: "curl -fsSL https://hanzo.sh | sh", label: t('Copy install command') })] })] })] })) : null] }), _jsxs(Group, { title: t('Linked'), children: [list.value === UNREAD ? (_jsx(Soft, { children: list.error ? told(list.error, t) : t('Reading machines…') })) : list.value.length === 0 ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Soft, { children: t('No machine is linked yet.') }), _jsx(Card, { children: _jsxs(YStack, { p: "$4", gap: "$3", children: [_jsxs(XStack, { items: "center", gap: "$2.5", children: [_jsx(Server, { size: 20 }), _jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$3", color: "$ink", style: { fontWeight: 600 }, children: t('Connect your first computer or GPU server') }), _jsx(SizableText, { size: "$2", color: "$soft", children: t('Execute code, runs, and local models directly on your hardware with full privacy and zero cloud fees.') })] })] }), _jsxs(YStack, { gap: "$2", pt: "$1", children: [_jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "1." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Install Hanzo CLI: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "curl -fsSL https://hanzo.sh | sh" }) }) })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "2." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Sign in with your account: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo login" }) }) })] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: { ...mono, width: 20 }, children: "3." }), _jsx(SizableText, { size: "$2", color: "$ink", children: t('Link the machine: {command}', { command: _jsx(SizableText, { size: "$2", color: "$ink", fontFamily: "$mono", 'data-i18n-ltr': '', children: "hanzo link" }) }) })] })] }), _jsx(XStack, { pt: "$1", children: _jsxs(Button, { size: "sm", variant: "outline", onPress: () => setAdding(true), children: [_jsx(Plus, { size: 14 }), " ", t('Register one by hand')] }) })] }) })] })) : (_jsx(Card, { children: list.value.map((m, i) => {
|
|
109
130
|
const isExpanded = expandedId === m.id;
|
|
110
131
|
const hasTelemetry = Boolean(m.metrics || m.spec);
|
|
111
|
-
return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children:
|
|
112
|
-
{ key: 'details', label: isExpanded ? 'Hide details' : 'Show details', onSelect: () => setExpandedId(isExpanded ? null : m.id) },
|
|
113
|
-
{ key: 'rename', label: 'Rename', onSelect: () => setRenaming(m) },
|
|
132
|
+
return (_jsxs(YStack, { borderTopWidth: i > 0 ? 1 : 0, borderColor: "$borderColor", children: [_jsx(Row, { first: i === 0, leading: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(Dot, { m: m }), _jsx(KindIcon, { kind: m.kind, size: 16 })] }), title: m.label, detail: about(m, t), trailing: _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(StatusBadge, { m: m }), m.serving ? (_jsx(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", borderWidth: 1, borderColor: "$green10", children: _jsx(SizableText, { size: "$1", color: "$green10", children: t('Serving') }) })) : null, m.running > 0 ? (_jsxs(XStack, { px: "$1.5", py: "$0.5", rounded: "$10", bg: "$raised", items: "center", gap: "$1", children: [_jsx(Activity, { size: 12, color: "$green10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{n} active', { n: m.running }) })] })) : null, _jsx(CopyButton, { value: `hanzo link --target ${m.id}`, label: t('Copy link command for {name}', { name: m.label }) }), _jsx(DropdownMenu, { trigger: _jsx(XStack, { render: "button", "aria-label": t('Actions for {name}', { name: m.label }), px: "$1.5", py: "$1", rounded: "$2", hoverStyle: { bg: '$hover' }, children: _jsx(MoreHorizontal, { size: 16 }) }), items: [
|
|
133
|
+
{ key: 'details', label: isExpanded ? t('Hide details') : t('Show details'), onSelect: () => setExpandedId(isExpanded ? null : m.id) },
|
|
134
|
+
{ key: 'rename', label: t('Rename'), onSelect: () => setRenaming(m) },
|
|
114
135
|
m.status === 'draining'
|
|
115
|
-
? { key: 'online', label: 'Take runs again', onSelect: () => void drain(m, 'online') }
|
|
116
|
-
: { key: 'drain', label: 'Drain', description: 'Finish what it has, take nothing new', onSelect: () => void drain(m, 'draining') },
|
|
117
|
-
{ key: 'key', label: 'Claim key', onSelect: () => setKeying(m) },
|
|
136
|
+
? { key: 'online', label: t('Take runs again'), onSelect: () => void drain(m, 'online') }
|
|
137
|
+
: { key: 'drain', label: t('Drain'), description: t('Finish what it has, take nothing new'), onSelect: () => void drain(m, 'draining') },
|
|
138
|
+
{ key: 'key', label: t('Claim key'), onSelect: () => setKeying(m) },
|
|
118
139
|
{ type: 'separator' },
|
|
119
|
-
{ key: 'remove', label: 'Remove', destructive: true, onSelect: () => setRemoving(m) },
|
|
120
|
-
] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }),
|
|
121
|
-
}) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: list.error
|
|
122
|
-
setNote(
|
|
140
|
+
{ key: 'remove', label: t('Remove'), destructive: true, onSelect: () => setRemoving(m) },
|
|
141
|
+
] })] }) }), isExpanded || hasTelemetry ? (_jsxs(YStack, { px: "$4", pb: "$3", pt: "$1", gap: "$2.5", children: [m.spec ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$2", items: "center", children: [m.spec.os ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Terminal, { size: 12 }), _jsxs(SizableText, { size: "$1", color: "$ink", children: [m.spec.os, m.spec.arch ? ` · ${m.spec.arch}` : ''] })] })) : null, m.spec.cpus ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Cpu, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{n} cores', { n: m.spec.cpus }) })] })) : null, m.spec.memory ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(HardDrive, { size: 12 }), _jsx(SizableText, { size: "$1", color: "$ink", children: t('{size} RAM', { size: formatBytes(m.spec.memory) }) })] })) : null, m.spec.gpus && m.spec.gpus.length > 0 ? (_jsxs(XStack, { px: "$2", py: "$1", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", gap: "$1.5", children: [_jsx(Zap, { size: 12, color: "$yellow10" }), _jsx(SizableText, { size: "$1", color: "$ink", children: m.spec.gpus.map((g) => `${g.model || g.vendor || 'GPU'}${g.memory ? ` (${formatBytes(g.memory)})` : ''}`).join(', ') })] })) : null] })) : null, m.metrics && (m.metrics.cpuUtil || m.metrics.load1 || m.metrics.memUsed || m.metrics.gpuUtil) ? (_jsxs(XStack, { flexWrap: "wrap", gap: "$3", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", items: "center", children: [m.metrics.cpuUtil ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('CPU Utilization') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.cpuUtil), " ", m.metrics.cpuTemp ? `· ${m.metrics.cpuTemp}°C` : ''] })] })) : m.metrics.load1 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Load (1m, 5m, 15m)') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.load1, ", ", m.metrics.load5 ?? 0, ", ", m.metrics.load15 ?? 0] })] })) : null, m.metrics.memUsed ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Memory') }), _jsx(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: m.spec?.memory ? `${formatBytes(m.metrics.memUsed)} / ${formatBytes(m.spec.memory)}` : t('{size} used', { size: formatBytes(m.metrics.memUsed) }) })] })) : null, m.metrics.gpuUtil !== undefined && m.metrics.gpuUtil > 0 ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('GPU') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [formatPercent(m.metrics.gpuUtil), " ", m.metrics.gpuTemp ? `· ${m.metrics.gpuTemp}°C` : '', " ", m.metrics.gpuPower ? `· ${m.metrics.gpuPower}W` : ''] })] })) : null, m.metrics.model ? (_jsxs(YStack, { gap: "$0.5", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Serving Model') }), _jsxs(SizableText, { size: "$2", color: "$ink", style: { fontWeight: 600 }, children: [m.metrics.model, " ", m.metrics.decode ? `(${m.metrics.decode} tok/s)` : ''] })] })) : null] })) : null, _jsxs(XStack, { justify: "space-between", items: "center", flexWrap: "wrap", gap: "$2", pt: "$0.5", children: [_jsxs(XStack, { items: "center", gap: "$3", children: [_jsx(SizableText, { size: "$1", color: "$soft", style: mono, children: t('ID: {id}', { id: m.id }) }), m.seen ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Last heartbeat: {when}', { when: formatRelative(m.seen) }) })) : null] }), _jsxs(XStack, { items: "center", gap: "$2", children: [_jsx(SizableText, { size: "$1", color: "$soft", children: t('Run directly on this machine:') }), _jsxs(XStack, { items: "center", gap: "$1", px: "$2", py: "$0.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsxs(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: ["hanzo run --target ", m.id] }), _jsx(CopyButton, { value: `hanzo run --target ${m.id}`, label: t('Copy run command for {name}', { name: m.label }) })] })] })] })] })) : null] }, m.id));
|
|
142
|
+
}) })), list.error && list.value !== UNREAD ? _jsx(Soft, { children: told(list.error, t) }) : null] }), _jsx(Note, { children: note }), _jsx(Adding, { open: adding, onOpenChange: setAdding, onAdded: (m) => {
|
|
143
|
+
setNote(t('{name} is registered', { name: m.label }));
|
|
123
144
|
list.reload();
|
|
124
|
-
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title:
|
|
145
|
+
} }), _jsx(Rename, { open: renaming !== null, onOpenChange: (o) => !o && setRenaming(null), title: t('Rename machine'), name: renaming?.label ?? '',
|
|
125
146
|
// Each dialog acts only while it is open, which is while its machine is chosen.
|
|
126
147
|
run: async (label) => {
|
|
127
|
-
await change(
|
|
128
|
-
setNote(
|
|
148
|
+
await change(target, renaming.id, { label });
|
|
149
|
+
setNote(t('Renamed to {name}', { name: label }));
|
|
129
150
|
list.reload();
|
|
130
|
-
} }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title:
|
|
131
|
-
await remove(
|
|
132
|
-
setNote(
|
|
151
|
+
} }), _jsx(Confirm, { open: removing !== null, onOpenChange: (o) => !o && setRemoving(null), title: removing ? t('Remove {name}?', { name: removing.label }) : t('Remove this machine?'), says: t('Runs can no longer be sent to it, and its claim key stops working. Linking it again registers it anew.'), act: t('Remove'), run: async () => {
|
|
152
|
+
await remove(target, removing.id);
|
|
153
|
+
setNote(t('{name} is removed', { name: removing.label }));
|
|
133
154
|
list.reload();
|
|
134
|
-
} }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title:
|
|
135
|
-
const k = await key(
|
|
155
|
+
} }), _jsx(Confirm, { open: keying !== null, onOpenChange: (o) => !o && setKeying(null), title: keying ? t('Mint a claim key for {name}?', { name: keying.label }) : t('Mint a claim key for this machine?'), says: t('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: t('Mint key'), run: async () => {
|
|
156
|
+
const k = await key(target, keying.id);
|
|
136
157
|
setMinted({ machine: keying, key: k });
|
|
137
|
-
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [
|
|
158
|
+
} }), _jsx(Dialog, { open: minted !== null, onOpenChange: (o) => !o && setMinted(null), children: _jsxs(DialogContent, { maxW: 480, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Claim key for {name}', { name: minted?.machine.label ?? '' }) }), minted ? (_jsxs(YStack, { gap: "$3", children: [_jsx(Once, { value: minted.key, label: t('Claim key'), copy: t('Copy claim key'), children: _jsx(SizableText, { size: "$1", color: "$soft", children: t('A runner on this machine sends it as {header} when it claims the runs sent here, at {route}.', {
|
|
159
|
+
header: 'X-Target-Key',
|
|
160
|
+
route: `POST /v1/agent/targets/${minted.machine.id}/claim`,
|
|
161
|
+
}) }) }), _jsxs(YStack, { gap: "$1", p: "$2.5", rounded: "$2", bg: "$raised", borderWidth: 1, borderColor: "$borderColor", children: [_jsx(SizableText, { size: "$1", color: "$ink", style: { fontWeight: 600 }, children: t('Start background worker with this key:') }), _jsxs(XStack, { items: "center", justify: "space-between", px: "$2", py: "$1", rounded: "$2", bg: "$raised", children: [_jsxs(SizableText, { size: "$1", color: "$ink", 'data-i18n-ltr': '', fontFamily: "$mono", children: ["export HANZO_TARGET_KEY=\"", minted.key, "\""] }), _jsx(CopyButton, { value: `export HANZO_TARGET_KEY="${minted.key}"\nhanzo code --serve --target ${minted.machine.id}`, label: t('Copy key command') })] })] })] })) : null, _jsx(XStack, { justify: "flex-end", pt: "$2", children: _jsx(Button, { size: "sm", onPress: () => setMinted(null), children: t('Done') }) })] }) })] }));
|
|
138
162
|
}
|
|
139
163
|
/** Registering a machine by hand: its name, what it is, and the hostname its runs report. */
|
|
140
164
|
function Adding({ open, onOpenChange, onAdded }) {
|
|
141
|
-
const t =
|
|
165
|
+
const { t } = useI18n();
|
|
166
|
+
const target = useTarget();
|
|
142
167
|
const [label, setLabel] = useState('');
|
|
143
168
|
const [kind, setKind] = useState('machine');
|
|
144
169
|
const [hostname, setHostname] = useState('');
|
|
@@ -161,16 +186,16 @@ function Adding({ open, onOpenChange, onAdded }) {
|
|
|
161
186
|
try {
|
|
162
187
|
const targetLabel = label.trim() || hostname.trim();
|
|
163
188
|
if (!targetLabel)
|
|
164
|
-
throw new Error('A machine needs a name or a hostname');
|
|
165
|
-
const m = await add(
|
|
189
|
+
throw new Error(phrase('A machine needs a name or a hostname'));
|
|
190
|
+
const m = await add(target, { label: targetLabel, kind, host: hostname, capacity: capacity.trim() || undefined });
|
|
166
191
|
setWorking(false);
|
|
167
192
|
onAdded(m);
|
|
168
193
|
close();
|
|
169
194
|
}
|
|
170
195
|
catch (e) {
|
|
171
|
-
setNote(e
|
|
196
|
+
setNote(told(e, t));
|
|
172
197
|
setWorking(false);
|
|
173
198
|
}
|
|
174
199
|
};
|
|
175
|
-
return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsx(DialogTitle, { children:
|
|
200
|
+
return (_jsx(Dialog, { open: open, onOpenChange: (o) => !o && close(), children: _jsxs(DialogContent, { maxW: 460, showCloseButton: false, children: [_jsx(DialogTitle, { children: t('Register a machine') }), _jsxs(YStack, { gap: "$3", children: [_jsx(Field, { label: t('Name'), hint: t('Left empty, it takes the hostname.'), children: _jsx(Input, { value: label, onChangeText: setLabel, "aria-label": t('Machine name'), autoFocus: true }) }), _jsx(Field, { label: t('Kind'), children: _jsx(XStack, { flexWrap: "wrap", gap: "$1.5", role: "radiogroup", "aria-label": t('Kind'), children: KINDS.map((k) => (_jsxs(XStack, { render: "button", role: "radio", "aria-checked": k === kind, "aria-label": kindOf(k, t), onPress: () => setKind(k), px: "$2.5", py: "$1", rounded: "$10", borderWidth: 1, borderColor: k === kind ? '$ink' : '$borderColor', items: "center", gap: "$1.5", children: [_jsx(KindIcon, { kind: k, size: 14 }), _jsx(SizableText, { size: "$1", color: k === kind ? '$ink' : '$soft', children: kindOf(k, t) })] }, k))) }) }), _jsx(Field, { label: t('Hostname'), hint: t('Optional. What its runs report. {command}, signed in as you on a machine with this hostname, takes this row over.', { command: 'hanzo link' }), children: _jsx(Input, { value: hostname, onChangeText: setHostname, "aria-label": t('Hostname'), autoCapitalize: "none" }) }), _jsx(Field, { label: t('Capacity / Hardware'), hint: t('Optional human summary (e.g. {example}).', { example: '16 vCPU / 64G / 1× RTX 4090' }), children: _jsx(Input, { value: capacity, onChangeText: setCapacity, "aria-label": t('Capacity'), placeholder: t('e.g. {example}', { example: '16 vCPU / 64G / 1× RTX 4090' }) }) }), 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: t('Cancel') }), _jsx(Button, { size: "sm", disabled: working, onPress: () => void save(), children: t('Register') })] })] })] }) }));
|
|
176
201
|
}
|
package/lib/settings/members.js
CHANGED
|
@@ -3,31 +3,39 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
* Members: who is in the organization and in what role, and — for an org admin —
|
|
4
4
|
* who has been invited, inviting someone by email, and withdrawing an invitation.
|
|
5
5
|
* IAM holds both lists and decides who may read and write them.
|
|
6
|
+
*
|
|
7
|
+
* Its words are the reader's language (@hanzo/i18n `t`); a person's name,
|
|
8
|
+
* address and id, an invitation's code and IAM's own state are drawn as given.
|
|
6
9
|
*/
|
|
7
10
|
import { SizableText, XStack, YStack } from '@hanzo/gui';
|
|
11
|
+
import { phrase } from '@hanzo/i18n';
|
|
12
|
+
import { useI18n } from '@hanzo/i18n/react';
|
|
8
13
|
import { UserPlus } from '@hanzogui/lucide-icons-2';
|
|
9
14
|
import { Button, Input } from '@hanzo/ui';
|
|
10
15
|
import { useState } from 'react';
|
|
16
|
+
import { told } from '../api/call.js';
|
|
11
17
|
import { EMAIL, invitations, invite, revoke, roster } from '../api/members.js';
|
|
12
18
|
import { useRead } from '../data.js';
|
|
13
19
|
import { useHost, useTarget } from '../host.js';
|
|
14
20
|
import { Card, day, Group, Heading, Note, Row, Soft } from './ui.js';
|
|
15
|
-
|
|
21
|
+
/** A role by its name, English and marked (`phrase`), drawn through `t`. */
|
|
22
|
+
const ROLE = { owner: phrase('Owner'), admin: phrase('Admin'), member: phrase('Member') };
|
|
16
23
|
function Initial({ name }) {
|
|
17
24
|
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
25
|
}
|
|
19
26
|
export function Members() {
|
|
27
|
+
const { t } = useI18n();
|
|
20
28
|
const host = useHost();
|
|
21
|
-
const
|
|
29
|
+
const target = useTarget();
|
|
22
30
|
const org = host.org ?? '';
|
|
23
31
|
const signed = Boolean(host.person && org);
|
|
24
|
-
const people = useRead(signed ? () => roster(
|
|
25
|
-
const asked = useRead(signed && host.admin ? () => invitations(
|
|
32
|
+
const people = useRead(signed ? () => roster(target, org) : null, [], [target, org, signed]);
|
|
33
|
+
const asked = useRead(signed && host.admin ? () => invitations(target, org) : null, [], [target, org, signed, host.admin]);
|
|
26
34
|
const [email, setEmail] = useState('');
|
|
27
35
|
const [working, setWorking] = useState(false);
|
|
28
36
|
const [note, setNote] = useState('');
|
|
29
37
|
if (!signed) {
|
|
30
|
-
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title:
|
|
38
|
+
return (_jsxs(YStack, { gap: "$5", children: [_jsx(Heading, { title: t('Members') }), _jsx(Soft, { children: host.person ? t('Choose an organization to see its members.') : t('Sign in to see this organization’s members.') })] }));
|
|
31
39
|
}
|
|
32
40
|
const act = async (what, done, after) => {
|
|
33
41
|
setWorking(true);
|
|
@@ -38,7 +46,7 @@ export function Members() {
|
|
|
38
46
|
setNote(done);
|
|
39
47
|
}
|
|
40
48
|
catch (e) {
|
|
41
|
-
setNote(e
|
|
49
|
+
setNote(told(e, t));
|
|
42
50
|
}
|
|
43
51
|
finally {
|
|
44
52
|
setWorking(false);
|
|
@@ -48,21 +56,22 @@ export function Members() {
|
|
|
48
56
|
const address = email.trim();
|
|
49
57
|
// Invite is offered only once something is typed.
|
|
50
58
|
if (!EMAIL.test(address)) {
|
|
51
|
-
setNote(
|
|
59
|
+
setNote(t('{email} is not an email address', { email: address }));
|
|
52
60
|
return;
|
|
53
61
|
}
|
|
54
|
-
void act(() => invite(
|
|
62
|
+
void act(() => invite(target, org, address), t('{email} is invited. IAM sends no email: share the code with them.', { email: address }), () => {
|
|
55
63
|
setEmail('');
|
|
56
64
|
asked.reload();
|
|
57
65
|
});
|
|
58
66
|
};
|
|
59
67
|
const open = asked.value.filter((i) => i.state === 'Active' && i.used < i.seats);
|
|
60
|
-
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title:
|
|
61
|
-
i.code ?
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
68
|
+
return (_jsxs(YStack, { gap: "$6", children: [_jsx(Heading, { title: t('Members'), detail: t('The people who can act in {org}, and the role each one holds.', { org }) }), _jsx(Group, { title: t('Members'), detail: people.value.length ? t('{n} in {org}', { n: people.value.length, org }) : undefined, children: people.error ? (_jsx(Soft, { children: told(people.error, t) })) : people.loading && !people.value.length ? (_jsx(Soft, { children: t('Reading members…') })) : people.value.length === 0 ? (_jsx(Card, { children: _jsx(Soft, { children: t('No one is on this organization’s 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 ? t('since {day}', { day: day(m.since) }) : ''].filter(Boolean).join(' · '), trailing: _jsx(SizableText, { size: "$1", color: m.role === 'member' ? '$soft' : '$ink', children: t(ROLE[m.role]) }) }, m.user))) })) }), host.admin ? (_jsxs(Group, { title: t('Invitations'), detail: t('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": t('Email to invite'), inputMode: "email", autoCapitalize: "none" }) }), _jsxs(Button, { size: "sm", disabled: working || !email.trim(), onPress: send, children: [_jsx(UserPlus, { size: 14 }), " ", t('Invite')] })] }), asked.error ? (_jsx(YStack, { borderTopWidth: 1, borderColor: "$borderColor", children: _jsx(Soft, { children: told(asked.error, t) }) })) : null, asked.value.map((i) => (_jsx(Row, { title: i.email || i.name, mono: !i.email, detail: [
|
|
69
|
+
i.code ? t('Code {code}', { code: i.code }) : '',
|
|
70
|
+
t('{used} of {seats} joined', { used: i.used, seats: i.seats }),
|
|
71
|
+
// IAM's own word for a state, as given; none at all is ours.
|
|
72
|
+
i.state !== 'Active' ? i.state.toLowerCase() || t('not redeemable') : '',
|
|
73
|
+
day(i.created) ? t('invited {day}', { day: day(i.created) }) : '',
|
|
65
74
|
]
|
|
66
75
|
.filter(Boolean)
|
|
67
|
-
.join(' · '), trailing: _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label":
|
|
76
|
+
.join(' · '), trailing: _jsx(Button, { size: "sm", variant: "ghost", disabled: working, "aria-label": t('Revoke the invitation for {who}', { who: i.email || i.name }), onPress: () => void act(() => revoke(target, i), t('The invitation is withdrawn'), asked.reload), children: t('Revoke') }) }, i.name)))] }), !asked.loading && !asked.error && asked.value.length > 0 && open.length === 0 ? (_jsx(SizableText, { size: "$1", color: "$soft", children: t('Every invitation has been used or stopped.') })) : null] })) : (_jsx(SizableText, { size: "$1", color: "$soft", children: t('An org admin invites people and sees the open invitations.') })), _jsx(Note, { children: note })] }));
|
|
68
77
|
}
|