Skip to content

Commit 51bbb76

Browse files
Improve Functionality of Remote Execution Device Dialog (#2217)
* Switch 'secret' field to use InputGroup element Uses the alternative implementation of creating input groups as specified by docs: https://blueprintjs.com/docs/#core/components/text-inputs.input-group * Add react-qr-reader dependency * Add ability to paste secret from QR code * Add support for prefilled remote execution device dialog using URL params * Add Remote Execution tab to mobile workspace * Refactor how tabs are shown in Playground * Backpropagate device dialog state update Fixes bug where dialog would keep popping up when the workspace switches between mobile and desktop. * Fix mobile UI styling Co-authored-by: Martin Henz <[email protected]>
1 parent e8fe5dc commit 51bbb76

File tree

5 files changed

+154
-38
lines changed

5 files changed

+154
-38
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@
6363
"react-latex-next": "^2.1.0",
6464
"react-mde": "^11.5.0",
6565
"react-papaparse": "^4.0.2",
66+
"react-qr-reader": "^3.0.0-beta-1",
6667
"react-redux": "^8.0.2",
6768
"react-responsive": "^9.0.0-beta.10",
6869
"react-router-dom": "^5.3.0",

src/commons/sideContent/SideContentRemoteExecution.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
Spinner
1010
} from '@blueprintjs/core';
1111
import classNames from 'classnames';
12-
import React from 'react';
12+
import React, { SetStateAction } from 'react';
1313
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
1414
import { NavLink } from 'react-router-dom';
1515
import { Dispatch } from 'redux';
@@ -25,6 +25,8 @@ import { WorkspaceLocation } from '../workspace/WorkspaceTypes';
2525

2626
export interface SideContentRemoteExecutionProps {
2727
workspace: WorkspaceLocation;
28+
secretParams?: string;
29+
callbackFunction?: React.Dispatch<SetStateAction<string | undefined>>;
2830
}
2931

