@100mslive/roomkit-react 0.3.13 → 0.3.14-alpha.1

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 (42) hide show
  1. package/dist/Diagnostics/AudioTest.d.ts +2 -0
  2. package/dist/Diagnostics/ConnectivityTest.d.ts +7 -0
  3. package/dist/Diagnostics/Diagnostics.d.ts +2 -0
  4. package/dist/Diagnostics/VideoTest.d.ts +2 -0
  5. package/dist/Diagnostics/components.d.ts +16 -0
  6. package/dist/Diagnostics/hms.d.ts +9 -0
  7. package/dist/Diagnostics/index.d.ts +1 -0
  8. package/dist/{HLSView-MSHPSUUJ.css → HLSView-772PCEEZ.css} +3 -3
  9. package/dist/{HLSView-MSHPSUUJ.css.map → HLSView-772PCEEZ.css.map} +1 -1
  10. package/dist/{HLSView-U4CRLAPX.js → HLSView-VSU7IPCJ.js} +2 -2
  11. package/dist/Prebuilt/App.d.ts +3 -1
  12. package/dist/Stats/index.d.ts +1 -0
  13. package/dist/{chunk-XIFK5DB3.js → chunk-VE34B77C.js} +12337 -1498
  14. package/dist/chunk-VE34B77C.js.map +7 -0
  15. package/dist/index.cjs.css +2 -2
  16. package/dist/index.cjs.css.map +1 -1
  17. package/dist/index.cjs.js +14570 -3643
  18. package/dist/index.cjs.js.map +4 -4
  19. package/dist/index.css +2 -2
  20. package/dist/index.css.map +1 -1
  21. package/dist/index.d.ts +1 -0
  22. package/dist/index.js +5 -1
  23. package/dist/meta.cjs.json +904 -213
  24. package/dist/meta.esbuild.json +916 -221
  25. package/package.json +7 -7
  26. package/src/Diagnostics/AudioTest.tsx +153 -0
  27. package/src/Diagnostics/ConnectivityTest.tsx +355 -0
  28. package/src/Diagnostics/DeviceSelector.jsx +71 -0
  29. package/src/Diagnostics/Diagnostics.tsx +112 -0
  30. package/src/Diagnostics/VideoTest.tsx +60 -0
  31. package/src/Diagnostics/components.tsx +84 -0
  32. package/src/Diagnostics/hms.ts +9 -0
  33. package/src/Diagnostics/index.ts +1 -0
  34. package/src/Prebuilt/App.tsx +12 -1
  35. package/src/Prebuilt/components/Chat/ChatFooter.tsx +16 -2
  36. package/src/Prebuilt/components/MoreSettings/SplitComponents/MwebOptions.tsx +2 -2
  37. package/src/Prebuilt/components/StatsForNerds.jsx +1 -13
  38. package/src/Prebuilt/components/VideoLayouts/GridLayout.tsx +0 -17
  39. package/src/Stats/index.tsx +1 -0
  40. package/src/index.ts +1 -0
  41. package/dist/chunk-XIFK5DB3.js.map +0 -7
  42. /package/dist/{HLSView-U4CRLAPX.js.map → HLSView-VSU7IPCJ.js.map} +0 -0
package/package.json CHANGED
@@ -10,7 +10,7 @@
10
10
  "prebuilt",
11
11
  "roomkit"
12
12
  ],
13
- "version": "0.3.13",
13
+ "version": "0.3.14-alpha.1",
14
14
  "author": "100ms",
15
15
  "license": "MIT",
16
16
  "repository": {
@@ -74,12 +74,12 @@
74
74
  "react": ">=17.0.2 <19.0.0"
75
75
  },
