@dxos/plugin-debug 0.6.8-main.046e6cf
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/LICENSE +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/DebugGlobal-H37KGFEG.mjs +179 -0
- package/dist/lib/browser/DebugGlobal-H37KGFEG.mjs.map +7 -0
- package/dist/lib/browser/DebugSpace-ZZ2JLU6F.mjs +407 -0
- package/dist/lib/browser/DebugSpace-ZZ2JLU6F.mjs.map +7 -0
- package/dist/lib/browser/DevtoolsMain-DTAWVF22.mjs +16 -0
- package/dist/lib/browser/DevtoolsMain-DTAWVF22.mjs.map +7 -0
- package/dist/lib/browser/chunk-ED5L5YYI.mjs +27 -0
- package/dist/lib/browser/chunk-ED5L5YYI.mjs.map +7 -0
- package/dist/lib/browser/chunk-RYK3J66D.mjs +24 -0
- package/dist/lib/browser/chunk-RYK3J66D.mjs.map +7 -0
- package/dist/lib/browser/chunk-WEGYHXMB.mjs +21 -0
- package/dist/lib/browser/chunk-WEGYHXMB.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +658 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +9 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/types/src/DebugPlugin.d.ts +5 -0
- package/dist/types/src/DebugPlugin.d.ts.map +1 -0
- package/dist/types/src/components/DebugGlobal.d.ts +7 -0
- package/dist/types/src/components/DebugGlobal.d.ts.map +1 -0
- package/dist/types/src/components/DebugPanel.d.ts +5 -0
- package/dist/types/src/components/DebugPanel.d.ts.map +1 -0
- package/dist/types/src/components/DebugSettings.d.ts +6 -0
- package/dist/types/src/components/DebugSettings.d.ts.map +1 -0
- package/dist/types/src/components/DebugSpace.d.ts +9 -0
- package/dist/types/src/components/DebugSpace.d.ts.map +1 -0
- package/dist/types/src/components/DebugSpace.stories.d.ts +16 -0
- package/dist/types/src/components/DebugSpace.stories.d.ts.map +1 -0
- package/dist/types/src/components/DebugStatus.d.ts +3 -0
- package/dist/types/src/components/DebugStatus.d.ts.map +1 -0
- package/dist/types/src/components/DevtoolsMain.d.ts +4 -0
- package/dist/types/src/components/DevtoolsMain.d.ts.map +1 -0
- package/dist/types/src/components/ObjectCreator.d.ts +17 -0
- package/dist/types/src/components/ObjectCreator.d.ts.map +1 -0
- package/dist/types/src/components/ObjectCreator.stories.d.ts +17 -0
- package/dist/types/src/components/ObjectCreator.stories.d.ts.map +1 -0
- package/dist/types/src/components/Tree.d.ts +20 -0
- package/dist/types/src/components/Tree.d.ts.map +1 -0
- package/dist/types/src/components/Wireframe.d.ts +7 -0
- package/dist/types/src/components/Wireframe.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +13 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +15 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/scaffolding/generator.d.ts +11 -0
- package/dist/types/src/scaffolding/generator.d.ts.map +1 -0
- package/dist/types/src/scaffolding/index.d.ts +2 -0
- package/dist/types/src/scaffolding/index.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +29 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +19 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/types.test.d.ts +2 -0
- package/dist/types/src/types.test.d.ts.map +1 -0
- package/package.json +92 -0
- package/src/DebugPlugin.tsx +270 -0
- package/src/components/DebugGlobal.tsx +80 -0
- package/src/components/DebugPanel.tsx +34 -0
- package/src/components/DebugSettings.tsx +159 -0
- package/src/components/DebugSpace.stories.tsx +40 -0
- package/src/components/DebugSpace.tsx +195 -0
- package/src/components/DebugStatus.tsx +221 -0
- package/src/components/DevtoolsMain.tsx +16 -0
- package/src/components/ObjectCreator.stories.tsx +44 -0
- package/src/components/ObjectCreator.tsx +100 -0
- package/src/components/Tree.tsx +113 -0
- package/src/components/Wireframe.tsx +35 -0
- package/src/components/index.ts +14 -0
- package/src/index.ts +9 -0
- package/src/meta.tsx +19 -0
- package/src/scaffolding/generator.ts +143 -0
- package/src/scaffolding/index.ts +5 -0
- package/src/translations.ts +36 -0
- package/src/types.test.ts +13 -0
- package/src/types.ts +45 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
ArrowClockwise,
|
|
7
|
+
DownloadSimple,
|
|
8
|
+
Flag,
|
|
9
|
+
FlagPennant,
|
|
10
|
+
HandPalm,
|
|
11
|
+
Play,
|
|
12
|
+
PlusMinus,
|
|
13
|
+
Timer,
|
|
14
|
+
UserCirclePlus,
|
|
15
|
+
} from '@phosphor-icons/react';
|
|
16
|
+
import React, { type FC, useContext, useMemo, useState } from 'react';
|
|
17
|
+
|
|
18
|
+
import { type ReactiveObject } from '@dxos/echo-schema';
|
|
19
|
+
import { DocumentType } from '@dxos/plugin-markdown/types';
|
|
20
|
+
import { Invitation } from '@dxos/protocols/proto/dxos/client/services';
|
|
21
|
+
import { faker } from '@dxos/random';
|
|
22
|
+
import { useAsyncEffect } from '@dxos/react-async';
|
|
23
|
+
import { useClient } from '@dxos/react-client';
|
|
24
|
+
import { Filter, type Space, useSpaceInvitation } from '@dxos/react-client/echo';
|
|
25
|
+
import { InvitationEncoder } from '@dxos/react-client/invitations';
|
|
26
|
+
import { Button, Input, useFileDownload } from '@dxos/react-ui';
|
|
27
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
28
|
+
import { safeParseInt } from '@dxos/util';
|
|
29
|
+
|
|
30
|
+
import { DebugPanel } from './DebugPanel';
|
|
31
|
+
import { ObjectCreator } from './ObjectCreator';
|
|
32
|
+
import { createSpaceObjectGenerator } from '../scaffolding';
|
|
33
|
+
import { DebugContext } from '../types';
|
|
34
|
+
|
|
35
|
+
const DEFAULT_COUNT = 100;
|
|
36
|
+
const DEFAULT_PERIOD = 500;
|
|
37
|
+
const DEFAULT_JITTER = 50;
|
|
38
|
+
|
|
39
|
+
// TODO(burdon): Factor out.
|
|
40
|
+
const useRefresh = (): [any, () => void] => {
|
|
41
|
+
const [update, setUpdate] = useState({});
|
|
42
|
+
return [update, () => setUpdate({})];
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const DebugSpace: FC<{
|
|
46
|
+
space: Space;
|
|
47
|
+
onAddObjects?: (objects: ReactiveObject<any>[]) => void;
|
|
48
|
+
}> = ({ space, onAddObjects }) => {
|
|
49
|
+
const { connect } = useSpaceInvitation(space?.key);
|
|
50
|
+
const client = useClient();
|
|
51
|
+
const [data, setData] = useState<any>({});
|
|
52
|
+
|
|
53
|
+
const [update, handleUpdate] = useRefresh();
|
|
54
|
+
useAsyncEffect(
|
|
55
|
+
async (isMounted) => {
|
|
56
|
+
const data = await client.diagnostics({ truncate: true });
|
|
57
|
+
if (isMounted()) {
|
|
58
|
+
setData(
|
|
59
|
+
data?.diagnostics?.spaces?.find(({ key }: any) => {
|
|
60
|
+
return space.key.toHex().startsWith(key);
|
|
61
|
+
}),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
[space, update],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const download = useFileDownload();
|
|
69
|
+
const handleCopy = async () => {
|
|
70
|
+
download(
|
|
71
|
+
new Blob([JSON.stringify(data, undefined, 2)], { type: 'text/plain' }),
|
|
72
|
+
`${new Date().toISOString().replace(/\W/g, '-')}.json`,
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const [mutationCount, setMutationCount] = useState(String(DEFAULT_COUNT));
|
|
77
|
+
const [mutationInterval, setMutationInterval] = useState(String(DEFAULT_PERIOD));
|
|
78
|
+
const [mutationJitter, setMutationJitter] = useState(String(DEFAULT_JITTER));
|
|
79
|
+
|
|
80
|
+
const generator = useMemo(() => createSpaceObjectGenerator(space), [space]);
|
|
81
|
+
|
|
82
|
+
// TODO(burdon): Note: this is shared across all spaces!
|
|
83
|
+
const { running, start, stop } = useContext(DebugContext);
|
|
84
|
+
const handleToggleRunning = () => {
|
|
85
|
+
if (running) {
|
|
86
|
+
stop();
|
|
87
|
+
handleUpdate();
|
|
88
|
+
} else {
|
|
89
|
+
start(
|
|
90
|
+
async () => {
|
|
91
|
+
const { objects } = await space.db.query(Filter.schema(DocumentType)).run();
|
|
92
|
+
if (objects.length) {
|
|
93
|
+
const object = faker.helpers.arrayElement(objects);
|
|
94
|
+
await generator.mutateObject(object, { count: 10, mutationSize: 10, maxContentLength: 1000 });
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
count: safeParseInt(mutationCount) ?? 0,
|
|
99
|
+
interval: safeParseInt(mutationInterval) ?? 0,
|
|
100
|
+
jitter: safeParseInt(mutationJitter) ?? 0,
|
|
101
|
+
},
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleCreateInvitation = () => {
|
|
107
|
+
const invitation = space.share({
|
|
108
|
+
type: Invitation.Type.INTERACTIVE,
|
|
109
|
+
authMethod: Invitation.AuthMethod.NONE,
|
|
110
|
+
multiUse: true,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// TODO(burdon): Refactor.
|
|
114
|
+
// TODO(burdon): Unsubscribe?
|
|
115
|
+
connect(invitation);
|
|
116
|
+
const code = InvitationEncoder.encode(invitation.get());
|
|
117
|
+
new URL(window.origin).searchParams.set('spaceInvitationCode', code);
|
|
118
|
+
const url = `${window.origin}?spaceInvitationCode=${code}`;
|
|
119
|
+
void navigator.clipboard.writeText(url);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleCreateEpoch = async () => {
|
|
123
|
+
await space.internal.createEpoch();
|
|
124
|
+
handleUpdate();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<DebugPanel
|
|
129
|
+
menu={
|
|
130
|
+
<>
|
|
131
|
+
<div className='relative' title='mutation count'>
|
|
132
|
+
<Input.Root>
|
|
133
|
+
<Input.TextInput
|
|
134
|
+
autoComplete='off'
|
|
135
|
+
size={5}
|
|
136
|
+
classNames='w-[100px] text-right pie-[22px]'
|
|
137
|
+
placeholder='Count'
|
|
138
|
+
value={mutationCount}
|
|
139
|
+
onChange={({ target: { value } }) => setMutationCount(value)}
|
|
140
|
+
/>
|
|
141
|
+
</Input.Root>
|
|
142
|
+
<Flag className={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
|
|
143
|
+
</div>
|
|
144
|
+
<div className='relative' title='mutation period'>
|
|
145
|
+
<Input.Root>
|
|
146
|
+
<Input.TextInput
|
|
147
|
+
autoComplete='off'
|
|
148
|
+
size={5}
|
|
149
|
+
classNames='w-[100px] text-right pie-[22px]'
|
|
150
|
+
placeholder='Interval'
|
|
151
|
+
value={mutationInterval}
|
|
152
|
+
onChange={({ target: { value } }) => setMutationInterval(value)}
|
|
153
|
+
/>
|
|
154
|
+
</Input.Root>
|
|
155
|
+
<Timer className={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
|
|
156
|
+
</div>
|
|
157
|
+
<div className='relative' title='mutation jitter'>
|
|
158
|
+
<Input.Root>
|
|
159
|
+
<Input.TextInput
|
|
160
|
+
autoComplete='off'
|
|
161
|
+
size={5}
|
|
162
|
+
classNames='w-[100px] text-right pie-[22px]'
|
|
163
|
+
placeholder='Jitter'
|
|
164
|
+
value={mutationJitter}
|
|
165
|
+
onChange={({ target: { value } }) => setMutationJitter(value)}
|
|
166
|
+
/>
|
|
167
|
+
</Input.Root>
|
|
168
|
+
<PlusMinus className={mx('absolute inline-end-1 block-start-1 mt-[6px]', getSize(3))} />
|
|
169
|
+
</div>
|
|
170
|
+
<Button onClick={handleToggleRunning}>
|
|
171
|
+
{running ? <HandPalm className={getSize(5)} /> : <Play className={getSize(5)} />}
|
|
172
|
+
</Button>
|
|
173
|
+
<Button onClick={handleUpdate}>
|
|
174
|
+
<ArrowClockwise className={getSize(5)} />
|
|
175
|
+
</Button>
|
|
176
|
+
<Button onClick={handleCopy}>
|
|
177
|
+
<DownloadSimple className={getSize(5)} />
|
|
178
|
+
</Button>
|
|
179
|
+
|
|
180
|
+
<div className='grow' />
|
|
181
|
+
<Button onClick={handleCreateEpoch} title='Create epoch'>
|
|
182
|
+
<FlagPennant className={mx(getSize(5))} />
|
|
183
|
+
</Button>
|
|
184
|
+
<Button onClick={handleCreateInvitation} title='Create Space invitation'>
|
|
185
|
+
<UserCirclePlus className={mx(getSize(5), 'text-blue-500')} />
|
|
186
|
+
</Button>
|
|
187
|
+
</>
|
|
188
|
+
}
|
|
189
|
+
>
|
|
190
|
+
<ObjectCreator space={space} onAddObjects={onAddObjects} />
|
|
191
|
+
</DebugPanel>
|
|
192
|
+
);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
export default DebugSpace;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { ChartBar, Circle, Lightning, LightningSlash } from '@phosphor-icons/react';
|
|
6
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
7
|
+
|
|
8
|
+
import { firstIdInPart, parseGraphPlugin, parseNavigationPlugin, useResolvePlugin } from '@dxos/app-framework';
|
|
9
|
+
import { TimeoutError } from '@dxos/async';
|
|
10
|
+
import { StatsPanel, useStats } from '@dxos/devtools';
|
|
11
|
+
import { log } from '@dxos/log';
|
|
12
|
+
import { getActiveSpace } from '@dxos/plugin-space';
|
|
13
|
+
import { StatusBar } from '@dxos/plugin-status-bar';
|
|
14
|
+
import { ConnectionState } from '@dxos/protocols/proto/dxos/client/services';
|
|
15
|
+
import { useNetworkStatus } from '@dxos/react-client/mesh';
|
|
16
|
+
import { getSize, mx } from '@dxos/react-ui-theme';
|
|
17
|
+
|
|
18
|
+
const styles = {
|
|
19
|
+
success: 'text-sky-300 dark:text-green-700',
|
|
20
|
+
warning: 'text-orange-300 dark:text-orange-600',
|
|
21
|
+
error: 'text-red-300 dark:text-red-600',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
// TODO(burdon): Move out of debug plugin.
|
|
25
|
+
// TODO(burdon): Make pluggable (move indicators to relevant plugins).
|
|
26
|
+
// TODO(burdon): Vault heartbeat indicator (global scope)?
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Ensure light doesn't flicker immediately after start.
|
|
30
|
+
*/
|
|
31
|
+
// TODO(burdon): Move to @dxos/async (debounce?)
|
|
32
|
+
const _timer = (cb: (err?: Error) => void, options?: { min?: number; max?: number }) => {
|
|
33
|
+
const min = options?.min ?? 500;
|
|
34
|
+
let start: number;
|
|
35
|
+
let pending: NodeJS.Timeout;
|
|
36
|
+
let timeout: NodeJS.Timeout;
|
|
37
|
+
return {
|
|
38
|
+
start: () => {
|
|
39
|
+
start = Date.now();
|
|
40
|
+
clearTimeout(pending);
|
|
41
|
+
if (options?.max) {
|
|
42
|
+
clearTimeout(timeout);
|
|
43
|
+
timeout = setTimeout(() => {
|
|
44
|
+
cb(new TimeoutError(options.max));
|
|
45
|
+
}, options.max);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
stop: () => {
|
|
49
|
+
clearTimeout(timeout);
|
|
50
|
+
const delta = Date.now() - start;
|
|
51
|
+
if (delta < min) {
|
|
52
|
+
pending = setTimeout(() => {
|
|
53
|
+
cb();
|
|
54
|
+
}, min - delta);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Global error handler.
|
|
62
|
+
*/
|
|
63
|
+
// TODO(burdon): Integrate with Sentry?
|
|
64
|
+
const ErrorIndicator = () => {
|
|
65
|
+
const [, forceUpdate] = useState({});
|
|
66
|
+
const errorRef = useRef<Error>();
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const errorListener = (event: any) => {
|
|
69
|
+
const error: Error = event.error ?? event.reason;
|
|
70
|
+
// event.preventDefault();
|
|
71
|
+
if (errorRef.current !== error) {
|
|
72
|
+
// eslint-disable-next-line no-console
|
|
73
|
+
log.error('onError', { event });
|
|
74
|
+
errorRef.current = error;
|
|
75
|
+
forceUpdate({});
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// TODO(burdon): Register globally?
|
|
80
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/error_event
|
|
81
|
+
window.addEventListener('error', errorListener);
|
|
82
|
+
|
|
83
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/Window/unhandledrejection_event
|
|
84
|
+
window.addEventListener('unhandledrejection', errorListener);
|
|
85
|
+
|
|
86
|
+
return () => {
|
|
87
|
+
window.removeEventListener('error', errorListener);
|
|
88
|
+
window.removeEventListener('unhandledrejection', errorListener);
|
|
89
|
+
};
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
const handleReset = () => {
|
|
93
|
+
errorRef.current = undefined;
|
|
94
|
+
forceUpdate({});
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
if (errorRef.current) {
|
|
98
|
+
return (
|
|
99
|
+
<StatusBar.Button title={errorRef.current.message} onClick={handleReset}>
|
|
100
|
+
<Circle weight='fill' className={mx(styles.error, getSize(3))} />
|
|
101
|
+
</StatusBar.Button>
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
return (
|
|
105
|
+
<StatusBar.Item title='No errors.'>
|
|
106
|
+
<Circle weight='fill' className={getSize(3)} />
|
|
107
|
+
</StatusBar.Item>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Swarm connection handler.
|
|
114
|
+
*/
|
|
115
|
+
const SwarmIndicator = () => {
|
|
116
|
+
const [state, setState] = useState(0);
|
|
117
|
+
const { swarm } = useNetworkStatus();
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
setState(swarm === ConnectionState.ONLINE ? 0 : 1);
|
|
120
|
+
}, [swarm]);
|
|
121
|
+
|
|
122
|
+
if (state === 0) {
|
|
123
|
+
return (
|
|
124
|
+
<StatusBar.Item title='Connected to swarm.'>
|
|
125
|
+
<Lightning className={getSize(4)} />
|
|
126
|
+
</StatusBar.Item>
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
return (
|
|
130
|
+
<StatusBar.Item title='Disconnected from swarm.'>
|
|
131
|
+
<LightningSlash className={mx(styles.warning, getSize(4))} />
|
|
132
|
+
</StatusBar.Item>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Space saving indicator.
|
|
139
|
+
*/
|
|
140
|
+
const SavingIndicator = () => {
|
|
141
|
+
const [state, _setState] = useState(0);
|
|
142
|
+
const navigationPlugin = useResolvePlugin(parseNavigationPlugin);
|
|
143
|
+
const graphPlugin = useResolvePlugin(parseGraphPlugin);
|
|
144
|
+
const location = navigationPlugin?.provides.location;
|
|
145
|
+
const graph = graphPlugin?.provides.graph;
|
|
146
|
+
const _space = location && graph ? getActiveSpace(graph, firstIdInPart(location.active, 'main')) : undefined;
|
|
147
|
+
// TODO(dmaretskyi): Fix this when we have save status for automerge.
|
|
148
|
+
// useEffect(() => {
|
|
149
|
+
// if (!space) {
|
|
150
|
+
// return;
|
|
151
|
+
// }
|
|
152
|
+
// const { start, stop } = timer(() => setState(0), { min: 250 });
|
|
153
|
+
// return space.db.pendingBatch.on(({ duration, error }) => {
|
|
154
|
+
// if (error) {
|
|
155
|
+
// setState(2);
|
|
156
|
+
// stop();
|
|
157
|
+
// } else if (duration === undefined) {
|
|
158
|
+
// setState(1);
|
|
159
|
+
// start();
|
|
160
|
+
// } else {
|
|
161
|
+
// stop();
|
|
162
|
+
// }
|
|
163
|
+
// });
|
|
164
|
+
// }, [space]);
|
|
165
|
+
|
|
166
|
+
switch (state) {
|
|
167
|
+
case 2:
|
|
168
|
+
return (
|
|
169
|
+
<StatusBar.Item title='Edit not saved.'>
|
|
170
|
+
<Circle weight='fill' className={mx(styles.warning, getSize(3))} />
|
|
171
|
+
</StatusBar.Item>
|
|
172
|
+
);
|
|
173
|
+
case 1:
|
|
174
|
+
return (
|
|
175
|
+
<StatusBar.Item title='Saving...'>
|
|
176
|
+
<Circle weight='fill' className={mx(styles.success, getSize(3))} />
|
|
177
|
+
</StatusBar.Item>
|
|
178
|
+
);
|
|
179
|
+
case 0:
|
|
180
|
+
default:
|
|
181
|
+
return (
|
|
182
|
+
<StatusBar.Item title='Modified indicator.'>
|
|
183
|
+
<Circle weight='fill' className={getSize(3)} />
|
|
184
|
+
</StatusBar.Item>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const PerformanceIndicator = () => {
|
|
190
|
+
const [visible, setVisible] = useState(false);
|
|
191
|
+
const [stats, refreshStats] = useStats();
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<>
|
|
195
|
+
<StatusBar.Button onClick={() => setVisible((visible) => !visible)} title='Performance panels'>
|
|
196
|
+
<ChartBar />
|
|
197
|
+
</StatusBar.Button>
|
|
198
|
+
<div
|
|
199
|
+
className={mx(
|
|
200
|
+
'z-20 absolute transition-[right] bottom-[24px] w-[450px]',
|
|
201
|
+
'border-l border-y border-neutral-300 dark:border-neutral-700',
|
|
202
|
+
visible ? 'right-0' : 'right-[-450px]',
|
|
203
|
+
)}
|
|
204
|
+
>
|
|
205
|
+
<StatsPanel stats={stats} onRefresh={refreshStats} />
|
|
206
|
+
</div>
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const indicators = [PerformanceIndicator, SavingIndicator, ErrorIndicator, SwarmIndicator];
|
|
212
|
+
|
|
213
|
+
export const DebugStatus = () => {
|
|
214
|
+
return (
|
|
215
|
+
<>
|
|
216
|
+
{indicators.map((Indicator) => (
|
|
217
|
+
<Indicator key={Indicator.name} />
|
|
218
|
+
))}
|
|
219
|
+
</>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
|
|
7
|
+
import { Devtools } from '@dxos/devtools';
|
|
8
|
+
import { useClient, type ClientServices } from '@dxos/react-client';
|
|
9
|
+
|
|
10
|
+
const DevtoolsMain = () => {
|
|
11
|
+
const client = useClient();
|
|
12
|
+
|
|
13
|
+
return <Devtools client={client} services={client.services.services as ClientServices} />;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default DevtoolsMain;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import '@dxosTheme';
|
|
6
|
+
|
|
7
|
+
import React, { type FC, useEffect } from 'react';
|
|
8
|
+
|
|
9
|
+
import { createSpaceObjectGenerator } from '@dxos/echo-generator';
|
|
10
|
+
import { type ReactiveObject, useSpaces } from '@dxos/react-client/echo';
|
|
11
|
+
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
12
|
+
|
|
13
|
+
import { ObjectCreator } from './ObjectCreator';
|
|
14
|
+
|
|
15
|
+
const Story: FC = () => {
|
|
16
|
+
const [space] = useSpaces();
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (space) {
|
|
19
|
+
const generator = createSpaceObjectGenerator(space);
|
|
20
|
+
generator.addSchemas();
|
|
21
|
+
}
|
|
22
|
+
}, [space]);
|
|
23
|
+
|
|
24
|
+
const handleCreate = (objects: ReactiveObject<any>[]) => {
|
|
25
|
+
console.log('Created:', objects);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!space) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return <ObjectCreator space={space} onAddObjects={handleCreate} />;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default {
|
|
36
|
+
title: 'plugin-debug/SchemaList',
|
|
37
|
+
component: ObjectCreator,
|
|
38
|
+
render: () => <ClientRepeater component={Story} createSpace />,
|
|
39
|
+
parameters: {
|
|
40
|
+
layout: 'fullscreen',
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Default = {};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type FC, useMemo, useState } from 'react';
|
|
6
|
+
|
|
7
|
+
import { type EchoReactiveObject, type ReactiveObject } from '@dxos/echo-schema';
|
|
8
|
+
import { type Space } from '@dxos/react-client/echo';
|
|
9
|
+
import { Button, DensityProvider } from '@dxos/react-ui';
|
|
10
|
+
import { createColumnBuilder, type TableColumnDef, Table } from '@dxos/react-ui-table';
|
|
11
|
+
|
|
12
|
+
import { SchemasNames, createSpaceObjectGenerator } from '../scaffolding';
|
|
13
|
+
|
|
14
|
+
export type CreateObjectsParams = {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
schema: SchemasNames;
|
|
17
|
+
objectsCount: number;
|
|
18
|
+
mutationsCount: number;
|
|
19
|
+
maxContentLength: number;
|
|
20
|
+
mutationSize: number;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const CREATE_OBJECTS_IN_ONE_CHUNK = 10;
|
|
24
|
+
|
|
25
|
+
export const ObjectCreator: FC<{
|
|
26
|
+
space: Space;
|
|
27
|
+
onAddObjects?: (objects: ReactiveObject<any>[]) => void;
|
|
28
|
+
}> = ({ space, onAddObjects }) => {
|
|
29
|
+
const generator = useMemo(() => createSpaceObjectGenerator(space), [space]);
|
|
30
|
+
|
|
31
|
+
const [objectsToCreate, setObjectsToCreate] = useState<CreateObjectsParams[]>([
|
|
32
|
+
{
|
|
33
|
+
enabled: true,
|
|
34
|
+
schema: SchemasNames.document,
|
|
35
|
+
objectsCount: 10,
|
|
36
|
+
mutationsCount: 10,
|
|
37
|
+
mutationSize: 10,
|
|
38
|
+
maxContentLength: 1000,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
enabled: true,
|
|
42
|
+
schema: SchemasNames.diagram,
|
|
43
|
+
objectsCount: 10,
|
|
44
|
+
mutationsCount: 10,
|
|
45
|
+
mutationSize: 10,
|
|
46
|
+
maxContentLength: 1000,
|
|
47
|
+
},
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
const handleCreate = async () => {
|
|
51
|
+
for (const params of objectsToCreate) {
|
|
52
|
+
if (!params.enabled) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
let objectsCreated = 0;
|
|
56
|
+
while (objectsCreated < params.objectsCount) {
|
|
57
|
+
const objects = (await generator.createObjects({
|
|
58
|
+
[params.schema]: Math.min(CREATE_OBJECTS_IN_ONE_CHUNK, params.objectsCount - objectsCreated),
|
|
59
|
+
})) as EchoReactiveObject<any>[];
|
|
60
|
+
|
|
61
|
+
await generator.mutateObjects(objects, {
|
|
62
|
+
count: params.mutationsCount,
|
|
63
|
+
mutationSize: params.mutationSize,
|
|
64
|
+
maxContentLength: params.maxContentLength,
|
|
65
|
+
});
|
|
66
|
+
objectsCreated += objects.length;
|
|
67
|
+
onAddObjects?.(objects);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
await space.db.flush();
|
|
71
|
+
};
|
|
72
|
+
const handleChangeOnRow = (row: CreateObjectsParams, key: string, value: any) => {
|
|
73
|
+
const newObjects = [...objectsToCreate];
|
|
74
|
+
Object.assign(newObjects.find((object) => object.schema === row.schema)!, { [key]: value });
|
|
75
|
+
setObjectsToCreate(newObjects);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const { helper, builder } = createColumnBuilder<CreateObjectsParams>();
|
|
79
|
+
const columns: TableColumnDef<CreateObjectsParams>[] = [
|
|
80
|
+
helper.accessor('enabled', builder.switch({ label: 'Enabled', onUpdate: handleChangeOnRow })),
|
|
81
|
+
helper.accessor('schema', builder.string({ classNames: 'font-mono', label: 'Schema' })),
|
|
82
|
+
helper.accessor('objectsCount', builder.number({ label: 'Objects', onUpdate: handleChangeOnRow })),
|
|
83
|
+
helper.accessor('mutationsCount', builder.number({ label: 'Mutations', onUpdate: handleChangeOnRow })),
|
|
84
|
+
helper.accessor('mutationSize', builder.number({ label: 'Mut. Size', onUpdate: handleChangeOnRow })),
|
|
85
|
+
helper.accessor('maxContentLength', builder.number({ label: 'Length', onUpdate: handleChangeOnRow })),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<>
|
|
90
|
+
<DensityProvider density={'fine'}>
|
|
91
|
+
<Table.Root>
|
|
92
|
+
<Table.Viewport>
|
|
93
|
+
<Table.Main<CreateObjectsParams> columns={columns} data={objectsToCreate} />
|
|
94
|
+
</Table.Viewport>
|
|
95
|
+
</Table.Root>
|
|
96
|
+
</DensityProvider>
|
|
97
|
+
<Button onClick={handleCreate}>Create</Button>
|
|
98
|
+
</>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import React, { type FC, type HTMLAttributes, useState } from 'react';
|
|
6
|
+
import SyntaxHighlighter from 'react-syntax-highlighter';
|
|
7
|
+
// eslint-disable-next-line no-restricted-imports
|
|
8
|
+
import styleDark from 'react-syntax-highlighter/dist/esm/styles/hljs/a11y-dark';
|
|
9
|
+
// eslint-disable-next-line no-restricted-imports
|
|
10
|
+
import styleLight from 'react-syntax-highlighter/dist/esm/styles/hljs/a11y-light';
|
|
11
|
+
|
|
12
|
+
import { type ThemeMode } from '@dxos/react-ui';
|
|
13
|
+
import { mx } from '@dxos/react-ui-theme';
|
|
14
|
+
import { arrayToBuffer } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
// TODO(burdon): Copied form devtools.
|
|
17
|
+
|
|
18
|
+
export const replacer = (key: any, value: any) => {
|
|
19
|
+
if (typeof value === 'object') {
|
|
20
|
+
if (value instanceof Uint8Array) {
|
|
21
|
+
return arrayToBuffer(value).toString('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (value?.type === 'Buffer') {
|
|
25
|
+
return Buffer.from(value.data).toString('hex');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (key === 'downloaded') {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Json: FC<{ data?: object; theme: ThemeMode }> = ({ data, theme }) => {
|
|
37
|
+
const style = theme === 'dark' ? styleDark : styleLight;
|
|
38
|
+
return (
|
|
39
|
+
<SyntaxHighlighter language='json' style={style} className='w-full'>
|
|
40
|
+
{JSON.stringify(data, replacer, 2)}
|
|
41
|
+
</SyntaxHighlighter>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const Tree: FC<{ data?: object }> = ({ data }) => {
|
|
46
|
+
return (
|
|
47
|
+
<div className='flex overflow-auto ml-2 border-l-2 border-blue-500'>
|
|
48
|
+
<Node data={data} root />
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Node: FC<{ data?: any; root?: boolean }> = ({ data, root }) => {
|
|
54
|
+
if (typeof data !== 'object' || data === undefined || data === null) {
|
|
55
|
+
return <Scalar value={data} />;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Array.isArray(data)) {
|
|
59
|
+
return (
|
|
60
|
+
<div className='flex flex-col space-y-2'>
|
|
61
|
+
{data.map((value, index) => (
|
|
62
|
+
<KeyValue key={index} label={String(index)} data={value} className='bg-teal-50' />
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className='flex flex-col space-y-2'>
|
|
70
|
+
{Object.entries(data).map(([key, value]) => (
|
|
71
|
+
<KeyValue key={key} label={key} data={value} className='bg-blue-50' />
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const KeyValue: FC<{ label: string; data?: any; className?: string }> = ({ label, data, className }) => {
|
|
78
|
+
const [open, setOpen] = useState(true);
|
|
79
|
+
if (data === undefined) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className='flex'>
|
|
85
|
+
<Box
|
|
86
|
+
className={mx('border-blue-200 text-sm select-none cursor-pointer', className)}
|
|
87
|
+
onClick={() => setOpen((open) => !open)}
|
|
88
|
+
>
|
|
89
|
+
{label}
|
|
90
|
+
</Box>
|
|
91
|
+
{open && <Node data={data} />}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const Scalar: FC<{ value: any }> = ({ value }) => {
|
|
97
|
+
return (
|
|
98
|
+
<Box className='bg-green-50 border-green-200 rounded-r text-sm font-thin'>
|
|
99
|
+
{(value === undefined && 'undefined') ||
|
|
100
|
+
(value === null && 'null') ||
|
|
101
|
+
(typeof value === 'string' && value) ||
|
|
102
|
+
JSON.stringify(value)}
|
|
103
|
+
</Box>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const Box: FC<HTMLAttributes<HTMLDivElement>> = ({ children, className, ...props }) => {
|
|
108
|
+
return (
|
|
109
|
+
<div className={mx('flex px-2 border border-l-0 font-mono truncate', className)} {...props}>
|
|
110
|
+
{children}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
};
|