3032
interface DeviceMenuItemButtonsProps {
@@ -116,7 +118,10 @@ const DeviceContent = ({ session }: { session?: DeviceSession }) => {
116118
};
117119

118120
const SideContentRemoteExecution: React.FC<SideContentRemoteExecutionProps> = props => {
119-
const [dialogState, setDialogState] = React.useState<Device | true | undefined>(undefined);
121+
const [dialogState, setDialogState] = React.useState<Device | true | undefined>(
122+
props.secretParams ? true : undefined
123+
);
124+
const [secretParams, setSecretParams] = React.useState(props.secretParams);
120125

121126
const [isLoggedIn, devices, currentSession]: [
122127
boolean,
@@ -212,7 +217,14 @@ const SideContentRemoteExecution: React.FC<SideContentRemoteExecutionProps> = pr
212217
<RemoteExecutionAddDeviceDialog
213218
isOpen={!!dialogState}
214219
deviceToEdit={typeof dialogState === 'object' ? dialogState : undefined}
215-
onClose={() => setDialogState(undefined)}
220+
defaultSecret={dialogState === true ? secretParams : undefined}
221+
onClose={() => {
222+
setDialogState(undefined);
223+
setSecretParams(undefined);
224+
if (props.callbackFunction) {
225+
props.callbackFunction(undefined);
226+
}
227+
}}
216228
/>
217229
</div>
218230
);

src/features/remoteExecution/RemoteExecutionDeviceDialog.tsx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
1-
import { Button, Callout, Classes, Dialog, FormGroup, HTMLSelect } from '@blueprintjs/core';
1+
import {
2+
Button,
3+
Callout,
4+
Classes,
5+
Dialog,
6+
FormGroup,
7+
HTMLSelect,
8+
InputGroup
9+
} from '@blueprintjs/core';
10+
import { Tooltip2 } from '@blueprintjs/popover2';
211
import classNames from 'classnames';
312
import React from 'react';
13+
import { QrReader } from 'react-qr-reader';
414
import { useDispatch } from 'react-redux';
515

616
import { editDevice, registerDevice } from '../../commons/sagas/RequestsSaga';
@@ -17,12 +27,14 @@ export interface RemoteExecutionDeviceDialogProps {
1727
isOpen: boolean;
1828
onClose: () => void;
1929
deviceToEdit?: Device;
30+
defaultSecret?: string;
2031
}
2132

2233
export default function RemoteExecutionDeviceDialog({
2334
isOpen,
2435
onClose,
25-
deviceToEdit
36+
deviceToEdit,
37+
defaultSecret
2638
}: RemoteExecutionDeviceDialogProps) {
2739
const dispatch = useDispatch();
2840
const nameField = useField<HTMLInputElement>(validateNotEmpty);
@@ -31,6 +43,7 @@ export default function RemoteExecutionDeviceDialog({
3143

3244
const [isSubmitting, setIsSubmitting] = React.useState(false);
3345
const [errorMessage, setErrorMessage] = React.useState<string | undefined>();
46+
const [showScanner, setShowScanner] = React.useState(false);
3447

3548
const onSubmit = async () => {
3649
const fields = collectFieldValues(nameField, typeField, secretField);
@@ -59,6 +72,12 @@ export default function RemoteExecutionDeviceDialog({
5972
setIsSubmitting(false);
6073
};
6174

75+
const scanButton = (
76+
<Tooltip2 content="Scan QR Code">
77+
<Button minimal icon="clip" onClick={() => setShowScanner(() => !showScanner)} />
78+
</Tooltip2>
79+
);
80+
6281
return (
6382
<Dialog
6483
icon={deviceToEdit ? 'edit' : 'add'}
@@ -113,22 +132,40 @@ export default function RemoteExecutionDeviceDialog({
113132
</FormGroup>
114133

115134
<FormGroup label="Secret" labelFor="sa-remote-execution-secret">
116-
<input
135+
<InputGroup
117136
id="sa-remote-execution-secret"
118-
className={classNames(
119-
Classes.INPUT,
120-
Classes.FILL,
121-
secretField.isValid || Classes.INTENT_DANGER
122-
)}
137+
className={classNames(Classes.FILL, secretField.isValid || Classes.INTENT_DANGER)}
123138
type="text"
124-
ref={secretField.ref}
139+
inputRef={secretField.ref}
125140
onChange={secretField.onChange}
126141
disabled={isSubmitting}
127142
readOnly={!!deviceToEdit}
128-
{...(deviceToEdit ? { value: deviceToEdit.secret } : undefined)}
143+
defaultValue={defaultSecret}
144+
{...(deviceToEdit ? { value: deviceToEdit.secret } : { rightElement: scanButton })}
129145
/>
130146
</FormGroup>
131147

148+
{showScanner && (
149+
<QrReader
150+
onResult={(result, err) => {
151+
if (result) {
152+
setShowScanner(false);
153+
const element = secretField.ref.current;
154+
if (element) {
155+
element.value = result.getText();
156+
}
157+
}
158+
}}
159+
constraints={{
160+
aspectRatio: 1,
161+
frameRate: { ideal: 12 },
162+
deviceId: { ideal: '0' }
163+
}}
164+
containerStyle={{ width: '50%', marginInline: 'auto' }}
165+
videoStyle={{ borderRadius: '0.3em' }}
166+
/>
167+
)}
168+
132169
{errorMessage && <Callout intent="danger">{errorMessage}</Callout>}
133170
</div>
134171
<div className={Classes.DIALOG_FOOTER}>

src/pages/playground/Playground.tsx

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as React from 'react';
1111
import { HotKeys } from 'react-hotkeys';
1212
import { useSelector } from 'react-redux';
1313
import { useMediaQuery } from 'react-responsive';
14-
import { RouteComponentProps } from 'react-router';
14+
import { RouteComponentProps, useHistory, useLocation } from 'react-router';
1515
import { showFullJSWarningOnUrlLoad } from 'src/commons/fullJS/FullJSUtils';
1616

1717
import {
@@ -170,9 +170,25 @@ const Playground: React.FC<PlaygroundProps> = props => {
170170
const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
171171
const propsRef = React.useRef(props);
172172
propsRef.current = props;
173+
174+
const [deviceSecret, setDeviceSecret] = React.useState<string | undefined>();
175+
const location = useLocation();
176+
const history = useHistory();
177+
const searchParams = new URLSearchParams(location.search);
178+
const shouldAddDevice = searchParams.get('add_device');
179+
180+
// Hide search query from URL to maintain an illusion of security. The device secret
181+
// is still exposed via the 'Referer' header when requesting external content (e.g. Google API fonts)
182+
if (shouldAddDevice && !deviceSecret) {
183+
setDeviceSecret(shouldAddDevice);
184+
history.replace(location.pathname);
185+
}
186+
173187
const [lastEdit, setLastEdit] = React.useState(new Date());
174188
const [isGreen, setIsGreen] = React.useState(false);
175-
const [selectedTab, setSelectedTab] = React.useState(SideContentType.introduction);
189+
const [selectedTab, setSelectedTab] = React.useState(
190+
shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction
191+
);
176192
const [hasBreakpoints, setHasBreakpoints] = React.useState(false);
177193
const [sessionId, setSessionId] = React.useState(() =>
178194
initSession('playground', {
@@ -181,6 +197,23 @@ const Playground: React.FC<PlaygroundProps> = props => {
181197
})
182198
);
183199

200+
const remoteExecutionTab: SideContentTab = React.useMemo(
201+
() => ({
202+
label: 'Remote Execution',
203+
iconName: IconNames.SATELLITE,
204+
body: (
205+
<SideContentRemoteExecution
206+
workspace="playground"
207+
secretParams={deviceSecret || undefined}
208+
callbackFunction={setDeviceSecret}
209+
/>
210+
),
211+
id: SideContentType.remoteExecution,
212+
toSpawn: () => true
213+
}),
214+
[deviceSecret]
215+
);
216+
184217
const usingRemoteExecution =
185218
useSelector((state: OverallState) => !!state.session.remoteExecutionSession) && !isSicpEditor;
186219
// this is still used by remote execution (EV3)
@@ -216,17 +249,9 @@ const Playground: React.FC<PlaygroundProps> = props => {
216249
* Handles toggling of relevant SideContentTabs when mobile breakpoint it hit
217250
*/
218251
React.useEffect(() => {
219-
if (
220-
isMobileBreakpoint &&
221-
(selectedTab === SideContentType.introduction ||
222-
selectedTab === SideContentType.remoteExecution)
223-
) {
252+
if (isMobileBreakpoint && desktopOnlyTabIds.includes(selectedTab)) {
224253
setSelectedTab(SideContentType.mobileEditor);
225-
} else if (
226-
!isMobileBreakpoint &&
227-
(selectedTab === SideContentType.mobileEditor ||
228-
selectedTab === SideContentType.mobileEditorRun)
229-
) {
254+
} else if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) {
230255
setSelectedTab(SideContentType.introduction);
231256
}
232257
}, [isMobileBreakpoint, selectedTab]);
@@ -588,13 +613,12 @@ const Playground: React.FC<PlaygroundProps> = props => {
588613
props.output,
589614
props.playgroundSourceChapter,
590615
props.playgroundSourceVariant,
591-
usingRemoteExecution
616+
usingRemoteExecution,
617+
remoteExecutionTab
592618
]);
593619

594620
// Remove Intro and Remote Execution tabs for mobile
595-
const mobileTabs = [...tabs].filter(
596-
x => x !== playgroundIntroductionTab && x !== remoteExecutionTab
597-
);
621+
const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id)));
598622

599623
const onLoadMethod = React.useCallback(
600624
(editor: Ace.Editor) => {
@@ -781,7 +805,9 @@ const Playground: React.FC<PlaygroundProps> = props => {
781805
};
782806

783807
return isMobileBreakpoint ? (
784-
<MobileWorkspace {...mobileWorkspaceProps} />
808+
<div className={classNames('Playground', Classes.DARK, isGreen ? 'GreenScreen' : undefined)}>
809+
<MobileWorkspace {...mobileWorkspaceProps} />
810+
</div>
785811
) : (
786812
<HotKeys
787813
className={classNames('Playground', Classes.DARK, isGreen ? 'GreenScreen' : undefined)}
@@ -793,6 +819,12 @@ const Playground: React.FC<PlaygroundProps> = props => {
793819
);
794820
};
795821

822+
const mobileOnlyTabIds: readonly SideContentType[] = [
823+
SideContentType.mobileEditor,
824+
SideContentType.mobileEditorRun
825+
];
826+
const desktopOnlyTabIds: readonly SideContentType[] = [SideContentType.introduction];
827+
796828
const dataVisualizerTab: SideContentTab = {
797829
label: 'Data Visualizer',
798830
iconName: IconNames.EYE_OPEN,
@@ -809,12 +841,4 @@ const envVisualizerTab: SideContentTab = {
809841
toSpawn: () => true
810842
};
811843

812-
const remoteExecutionTab: SideContentTab = {
813-
label: 'Remote Execution',
814-
iconName: IconNames.SATELLITE,
815-
body: <SideContentRemoteExecution workspace="playground" />,
816-
id: SideContentType.remoteExecution,
817-
toSpawn: () => true
818-
};
819-
820844
export default Playground;

yarn.lock

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2756,6 +2756,27 @@
27562756
resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
27572757
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
27582758

2759+
2760+
version "0.0.7"
2761+
resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7"
2762+
integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==
2763+
optionalDependencies:
2764+
"@zxing/text-encoding" "^0.9.0"
2765+
2766+
"@zxing/library@^0.18.3":
2767+
version "0.18.6"
2768+
resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c"
2769+
integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==
2770+
dependencies:
2771+
ts-custom-error "^3.0.0"
2772+
optionalDependencies:
2773+
"@zxing/text-encoding" "~0.9.0"
2774+
2775+
"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0":
2776+
version "0.9.0"
2777+
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
2778+
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==
2779+
27592780
abab@^2.0.3, abab@^2.0.5:
27602781
version "2.0.5"
27612782
resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz"
@@ -11299,6 +11320,15 @@ react-popper@^2.2.4:
1129911320
react-fast-compare "^3.0.1"
1130011321
warning "^4.0.2"
1130111322

11323+
react-qr-reader@^3.0.0-beta-1:
11324+
version "3.0.0-beta-1"
11325+
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68"
11326+
integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==
11327+
dependencies:
11328+
"@zxing/browser" "0.0.7"
11329+
"@zxing/library" "^0.18.3"
11330+
rollup "^2.67.2"
11331+
1130211332
react-reconciler@~0.26.2:
1130311333
version "0.26.2"
1130411334
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.26.2.tgz"
@@ -12000,6 +12030,13 @@ rollup@^1.31.1:
1200012030
"@types/node" "*"
1200112031
acorn "^7.1.0"
1200212032

12033+
rollup@^2.67.2:
12034+
version "2.78.0"
12035+
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"
12036+
integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==
12037+
optionalDependencies:
12038+
fsevents "~2.3.2"
12039+
1200312040
rst-selector-parser@^2.2.3:
1200412041
version "2.2.3"
1200512042
resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz"
@@ -13276,6 +13313,11 @@ tryer@^1.0.1:
1327613313
resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz"
1327713314
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==
1327813315

13316+
ts-custom-error@^3.0.0:
13317+
version "3.2.0"
13318+
resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.2.0.tgz#ff8f80a3812bab9dc448536312da52dce1b720fb"
13319+
integrity sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==
13320+
1327913321
ts-node@^10.4.0:
1328013322
version "10.4.0"
1328113323
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz"

0 commit comments

Comments
 (0)