76
76
  "dependencies": {
77
- "@100mslive/hls-player": "0.3.13",
77
+ "@100mslive/hls-player": "0.3.14-alpha.1",
78
78
  "@100mslive/hms-noise-cancellation": "0.0.1",
79
- "@100mslive/hms-virtual-background": "1.13.13",
80
- "@100mslive/hms-whiteboard": "0.0.3",
81
- "@100mslive/react-icons": "0.10.13",
82
- "@100mslive/react-sdk": "0.10.13",
79
+ "@100mslive/hms-virtual-background": "1.13.14-alpha.1",
80
+ "@100mslive/hms-whiteboard": "0.0.4-alpha.1",
81
+ "@100mslive/react-icons": "0.10.14-alpha.1",
82
+ "@100mslive/react-sdk": "0.10.14-alpha.1",
83
83
  "@100mslive/types-prebuilt": "0.12.9",
84
84
  "@emoji-mart/data": "^1.0.6",
85
85
  "@emoji-mart/react": "^1.0.1",
@@ -115,5 +115,5 @@
115
115
  "uuid": "^8.3.2",
116
116
  "worker-timers": "^7.0.40"
117
117
  },
118
- "gitHead": "802d2f81f4920926428d58c1466bc03e32ce7c10"
118
+ "gitHead": "92efc31642f74553fa513d53112e9e680c88bc8b"
119
119
  }
