@dxos/shell 0.3.11-main.9c729f3 → 0.3.11-main.9fa973d

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.
Files changed (85) hide show
  1. package/dist/bundle/shell.mjs +36540 -0
  2. package/dist/bundle/shell.mjs.map +1 -0
  3. package/dist/bundle/shell.umd.js +210 -0
  4. package/dist/bundle/shell.umd.js.map +1 -0
  5. package/dist/bundle/style.css +1 -0
  6. package/dist/lib/browser/meta.json +1 -1
  7. package/dist/lib/browser/react.mjs +606 -375
  8. package/dist/lib/browser/react.mjs.map +4 -4
  9. package/dist/types/src/components/IdentityList/DeviceList.d.ts.map +1 -1
  10. package/dist/types/src/components/IdentityList/DeviceList.js +5 -3
  11. package/dist/types/src/components/IdentityList/DeviceList.js.map +1 -1
  12. package/dist/types/src/composites/Shell/Shell.d.ts.map +1 -1
  13. package/dist/types/src/composites/Shell/Shell.js +10 -4
  14. package/dist/types/src/composites/Shell/Shell.js.map +1 -1
  15. package/dist/types/src/panels/IdentityPanel/IdentityPanel.d.ts.map +1 -1
  16. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js +16 -3
  17. package/dist/types/src/panels/IdentityPanel/IdentityPanel.js.map +1 -1
  18. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.d.ts +6 -0
  19. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.d.ts.map +1 -1
  20. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js +26 -3
  21. package/dist/types/src/panels/IdentityPanel/IdentityPanel.stories.js.map +1 -1
  22. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts +8 -7
  23. package/dist/types/src/panels/IdentityPanel/IdentityPanelProps.d.ts.map +1 -1
  24. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts +1 -1
  25. package/dist/types/src/panels/IdentityPanel/identityMachine.d.ts.map +1 -1
  26. package/dist/types/src/panels/IdentityPanel/identityMachine.js +10 -1
  27. package/dist/types/src/panels/IdentityPanel/identityMachine.js.map +1 -1
  28. package/dist/types/src/panels/IdentityPanel/steps/AgentPanel.d.ts +19 -0
  29. package/dist/types/src/panels/IdentityPanel/steps/AgentPanel.d.ts.map +1 -0
  30. package/dist/types/src/panels/IdentityPanel/steps/AgentPanel.js +122 -0
  31. package/dist/types/src/panels/IdentityPanel/steps/AgentPanel.js.map +1 -0
  32. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.d.ts +10 -0
  33. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.d.ts.map +1 -0
  34. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.js +23 -0
  35. package/dist/types/src/panels/IdentityPanel/steps/DeviceManager.js.map +1 -0
  36. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.d.ts.map +1 -1
  37. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js +8 -0
  38. package/dist/types/src/panels/IdentityPanel/steps/IdentityActionChooser.js.map +1 -1
  39. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.d.ts +0 -1
  40. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.d.ts.map +1 -1
  41. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js +2 -3
  42. package/dist/types/src/panels/IdentityPanel/steps/ProfileForm.js.map +1 -1
  43. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts +2 -0
  44. package/dist/types/src/panels/IdentityPanel/steps/index.d.ts.map +1 -1
  45. package/dist/types/src/panels/IdentityPanel/steps/index.js +2 -0
  46. package/dist/types/src/panels/IdentityPanel/steps/index.js.map +1 -1
  47. package/dist/types/src/panels/JoinPanel/JoinPanel.js +4 -3
  48. package/dist/types/src/panels/JoinPanel/JoinPanel.js.map +1 -1
  49. package/dist/types/src/panels/JoinPanel/joinMachine.js +1 -1
  50. package/dist/types/src/panels/JoinPanel/joinMachine.js.map +1 -1
  51. package/dist/types/src/panels/SpacePanel/SpacePanel.d.ts.map +1 -1
  52. package/dist/types/src/panels/SpacePanel/SpacePanel.js +5 -3
  53. package/dist/types/src/panels/SpacePanel/SpacePanel.js.map +1 -1
  54. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.d.ts.map +1 -1
  55. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js +2 -2
  56. package/dist/types/src/panels/SpacePanel/SpacePanel.stories.js.map +1 -1
  57. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts +1 -0
  58. package/dist/types/src/panels/SpacePanel/SpacePanelProps.d.ts.map +1 -1
  59. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts +1 -0
  60. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.d.ts.map +1 -1
  61. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js +3 -1
  62. package/dist/types/src/panels/SpacePanel/steps/SpaceManager.js.map +1 -1
  63. package/dist/types/src/translations/locales/en-US.d.ts +1 -0
  64. package/dist/types/src/translations/locales/en-US.d.ts.map +1 -1
  65. package/dist/types/src/translations/locales/en-US.js +2 -1
  66. package/dist/types/src/translations/locales/en-US.js.map +1 -1
  67. package/package.json +18 -17
  68. package/src/components/IdentityList/DeviceList.tsx +7 -5
  69. package/src/composites/Shell/Shell.tsx +9 -2
  70. package/src/panels/IdentityPanel/IdentityPanel.stories.tsx +44 -2
  71. package/src/panels/IdentityPanel/IdentityPanel.tsx +20 -4
  72. package/src/panels/IdentityPanel/IdentityPanelProps.ts +13 -7
  73. package/src/panels/IdentityPanel/identityMachine.ts +11 -2
  74. package/src/panels/IdentityPanel/steps/AgentPanel.tsx +165 -0
  75. package/src/panels/IdentityPanel/steps/DeviceManager.tsx +41 -0
  76. package/src/panels/IdentityPanel/steps/IdentityActionChooser.tsx +11 -1
  77. package/src/panels/IdentityPanel/steps/ProfileForm.tsx +1 -1
  78. package/src/panels/IdentityPanel/steps/index.ts +2 -0
  79. package/src/panels/JoinPanel/JoinPanel.tsx +2 -2
  80. package/src/panels/JoinPanel/joinMachine.ts +1 -1
  81. package/src/panels/SpacePanel/SpacePanel.stories.tsx +2 -1
  82. package/src/panels/SpacePanel/SpacePanel.tsx +4 -1
  83. package/src/panels/SpacePanel/SpacePanelProps.ts +1 -0
  84. package/src/panels/SpacePanel/steps/SpaceManager.tsx +4 -1
  85. package/src/translations/locales/en-US.ts +2 -1
