@dxos/plugin-space 0.6.14-main.f49f251 → 0.6.14-staging.54a8bab
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/browser/{chunk-FOI7DAUV.mjs → chunk-WZAM3FNP.mjs} +1 -1
- package/dist/lib/browser/{chunk-FOI7DAUV.mjs.map → chunk-WZAM3FNP.mjs.map} +2 -2
- package/dist/lib/browser/index.mjs +432 -387
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types/index.mjs +1 -1
- package/dist/lib/node/{chunk-OTDRTHT4.cjs → chunk-HTAM5LQD.cjs} +4 -4
- package/dist/lib/node/{chunk-OTDRTHT4.cjs.map → chunk-HTAM5LQD.cjs.map} +2 -2
- package/dist/lib/node/index.cjs +474 -433
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types/index.cjs +11 -11
- package/dist/lib/node/types/index.cjs.map +1 -1
- package/dist/lib/node-esm/{chunk-FYDGMPSC.mjs → chunk-TRJKV4PK.mjs} +1 -1
- package/dist/lib/node-esm/{chunk-FYDGMPSC.mjs.map → chunk-TRJKV4PK.mjs.map} +2 -2
- package/dist/lib/node-esm/index.mjs +432 -387
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/types/index.mjs +1 -1
- package/dist/types/src/SpacePlugin.d.ts.map +1 -1
- package/dist/types/src/components/SpaceSettingsPanel.d.ts.map +1 -1
- package/dist/types/src/components/SyncStatus/Space.d.ts +8 -0
- package/dist/types/src/components/SyncStatus/Space.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +3 -2
- package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -1
- package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +5 -20
- package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -1
- package/dist/types/src/components/SyncStatus/save-tracker.d.ts +3 -0
- package/dist/types/src/components/SyncStatus/save-tracker.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/status.d.ts +9 -0
- package/dist/types/src/components/SyncStatus/status.d.ts.map +1 -0
- package/dist/types/src/components/SyncStatus/{types.d.ts → sync-state.d.ts} +1 -1
- package/dist/types/src/components/SyncStatus/sync-state.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +0 -2
- package/dist/types/src/components/index.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +6 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/types.d.ts +2 -1
- package/dist/types/src/types/types.d.ts.map +1 -1
- package/package.json +34 -33
- package/src/SpacePlugin.tsx +55 -30
- package/src/components/SpaceSettings.tsx +5 -5
- package/src/components/SpaceSettingsPanel.tsx +18 -8
- package/src/components/SyncStatus/Space.tsx +109 -0
- package/src/components/SyncStatus/SyncStatus.stories.tsx +13 -4
- package/src/components/SyncStatus/SyncStatus.tsx +43 -129
- package/src/components/{SaveStatus.tsx → SyncStatus/save-tracker.ts} +1 -25
- package/src/components/SyncStatus/status.ts +44 -0
- package/src/components/index.ts +0 -2
- package/src/translations.ts +6 -0
- package/src/types/types.ts +3 -1
- package/dist/types/src/components/MissingObject.d.ts +0 -5
- package/dist/types/src/components/MissingObject.d.ts.map +0 -1
- package/dist/types/src/components/SaveStatus.d.ts +0 -3
- package/dist/types/src/components/SaveStatus.d.ts.map +0 -1
- package/dist/types/src/components/SyncStatus/types.d.ts.map +0 -1
- package/src/components/MissingObject.tsx +0 -54
- /package/src/components/SyncStatus/{types.ts → sync-state.ts} +0 -0
package/src/SpacePlugin.tsx
CHANGED
|
@@ -6,9 +6,11 @@ import { signal } from '@preact/signals-core';
|
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
type GraphProvides,
|
|
9
10
|
type IntentDispatcher,
|
|
10
11
|
type IntentPluginProvides,
|
|
11
12
|
LayoutAction,
|
|
13
|
+
type LayoutProvides,
|
|
12
14
|
type LocationProvides,
|
|
13
15
|
NavigationAction,
|
|
14
16
|
type Plugin,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
openIds,
|
|
19
21
|
parseGraphPlugin,
|
|
20
22
|
parseIntentPlugin,
|
|
23
|
+
parseLayoutPlugin,
|
|
21
24
|
parseMetadataResolverPlugin,
|
|
22
25
|
parseNavigationPlugin,
|
|
23
26
|
resolvePlugin,
|
|
@@ -48,6 +51,7 @@ import {
|
|
|
48
51
|
isSpace,
|
|
49
52
|
loadObjectReferences,
|
|
50
53
|
parseId,
|
|
54
|
+
FQ_ID_LENGTH,
|
|
51
55
|
} from '@dxos/react-client/echo';
|
|
52
56
|
import { Dialog } from '@dxos/react-ui';
|
|
53
57
|
import { ClipboardProvider, InvitationManager, type InvitationManagerProps, osTranslations } from '@dxos/shell/react';
|
|
@@ -59,10 +63,8 @@ import {
|
|
|
59
63
|
CollectionSection,
|
|
60
64
|
DefaultObjectSettings,
|
|
61
65
|
MenuFooter,
|
|
62
|
-
MissingObject,
|
|
63
66
|
PopoverRenameObject,
|
|
64
67
|
PopoverRenameSpace,
|
|
65
|
-
SaveStatus,
|
|
66
68
|
ShareSpaceButton,
|
|
67
69
|
SmallPresence,
|
|
68
70
|
SmallPresenceLive,
|
|
@@ -91,7 +93,6 @@ import {
|
|
|
91
93
|
} from './util';
|
|
92
94
|
|
|
93
95
|
const ACTIVE_NODE_BROADCAST_INTERVAL = 30_000;
|
|
94
|
-
const OBJECT_ID_LENGTH = 60; // 33 (space id) + 26 (object id) + 1 (separator).
|
|
95
96
|
const SPACE_MAX_OBJECTS = 500;
|
|
96
97
|
// https://stackoverflow.com/a/19016910
|
|
97
98
|
const DIRECTORY_TYPE = 'text/directory';
|
|
@@ -138,17 +139,22 @@ export const SpacePlugin = ({
|
|
|
138
139
|
const graphSubscriptions = new Map<string, UnsubscribeCallback>();
|
|
139
140
|
|
|
140
141
|
let clientPlugin: Plugin<ClientPluginProvides> | undefined;
|
|
142
|
+
let graphPlugin: Plugin<GraphProvides> | undefined;
|
|
141
143
|
let intentPlugin: Plugin<IntentPluginProvides> | undefined;
|
|
144
|
+
let layoutPlugin: Plugin<LayoutProvides> | undefined;
|
|
142
145
|
let navigationPlugin: Plugin<LocationProvides> | undefined;
|
|
143
146
|
let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
|
|
144
147
|
|
|
145
148
|
const onSpaceReady = async () => {
|
|
146
|
-
if (!clientPlugin || !navigationPlugin || !attentionPlugin) {
|
|
149
|
+
if (!clientPlugin || !intentPlugin || !graphPlugin || !navigationPlugin || !layoutPlugin || !attentionPlugin) {
|
|
147
150
|
return;
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
const client = clientPlugin.provides.client;
|
|
154
|
+
const dispatch = intentPlugin.provides.intent.dispatch;
|
|
155
|
+
const graph = graphPlugin.provides.graph;
|
|
151
156
|
const location = navigationPlugin.provides.location;
|
|
157
|
+
const layout = layoutPlugin.provides.layout;
|
|
152
158
|
const attention = attentionPlugin.provides.attention;
|
|
153
159
|
const defaultSpace = client.spaces.default;
|
|
154
160
|
|
|
@@ -166,6 +172,26 @@ export const SpacePlugin = ({
|
|
|
166
172
|
defaultSpace.db.add(create({ key: SHARED, order: [] }));
|
|
167
173
|
}
|
|
168
174
|
|
|
175
|
+
// Await missing objects.
|
|
176
|
+
subscriptions.add(
|
|
177
|
+
scheduledEffect(
|
|
178
|
+
() => ({
|
|
179
|
+
layoutMode: layout.layoutMode,
|
|
180
|
+
soloPart: location.active.solo?.[0],
|
|
181
|
+
}),
|
|
182
|
+
({ layoutMode, soloPart }) => {
|
|
183
|
+
if (layoutMode !== 'solo' || !soloPart) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const node = graph.findNode(soloPart.id);
|
|
188
|
+
if (!node && soloPart.id.length === FQ_ID_LENGTH) {
|
|
189
|
+
void dispatch({ plugin: SPACE_PLUGIN, action: SpaceAction.WAIT_FOR_OBJECT, data: { id: soloPart.id } });
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
|
|
169
195
|
// Cache space names.
|
|
170
196
|
subscriptions.add(
|
|
171
197
|
client.spaces.subscribe(async (spaces) => {
|
|
@@ -289,6 +315,8 @@ export const SpacePlugin = ({
|
|
|
289
315
|
settings.prop({ key: 'showHidden', type: LocalStorageStore.bool({ allowUndefined: true }) });
|
|
290
316
|
state.prop({ key: 'spaceNames', type: LocalStorageStore.json<Record<string, string>>() });
|
|
291
317
|
|
|
318
|
+
graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
|
|
319
|
+
layoutPlugin = resolvePlugin(plugins, parseLayoutPlugin);
|
|
292
320
|
navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
|
|
293
321
|
attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
|
|
294
322
|
clientPlugin = resolvePlugin(plugins, parseClientPlugin);
|
|
@@ -311,19 +339,20 @@ export const SpacePlugin = ({
|
|
|
311
339
|
await onFirstRun?.({ client, dispatch });
|
|
312
340
|
};
|
|
313
341
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
342
|
+
subscriptions.add(
|
|
343
|
+
client.spaces.isReady.subscribe(async (ready) => {
|
|
344
|
+
if (ready) {
|
|
345
|
+
await clientPlugin?.provides.client.spaces.default.waitUntilReady();
|
|
346
|
+
if (firstRun) {
|
|
347
|
+
void firstRun?.wait().then(handleFirstRun);
|
|
348
|
+
} else {
|
|
349
|
+
await handleFirstRun();
|
|
350
|
+
}
|
|
323
351
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
352
|
+
await onSpaceReady();
|
|
353
|
+
}
|
|
354
|
+
}).unsubscribe,
|
|
355
|
+
);
|
|
327
356
|
},
|
|
328
357
|
unload: async () => {
|
|
329
358
|
settings.close();
|
|
@@ -336,6 +365,11 @@ export const SpacePlugin = ({
|
|
|
336
365
|
space: state.values,
|
|
337
366
|
settings: settings.values,
|
|
338
367
|
translations: [...translations, osTranslations],
|
|
368
|
+
complementary: {
|
|
369
|
+
panels: [
|
|
370
|
+
{ id: 'settings', label: ['open settings panel label', { ns: SPACE_PLUGIN }], icon: 'ph--gear--regular' },
|
|
371
|
+
],
|
|
372
|
+
},
|
|
339
373
|
root: () => (state.values.awaiting ? <AwaitingObject id={state.values.awaiting} /> : null),
|
|
340
374
|
metadata: {
|
|
341
375
|
records: {
|
|
@@ -356,24 +390,20 @@ export const SpacePlugin = ({
|
|
|
356
390
|
},
|
|
357
391
|
surface: {
|
|
358
392
|
component: ({ data, role, ...rest }) => {
|
|
359
|
-
const primary = data.active ?? data.object;
|
|
360
393
|
switch (role) {
|
|
361
394
|
case 'article':
|
|
362
|
-
case 'main':
|
|
363
395
|
// TODO(wittjosiah): Need to avoid shotgun parsing space state everywhere.
|
|
364
|
-
return isSpace(
|
|
396
|
+
return isSpace(data.object) && data.object.state.get() === SpaceState.SPACE_READY ? (
|
|
365
397
|
<Surface
|
|
366
|
-
data={{
|
|
398
|
+
data={{ object: data.object.properties[CollectionType.typename], id: data.object.id }}
|
|
367
399
|
role={role}
|
|
368
400
|
{...rest}
|
|
369
401
|
/>
|
|
370
|
-
) :
|
|
402
|
+
) : data.object instanceof CollectionType ? (
|
|
371
403
|
{
|
|
372
|
-
node: <CollectionMain collection={
|
|
404
|
+
node: <CollectionMain collection={data.object} />,
|
|
373
405
|
disposition: 'fallback',
|
|
374
406
|
}
|
|
375
|
-
) : typeof primary === 'string' && primary.length === OBJECT_ID_LENGTH ? (
|
|
376
|
-
<MissingObject id={primary} />
|
|
377
407
|
) : null;
|
|
378
408
|
case 'complementary--settings':
|
|
379
409
|
return isSpace(data.subject) ? (
|
|
@@ -450,12 +480,7 @@ export const SpacePlugin = ({
|
|
|
450
480
|
return null;
|
|
451
481
|
}
|
|
452
482
|
case 'status': {
|
|
453
|
-
return
|
|
454
|
-
<>
|
|
455
|
-
<SyncStatus />
|
|
456
|
-
<SaveStatus />
|
|
457
|
-
</>
|
|
458
|
-
);
|
|
483
|
+
return <SyncStatus />;
|
|
459
484
|
}
|
|
460
485
|
default:
|
|
461
486
|
return null;
|
|
@@ -6,7 +6,7 @@ import React from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { useIntentDispatcher, useResolvePlugins } from '@dxos/app-framework';
|
|
8
8
|
import { Input, Select, toLocalizedString, useTranslation } from '@dxos/react-ui';
|
|
9
|
-
import {
|
|
9
|
+
import { DeprecatedFormInput } from '@dxos/react-ui-data';
|
|
10
10
|
|
|
11
11
|
import { SpaceAction, SPACE_PLUGIN } from '../meta';
|
|
12
12
|
import { parseSpaceInitPlugin, type SpaceSettingsProps } from '../types';
|
|
@@ -18,7 +18,7 @@ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) =>
|
|
|
18
18
|
|
|
19
19
|
return (
|
|
20
20
|
<>
|
|
21
|
-
<
|
|
21
|
+
<DeprecatedFormInput label={t('show hidden spaces label')}>
|
|
22
22
|
<Input.Switch
|
|
23
23
|
checked={settings.showHidden}
|
|
24
24
|
onCheckedChange={(checked) =>
|
|
@@ -29,9 +29,9 @@ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) =>
|
|
|
29
29
|
})
|
|
30
30
|
}
|
|
31
31
|
/>
|
|
32
|
-
</
|
|
32
|
+
</DeprecatedFormInput>
|
|
33
33
|
|
|
34
|
-
<
|
|
34
|
+
<DeprecatedFormInput label={t('default on space create label')}>
|
|
35
35
|
<Select.Root
|
|
36
36
|
value={settings.onSpaceCreate}
|
|
37
37
|
onValueChange={(value) => {
|
|
@@ -57,7 +57,7 @@ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) =>
|
|
|
57
57
|
</Select.Content>
|
|
58
58
|
</Select.Portal>
|
|
59
59
|
</Select.Root>
|
|
60
|
-
</
|
|
60
|
+
</DeprecatedFormInput>
|
|
61
61
|
</>
|
|
62
62
|
);
|
|
63
63
|
};
|
|
@@ -6,7 +6,9 @@ import React, { useCallback, useState } from 'react';
|
|
|
6
6
|
|
|
7
7
|
import { log } from '@dxos/log';
|
|
8
8
|
import { EdgeReplicationSetting } from '@dxos/protocols/proto/dxos/echo/metadata';
|
|
9
|
+
import { useClient } from '@dxos/react-client';
|
|
9
10
|
import { type Space } from '@dxos/react-client/echo';
|
|
11
|
+
import { DeviceType, useDevices } from '@dxos/react-client/halo';
|
|
10
12
|
import { Input, useTranslation } from '@dxos/react-ui';
|
|
11
13
|
|
|
12
14
|
import { SPACE_PLUGIN } from '../meta';
|
|
@@ -17,10 +19,16 @@ export type SpaceSettingsPanelProps = {
|
|
|
17
19
|
|
|
18
20
|
export const SpaceSettingsPanel = ({ space }: SpaceSettingsPanelProps) => {
|
|
19
21
|
const { t } = useTranslation(SPACE_PLUGIN);
|
|
22
|
+
|
|
23
|
+
const client = useClient();
|
|
24
|
+
const devices = useDevices();
|
|
25
|
+
const managedDeviceAvailable = devices.find((device) => device.profile?.type === DeviceType.AGENT_MANAGED);
|
|
26
|
+
const edgeAgents = Boolean(client.config.values.runtime?.client?.edgeFeatures?.agents);
|
|
27
|
+
const edgeReplicationAvailable = edgeAgents && managedDeviceAvailable;
|
|
28
|
+
|
|
20
29
|
const [edgeReplication, setEdgeReplication] = useState(
|
|
21
30
|
space.internal.data.edgeReplication === EdgeReplicationSetting.ENABLED,
|
|
22
31
|
);
|
|
23
|
-
|
|
24
32
|
const toggleEdgeReplication = useCallback(
|
|
25
33
|
async (next: boolean) => {
|
|
26
34
|
setEdgeReplication(next);
|
|
@@ -41,19 +49,21 @@ export const SpaceSettingsPanel = ({ space }: SpaceSettingsPanelProps) => {
|
|
|
41
49
|
<Input.Label>{t('name label')}</Input.Label>
|
|
42
50
|
<Input.TextInput
|
|
43
51
|
placeholder={t('name placeholder')}
|
|
44
|
-
value={space.properties.name}
|
|
52
|
+
value={space.properties.name ?? ''}
|
|
45
53
|
onChange={(event) => {
|
|
46
54
|
space.properties.name = event.target.value;
|
|
47
55
|
}}
|
|
48
56
|
/>
|
|
49
57
|
</div>
|
|
50
58
|
</Input.Root>
|
|
51
|
-
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
59
|
+
{edgeReplicationAvailable && (
|
|
60
|
+
<Input.Root>
|
|
61
|
+
<div role='none' className='flex justify-between'>
|
|
62
|
+
<Input.Label>{t('edge replication label')}</Input.Label>
|
|
63
|
+
<Input.Switch checked={edgeReplication} onCheckedChange={toggleEdgeReplication} />
|
|
64
|
+
</div>
|
|
65
|
+
</Input.Root>
|
|
66
|
+
)}
|
|
57
67
|
</div>
|
|
58
68
|
);
|
|
59
69
|
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type HTMLAttributes, useEffect, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Icon } from '@dxos/react-ui';
|
|
8
|
+
import { type ThemedClassName } from '@dxos/react-ui';
|
|
9
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
10
|
+
|
|
11
|
+
import { type Progress, type PeerSyncState } from './sync-state';
|
|
12
|
+
|
|
13
|
+
export const SYNC_STALLED_TIMEOUT = 5_000;
|
|
14
|
+
|
|
15
|
+
// TODO(wittjosiah): Define sematic color tokens.
|
|
16
|
+
const styles = {
|
|
17
|
+
barBg: 'bg-neutral-50 dark:bg-green-900 text-black',
|
|
18
|
+
barFg: 'bg-neutral-100 bg-green-500',
|
|
19
|
+
barHover: 'dark:hover:bg-green-500',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const useActive = (count: number) => {
|
|
23
|
+
const [current, setCurrent] = useState(count);
|
|
24
|
+
const [active, setActive] = useState(false);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
let t: NodeJS.Timeout | undefined;
|
|
27
|
+
if (count !== current) {
|
|
28
|
+
setActive(true);
|
|
29
|
+
setCurrent(count);
|
|
30
|
+
t && clearTimeout(t);
|
|
31
|
+
t = setTimeout(() => {
|
|
32
|
+
setActive(false);
|
|
33
|
+
}, SYNC_STALLED_TIMEOUT);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
setActive(false);
|
|
38
|
+
clearTimeout(t);
|
|
39
|
+
};
|
|
40
|
+
}, [count, current]);
|
|
41
|
+
return active;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const SpaceRow = ({
|
|
45
|
+
spaceId,
|
|
46
|
+
state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
|
|
47
|
+
}: {
|
|
48
|
+
spaceId: string;
|
|
49
|
+
state: PeerSyncState;
|
|
50
|
+
}) => {
|
|
51
|
+
const downActive = useActive(localDocumentCount);
|
|
52
|
+
const upActive = useActive(remoteDocumentCount);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={mx('flex items-center mx-[2px] gap-[2px] cursor-pointer', styles.barHover)}
|
|
57
|
+
title={spaceId}
|
|
58
|
+
onClick={() => {
|
|
59
|
+
void navigator.clipboard.writeText(spaceId);
|
|
60
|
+
}}
|
|
61
|
+
>
|
|
62
|
+
<Icon
|
|
63
|
+
icon='ph--arrow-fat-line-left--regular'
|
|
64
|
+
size={3}
|
|
65
|
+
classNames={mx(downActive && 'animate-[pulse_1s_infinite]')}
|
|
66
|
+
/>
|
|
67
|
+
<Candle
|
|
68
|
+
up={{ count: remoteDocumentCount, total: remoteDocumentCount + missingOnRemote }}
|
|
69
|
+
down={{ count: localDocumentCount, total: localDocumentCount + missingOnLocal }}
|
|
70
|
+
title={spaceId}
|
|
71
|
+
/>
|
|
72
|
+
<Icon
|
|
73
|
+
icon='ph--arrow-fat-line-right--regular'
|
|
74
|
+
size={3}
|
|
75
|
+
classNames={mx(upActive && 'animate-[pulse_1s_step-start_infinite]')}
|
|
76
|
+
/>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type CandleProps = ThemedClassName<Pick<HTMLAttributes<HTMLDivElement>, 'title'>> & { up: Progress; down: Progress };
|
|
82
|
+
|
|
83
|
+
const Candle = ({ classNames, up, down }: CandleProps) => {
|
|
84
|
+
return (
|
|
85
|
+
<div className={mx('grid grid-cols-[1fr_2rem_1fr] w-full h-3', classNames)}>
|
|
86
|
+
<Bar classNames='justify-end' {...up} />
|
|
87
|
+
<div className='relative'>
|
|
88
|
+
<div className={mx('absolute inset-0 flex items-center justify-center text-xs', styles.barBg)}>{up.total}</div>
|
|
89
|
+
</div>
|
|
90
|
+
<Bar {...down} />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const Bar = ({ classNames, count, total }: ThemedClassName<Progress>) => {
|
|
96
|
+
let p = (count / total) * 100;
|
|
97
|
+
if (count < total) {
|
|
98
|
+
p = Math.min(p, 95);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={mx('relative flex w-full', styles.barBg, classNames)}>
|
|
103
|
+
<div className={mx('shrink-0', styles.barFg)} style={{ width: `${p}%` }}></div>
|
|
104
|
+
{count !== total && (
|
|
105
|
+
<div className='absolute top-0 bottom-0 flex items-center mx-0.5 text-black text-xs'>{count}</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
@@ -4,14 +4,14 @@
|
|
|
4
4
|
|
|
5
5
|
import '@dxos-theme';
|
|
6
6
|
|
|
7
|
-
import { type Meta } from '@storybook/react';
|
|
7
|
+
import { type Meta, type StoryObj } from '@storybook/react';
|
|
8
8
|
import React from 'react';
|
|
9
9
|
|
|
10
10
|
import { SpaceId } from '@dxos/keys';
|
|
11
11
|
import { withTheme, withLayout } from '@dxos/storybook-utils';
|
|
12
12
|
|
|
13
13
|
import { SyncStatusDetail, SyncStatusIndicator } from './SyncStatus';
|
|
14
|
-
import { getSyncSummary, type SpaceSyncStateMap } from './
|
|
14
|
+
import { getSyncSummary, type SpaceSyncStateMap } from './sync-state';
|
|
15
15
|
import translations from '../../translations';
|
|
16
16
|
|
|
17
17
|
const DefaultStory = (props: any) => {
|
|
@@ -39,13 +39,22 @@ const state: SpaceSyncStateMap = Array.from({ length: 5 }).reduce<SpaceSyncState
|
|
|
39
39
|
return map;
|
|
40
40
|
}, {});
|
|
41
41
|
|
|
42
|
-
export const Default = {
|
|
42
|
+
export const Default: StoryObj<typeof SyncStatusIndicator> = {
|
|
43
43
|
args: {
|
|
44
44
|
state,
|
|
45
|
+
saved: true,
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
export const
|
|
49
|
+
export const Saving: StoryObj<typeof SyncStatusIndicator> = {
|
|
50
|
+
args: {
|
|
51
|
+
state,
|
|
52
|
+
saved: false,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// TODO(wittjosiah): Separate story path for separate component.
|
|
57
|
+
export const Detail: StoryObj<typeof SyncStatusDetail> = {
|
|
49
58
|
render: SyncStatusDetail,
|
|
50
59
|
args: {
|
|
51
60
|
state,
|
|
@@ -2,73 +2,76 @@
|
|
|
2
2
|
// Copyright 2024 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import React, {
|
|
5
|
+
import React, { useEffect, useState } from 'react';
|
|
6
6
|
|
|
7
7
|
import { StatusBar } from '@dxos/plugin-status-bar';
|
|
8
|
+
import { useClient } from '@dxos/react-client';
|
|
8
9
|
import { Icon, Popover, useTranslation } from '@dxos/react-ui';
|
|
9
10
|
import { type ThemedClassName } from '@dxos/react-ui';
|
|
10
11
|
import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
|
|
11
12
|
import { mx } from '@dxos/react-ui-theme';
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
+
import { SpaceRow, SYNC_STALLED_TIMEOUT } from './Space';
|
|
15
|
+
import { createClientSaveTracker } from './save-tracker';
|
|
16
|
+
import { getIcon, getStatus } from './status';
|
|
17
|
+
import { type PeerSyncState, type SpaceSyncStateMap, getSyncSummary, useSyncState } from './sync-state';
|
|
14
18
|
import { SPACE_PLUGIN } from '../../meta';
|
|
15
19
|
|
|
16
|
-
const SYNC_STALLED_TIMEOUT = 5_000;
|
|
17
|
-
|
|
18
|
-
const styles = {
|
|
19
|
-
barBg: 'bg-neutral-50 dark:bg-green-900 text-black',
|
|
20
|
-
barFg: 'bg-neutral-100 bg-green-500',
|
|
21
|
-
barHover: 'dark:hover:bg-green-500',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
20
|
export const SyncStatus = () => {
|
|
21
|
+
const client = useClient();
|
|
25
22
|
const state = useSyncState();
|
|
26
|
-
|
|
23
|
+
const [saved, setSaved] = useState(true);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
return createClientSaveTracker(client, (state) => {
|
|
27
|
+
setSaved(state === 'saved');
|
|
28
|
+
});
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
return <SyncStatusIndicator state={state} saved={saved} />;
|
|
27
32
|
};
|
|
28
33
|
|
|
29
|
-
export const SyncStatusIndicator = ({ state }: { state: SpaceSyncStateMap }) => {
|
|
34
|
+
export const SyncStatusIndicator = ({ state, saved }: { state: SpaceSyncStateMap; saved: Boolean }) => {
|
|
35
|
+
const { t } = useTranslation(SPACE_PLUGIN);
|
|
30
36
|
const summary = getSyncSummary(state);
|
|
31
|
-
const offline =
|
|
32
|
-
|
|
37
|
+
const offline = Object.values(state).length === 0;
|
|
33
38
|
const needsToUpload = summary.differentDocuments > 0 || summary.missingOnRemote > 0;
|
|
34
39
|
const needsToDownload = summary.differentDocuments > 0 || summary.missingOnLocal > 0;
|
|
40
|
+
const status = getStatus({ offline, saved, needsToUpload, needsToDownload });
|
|
41
|
+
|
|
35
42
|
const [classNames, setClassNames] = useState<string>();
|
|
36
43
|
useEffect(() => {
|
|
37
44
|
setClassNames(undefined);
|
|
38
|
-
if (!needsToUpload && !needsToDownload) {
|
|
45
|
+
if (offline || (!needsToUpload && !needsToDownload)) {
|
|
39
46
|
return;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
const t = setTimeout(() => {
|
|
50
|
+
// TODO(wittjosiah): Use semantic color tokens.
|
|
43
51
|
setClassNames('text-orange-500');
|
|
44
52
|
}, SYNC_STALLED_TIMEOUT);
|
|
45
53
|
return () => clearTimeout(t);
|
|
46
|
-
}, [needsToUpload, needsToDownload]);
|
|
54
|
+
}, [offline, needsToUpload, needsToDownload]);
|
|
47
55
|
|
|
48
|
-
|
|
49
|
-
|
|
56
|
+
const title = t(`${status} label`);
|
|
57
|
+
const icon = <Icon icon={getIcon(status)} size={4} classNames={classNames} />;
|
|
58
|
+
|
|
59
|
+
if (offline) {
|
|
60
|
+
return <StatusBar.Item title={title}>{icon}</StatusBar.Item>;
|
|
61
|
+
} else {
|
|
62
|
+
return (
|
|
50
63
|
<Popover.Root>
|
|
51
|
-
<Popover.Trigger>
|
|
52
|
-
<
|
|
53
|
-
icon={
|
|
54
|
-
offline
|
|
55
|
-
? 'ph--cloud-x--regular'
|
|
56
|
-
: needsToUpload
|
|
57
|
-
? 'ph--cloud-arrow-up--regular'
|
|
58
|
-
: needsToDownload
|
|
59
|
-
? 'ph--cloud-arrow-down--regular'
|
|
60
|
-
: 'ph--cloud-check--regular'
|
|
61
|
-
}
|
|
62
|
-
size={4}
|
|
63
|
-
classNames={classNames}
|
|
64
|
-
/>
|
|
64
|
+
<Popover.Trigger asChild>
|
|
65
|
+
<StatusBar.Button title={title}>{icon}</StatusBar.Button>
|
|
65
66
|
</Popover.Trigger>
|
|
66
|
-
<Popover.
|
|
67
|
-
<
|
|
68
|
-
|
|
67
|
+
<Popover.Portal>
|
|
68
|
+
<Popover.Content sideOffset={16}>
|
|
69
|
+
<SyncStatusDetail state={state} summary={summary} debug={false} />
|
|
70
|
+
</Popover.Content>
|
|
71
|
+
</Popover.Portal>
|
|
69
72
|
</Popover.Root>
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
);
|
|
74
|
+
}
|
|
72
75
|
};
|
|
73
76
|
|
|
74
77
|
export const SyncStatusDetail = ({
|
|
@@ -86,9 +89,9 @@ export const SyncStatusDetail = ({
|
|
|
86
89
|
|
|
87
90
|
// TODO(burdon): Normalize to max document count?
|
|
88
91
|
return (
|
|
89
|
-
<div className={mx('flex flex-col text-xs min-w-[16rem]', classNames)}>
|
|
90
|
-
<h1
|
|
91
|
-
<div className='flex flex-col gap-
|
|
92
|
+
<div className={mx('flex flex-col gap-3 p-2 text-xs min-w-[16rem]', classNames)}>
|
|
93
|
+
<h1>{t('sync status title')}</h1>
|
|
94
|
+
<div className='flex flex-col gap-2'>
|
|
92
95
|
{entries.map(([spaceId, state]) => (
|
|
93
96
|
<SpaceRow key={spaceId} spaceId={spaceId} state={state} />
|
|
94
97
|
))}
|
|
@@ -97,92 +100,3 @@ export const SyncStatusDetail = ({
|
|
|
97
100
|
</div>
|
|
98
101
|
);
|
|
99
102
|
};
|
|
100
|
-
|
|
101
|
-
const useActive = (count: number) => {
|
|
102
|
-
const [current, setCurrent] = useState(count);
|
|
103
|
-
const [active, setActive] = useState(false);
|
|
104
|
-
useEffect(() => {
|
|
105
|
-
let t: NodeJS.Timeout | undefined;
|
|
106
|
-
if (count !== current) {
|
|
107
|
-
setActive(true);
|
|
108
|
-
setCurrent(count);
|
|
109
|
-
t && clearTimeout(t);
|
|
110
|
-
t = setTimeout(() => {
|
|
111
|
-
setActive(false);
|
|
112
|
-
}, SYNC_STALLED_TIMEOUT);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return () => {
|
|
116
|
-
setActive(false);
|
|
117
|
-
clearTimeout(t);
|
|
118
|
-
};
|
|
119
|
-
}, [count, current]);
|
|
120
|
-
return active;
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const SpaceRow = ({
|
|
124
|
-
spaceId,
|
|
125
|
-
state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
|
|
126
|
-
}: {
|
|
127
|
-
spaceId: string;
|
|
128
|
-
state: PeerSyncState;
|
|
129
|
-
}) => {
|
|
130
|
-
const downActive = useActive(localDocumentCount);
|
|
131
|
-
const upActive = useActive(remoteDocumentCount);
|
|
132
|
-
|
|
133
|
-
return (
|
|
134
|
-
<div
|
|
135
|
-
className={mx('flex items-center mx-[2px] gap-[2px] cursor-pointer', styles.barHover)}
|
|
136
|
-
title={spaceId}
|
|
137
|
-
onClick={() => {
|
|
138
|
-
void navigator.clipboard.writeText(spaceId);
|
|
139
|
-
}}
|
|
140
|
-
>
|
|
141
|
-
<Icon
|
|
142
|
-
icon='ph--arrow-fat-line-left--regular'
|
|
143
|
-
size={3}
|
|
144
|
-
classNames={mx(downActive && 'animate-[pulse_1s_infinite]')}
|
|
145
|
-
/>
|
|
146
|
-
<Candle
|
|
147
|
-
up={{ count: remoteDocumentCount, total: remoteDocumentCount + missingOnRemote }}
|
|
148
|
-
down={{ count: localDocumentCount, total: localDocumentCount + missingOnLocal }}
|
|
149
|
-
title={spaceId}
|
|
150
|
-
/>
|
|
151
|
-
<Icon
|
|
152
|
-
icon='ph--arrow-fat-line-right--regular'
|
|
153
|
-
size={3}
|
|
154
|
-
classNames={mx(upActive && 'animate-[pulse_1s_step-start_infinite]')}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
type CandleProps = ThemedClassName<Pick<HTMLAttributes<HTMLDivElement>, 'title'>> & { up: Progress; down: Progress };
|
|
161
|
-
|
|
162
|
-
const Candle = ({ classNames, up, down }: CandleProps) => {
|
|
163
|
-
return (
|
|
164
|
-
<div className={mx('grid grid-cols-[1fr_2rem_1fr] w-full h-3', classNames)}>
|
|
165
|
-
<Bar classNames='justify-end' {...up} />
|
|
166
|
-
<div className='relative'>
|
|
167
|
-
<div className={mx('absolute inset-0 flex items-center justify-center text-xs', styles.barBg)}>{up.total}</div>
|
|
168
|
-
</div>
|
|
169
|
-
<Bar {...down} />
|
|
170
|
-
</div>
|
|
171
|
-
);
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const Bar = ({ classNames, count, total }: ThemedClassName<Progress>) => {
|
|
175
|
-
let p = (count / total) * 100;
|
|
176
|
-
if (count < total) {
|
|
177
|
-
p = Math.min(p, 95);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return (
|
|
181
|
-
<div className={mx('relative flex w-full', styles.barBg, classNames)}>
|
|
182
|
-
<div className={mx('shrink-0', styles.barFg)} style={{ width: `${p}%` }}></div>
|
|
183
|
-
{count !== total && (
|
|
184
|
-
<div className='absolute top-0 bottom-0 flex items-center mx-0.5 text-black text-xs'>{count}</div>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
);
|
|
188
|
-
};
|