@@ -0,0 +1,153 @@
1
+ /* eslint-disable react/prop-types */
2
+ import React, { useEffect, useState } from 'react';
3
+ import {
4
+ selectDevices,
5
+ selectLocalAudioTrackID,
6
+ selectLocalMediaSettings,
7
+ selectTrackAudioByID,
8
+ useHMSActions,
9
+ useHMSStore,
10
+ } from '@100mslive/react-sdk';
11
+ import { MicOnIcon, SpeakerIcon } from '@100mslive/react-icons';
12
+ import { TestContainer, TestFooter } from './components';
13
+ import { Button } from '../Button';
14
+ import { Box, Flex } from '../Layout';
15
+ import { Progress } from '../Progress';
16
+ import { Text } from '../Text';
17
+ // @ts-ignore: No implicit any
18
+ import { DeviceSelector } from './DeviceSelector';
19
+ import { hmsDiagnostics } from './hms';
20
+ import { useAudioOutputTest } from '../Prebuilt/components/hooks/useAudioOutputTest';
21
+ import { TEST_AUDIO_URL } from '../Prebuilt/common/constants';
22
+
23
+ const SelectContainer = ({ children }: { children: React.ReactNode }) => (
24
+ <Box css={{ w: '50%', '@lg': { w: '100%' } }}>{children}</Box>
25
+ );
26
+
27
+ const MicTest = () => {
28
+ const devices = useHMSStore(selectDevices);
29
+ const [isRecording, setIsRecording] = useState(false);
30
+ const { audioInputDeviceId } = useHMSStore(selectLocalMediaSettings);
31
+ const [selectedMic, setSelectedMic] = useState(audioInputDeviceId || 'default');
32
+ const trackID = useHMSStore(selectLocalAudioTrackID);
33
+ const audioLevel = useHMSStore(selectTrackAudioByID(trackID));
34
+
35
+ return (
36
+ <SelectContainer>
37
+ <DeviceSelector
38
+ title="Microphone(Input)"
39
+ devices={devices.audioInput}
40
+ selection={selectedMic}
41
+ icon={<MicOnIcon />}
42
+ onChange={(deviceId: string) => {
43
+ setSelectedMic(deviceId);
44
+ hmsDiagnostics.stopMicCheck();
45
+ setIsRecording(false);
46
+ }}
47
+ />
48
+ <Flex css={{ gap: '$6', alignItems: 'center' }}>
49
+ <Button
50
+ onClick={() =>
51
+ hmsDiagnostics
52
+ .startMicCheck(selectedMic, () => {
53
+ setIsRecording(false);
54
+ })
55
+ .then(() => {
56
+ setIsRecording(true);
57
+ })
58
+ }
59
+ disabled={isRecording}
60
+ >
61
+ {isRecording ? 'Recording...' : 'Record'}
62
+ </Button>
63
+ {isRecording && (
64
+ <>
65
+ <Text>
66
+ <MicOnIcon />
67
+ </Text>
68
+ <Progress.Root value={audioLevel} css={{ h: '$2' }}>
69
+ <Progress.Content
70
+ style={{
71
+ transform: `translateX(-${100 - audioLevel}%)`,
72
+ transition: 'transform 0.3s',
73
+ }}
74
+ />
75
+ </Progress.Root>
76
+ </>
77
+ )}
78
+ </Flex>
79
+ </SelectContainer>
80
+ );
81
+ };
82
+
83
+ const SpeakerTest = () => {
84
+ const actions = useHMSActions();
85
+ const devices = useHMSStore(selectDevices);
86
+ const { audioOutputDeviceId } = useHMSStore(selectLocalMediaSettings);
87
+ const { playing, setPlaying, audioRef } = useAudioOutputTest({ deviceId: audioOutputDeviceId || 'default' });
88
+
89
+ return (
90
+ <SelectContainer>
91
+ <DeviceSelector
92
+ title="Speaker(output)"
93
+ devices={devices.audioOutput}
94
+ selection={audioOutputDeviceId || 'default'}
95
+ icon={<SpeakerIcon />}
96
+ onChange={(deviceId: string) => {
97
+ actions.setAudioOutputDevice(deviceId);
98
+ }}
99
+ />
100
+ <Button
101
+ onClick={() => {
102
+ if (audioRef.current) {
103
+ audioRef.current.src = hmsDiagnostics.getRecordedAudio() || TEST_AUDIO_URL;
104
+ audioRef.current.play();
105
+ }
106
+ }}
107
+ disabled={playing}
108
+ >
109
+ <SpeakerIcon />
110
+ <Text css={{ ml: '$4' }}>{playing ? 'Playing' : 'Playback'}</Text>
111
+ </Button>
112
+ <audio
113
+ ref={audioRef}
114
+ onEnded={() => setPlaying(false)}
115
+ onPlay={() => setPlaying(true)}
116
+ style={{ display: 'none' }}
117
+ />
118
+ </SelectContainer>
119
+ );
120
+ };
121
+
122
+ export const AudioTest = () => {
123
+ const [error, setError] = useState<Error | undefined>();
124
+ useEffect(() => {
125
+ hmsDiagnostics.requestPermission({ audio: true }).catch(error => setError(error));
126
+ }, []);
127
+
128
+ return (
129
+ <>
130
+ <TestContainer>
131
+ <Text variant="body2" css={{ c: '$on_primary_medium' }}>
132
+ Record an audio clip and play it back to check that your microphone and speaker are working. If they aren't,
133
+ make sure your volume is turned up, try a different speaker or microphone, or check your bluetooth settings.
134
+ </Text>
135
+
136
+ <Flex
137
+ css={{
138
+ mt: '$10',
139
+ gap: '$10',
140
+ '@lg': {
141
+ flexDirection: 'column',
142
+ gap: '$8',
143
+ },
144
+ }}
145
+ >
146
+ {!error && <MicTest />}
147
+ <SpeakerTest />
148
+ </Flex>
149
+ </TestContainer>
150
+ <TestFooter error={error} ctaText="Does your audio sound good?" />
151
+ </>
152
+ );
153
+ };
@@ -0,0 +1,355 @@
1
+ import React, { useState } from 'react';
2
+ import { ConnectivityCheckResult, ConnectivityState, DiagnosticsRTCStats } from '@100mslive/react-sdk';
3
+ import { CheckCircleIcon, CrossCircleIcon, LinkIcon } from '@100mslive/react-icons';
4
+ import { TestContainer, TestFooter } from './components';
5
+ import { Button } from '../Button';
6
+ import { Box, Flex } from '../Layout';
7
+ import { Loading } from '../Loading';
8
+ import { formatBytes } from '../Stats';
9
+ import { Text } from '../Text';
10
+ import { hmsDiagnostics } from './hms';
11
+
12
+ const Regions = {
13
+ in: 'India',
14
+ eu: 'Europe',
15
+ us: 'United States',
16
+ };
17
+
18
+ const ConnectivityStateMessage = {
19
+ [ConnectivityState.STARTING]: 'Fetching Init',
20
+ [ConnectivityState.INIT_FETCHED]: 'Connecting to signal server',
21
+ [ConnectivityState.SIGNAL_CONNECTED]: 'Establishing ICE connection',
22
+ [ConnectivityState.ICE_ESTABLISHED]: 'Capturing Media',
23
+ [ConnectivityState.MEDIA_CAPTURED]: 'Publishing Media',
24
+ [ConnectivityState.MEDIA_PUBLISHED]: 'Finishing Up',
25
+ [ConnectivityState.COMPLETED]: 'Completed',
26
+ };
27
+
28
+ export const ConnectivityTestStepResult = ({
29
+ title,
30
+ success,
31
+ children,
32
+ }: {
33
+ title: string;
34
+ success?: boolean;
35
+ children: React.ReactNode;
36
+ }) => {
37
+ return (
38
+ <Box css={{ my: '$10', p: '$10', r: '$1', bg: '$surface_bright' }}>
39
+ <Text css={{ c: '$on_primary_medium', mb: '$6' }}>{title}</Text>
40
+ {success ? (
41
+ <Flex>
42
+ <Text css={{ c: '$alert_success' }}>
43
+ <CheckCircleIcon width="1.5rem" height="1.5rem" />
44
+ </Text>
45
+ <Text variant="lg" css={{ ml: '$4' }}>
46
+ Connected
47
+ </Text>
48
+ </Flex>
49
+ ) : (
50
+ <Flex>
51
+ <Text css={{ c: '$alert_error_bright' }}>
52
+ <CrossCircleIcon width="1.5rem" height="1.5rem" />
53
+ </Text>
54
+ <Text variant="lg" css={{ ml: '$4' }}>
55
+ Failed
56
+ </Text>
57
+ </Flex>
58
+ )}
59
+ <Box>{children}</Box>
60
+ </Box>
61
+ );
62
+ };
63
+
64
+ const DetailedInfo = ({
65
+ title,
66
+ value,
67
+ Icon,
68
+ }: {
69
+ title: string;
70
+ value: string;
71
+ Icon?: (props: React.SVGProps<SVGSVGElement>) => React.JSX.Element;
72
+ }) => {
73
+ return (
74
+ <Box css={{ flex: '50%', mt: '$6' }}>
75
+ <Text variant="caption" css={{ fontWeight: '$semiBold', c: '$on_primary_medium' }}>
76
+ {title}
77
+ </Text>
78
+ <Flex css={{ mt: '$xs', alignItems: 'flex-start' }}>
79
+ {Icon && (
80
+ <Text css={{ mr: '$4' }}>
81
+ <Icon width="1rem" height="1rem" />
82
+ </Text>
83
+ )}
84
+ <Text variant="caption">{value}</Text>
85
+ </Flex>
86
+ </Box>
87
+ );
88
+ };
89
+
90
+ const MediaServerResult = ({ result }: { result?: ConnectivityCheckResult['mediaServerReport'] }) => {
91
+ return (
92
+ <ConnectivityTestStepResult
93
+ title="Media server connection test"
94
+ success={result?.isPublishICEConnected && result.isSubscribeICEConnected}
95
+ >
96
+ <Flex css={{ flexWrap: 'wrap' }}>
97
+ <DetailedInfo
98
+ title="Media Captured"
99
+ value={result?.stats?.audio.bytesSent ? 'Yes' : 'No'}
100
+ Icon={result?.stats?.audio.bytesSent ? CheckCircleIcon : CrossCircleIcon}
101
+ />
102
+ <DetailedInfo
103
+ title="Media Published"
104
+ value={result?.stats?.audio.bitrateSent ? 'Yes' : 'No'}
105
+ Icon={result?.stats?.audio.bytesSent ? CheckCircleIcon : CrossCircleIcon}
106
+ />
107
+ {result?.connectionQualityScore ? (
108
+ <DetailedInfo
109
+ title="Connection Quality Score (CQS)"
110
+ value={`${result.connectionQualityScore.toFixed(2)} (out of 5)`}
111
+ />
112
+ ) : null}
113
+ </Flex>
114
+ </ConnectivityTestStepResult>
115
+ );
116
+ };
117
+
118
+ const SignallingResult = ({ result }: { result?: ConnectivityCheckResult['signallingReport'] }) => {
119
+ return (
120
+ <ConnectivityTestStepResult title="Signalling server connection test" success={result?.isConnected}>
121
+ <Flex css={{ flexWrap: 'wrap' }}>
122
+ <DetailedInfo
123
+ title="Signalling Gateway"
124
+ value={result?.isConnected ? 'Reachable' : 'Unreachable'}
125
+ Icon={result?.isConnected ? CheckCircleIcon : CrossCircleIcon}
126
+ />
127
+ <DetailedInfo title="Websocket URL" value={result?.websocketUrl || 'N/A'} Icon={LinkIcon} />
128
+ </Flex>
129
+ </ConnectivityTestStepResult>
130
+ );
131
+ };
132
+
133
+ const AudioStats = ({ stats }: { stats: DiagnosticsRTCStats | undefined }) => {
134
+ return (
135
+ <ConnectivityTestStepResult title="Audio" success={!!stats?.bytesSent}>
136
+ {stats && (
137
+ <Flex css={{ flexWrap: 'wrap' }}>
138
+ <DetailedInfo title="Bytes Sent" value={formatBytes(stats.bytesSent)} />
139
+ <DetailedInfo title="Bytes Received" value={formatBytes(stats.bytesReceived)} />
140
+ <DetailedInfo title="Packets Received" value={stats.packetsReceived.toString()} />
141
+ <DetailedInfo title="Packets Lost" value={stats.packetsLost.toString()} />
142
+ <DetailedInfo title="Bitrate Sent" value={formatBytes(stats.bitrateSent, 'b/s')} />
143
+ <DetailedInfo title="Bitrate Received" value={formatBytes(stats.bitrateReceived, 'b/s')} />
144
+ <DetailedInfo title="Round Trip Time" value={`${stats.roundTripTime} ms`} />
145
+ </Flex>
146
+ )}
147
+ </ConnectivityTestStepResult>
148
+ );
149
+ };
150
+
151
+ const VideoStats = ({ stats }: { stats: DiagnosticsRTCStats | undefined }) => {
152
+ return (
153
+ <ConnectivityTestStepResult title="Video" success={!!stats?.bytesSent}>
154
+ {stats && (
155
+ <Flex css={{ flexWrap: 'wrap' }}>
156
+ <DetailedInfo title="Bytes Sent" value={formatBytes(stats.bytesSent)} />
157
+ <DetailedInfo title="Bytes Received" value={formatBytes(stats.bytesReceived)} />
158
+ <DetailedInfo title="Packets Received" value={stats.packetsReceived.toString()} />
159
+ <DetailedInfo title="Packets Lost" value={stats.packetsLost.toString()} />
160
+ <DetailedInfo title="Bitrate Sent" value={formatBytes(stats.bitrateSent, 'b/s')} />
161
+ <DetailedInfo title="Bitrate Received" value={formatBytes(stats.bitrateReceived, 'b/s')} />
162
+ <DetailedInfo title="Round Trip Time" value={`${stats.roundTripTime} ms`} />
163
+ </Flex>
164
+ )}
165
+ </ConnectivityTestStepResult>
166
+ );
167
+ };
168
+
169
+ const Footer = ({
170
+ error,
171
+ result,
172
+ restart,
173
+ }: {
174
+ result?: ConnectivityCheckResult;
175
+ restart: () => void;
176
+ error?: Error;
177
+ }) => {
178
+ return (
179
+ <TestFooter error={error}>
180
+ <Flex css={{ gap: '$8', '@lg': { flexDirection: 'column' } }}>
181
+ <Button variant="standard" onClick={restart}>
182
+ Restart Test
183
+ </Button>
184
+ <Button disabled={!result} onClick={() => result && downloadJson(result, 'hms_diagnostics_results')}>
185
+ Download Test Report
186
+ </Button>
187
+ </Flex>
188
+ </TestFooter>
189
+ );
190
+ };
191
+
192
+ const ConnectivityTestReport = ({
193
+ error,
194
+ result,
195
+ progress,
196
+ startTest,
197
+ }: {
198
+ error?: Error;
199
+ result?: ConnectivityCheckResult;
200
+ progress?: ConnectivityState;
201
+ startTest: () => void;
202
+ }) => {
203
+ if (error) {
204
+ return (
205
+ <>
206
+ <TestContainer css={{ textAlign: 'center' }}>
207
+ <Text css={{ c: '$alert_error_default', mb: '$4' }}>
208
+ <CrossCircleIcon />
209
+ </Text>
210
+ <Text variant="h6">Connectivity Test Failed</Text>
211
+ <Text variant="body2" css={{ c: '$on_primary_medium' }}>
212
+ {error.message}
213
+ </Text>
214
+ </TestContainer>
215
+ <Footer restart={startTest} error={error} />
216
+ </>
217
+ );
218
+ }
219
+
220
+ if (result) {
221
+ return (
222
+ <>
223
+ <TestContainer>
224
+ <Text css={{ c: '$on_primary_medium' }}>Connectivity test has been completed.</Text>
225
+ <SignallingResult result={result?.signallingReport} />
226
+ <MediaServerResult result={result?.mediaServerReport} />
227
+ <AudioStats stats={result?.mediaServerReport?.stats?.audio} />
228
+ <VideoStats stats={result?.mediaServerReport?.stats?.video} />
229
+ </TestContainer>
230
+ <Footer result={result} restart={startTest} error={error} />
231
+ </>
232
+ );
233
+ }
234
+
235
+ if (progress) {
236
+ return (
237
+ <TestContainer css={{ textAlign: 'center' }}>
238
+ <Text css={{ c: '$primary_bright' }}>
239
+ <Loading size="3.5rem" color="currentColor" />
240
+ </Text>
241
+ <Text variant="h6" css={{ mt: '$8' }}>
242
+ Checking your connection...
243
+ </Text>
244
+ <Text
245
+ variant="body2"
246
+ css={{ c: '$on_primary_medium', mt: '$4' }}
247
+ >{`${ConnectivityStateMessage[progress]}...`}</Text>
248
+ </TestContainer>
249
+ );
250
+ }
251
+
252
+ return null;
253
+ };
254
+
255
+ const RegionSelector = ({
256
+ region,
257
+ setRegion,
258
+ startTest,
259
+ }: {
260
+ region?: string;
261
+ startTest?: () => void;
262
+ setRegion: (region: string) => void;
263
+ }) => {
264
+ return (
265
+ <TestContainer css={{ borderBottom: '1px solid $border_default' }}>
266
+ <Text variant="body1">Select a region</Text>
267
+ <Text variant="body2" css={{ c: '$on_secondary_low' }}>
268
+ Select the closest region for best results
269
+ </Text>
270
+ <Flex
271
+ justify="between"
272
+ css={{
273
+ mt: '$md',
274
+ '@lg': {
275
+ flexDirection: 'column',
276
+ gap: '$8',
277
+ },
278
+ }}
279
+ >
280
+ <Flex
281
+ css={{
282
+ gap: '$4',
283
+ '@lg': {
284
+ flexDirection: 'column',
285
+ },
286
+ }}
287
+ >
288
+ {Object.entries(Regions).map(([key, value]) => (
289
+ <Button
290
+ key={key}
291
+ outlined={region !== key}
292
+ variant={region === key ? 'primary' : 'standard'}
293
+ css={region === key ? { bg: '$primary_dim' } : {}}
294
+ onClick={() => setRegion(key)}
295
+ >
296
+ {value}
297
+ </Button>
298
+ ))}
299
+ </Flex>
300
+ <Flex css={{ '@lg': { flexDirection: 'column' } }}>
301
+ <Button variant="primary" onClick={startTest} disabled={!startTest}>
302
+ {startTest ? 'Start Test' : 'Testing...'}
303
+ </Button>
304
+ </Flex>
305
+ </Flex>
306
+ </TestContainer>
307
+ );
308
+ };
309
+
310
+ export const ConnectivityTest = () => {
311
+ const [region, setRegion] = useState<string | undefined>(Object.keys(Regions)[0]);
312
+ const [error, setError] = useState<Error | undefined>();
313
+ const [progress, setProgress] = useState<ConnectivityState>();
314
+ const [result, setResult] = useState<ConnectivityCheckResult | undefined>();
315
+
316
+ const startTest = () => {
317
+ setError(undefined);
318
+ setProgress(ConnectivityState.STARTING);
319
+ setResult(undefined);
320
+ hmsDiagnostics
321
+ .startConnectivityCheck(
322
+ state => {
323
+ setProgress(state);
324
+ },
325
+ result => {
326
+ setResult(result);
327
+ },
328
+ region,
329
+ )
330
+ .catch(error => {
331
+ setError(error);
332
+ });
333
+ };
334
+
335
+ return (
336
+ <>
337
+ <RegionSelector
338
+ region={region}
339
+ setRegion={setRegion}
340
+ startTest={!progress || progress === ConnectivityState.COMPLETED ? startTest : undefined}
341
+ />
342
+ <ConnectivityTestReport error={error} result={result} progress={progress} startTest={startTest} />
343
+ </>
344
+ );
345
+ };
346
+
347
+ const downloadJson = (obj: object, fileName: string) => {
348
+ const a = document.createElement('a');
349
+ const file = new Blob([JSON.stringify(obj, null, 2)], {
350
+ type: 'application/json',
351
+ });
352
+ a.href = URL.createObjectURL(file);
353
+ a.download = `${fileName}.json`;
354
+ a.click();
355
+ };
@@ -0,0 +1,71 @@
1
+ import React, { useRef, useState } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { Dropdown } from '../Dropdown';
4
+ import { Box, Flex } from '../Layout';
5
+ import { DialogDropdownTrigger } from '../Prebuilt/primitives/DropdownTrigger';
6
+ import { Text } from '../Text';
7
+
8
+ export const DeviceSelector = ({ title, devices, selection, onChange, icon, children = null }) => {
9
+ const [open, setOpen] = useState(false);
10
+ const ref = useRef(null);
11
+ return (
12
+ <Box css={{ mb: '$6' }}>
13
+ <Text css={{ mb: '$4' }}>{title}</Text>
14
+ <Flex
15
+ align="center"
16
+ css={{
17
+ gap: '$4',
18
+ '@md': {
19
+ flexDirection: children ? 'column' : 'row',
20
+ alignItems: children ? 'start' : 'center',
21
+ },
22
+ }}
23
+ >
24
+ <Dropdown.Root open={open} onOpenChange={setOpen}>
25
+ <DialogDropdownTrigger
26
+ ref={ref}
27
+ icon={icon}
28
+ title={devices.find(({ deviceId }) => deviceId === selection)?.label || 'Select device from list'}
29
+ open={open}
30
+ />
31
+ <Dropdown.Portal>
32
+ <Dropdown.Content
33
+ align="start"
34
+ sideOffset={8}
35
+ css={{
36
+ w:
37
+ // @ts-ignore
38
+ ref.current?.clientWidth,
39
+ zIndex: 1001,
40
+ }}
41
+ >
42
+ {devices.map(device => {
43
+ return (
44
+ <Dropdown.Item
45
+ key={device.label}
46
+ onSelect={() => onChange(device.deviceId)}
47
+ css={{
48
+ px: '$9',
49
+ }}
50
+ >
51
+ {device.label}
52
+ </Dropdown.Item>
53
+ );
54
+ })}
55
+ </Dropdown.Content>
56
+ </Dropdown.Portal>
57
+ </Dropdown.Root>
58
+ {children}
59
+ </Flex>
60
+ </Box>
61
+ );
62
+ };
63
+
64
+ DeviceSelector.propTypes = {
65
+ title: PropTypes.string.isRequired,
66
+ devices: PropTypes.array.isRequired,
67
+ selection: PropTypes.string,
68
+ onChange: PropTypes.func.isRequired,
69
+ icon: PropTypes.node,
70
+ children: PropTypes.node,
71
+ };