@@ -22,11 +22,13 @@ export const DeviceList = ({ devices, onSelect }: DeviceListProps) => {
22
22
  {devices.map((device) => {
23
23
  const identity = {
24
24
  identityKey: device.deviceKey,
25
- profile: {
26
- displayName: `${device.profile?.platform} (${device.profile?.platformVersion}) on ${device.profile?.os} (${
27
- device.profile?.osVersion
28
- }) ${device.profile?.architecture ?? 'Unknown'}-bit`,
29
- },
25
+ profile: device.profile
26
+ ? {
27
+ displayName: `${device.profile?.platform} (${device.profile?.platformVersion}) on ${
28
+ device.profile?.os
29
+ } (${device.profile?.osVersion}) ${device.profile?.architecture ?? 'Unknown'}-bit`,
30
+ }
31
+ : undefined,
30
32
  };
31
33
  return (
32
34
  <IdentityListItem
@@ -13,10 +13,11 @@ import { JoinDialog } from '../JoinDialog';
13
13
  import { SpaceDialog } from '../SpaceDialog';
14
14
 
15
15
  export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: string }) => {
16
- const [{ layout, invitationCode, spaceKey }, setLayout] = useState<LayoutRequest>({
16
+ const [{ layout, invitationCode, spaceKey, target }, setLayout] = useState<LayoutRequest>({
17
17
  layout: runtime.layout,
18
18
  invitationCode: runtime.invitationCode,
19
19
  spaceKey: runtime.spaceKey,
20
+ target: runtime.target,
20
21
  });
21
22
 
22
23
  const space = useSpace(spaceKey);
@@ -69,6 +70,7 @@ export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: stri
69
70
  return space ? (
70
71
  <SpaceDialog
71
72
  space={space}
73
+ target={target}
72
74
  createInvitationUrl={(invitationCode) => `${origin}?spaceInvitationCode=${invitationCode}`}
73
75
  onDone={async () => {
74
76
  await runtime.setAppContext({ display: ShellDisplay.NONE });
@@ -82,7 +84,12 @@ export const Shell = ({ runtime, origin }: { runtime: ShellRuntime; origin: stri
82
84
  <JoinDialog
83
85
  initialInvitationCode={invitationCode}
84
86
  onDone={async (result) => {
85
- await runtime.setAppContext({ display: ShellDisplay.NONE, spaceKey: result?.spaceKey ?? undefined });
87
+ const target = result?.target ?? undefined;
88
+ await runtime.setAppContext({
89
+ display: ShellDisplay.NONE,
90
+ spaceKey: result?.spaceKey ?? undefined,
91
+ target,
92
+ });
86
93
  runtime.setLayout({ layout: ShellLayout.DEFAULT });
87
94
  }}
88
95
  onExit={async () => {
@@ -8,6 +8,7 @@ import { faker } from '@faker-js/faker';
8
8
  import React from 'react';
9
9
 
10
10
  import { PublicKey } from '@dxos/react-client';
11
+ import { DeviceKind } from '@dxos/react-client/halo';
11
12
  import { Invitation } from '@dxos/react-client/invitations';
12
13
  import { withTheme } from '@dxos/storybook-utils';
13
14
 
@@ -16,6 +17,7 @@ import type { IdentityPanelImplProps } from './IdentityPanelProps';
16
17
  import { IdentityActionChooserImpl } from './steps';
17
18
  import { StorybookDialog } from '../../components/StorybookDialog';
18
19
  import { InvitationManager, type InvitationManagerProps } from '../../steps';
20
+ import { osTranslations } from '../../translations';
19
21
 
20
22
  faker.seed(1234);
21
23
 
@@ -30,12 +32,13 @@ const noOpProps: IdentityPanelImplProps = {
30
32
  displayName: faker.person.firstName(),
31
33
  },
32
34
  },
35
+ devices: [],
33
36
  };
34
37
 
35
38
  export default {
36
- title: 'Panels/Identity',
39
+ title: 'react-shell/IdentityPanel',
37
40
  decorators: [withTheme],
38
- parameters: { chromatic: { disableSnapshot: false } },
41
+ parameters: { translations: [osTranslations], chromatic: { disableSnapshot: false } },
39
42
  };
40
43
 
41
44
  export const IdentityActionChooser = () => {
@@ -50,6 +53,45 @@ export const IdentityActionChooser = () => {
50
53
  );
51
54
  };
52
55
 
56
+ export const UpdateProfileForm = () => {
57
+ return (
58
+ <StorybookDialog inOverlayLayout>
59
+ <IdentityPanelImpl
60
+ {...noOpProps}
61
+ activeView='update profile form'
62
+ IdentityActionChooser={IdentityActionChooserImpl}
63
+ />
64
+ </StorybookDialog>
65
+ );
66
+ };
67
+
68
+ export const DeviceManager = () => {
69
+ return (
70
+ <StorybookDialog inOverlayLayout>
71
+ <IdentityPanelImpl
72
+ {...{
73
+ ...noOpProps,
74
+ devices: [
75
+ { deviceKey: PublicKey.random(), kind: DeviceKind.CURRENT },
76
+ { deviceKey: PublicKey.random(), kind: DeviceKind.TRUSTED },
77
+ { deviceKey: PublicKey.random(), kind: DeviceKind.TRUSTED },
78
+ ],
79
+ }}
80
+ activeView='device manager'
81
+ IdentityActionChooser={IdentityActionChooserImpl}
82
+ />
83
+ </StorybookDialog>
84
+ );
85
+ };
86
+
87
+ export const DeviceManagerEmpty = () => {
88
+ return (
89
+ <StorybookDialog inOverlayLayout>
90
+ <IdentityPanelImpl {...noOpProps} activeView='device manager' IdentityActionChooser={IdentityActionChooserImpl} />
91
+ </StorybookDialog>
92
+ );
93
+ };
94
+
53
95
  const DeviceInvitationManagerWithState = (extraProps: InvitationManagerProps) => (
54
96
  <StorybookDialog inOverlayLayout>
55
97
  <IdentityPanelImpl
@@ -6,7 +6,7 @@ import React, { useEffect, useMemo } from 'react';
6
6
  import { generateName } from '@dxos/display-name';
7
7
  import { log } from '@dxos/log';
8
8
  import { useClient } from '@dxos/react-client';
9
- import { type Identity, useIdentity } from '@dxos/react-client/halo';
9
+ import { type Identity, useIdentity, useDevices } from '@dxos/react-client/halo';
10
10
  import { useInvitationStatus } from '@dxos/react-client/invitations';
11
11
  import type { CancellableInvitationObservable } from '@dxos/react-client/invitations';
12
12
  import { Avatar, DensityProvider, useId, useJdenticonHref, useTranslation } from '@dxos/react-ui';
@@ -17,7 +17,7 @@ import {
17
17
  type IdentityPanelProps,
18
18
  } from './IdentityPanelProps';
19
19
  import { useIdentityMachine } from './identityMachine';
20
- import { IdentityActionChooser, ProfileForm } from './steps';
20
+ import { DeviceManager, IdentityActionChooser, ProfileForm, AgentForm } from './steps';
21
21
  import { Viewport, Heading, CloseButton } from '../../components';
22
22
  import { InvitationManager } from '../../steps';
23
23
 
@@ -53,13 +53,15 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
53
53
  const { t } = useTranslation('os');
54
54
  const title = useMemo(() => {
55
55
  switch (activeView) {
56
- case 'device manager':
56
+ case 'agent manager':
57
+ return 'Manage Agent';
57
58
  case 'device invitation manager':
58
- return t('choose devices label');
59
+ return t('choose add device label');
59
60
  default:
60
61
  return t('identity heading');
61
62
  }
62
63
  }, [activeView, t]);
64
+ console.log('[IdentityPanelImpl]', { activeView });
63
65
  return (
64
66
  <DensityProvider density='fine'>
65
67
  <IdentityHeading {...{ identity, titleId, title, onDone }} />
@@ -75,6 +77,9 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
75
77
  invitationUrl={rest.createInvitationUrl(rest.invitationCode!)}
76
78
  />
77
79
  </Viewport.View>
80
+ <Viewport.View id='device manager' classNames={viewStyles}>
81
+ <DeviceManager active={activeView === 'device manager'} {...rest} />
82
+ </Viewport.View>
78
83
  <Viewport.View classNames={viewStyles} id='update profile form'>
79
84
  <ProfileForm
80
85
  send={rest.send}
@@ -83,6 +88,9 @@ export const IdentityPanelImpl = (props: IdentityPanelImplProps) => {
83
88
  onUpdateProfile={onUpdateProfile}
84
89
  />
85
90
  </Viewport.View>
91
+ <Viewport.View classNames={viewStyles} id='agent manager'>
92
+ <AgentForm send={rest.send} active={activeView === 'agent manager'} identity={identity} />
93
+ </Viewport.View>
86
94
  {/* <Viewport.View id='signing out'></Viewport.View> */}
87
95
  </Viewport.Views>
88
96
  </Viewport.Root>
@@ -105,6 +113,7 @@ export const IdentityPanel = ({
105
113
  }: IdentityPanelProps) => {
106
114
  const titleId = useId('identityPanel__heading', propsTitleId);
107
115
  const client = useClient();
116
+ const devices = useDevices();
108
117
  const identity = useIdentity();
109
118
  if (!identity) {
110
119
  console.error('IdentityPanel rendered with no active identity.');
@@ -126,8 +135,12 @@ export const IdentityPanel = ({
126
135
  return 'identity action chooser';
127
136
  case identityState.matches('managingDeviceInvitation'):
128
137
  return 'device invitation manager';
138
+ case identityState.matches('managingDevices'):
139
+ return 'device manager';
129
140
  case [{ managingProfile: 'idle' }, { managingProfile: 'pending' }].some(identityState.matches):
130
141
  return 'update profile form';
142
+ case [{ managingAgent: 'idle' }, { managingAgent: 'pending' }].some(identityState.matches):
143
+ return 'agent manager';
131
144
  // case identityState.matches('signingOut'):
132
145
  // return 'identity exit';
133
146
  default:
@@ -135,6 +148,8 @@ export const IdentityPanel = ({
135
148
  }
136
149
  }, [identityState]);
137
150
 
151
+ console.log({ activeView, identityState });
152
+
138
153
  const onUpdateProfile = async (profile: NonNullable<Identity['profile']>) => {
139
154
  identitySend({ type: 'updateProfile' });
140
155
  await client.halo.updateProfile(profile);
@@ -143,6 +158,7 @@ export const IdentityPanel = ({
143
158
  const implProps = {
144
159
  ...props,
145
160
  identity,
161
+ devices,
146
162
  activeView,
147
163
  send: identitySend,
148
164
  titleId,
@@ -2,19 +2,25 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type cloneElement } from 'react';
6
- import { type Event, type SingleOrArray } from 'xstate';
5
+ import type { cloneElement } from 'react';
6
+ import type { Event, SingleOrArray } from 'xstate';
7
7
 
8
- import type { Identity } from '@dxos/react-client/halo';
9
- import { type InvitationStatus } from '@dxos/react-client/invitations';
8
+ import type { Device, Identity } from '@dxos/react-client/halo';
9
+ import type { InvitationStatus } from '@dxos/react-client/invitations';
10
10
 
11
- import { type IdentityEvent } from './identityMachine';
12
- import { type InvitationManagerProps } from '../../steps';
11
+ import type { IdentityEvent } from './identityMachine';
12
+ import type { InvitationManagerProps } from '../../steps';
13
13
 
14
14
  export type IdentityPanelImplProps = {
15
15
  titleId: string;
16
- activeView: 'device manager' | 'update profile form' | 'device invitation manager' | 'identity action chooser';
16
+ activeView:
17
+ | 'device manager'
18
+ | 'agent manager'
19
+ | 'update profile form'
20
+ | 'device invitation manager'
21
+ | 'identity action chooser';
17
22
  identity: Identity;
23
+ devices: Device[];
18
24
  onUpdateProfile?: (profile: NonNullable<Identity['profile']>) => Promise<void>;
19
25
  createInvitationUrl: (invitationCode: string) => string;
20
26
  send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
@@ -19,7 +19,7 @@ type IdentityMachineContext = {
19
19
  };
20
20
 
21
21
  type IdentityChooseActionEvent = {
22
- type: 'chooseDevices' | 'chooseProfile' | /* 'chooseSignOut' | */ 'unchooseAction';
22
+ type: 'chooseDevices' | 'chooseAgent' | 'chooseProfile' | /* 'chooseSignOut' | */ 'unchooseAction';
23
23
  };
24
24
 
25
25
  type IdentitySelectDeviceInvitationEvent = {
@@ -58,6 +58,14 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
58
58
  states: {
59
59
  choosingAction: {},
60
60
  managingDeviceInvitation: {},
61
+ managingAgent: {
62
+ initial: 'idle',
63
+ states: {
64
+ idle: {},
65
+ pending: {},
66
+ },
67
+ },
68
+ managingDevices: {},
61
69
  managingProfile: {
62
70
  initial: 'idle',
63
71
  states: {
@@ -73,10 +81,11 @@ const identityMachine = createMachine<IdentityMachineContext, IdentityEvent>(
73
81
  },
74
82
  on: {
75
83
  unchooseAction: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
76
- chooseDevices: { target: '.managingDeviceInvitation', actions: ['unsetInvitation', 'log'] },
77
84
  deselectInvitation: { target: '.choosingAction', actions: ['unsetInvitation', 'log'] },
78
85
  selectInvitation: { target: '.managingDeviceInvitation', actions: ['setInvitation', 'log'] },
86
+ chooseDevices: { target: '.managingDevices', actions: 'log' },
79
87
  chooseProfile: { target: '.managingProfile', actions: 'log' },
88
+ chooseAgent: { target: '.managingAgent', actions: 'log' },
80
89
  // chooseSignOut: { target: '.signingOut', actions: 'log' },
81
90
  },
82
91
  },
@@ -0,0 +1,165 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React, { useState, useCallback, useEffect } from 'react';
6
+ import { type Event, type SingleOrArray } from 'xstate';
7
+
8
+ import { invariant } from '@dxos/invariant';
9
+ import { useClient, useAgentHostingProviderClient } from '@dxos/react-client';
10
+ import { type Identity, useHaloInvitations } from '@dxos/react-client/halo';
11
+ import { Invitation, InvitationEncoder } from '@dxos/react-client/invitations';
12
+ import { useTranslation } from '@dxos/react-ui';
13
+
14
+ import { Action, Actions, StepHeading, Input } from '../../../components';
15
+ import { type IdentityPanelStepProps } from '../IdentityPanelProps';
16
+ import { type IdentityEvent } from '../identityMachine';
17
+
18
+ export interface AgentFormProps extends Omit<IdentityPanelStepProps, 'send'> {
19
+ send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
20
+ onAgentCreate?: () => Promise<void>;
21
+ onAgentDestroy?: () => Promise<void>;
22
+ identity?: Identity;
23
+ }
24
+
25
+ export type AgentFormImplProps = AgentFormProps & {
26
+ agentStatus?: string;
27
+ agentActive?: boolean;
28
+ agentProviderDisabled?: boolean;
29
+ validationMessage?: string;
30
+ };
31
+
32
+ export const AgentForm = (props: AgentFormProps) => {
33
+ const { identity } = props;
34
+ // const { t } = useTranslation('os');
35
+ const [validationMessage, setValidationMessage] = useState('');
36
+ const [agentStatus, setAgentStatus] = useState('');
37
+ const [agentActive, setAgentActive] = useState(false);
38
+ const client = useClient();
39
+ const invitations = useHaloInvitations();
40
+ const agentHostingProviderClient = useAgentHostingProviderClient(client.config);
41
+
42
+ useEffect(() => {
43
+ const fetchAgentStatus = async () => {
44
+ // TODO(nf): if agent exists, subscribe to updates instead of oneshot status
45
+ if (agentHostingProviderClient) {
46
+ const agentStatus = await agentHostingProviderClient?.getAgent(identity?.identityKey.truncate() ?? 'foo');
47
+ if (agentStatus) {
48
+ setAgentActive(true);
49
+ setAgentStatus(agentStatus);
50
+ } else {
51
+ setAgentActive(false);
52
+ setValidationMessage('No agent deployed via provider.');
53
+ }
54
+ } else {
55
+ setValidationMessage('No agent provider configured.');
56
+ }
57
+ };
58
+ void fetchAgentStatus();
59
+ }, []);
60
+
61
+ const onAgentCreateEvent = useCallback(async (invitation: Invitation) => {
62
+ const invitationCode = InvitationEncoder.encode(invitation);
63
+ if (invitation.state === Invitation.State.CONNECTING) {
64
+ console.log(JSON.stringify({ invitationCode, authCode: invitation.authCode }));
65
+ invariant(identity, 'Identity not found');
66
+ if (agentHostingProviderClient) {
67
+ try {
68
+ const res = await agentHostingProviderClient?.createAgent(invitationCode, identity.identityKey.truncate());
69
+ // TODO(nf): human-consumable response from API
70
+ setAgentStatus(res);
71
+ setAgentActive(true);
72
+ setValidationMessage('');
73
+ } catch (err: any) {
74
+ setValidationMessage(`error creating agent: ${err.message}`);
75
+ }
76
+ }
77
+ }
78
+ }, []);
79
+
80
+ const handleAgentCreate = async () => {
81
+ invitations.forEach((invitation) => invitation.cancel());
82
+
83
+ // TODO(nf): do this work in the hosting provider client?
84
+ const invitation = client.halo.share({ type: Invitation.Type.MULTIUSE, authMethod: Invitation.AuthMethod.NONE });
85
+
86
+ invitation.subscribe(onAgentCreateEvent);
87
+ };
88
+
89
+ const handleAgentDestroy = async () => {
90
+ const identity = client.halo.identity.get();
91
+ invariant(identity, 'Identity not found');
92
+ try {
93
+ await agentHostingProviderClient?.destroyAgent(identity.identityKey.truncate());
94
+ setValidationMessage('No agent deployed via provider.');
95
+ setAgentStatus('');
96
+ setAgentActive(false);
97
+ } catch (err: any) {
98
+ setValidationMessage(`error destroying agent: ${err.message}`);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <AgentFormImpl
104
+ {...props}
105
+ onAgentCreate={handleAgentCreate}
106
+ onAgentDestroy={handleAgentDestroy}
107
+ validationMessage={validationMessage}
108
+ agentStatus={agentStatus}
109
+ agentActive={agentActive}
110
+ agentProviderDisabled={!agentHostingProviderClient}
111
+ />
112
+ );
113
+ };
114
+
115
+ // TODO(zhenyasav): impl shouldn't need send()
116
+ export const AgentFormImpl = (props: AgentFormImplProps) => {
117
+ const {
118
+ active,
119
+ onAgentCreate,
120
+ onAgentDestroy,
121
+ send,
122
+ validationMessage,
123
+ agentStatus,
124
+ agentActive,
125
+ agentProviderDisabled,
126
+ } = props;
127
+ const disabled = !active;
128
+ const { t } = useTranslation('os');
129
+ return (
130
+ <>
131
+ <div role='none' className='grow flex flex-col justify-center'>
132
+ <Input
133
+ {...{ validationMessage }}
134
+ label={<StepHeading>Agent Status</StepHeading>}
135
+ disabled={disabled}
136
+ data-testid='agent-status'
137
+ value={agentStatus}
138
+ />
139
+ </div>
140
+ <Actions>
141
+ <Action
142
+ disabled={agentActive || agentProviderDisabled}
143
+ onClick={() => onAgentCreate?.()}
144
+ data-testid={'create-agent'}
145
+ >
146
+ Create Agent
147
+ </Action>
148
+ <Action
149
+ disabled={!agentActive || agentProviderDisabled}
150
+ onClick={() => onAgentDestroy?.()}
151
+ data-testid={'destroy-agent'}
152
+ >
153
+ Destroy Agent
154
+ </Action>
155
+ <Action
156
+ disabled={disabled}
157
+ onClick={() => send?.({ type: 'unchooseAction' })}
158
+ data-testid={'update-profile-form-back'}
159
+ >
160
+ {t('back label')}
161
+ </Action>
162
+ </Actions>
163
+ </>
164
+ );
165
+ };
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+ import { type Event, type SingleOrArray } from 'xstate';
7
+
8
+ import { type Device } from '@dxos/react-client/halo';
9
+ import { useTranslation } from '@dxos/react-ui';
10
+
11
+ import { Action, Actions, DeviceList } from '../../../components';
12
+ import type { IdentityPanelStepProps } from '../IdentityPanelProps';
13
+ import type { IdentityEvent } from '../identityMachine';
14
+
15
+ export interface DeviceManagerProps extends Omit<IdentityPanelStepProps, 'send'> {
16
+ send?: (event: SingleOrArray<Event<IdentityEvent>>) => void;
17
+ devices: Device[];
18
+ }
19
+
20
+ export const DeviceManager = ({ devices, active, send }: DeviceManagerProps) => {
21
+ const { t } = useTranslation('os');
22
+ const disabled = !active;
23
+
24
+ return (
25
+ <>
26
+ <div role='none' className='grow'>
27
+ <DeviceList devices={devices} />
28
+ </div>
29
+ <Actions>
30
+ <Action
31
+ variant='ghost'
32
+ disabled={disabled}
33
+ onClick={() => send?.({ type: 'unchooseAction' })}
34
+ data-testid={'manage-devices-back'}
35
+ >
36
+ {t('back label')}
37
+ </Action>
38
+ </Actions>
39
+ </>
40
+ );
41
+ };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { CaretRight, Plus, Power, UserGear } from '@phosphor-icons/react';
5
+ import { CaretRight, Devices, Plus, Power, UserGear, HardDrive } from '@phosphor-icons/react';
6
6
  import React, { useCallback } from 'react';
7
7
 
8
8
  import { useClient } from '@dxos/react-client';
@@ -63,6 +63,11 @@ export const IdentityActionChooserImpl = ({
63
63
  <div role='none' className='grow flex flex-col gap-1'>
64
64
  <DensityProvider density='coarse'>
65
65
  <div className='grow justify-center flex flex-col gap-1'>
66
+ <Action data-testid='manage-agent' onClick={() => send?.({ type: 'chooseAgent' })} classNames='plb-4'>
67
+ <HardDrive className={getSize(6)} />
68
+ <span className='grow mli-3'>Manage Agent</span>
69
+ <CaretRight weight='bold' className={getSize(4)} />
70
+ </Action>
66
71
  <Action
67
72
  disabled={!active}
68
73
  data-testid='devices-panel.create-invitation'
@@ -70,6 +75,11 @@ export const IdentityActionChooserImpl = ({
70
75
  classNames='plb-4'
71
76
  >
72
77
  <Plus className={getSize(6)} />
78
+ <span className='grow mli-3'>{t('choose add device label')}</span>
79
+ <CaretRight weight='bold' className={getSize(4)} />
80
+ </Action>
81
+ <Action data-testid='manage-devices' onClick={() => send?.({ type: 'chooseDevices' })} classNames='plb-4'>
82
+ <Devices className={getSize(6)} />
73
83
  <span className='grow mli-3'>{t('choose devices label')}</span>
74
84
  <CaretRight weight='bold' className={getSize(4)} />
75
85
  </Action>
@@ -37,7 +37,7 @@ export const ProfileForm = (props: ProfileFormProps) => {
37
37
  };
38
38
 
39
39
  // TODO(zhenyasav): impl shouldn't need send()
40
- export const ProfileFormImpl = (props: ProfileFormImplProps) => {
40
+ const ProfileFormImpl = (props: ProfileFormImplProps) => {
41
41
  const { active, profile, send, onUpdateProfile, validationMessage } = props;
42
42
  const disabled = !active;
43
43
  const { t } = useTranslation('os');
@@ -2,5 +2,7 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export * from './DeviceManager';
5
6
  export * from './IdentityActionChooser';
6
7
  export * from './ProfileForm';
8
+ export * from './AgentPanel';
@@ -193,8 +193,6 @@ export const JoinPanel = ({
193
193
  },
194
194
  });
195
195
 
196
- console.log('[joinState]', joinState);
197
-
198
196
  useEffect(() => {
199
197
  const subscription = joinService.subscribe((state) => {
200
198
  log('[state]', state);
@@ -368,6 +366,7 @@ export const JoinPanel = ({
368
366
  identityKey: joinState.context.halo.invitation?.identityKey ?? null,
369
367
  swarmKey: joinState.context.halo.invitation?.swarmKey ?? null,
370
368
  spaceKey: joinState.context.halo.invitation?.spaceKey ?? null,
369
+ target: joinState.context.halo.invitation?.target ?? null,
371
370
  });
372
371
  }, [joinState, propsOnDone]);
373
372
 
@@ -376,6 +375,7 @@ export const JoinPanel = ({
376
375
  identityKey: joinState.context.space.invitation?.identityKey ?? null,
377
376
  swarmKey: joinState.context.space.invitation?.swarmKey ?? null,
378
377
  spaceKey: joinState.context.space.invitation?.spaceKey ?? null,
378
+ target: joinState.context.space.invitation?.target ?? null,
379
379
  });
380
380
  }, [joinState, propsOnDone]);
381
381
 
@@ -397,7 +397,7 @@ const joinMachine = createMachine<JoinMachineContext, JoinEvent>(
397
397
  event.type.includes('Space') ? { ...context.space, invitation: event.invitation } : context.space,
398
398
  }),
399
399
  log: (context, event) => {
400
- log.info('[transition]', {
400
+ log('[transition]', {
401
401
  event,
402
402
  haloInvitation: context.halo.invitation,
403
403
  spaceInvitation: context.space.invitation,
@@ -129,6 +129,7 @@ export const SpaceManagerWithEvenMoreInvites = () => {
129
129
  identityKey = PublicKey.random(),
130
130
  swarmKey,
131
131
  spaceKey = PublicKey.random(),
132
+ target = null,
132
133
  } = invitation;
133
134
  return (
134
135
  <InvitationListItemImpl
@@ -140,7 +141,7 @@ export const SpaceManagerWithEvenMoreInvites = () => {
140
141
  invitationCode: id,
141
142
  authCode: state === Invitation.State.READY_FOR_AUTHENTICATION ? '123414' : undefined,
142
143
  id,
143
- result: { identityKey, swarmKey, spaceKey },
144
+ result: { identityKey, swarmKey, spaceKey, target },
144
145
  cancel: () => {},
145
146
  authenticate: async (authCode: string) => {},
146
147
  connect: () => {},
@@ -39,17 +39,19 @@ export const SpacePanelImpl = (props: SpacePanelImplProps) => {
39
39
  titleId,
40
40
  activeView,
41
41
  space,
42
+ target,
42
43
  SpaceManager: SpaceManagerComponent = SpaceManager,
43
44
  InvitationManager: InvitationManagerComponent = InvitationManager,
44
45
  ...rest
45
46
  } = props;
47
+
46
48
  return (
47
49
  <DensityProvider density='fine'>
48
50
  <SpacePanelHeading {...rest} {...{ titleId, space }} />
49
51
  <Viewport.Root activeView={activeView}>
50
52
  <Viewport.Views>
51
53
  <Viewport.View id='space manager' classNames={stepStyles}>
52
- <SpaceManagerComponent active={activeView === 'space manager'} space={space} {...rest} />
54
+ <SpaceManagerComponent active={activeView === 'space manager'} space={space} target={target} {...rest} />
53
55
  </Viewport.View>
54
56
  <Viewport.View id='space invitation manager' classNames={stepStyles}>
55
57
  <InvitationManagerComponent
@@ -115,6 +117,7 @@ export const SpacePanel = ({
115
117
  };
116
118
 
117
119
  return spaceState.context.invitation ? (
120
+ // TODO(wittjosiah): Unused? Remove?
118
121
  <SpacePanelWithInvitationImpl {...implProps} invitation={spaceState.context.invitation} />
119
122
  ) : (
120
123
  <SpacePanelImpl {...implProps} />
@@ -16,6 +16,7 @@ export type SpacePanelImplProps = {
16
16
  titleId: string;
17
17
  activeView: string;
18
18
  send: (event: SingleOrArray<Event<any>>) => void;
19
+ target?: string;
19
20
  createInvitationUrl: (invitationCode: string) => string;
20
21
  space: ErsatzSpace | Space;
21
22
  invitationUrl?: string;