@dxos/plugin-client 0.9.1-main.c7dcc2e112 → 0.9.1-staging.ee54ba693a
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/dist/lib/neutral/{AccountContainer-LIMGLO2Y.mjs → AccountContainer-7SF53UON.mjs} +15 -11
- package/dist/lib/neutral/AccountContainer-7SF53UON.mjs.map +7 -0
- package/dist/lib/neutral/ClientPlugin.node.mjs +2 -2
- package/dist/lib/neutral/ClientPlugin.node.mjs.map +3 -3
- package/dist/lib/neutral/{DevicesContainer-X26OGLFK.mjs → DevicesContainer-PRWIN36N.mjs} +25 -16
- package/dist/lib/neutral/DevicesContainer-PRWIN36N.mjs.map +7 -0
- package/dist/lib/neutral/{InvitationsContainer-OBBCJ7CJ.mjs → InvitationsContainer-BI5RAWET.mjs} +42 -31
- package/dist/lib/neutral/InvitationsContainer-BI5RAWET.mjs.map +7 -0
- package/dist/lib/neutral/{ProfileContainer-FGS2MVWA.mjs → ProfileContainer-YEAYOTX5.mjs} +21 -20
- package/dist/lib/neutral/ProfileContainer-YEAYOTX5.mjs.map +7 -0
- package/dist/lib/neutral/{RecoveryCredentialsContainer-SL4LYHBE.mjs → RecoveryCredentialsContainer-6BZ2IWX7.mjs} +22 -13
- package/dist/lib/neutral/RecoveryCredentialsContainer-6BZ2IWX7.mjs.map +7 -0
- package/dist/lib/neutral/UsageContainer-2DWCVVE4.mjs +241 -0
- package/dist/lib/neutral/UsageContainer-2DWCVVE4.mjs.map +7 -0
- package/dist/lib/neutral/account-cache-YVQKLNV2.mjs +16 -0
- package/dist/lib/neutral/account-cache-YVQKLNV2.mjs.map +7 -0
- package/dist/lib/neutral/{app-graph-builder-FTXJNX37.mjs → app-graph-builder-RVXTD6OE.mjs} +3 -3
- package/dist/lib/neutral/app-graph-builder-RVXTD6OE.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +4 -4
- package/dist/lib/neutral/capabilities/node.mjs +3 -3
- package/dist/lib/neutral/chunk-A7WDDIWY.mjs +162 -0
- package/dist/lib/neutral/chunk-A7WDDIWY.mjs.map +7 -0
- package/dist/lib/neutral/{chunk-JEF2FQGB.mjs → chunk-JY35S2CN.mjs} +2 -2
- package/dist/lib/neutral/{chunk-JEF2FQGB.mjs.map → chunk-JY35S2CN.mjs.map} +1 -1
- package/dist/lib/neutral/chunk-ZFABGG2E.mjs +8 -0
- package/dist/lib/neutral/containers/index.mjs +6 -6
- package/dist/lib/neutral/{create-agent-U567RM7Z.mjs → create-agent-6UKJMZS5.mjs} +2 -2
- package/dist/lib/neutral/{create-identity-IKN2FJ5B.mjs → create-identity-ULCRB3YH.mjs} +2 -2
- package/dist/lib/neutral/{create-passkey-JAD3ACY7.mjs → create-passkey-I7VDS3AC.mjs} +2 -2
- package/dist/lib/neutral/{create-recovery-code-JZYXR4BY.mjs → create-recovery-code-DH2B6EGP.mjs} +2 -2
- package/dist/lib/neutral/index.mjs +4 -2
- package/dist/lib/neutral/{layer-specs-VQCJLV33.mjs → layer-specs-YI7BVWKZ.mjs} +6 -8
- package/dist/lib/neutral/layer-specs-YI7BVWKZ.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/{navigation-handler-MPJAVHWY.mjs → navigation-handler-MQ6MZXLX.mjs} +2 -2
- package/dist/lib/neutral/{open-usage-24I7RAFL.mjs → open-usage-OJVTPGOY.mjs} +2 -2
- package/dist/lib/neutral/operations/index.mjs +1 -1
- package/dist/lib/neutral/plugin.mjs +1 -1
- package/dist/lib/neutral/{redeem-passkey-4DR7GPMU.mjs → redeem-passkey-TYP4XLHO.mjs} +2 -2
- package/dist/lib/neutral/{redeem-token-N5MBQG3I.mjs → redeem-token-2J33R3PV.mjs} +2 -2
- package/dist/lib/neutral/{share-identity-YJIFDIPJ.mjs → share-identity-TOROBLWL.mjs} +2 -2
- package/dist/lib/neutral/testing/index.mjs.map +2 -2
- package/dist/lib/neutral/types/index.mjs +3 -1
- package/dist/types/src/capabilities/account-cache.d.ts +1 -31
- package/dist/types/src/capabilities/app-graph-builder.d.ts +1 -2
- package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
- package/dist/types/src/capabilities/client.d.ts +1 -2
- package/dist/types/src/capabilities/client.d.ts.map +1 -1
- package/dist/types/src/capabilities/hub-http-client.d.ts +1 -2
- package/dist/types/src/capabilities/hub-http-client.d.ts.map +1 -1
- package/dist/types/src/capabilities/layer-specs.d.ts +1 -2
- package/dist/types/src/capabilities/layer-specs.d.ts.map +1 -1
- package/dist/types/src/capabilities/migrations.d.ts +1 -1
- package/dist/types/src/capabilities/navigation-handler/navigation-handler.d.ts +1 -2
- package/dist/types/src/capabilities/navigation-handler/navigation-handler.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-context.d.ts +1 -6
- package/dist/types/src/capabilities/react-context.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +1 -1
- package/dist/types/src/capabilities/schema-defs.d.ts +1 -1
- package/dist/types/src/containers/AccountContainer/AccountContainer.d.ts.map +1 -1
- package/dist/types/src/containers/AccountContainer/AccountContainer.stories.d.ts +14 -0
- package/dist/types/src/containers/AccountContainer/AccountContainer.stories.d.ts.map +1 -0
- package/dist/types/src/containers/DevicesContainer/DevicesContainer.d.ts.map +1 -1
- package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.d.ts.map +1 -1
- package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.stories.d.ts +14 -0
- package/dist/types/src/containers/InvitationsContainer/InvitationsContainer.stories.d.ts.map +1 -0
- package/dist/types/src/containers/ProfileContainer/ProfileContainer.d.ts.map +1 -1
- package/dist/types/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.d.ts.map +1 -1
- package/dist/types/src/containers/UsageContainer/UsageContainer.d.ts.map +1 -1
- package/dist/types/src/containers/UsageContainer/UsageView.d.ts +16 -7
- package/dist/types/src/containers/UsageContainer/UsageView.d.ts.map +1 -1
- package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts +2 -0
- package/dist/types/src/containers/UsageContainer/UsageView.stories.d.ts.map +1 -1
- package/dist/types/src/hooks/index.d.ts +2 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/{state/use-hub-http.d.ts → hooks/useHubClient.d.ts} +1 -1
- package/dist/types/src/hooks/useHubClient.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +4 -0
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/testing/initializeIdentity.d.ts.map +1 -1
- package/dist/types/src/{state → types}/account-cache.d.ts.map +1 -1
- package/dist/types/src/types/index.d.ts +1 -0
- package/dist/types/src/types/index.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +46 -45
- package/src/capabilities/account-cache.ts +1 -3
- package/src/capabilities/app-graph-builder.ts +80 -79
- package/src/capabilities/layer-specs.ts +4 -6
- package/src/commands/account/login/login.ts +2 -2
- package/src/containers/AccountContainer/AccountContainer.stories.tsx +46 -0
- package/src/containers/AccountContainer/AccountContainer.tsx +66 -62
- package/src/containers/DevicesContainer/DevicesContainer.tsx +69 -54
- package/src/containers/InvitationsContainer/InvitationsContainer.stories.tsx +46 -0
- package/src/containers/InvitationsContainer/InvitationsContainer.tsx +64 -61
- package/src/containers/ProfileContainer/ProfileContainer.tsx +37 -27
- package/src/containers/RecoveryCredentialsContainer/RecoveryCredentialsContainer.tsx +50 -41
- package/src/containers/UsageContainer/UsageContainer.tsx +11 -10
- package/src/containers/UsageContainer/UsageView.stories.tsx +13 -0
- package/src/containers/UsageContainer/UsageView.tsx +182 -150
- package/src/hooks/index.ts +5 -0
- package/src/testing/initializeIdentity.ts +1 -0
- package/src/types/capabilities.ts +1 -1
- package/src/types/index.ts +1 -0
- package/dist/lib/neutral/AccountContainer-LIMGLO2Y.mjs.map +0 -7
- package/dist/lib/neutral/DevicesContainer-X26OGLFK.mjs.map +0 -7
- package/dist/lib/neutral/InvitationsContainer-OBBCJ7CJ.mjs.map +0 -7
- package/dist/lib/neutral/ProfileContainer-FGS2MVWA.mjs.map +0 -7
- package/dist/lib/neutral/RecoveryCredentialsContainer-SL4LYHBE.mjs.map +0 -7
- package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs +0 -226
- package/dist/lib/neutral/UsageContainer-HBPMLIK7.mjs.map +0 -7
- package/dist/lib/neutral/account-cache-4EKGEUJI.mjs +0 -43
- package/dist/lib/neutral/account-cache-4EKGEUJI.mjs.map +0 -7
- package/dist/lib/neutral/app-graph-builder-FTXJNX37.mjs.map +0 -7
- package/dist/lib/neutral/chunk-54MDDWCV.mjs +0 -8
- package/dist/lib/neutral/chunk-DA3LZQSD.mjs +0 -136
- package/dist/lib/neutral/chunk-DA3LZQSD.mjs.map +0 -7
- package/dist/lib/neutral/layer-specs-VQCJLV33.mjs.map +0 -7
- package/dist/types/src/state/use-hub-http.d.ts.map +0 -1
- /package/dist/lib/neutral/{chunk-54MDDWCV.mjs.map → chunk-ZFABGG2E.mjs.map} +0 -0
- /package/dist/lib/neutral/{create-agent-U567RM7Z.mjs.map → create-agent-6UKJMZS5.mjs.map} +0 -0
- /package/dist/lib/neutral/{create-identity-IKN2FJ5B.mjs.map → create-identity-ULCRB3YH.mjs.map} +0 -0
- /package/dist/lib/neutral/{create-passkey-JAD3ACY7.mjs.map → create-passkey-I7VDS3AC.mjs.map} +0 -0
- /package/dist/lib/neutral/{create-recovery-code-JZYXR4BY.mjs.map → create-recovery-code-DH2B6EGP.mjs.map} +0 -0
- /package/dist/lib/neutral/{navigation-handler-MPJAVHWY.mjs.map → navigation-handler-MQ6MZXLX.mjs.map} +0 -0
- /package/dist/lib/neutral/{open-usage-24I7RAFL.mjs.map → open-usage-OJVTPGOY.mjs.map} +0 -0
- /package/dist/lib/neutral/{redeem-passkey-4DR7GPMU.mjs.map → redeem-passkey-TYP4XLHO.mjs.map} +0 -0
- /package/dist/lib/neutral/{redeem-token-N5MBQG3I.mjs.map → redeem-token-2J33R3PV.mjs.map} +0 -0
- /package/dist/lib/neutral/{share-identity-YJIFDIPJ.mjs.map → share-identity-TOROBLWL.mjs.map} +0 -0
- /package/dist/types/src/{state → types}/account-cache.d.ts +0 -0
- /package/src/{state/use-hub-http.ts → hooks/useHubClient.ts} +0 -0
- /package/src/{state → types}/account-cache.ts +0 -0
|
@@ -11,8 +11,9 @@ import { useClient, useMulticastObservable } from '@dxos/react-client';
|
|
|
11
11
|
import { type Device, useDevices } from '@dxos/react-client/halo';
|
|
12
12
|
import { type CancellableInvitationObservable, Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
|
|
13
13
|
import { useNetworkStatus } from '@dxos/react-client/mesh';
|
|
14
|
-
import { Button, Clipboard, Icon, IconButton,
|
|
15
|
-
import {
|
|
14
|
+
import { Button, Clipboard, Icon, IconButton, useId, useTranslation } from '@dxos/react-ui';
|
|
15
|
+
import { Form } from '@dxos/react-ui-form';
|
|
16
|
+
import { Listbox } from '@dxos/react-ui-list';
|
|
16
17
|
import { AuthCode, Centered, DeviceListItem, Emoji, Viewport } from '@dxos/shell/react';
|
|
17
18
|
import { osTranslations } from '@dxos/ui-theme';
|
|
18
19
|
import { hexToEmoji } from '@dxos/util';
|
|
@@ -44,58 +45,72 @@ export const DevicesContainer = ({ createInvitationUrl }: DevicesContainerProps)
|
|
|
44
45
|
|
|
45
46
|
return (
|
|
46
47
|
<Clipboard.Provider>
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
<
|
|
78
|
-
{t('
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
48
|
+
<Form.Root variant='settings'>
|
|
49
|
+
<Form.Viewport scroll>
|
|
50
|
+
<Form.Content>
|
|
51
|
+
<Form.Section
|
|
52
|
+
title={t('devices-verbose.label', { ns: meta.profile.key })}
|
|
53
|
+
description={t('devices.description', { ns: meta.profile.key })}
|
|
54
|
+
>
|
|
55
|
+
<Form.Group>
|
|
56
|
+
<div role='group' className='min-w-0'>
|
|
57
|
+
<h3 className='text-lg mb-2'>{t('devices.label', { ns: meta.profile.key })}</h3>
|
|
58
|
+
<Listbox.Root>
|
|
59
|
+
<Listbox.Content aria-label={t('devices.label', { ns: meta.profile.key })}>
|
|
60
|
+
{devices.map((device: Device) => (
|
|
61
|
+
<DeviceListItem
|
|
62
|
+
key={device.deviceKey.toHex()}
|
|
63
|
+
device={device}
|
|
64
|
+
connectionState={connectionState}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</Listbox.Content>
|
|
68
|
+
</Listbox.Root>
|
|
69
|
+
</div>
|
|
70
|
+
{createInvitationUrl && (
|
|
71
|
+
<div role='group' className='min-w-0'>
|
|
72
|
+
<h3 className='text-lg mb-2'>{t('add-device.label')}</h3>
|
|
73
|
+
<DeviceInvitation createInvitationUrl={createInvitationUrl} />
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
</Form.Group>
|
|
77
|
+
</Form.Section>
|
|
78
|
+
<Form.Section
|
|
79
|
+
title={t('danger-zone.title', { ns: meta.profile.key })}
|
|
80
|
+
description={t('danger-zone.description', { ns: meta.profile.key })}
|
|
81
|
+
>
|
|
82
|
+
<Form.Row
|
|
83
|
+
label={t('reset-device.label')}
|
|
84
|
+
description={t('reset-device.description', { ns: meta.profile.key })}
|
|
85
|
+
>
|
|
86
|
+
<Button variant='destructive' onClick={handleResetStorage} data-testid='devicesContainer.reset'>
|
|
87
|
+
{t('reset-device.label')}
|
|
88
|
+
</Button>
|
|
89
|
+
</Form.Row>
|
|
90
|
+
<Form.Row
|
|
91
|
+
label={t('recover-identity.label')}
|
|
92
|
+
description={t('recover-identity.description', { ns: meta.profile.key })}
|
|
93
|
+
>
|
|
94
|
+
<Button variant='destructive' onClick={handleRecover} data-testid='devicesContainer.recover'>
|
|
95
|
+
{t('recover-identity.label')}
|
|
96
|
+
</Button>
|
|
97
|
+
</Form.Row>
|
|
98
|
+
<Form.Row
|
|
99
|
+
label={t('join-new-identity.label')}
|
|
100
|
+
description={t('join-new-identity.description', { ns: meta.profile.key })}
|
|
101
|
+
>
|
|
102
|
+
<Button
|
|
103
|
+
variant='destructive'
|
|
104
|
+
onClick={handleJoinNewIdentity}
|
|
105
|
+
data-testid='devicesContainer.joinExisting'
|
|
106
|
+
>
|
|
107
|
+
{t('join-new-identity.label')}
|
|
108
|
+
</Button>
|
|
109
|
+
</Form.Row>
|
|
110
|
+
</Form.Section>
|
|
111
|
+
</Form.Content>
|
|
112
|
+
</Form.Viewport>
|
|
113
|
+
</Form.Root>
|
|
99
114
|
</Clipboard.Provider>
|
|
100
115
|
);
|
|
101
116
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2026 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Meta, type StoryObj } from '@storybook/react-vite';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
|
|
8
|
+
import { ProcessManagerPlugin } from '@dxos/app-framework';
|
|
9
|
+
import { withPluginManager } from '@dxos/app-framework/testing';
|
|
10
|
+
import { withLayout, withTheme } from '@dxos/react-ui/testing';
|
|
11
|
+
|
|
12
|
+
import { initializeIdentity } from '#testing';
|
|
13
|
+
import { translations } from '#translations';
|
|
14
|
+
|
|
15
|
+
import { ClientPlugin } from '../../ClientPlugin';
|
|
16
|
+
import { InvitationsContainer } from './InvitationsContainer';
|
|
17
|
+
|
|
18
|
+
const meta = {
|
|
19
|
+
title: 'plugins/plugin-client/containers/InvitationsContainer',
|
|
20
|
+
component: InvitationsContainer,
|
|
21
|
+
decorators: [
|
|
22
|
+
withTheme(),
|
|
23
|
+
withLayout({ layout: 'fullscreen' }),
|
|
24
|
+
withPluginManager({
|
|
25
|
+
plugins: [
|
|
26
|
+
ClientPlugin({
|
|
27
|
+
onClientInitialized: ({ client }) =>
|
|
28
|
+
Effect.gen(function* () {
|
|
29
|
+
yield* initializeIdentity(client);
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
32
|
+
ProcessManagerPlugin(),
|
|
33
|
+
],
|
|
34
|
+
}),
|
|
35
|
+
],
|
|
36
|
+
parameters: {
|
|
37
|
+
layout: 'fullscreen',
|
|
38
|
+
translations,
|
|
39
|
+
},
|
|
40
|
+
} satisfies Meta<typeof InvitationsContainer>;
|
|
41
|
+
|
|
42
|
+
export default meta;
|
|
43
|
+
|
|
44
|
+
type Story = StoryObj<typeof meta>;
|
|
45
|
+
|
|
46
|
+
export const Default: Story = {};
|
|
@@ -7,14 +7,14 @@ import React, { useCallback, useState } from 'react';
|
|
|
7
7
|
|
|
8
8
|
import { useCapability } from '@dxos/app-framework/ui';
|
|
9
9
|
import { Context } from '@dxos/context';
|
|
10
|
-
import { Clipboard, Icon, IconButton,
|
|
11
|
-
import {
|
|
10
|
+
import { Clipboard, Icon, IconButton, useAsyncEffect, useTranslation } from '@dxos/react-ui';
|
|
11
|
+
import { Form } from '@dxos/react-ui-form';
|
|
12
|
+
import { Listbox } from '@dxos/react-ui-list';
|
|
12
13
|
|
|
13
14
|
import { meta } from '#meta';
|
|
14
|
-
import { ClientCapabilities } from '#types';
|
|
15
|
+
import { type AccountCacheInvitation, ClientCapabilities } from '#types';
|
|
15
16
|
|
|
16
|
-
import {
|
|
17
|
-
import { useHubHttpClient } from '../../state/use-hub-http';
|
|
17
|
+
import { useHubHttpClient } from '../../hooks';
|
|
18
18
|
|
|
19
19
|
export const InvitationsContainer = () => {
|
|
20
20
|
const { t } = useTranslation(meta.profile.key);
|
|
@@ -24,27 +24,28 @@ export const InvitationsContainer = () => {
|
|
|
24
24
|
const [pending, setPending] = useState(false);
|
|
25
25
|
|
|
26
26
|
// Account/invitation routes live on hub-service, not the edge worker.
|
|
27
|
-
const
|
|
27
|
+
const hubClient = useHubHttpClient();
|
|
28
28
|
|
|
29
29
|
useAsyncEffect(async () => {
|
|
30
|
-
if (!
|
|
30
|
+
if (!hubClient) {
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
|
|
33
34
|
try {
|
|
34
|
-
const result = await
|
|
35
|
+
const result = await hubClient.listAccountInvitations(new Context());
|
|
35
36
|
setCache((prev) => ({ ...prev, invitations: result.invitations, fetchedAt: Date.now() }));
|
|
36
37
|
} catch {
|
|
37
38
|
// Offline: keep cache.
|
|
38
39
|
}
|
|
39
|
-
}, [
|
|
40
|
+
}, [hubClient, setCache]);
|
|
40
41
|
|
|
41
42
|
const handleIssue = useCallback(async () => {
|
|
42
|
-
if (!
|
|
43
|
+
if (!hubClient) {
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
setPending(true);
|
|
46
47
|
try {
|
|
47
|
-
const result = await
|
|
48
|
+
const result = await hubClient.issueAccountInvitation(new Context());
|
|
48
49
|
// Optimistically push the new code and decrement the remaining quota; the
|
|
49
50
|
// server consumes one slot at issue time. Next refresh reconciles.
|
|
50
51
|
setCache((prev) => ({
|
|
@@ -57,7 +58,7 @@ export const InvitationsContainer = () => {
|
|
|
57
58
|
} finally {
|
|
58
59
|
setPending(false);
|
|
59
60
|
}
|
|
60
|
-
}, [
|
|
61
|
+
}, [hubClient, setCache]);
|
|
61
62
|
|
|
62
63
|
const remaining = cache.account?.invitationsRemaining ?? 0;
|
|
63
64
|
const list = cache.invitations ?? [];
|
|
@@ -66,72 +67,74 @@ export const InvitationsContainer = () => {
|
|
|
66
67
|
|
|
67
68
|
return (
|
|
68
69
|
<Clipboard.Provider>
|
|
69
|
-
<
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
title={t('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
<Form.Root variant='settings'>
|
|
71
|
+
<Form.Viewport scroll>
|
|
72
|
+
<Form.Content>
|
|
73
|
+
<Form.Section title={t('invitations-section.title')} description={t('invitations-section.description')}>
|
|
74
|
+
<Form.Row
|
|
75
|
+
label={t('generate-invitation.label')}
|
|
76
|
+
description={t('generate-invitation.description', { count: remaining })}
|
|
77
|
+
>
|
|
78
|
+
<IconButton
|
|
79
|
+
icon='ph--plus--regular'
|
|
80
|
+
label={t('generate-invitation.label')}
|
|
81
|
+
variant='primary'
|
|
82
|
+
onClick={handleIssue}
|
|
83
|
+
disabled={pending || remaining <= 0}
|
|
84
|
+
/>
|
|
85
|
+
</Form.Row>
|
|
86
|
+
</Form.Section>
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
{available.length > 0 ? (
|
|
89
|
+
<Form.Section title={t('available-invitations.title')}>
|
|
90
|
+
<Listbox.Root>
|
|
91
|
+
<Listbox.Content classNames='gap-1'>
|
|
92
|
+
{available.map((row) => (
|
|
93
|
+
<AvailableInvitationItem key={row.code} row={row} />
|
|
94
|
+
))}
|
|
95
|
+
</Listbox.Content>
|
|
96
|
+
</Listbox.Root>
|
|
97
|
+
</Form.Section>
|
|
98
|
+
) : null}
|
|
94
99
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
{redeemed.length > 0 ? (
|
|
101
|
+
<Form.Section title={t('redeemed-invitations.title')}>
|
|
102
|
+
<Listbox.Root>
|
|
103
|
+
<Listbox.Content classNames='gap-1'>
|
|
104
|
+
{redeemed.map((row) => (
|
|
105
|
+
<RedeemedInvitationItem key={row.code} row={row} />
|
|
106
|
+
))}
|
|
107
|
+
</Listbox.Content>
|
|
108
|
+
</Listbox.Root>
|
|
109
|
+
</Form.Section>
|
|
110
|
+
) : null}
|
|
111
|
+
</Form.Content>
|
|
112
|
+
</Form.Viewport>
|
|
113
|
+
</Form.Root>
|
|
105
114
|
</Clipboard.Provider>
|
|
106
115
|
);
|
|
107
116
|
};
|
|
108
117
|
|
|
109
118
|
const AvailableInvitationItem = ({ row }: { row: AccountCacheInvitation }) => (
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
<Icon icon='ph--paper-plane-tilt--duotone' size={5} classNames='text-description' />
|
|
113
|
-
</ListItem.Endcap>
|
|
119
|
+
<Listbox.Item id={row.code} classNames='grid grid-cols-[min-content_1fr_min-content] items-center gap-2'>
|
|
120
|
+
<Icon icon='ph--paper-plane-tilt--duotone' size={5} classNames='text-description' />
|
|
114
121
|
<div className='flex flex-col min-w-0'>
|
|
115
|
-
<
|
|
122
|
+
<div className='font-mono truncate'>{row.code}</div>
|
|
116
123
|
<p className='text-description text-xs'>{new Date(row.createdAt).toLocaleString()}</p>
|
|
117
124
|
</div>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
</ListItem.Endcap>
|
|
121
|
-
</ListItem.Root>
|
|
125
|
+
<Clipboard.IconButton value={row.code} />
|
|
126
|
+
</Listbox.Item>
|
|
122
127
|
);
|
|
123
128
|
|
|
124
129
|
const RedeemedInvitationItem = ({ row }: { row: AccountCacheInvitation }) => {
|
|
125
130
|
const date = row.redeemedAt ?? row.createdAt;
|
|
126
131
|
return (
|
|
127
|
-
<
|
|
128
|
-
<
|
|
129
|
-
<Icon icon='ph--check-circle--duotone' size={5} classNames='text-success-text' />
|
|
130
|
-
</ListItem.Endcap>
|
|
132
|
+
<Listbox.Item id={row.code} classNames='grid grid-cols-[min-content_1fr] items-center gap-2'>
|
|
133
|
+
<Icon icon='ph--check-circle--duotone' size={5} classNames='text-success-text' />
|
|
131
134
|
<div className='flex flex-col min-w-0'>
|
|
132
|
-
<
|
|
135
|
+
<div className='font-mono truncate'>{row.code}</div>
|
|
133
136
|
<p className='text-description text-xs'>{new Date(date).toLocaleString()}</p>
|
|
134
137
|
</div>
|
|
135
|
-
</
|
|
138
|
+
</Listbox.Item>
|
|
136
139
|
);
|
|
137
140
|
};
|
|
@@ -9,7 +9,7 @@ import { debounce } from '@dxos/async';
|
|
|
9
9
|
import { useClient } from '@dxos/react-client';
|
|
10
10
|
import { type Identity, useIdentity } from '@dxos/react-client/halo';
|
|
11
11
|
import { ButtonGroup, Clipboard, Input, useTranslation } from '@dxos/react-ui';
|
|
12
|
-
import { Form, type FormFieldMap, type FormUpdateMeta
|
|
12
|
+
import { Form, type FormFieldMap, type FormUpdateMeta } from '@dxos/react-ui-form';
|
|
13
13
|
import { EmojiPickerBlock, HuePicker } from '@dxos/react-ui-pickers';
|
|
14
14
|
import { hexToEmoji, hexToHue } from '@dxos/util';
|
|
15
15
|
|
|
@@ -101,14 +101,16 @@ export const ProfileContainer = () => {
|
|
|
101
101
|
);
|
|
102
102
|
|
|
103
103
|
return (
|
|
104
|
-
<
|
|
105
|
-
<Input.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
<Form.Row label={label} description={t('display-name.description')}>
|
|
105
|
+
<Input.Root>
|
|
106
|
+
<Input.TextInput
|
|
107
|
+
value={getValue()}
|
|
108
|
+
onChange={handleChange}
|
|
109
|
+
placeholder={t('display-name-input.placeholder')}
|
|
110
|
+
classNames='min-w-64'
|
|
111
|
+
/>
|
|
112
|
+
</Input.Root>
|
|
113
|
+
</Form.Row>
|
|
112
114
|
);
|
|
113
115
|
},
|
|
114
116
|
emoji: ({ type, label, getValue, onValueChange }) => {
|
|
@@ -119,7 +121,7 @@ export const ProfileContainer = () => {
|
|
|
119
121
|
);
|
|
120
122
|
|
|
121
123
|
return (
|
|
122
|
-
<
|
|
124
|
+
<Form.Row label={label} description={t('icon.description')}>
|
|
123
125
|
<EmojiPickerBlock
|
|
124
126
|
triggerVariant='default'
|
|
125
127
|
emoji={getValue()}
|
|
@@ -127,7 +129,7 @@ export const ProfileContainer = () => {
|
|
|
127
129
|
onClickClear={handleEmojiReset}
|
|
128
130
|
classNames='justify-self-end'
|
|
129
131
|
/>
|
|
130
|
-
</
|
|
132
|
+
</Form.Row>
|
|
131
133
|
);
|
|
132
134
|
},
|
|
133
135
|
hue: ({ type, label, getValue, onValueChange }) => {
|
|
@@ -138,22 +140,24 @@ export const ProfileContainer = () => {
|
|
|
138
140
|
);
|
|
139
141
|
|
|
140
142
|
return (
|
|
141
|
-
<
|
|
143
|
+
<Form.Row label={label} description={t('hue.description')}>
|
|
142
144
|
<div className='flex justify-self-end'>
|
|
143
145
|
<HuePicker value={getValue()} onChange={handleChange} onReset={handleHueReset} />
|
|
144
146
|
</div>
|
|
145
|
-
</
|
|
147
|
+
</Form.Row>
|
|
146
148
|
);
|
|
147
149
|
},
|
|
148
150
|
// TODO(wittjosiah): We need text input annotations for disabled and copyable.
|
|
149
151
|
did: ({ label, getValue }) => {
|
|
150
152
|
return (
|
|
151
|
-
<
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
<Form.Row label={label} description={t('did.description')}>
|
|
154
|
+
<Input.Root>
|
|
155
|
+
<ButtonGroup classNames='w-full'>
|
|
156
|
+
<Input.TextInput value={getValue()} disabled classNames='min-w-64' />
|
|
157
|
+
<Clipboard.IconButton value={getValue() ?? ''} />
|
|
158
|
+
</ButtonGroup>
|
|
159
|
+
</Input.Root>
|
|
160
|
+
</Form.Row>
|
|
157
161
|
);
|
|
158
162
|
},
|
|
159
163
|
}),
|
|
@@ -162,15 +166,21 @@ export const ProfileContainer = () => {
|
|
|
162
166
|
|
|
163
167
|
return (
|
|
164
168
|
<Clipboard.Provider>
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
<Form.Root
|
|
170
|
+
variant='settings'
|
|
171
|
+
schema={UserProfile}
|
|
172
|
+
values={values}
|
|
173
|
+
fieldMap={fieldMap}
|
|
174
|
+
onValuesChanged={handleChange}
|
|
175
|
+
>
|
|
176
|
+
<Form.Viewport scroll>
|
|
177
|
+
<Form.Content>
|
|
178
|
+
<Form.Section title={t('profile.label')} description={t('profile.description')}>
|
|
169
179
|
<Form.FieldSet />
|
|
170
|
-
</Form.
|
|
171
|
-
</Form.
|
|
172
|
-
</
|
|
173
|
-
</
|
|
180
|
+
</Form.Section>
|
|
181
|
+
</Form.Content>
|
|
182
|
+
</Form.Viewport>
|
|
183
|
+
</Form.Root>
|
|
174
184
|
</Clipboard.Provider>
|
|
175
185
|
);
|
|
176
186
|
};
|
|
@@ -6,8 +6,9 @@ import React from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { useOperationInvoker } from '@dxos/app-framework/ui';
|
|
8
8
|
import { useCredentials } from '@dxos/react-client/halo';
|
|
9
|
-
import { Icon, IconButton,
|
|
10
|
-
import {
|
|
9
|
+
import { Icon, IconButton, Message, useTranslation } from '@dxos/react-ui';
|
|
10
|
+
import { Form } from '@dxos/react-ui-form';
|
|
11
|
+
import { Listbox } from '@dxos/react-ui-list';
|
|
11
12
|
|
|
12
13
|
import { meta } from '#meta';
|
|
13
14
|
import { ClientOperation } from '#operations';
|
|
@@ -23,44 +24,52 @@ export const RecoveryCredentialsContainer = () => {
|
|
|
23
24
|
);
|
|
24
25
|
|
|
25
26
|
return (
|
|
26
|
-
<
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
<
|
|
30
|
-
label={t('create-passkey.label')}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
label={t('create-recovery-code.label')}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
27
|
+
<Form.Root variant='settings'>
|
|
28
|
+
<Form.Viewport scroll>
|
|
29
|
+
<Form.Content>
|
|
30
|
+
<Form.Section title={t('recovery-setup-dialog.title')} description={t('recovery-setup-dialog.description')}>
|
|
31
|
+
<Form.Row label={t('create-passkey.label')} description={t('create-passkey.description')}>
|
|
32
|
+
<IconButton
|
|
33
|
+
label={t('create-passkey.label')}
|
|
34
|
+
icon='ph--key--duotone'
|
|
35
|
+
variant='primary'
|
|
36
|
+
onClick={() => invokePromise(ClientOperation.CreatePasskey)}
|
|
37
|
+
/>
|
|
38
|
+
</Form.Row>
|
|
39
|
+
<Form.Row label={t('create-recovery-code.label')} description={t('create-recovery-code.description')}>
|
|
40
|
+
<IconButton
|
|
41
|
+
label={t('create-recovery-code.label')}
|
|
42
|
+
icon='ph--receipt--duotone'
|
|
43
|
+
variant='default'
|
|
44
|
+
onClick={() => invokePromise(ClientOperation.CreateRecoveryCode)}
|
|
45
|
+
/>
|
|
46
|
+
</Form.Row>
|
|
47
|
+
</Form.Section>
|
|
48
|
+
<Form.Section title={t('credentials-list.label')}>
|
|
49
|
+
{recoveryCredentials.length < 1 ? (
|
|
50
|
+
<Message.Root valence='error'>
|
|
51
|
+
<Message.Title icon='ph--shield-warning--duotone'>{t('no-credentials.title')}</Message.Title>
|
|
52
|
+
<Message.Content>{t('no-credentials.message')}</Message.Content>
|
|
53
|
+
</Message.Root>
|
|
54
|
+
) : (
|
|
55
|
+
<Listbox.Root>
|
|
56
|
+
<Listbox.Content classNames='gap-1'>
|
|
57
|
+
{recoveryCredentials.map((credential, index) => (
|
|
58
|
+
<Listbox.Item
|
|
59
|
+
key={credential.id?.toHex() ?? index}
|
|
60
|
+
id={credential.id?.toHex() ?? `${index}`}
|
|
61
|
+
classNames='gap-2'
|
|
62
|
+
>
|
|
63
|
+
<Icon icon='ph--key--regular' />
|
|
64
|
+
<Listbox.ItemLabel>{credential.issuanceDate.toLocaleString()}</Listbox.ItemLabel>
|
|
65
|
+
</Listbox.Item>
|
|
66
|
+
))}
|
|
67
|
+
</Listbox.Content>
|
|
68
|
+
</Listbox.Root>
|
|
69
|
+
)}
|
|
70
|
+
</Form.Section>
|
|
71
|
+
</Form.Content>
|
|
72
|
+
</Form.Viewport>
|
|
73
|
+
</Form.Root>
|
|
65
74
|
);
|
|
66
75
|
};
|
|
@@ -9,39 +9,40 @@ import { log } from '@dxos/log';
|
|
|
9
9
|
import { type GetProfileUsageResponse } from '@dxos/protocols';
|
|
10
10
|
import { useAsyncEffect } from '@dxos/react-ui';
|
|
11
11
|
|
|
12
|
-
import { useHubHttpClient } from '../../
|
|
12
|
+
import { useHubHttpClient } from '../../hooks';
|
|
13
13
|
import { UsageView, type UsageViewState } from './UsageView';
|
|
14
14
|
|
|
15
|
+
/** State + payload kept together so `ready` always carries data (mirrors the discriminated `UsageViewProps`). */
|
|
16
|
+
type UsageFetchState = { state: Exclude<UsageViewState, 'ready'> } | { state: 'ready'; data: GetProfileUsageResponse };
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* Fetches rolling-window profile usage from the hub service and renders it.
|
|
17
20
|
* Connected wrapper around the presentational {@link UsageView}.
|
|
18
21
|
*/
|
|
19
22
|
export const UsageContainer = () => {
|
|
20
23
|
const hubHttp = useHubHttpClient();
|
|
21
|
-
const [
|
|
22
|
-
const [data, setData] = useState<GetProfileUsageResponse | undefined>();
|
|
24
|
+
const [fetchState, setFetchState] = useState<UsageFetchState>({ state: 'loading' });
|
|
23
25
|
const [lastUpdated, setLastUpdated] = useState<number | undefined>();
|
|
24
26
|
const [refreshCount, setRefreshCount] = useState(0);
|
|
25
27
|
|
|
26
28
|
useAsyncEffect(async () => {
|
|
27
29
|
if (!hubHttp) {
|
|
28
|
-
|
|
30
|
+
setFetchState({ state: 'unavailable' });
|
|
29
31
|
return;
|
|
30
32
|
}
|
|
31
|
-
// Keep prior data visible while refetching; only show the
|
|
32
|
-
|
|
33
|
+
// Keep prior data visible while refetching; only show the loading state on first load.
|
|
34
|
+
setFetchState((previous) => (previous.state === 'ready' ? previous : { state: 'loading' }));
|
|
33
35
|
try {
|
|
34
36
|
const result = await hubHttp.getProfileUsage(new Context());
|
|
35
|
-
|
|
37
|
+
setFetchState({ state: 'ready', data: result });
|
|
36
38
|
setLastUpdated(Date.now());
|
|
37
|
-
setState('ready');
|
|
38
39
|
} catch (err) {
|
|
39
40
|
log.catch(err);
|
|
40
|
-
|
|
41
|
+
setFetchState({ state: 'error' });
|
|
41
42
|
}
|
|
42
43
|
}, [hubHttp, refreshCount]);
|
|
43
44
|
|
|
44
45
|
const handleRefresh = useCallback(() => setRefreshCount((count) => count + 1), []);
|
|
45
46
|
|
|
46
|
-
return <UsageView
|
|
47
|
+
return <UsageView {...fetchState} lastUpdated={lastUpdated} onRefresh={handleRefresh} />;
|
|
47
48
|
};
|