Skip to content

Improve Functionality of Remote Execution Device Dialog #2217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"react-latex-next": "^2.1.0",
"react-mde": "^11.5.0",
"react-papaparse": "^4.0.2",
"react-qr-reader": "^3.0.0-beta-1",
"react-redux": "^8.0.2",
"react-responsive": "^9.0.0-beta.10",
"react-router-dom": "^5.3.0",
Expand Down
18 changes: 15 additions & 3 deletions src/commons/sideContent/SideContentRemoteExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
Spinner
} from '@blueprintjs/core';
import classNames from 'classnames';
import React from 'react';
import React, { SetStateAction } from 'react';
import { shallowEqual, useDispatch, useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { Dispatch } from 'redux';
Expand All @@ -25,6 +25,8 @@ import { WorkspaceLocation } from '../workspace/WorkspaceTypes';

export interface SideContentRemoteExecutionProps {
workspace: WorkspaceLocation;
secretParams?: string;
callbackFunction?: React.Dispatch<SetStateAction<string | undefined>>;
}

interface DeviceMenuItemButtonsProps {
Expand Down Expand Up @@ -116,7 +118,10 @@ const DeviceContent = ({ session }: { session?: DeviceSession }) => {
};

const SideContentRemoteExecution: React.FC<SideContentRemoteExecutionProps> = props => {
const [dialogState, setDialogState] = React.useState<Device | true | undefined>(undefined);
const [dialogState, setDialogState] = React.useState<Device | true | undefined>(
props.secretParams ? true : undefined
);
const [secretParams, setSecretParams] = React.useState(props.secretParams);

const [isLoggedIn, devices, currentSession]: [
boolean,
Expand Down Expand Up @@ -212,7 +217,14 @@ const SideContentRemoteExecution: React.FC<SideContentRemoteExecutionProps> = pr
<RemoteExecutionAddDeviceDialog
isOpen={!!dialogState}
deviceToEdit={typeof dialogState === 'object' ? dialogState : undefined}
onClose={() => setDialogState(undefined)}
defaultSecret={dialogState === true ? secretParams : undefined}
onClose={() => {
setDialogState(undefined);
setSecretParams(undefined);
if (props.callbackFunction) {
props.callbackFunction(undefined);
}
}}
/>
</div>
);
Expand Down
57 changes: 47 additions & 10 deletions src/features/remoteExecution/RemoteExecutionDeviceDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
import { Button, Callout, Classes, Dialog, FormGroup, HTMLSelect } from '@blueprintjs/core';
import {
Button,
Callout,
Classes,
Dialog,
FormGroup,
HTMLSelect,
InputGroup
} from '@blueprintjs/core';
import { Tooltip2 } from '@blueprintjs/popover2';
import classNames from 'classnames';
import React from 'react';
import { QrReader } from 'react-qr-reader';
import { useDispatch } from 'react-redux';

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

export default function RemoteExecutionDeviceDialog({
isOpen,
onClose,
deviceToEdit
deviceToEdit,
defaultSecret
}: RemoteExecutionDeviceDialogProps) {
const dispatch = useDispatch();
const nameField = useField<HTMLInputElement>(validateNotEmpty);
Expand All @@ -31,6 +43,7 @@ export default function RemoteExecutionDeviceDialog({

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

const onSubmit = async () => {
const fields = collectFieldValues(nameField, typeField, secretField);
Expand Down Expand Up @@ -59,6 +72,12 @@ export default function RemoteExecutionDeviceDialog({
setIsSubmitting(false);
};

const scanButton = (
<Tooltip2 content="Scan QR Code">
<Button minimal icon="clip" onClick={() => setShowScanner(() => !showScanner)} />
</Tooltip2>
);

return (
<Dialog
icon={deviceToEdit ? 'edit' : 'add'}
Expand Down Expand Up @@ -113,22 +132,40 @@ export default function RemoteExecutionDeviceDialog({
</FormGroup>

<FormGroup label="Secret" labelFor="sa-remote-execution-secret">
<input
<InputGroup
id="sa-remote-execution-secret"
className={classNames(
Classes.INPUT,
Classes.FILL,
secretField.isValid || Classes.INTENT_DANGER
)}
className={classNames(Classes.FILL, secretField.isValid || Classes.INTENT_DANGER)}
type="text"
ref={secretField.ref}
inputRef={secretField.ref}
onChange={secretField.onChange}
disabled={isSubmitting}
readOnly={!!deviceToEdit}
{...(deviceToEdit ? { value: deviceToEdit.secret } : undefined)}
defaultValue={defaultSecret}
{...(deviceToEdit ? { value: deviceToEdit.secret } : { rightElement: scanButton })}
/>
</FormGroup>

{showScanner && (
<QrReader
onResult={(result, err) => {
if (result) {
setShowScanner(false);
const element = secretField.ref.current;
if (element) {
element.value = result.getText();
}
}
}}
constraints={{
aspectRatio: 1,
frameRate: { ideal: 12 },
deviceId: { ideal: '0' }
}}
containerStyle={{ width: '50%', marginInline: 'auto' }}
videoStyle={{ borderRadius: '0.3em' }}
/>
)}

{errorMessage && <Callout intent="danger">{errorMessage}</Callout>}
</div>
<div className={Classes.DIALOG_FOOTER}>
Expand Down
74 changes: 49 additions & 25 deletions src/pages/playground/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as React from 'react';
import { HotKeys } from 'react-hotkeys';
import { useSelector } from 'react-redux';
import { useMediaQuery } from 'react-responsive';
import { RouteComponentProps } from 'react-router';
import { RouteComponentProps, useHistory, useLocation } from 'react-router';
import { showFullJSWarningOnUrlLoad } from 'src/commons/fullJS/FullJSUtils';

import {
Expand Down Expand Up @@ -170,9 +170,25 @@ const Playground: React.FC<PlaygroundProps> = props => {
const isMobileBreakpoint = useMediaQuery({ maxWidth: Constants.mobileBreakpoint });
const propsRef = React.useRef(props);
propsRef.current = props;

const [deviceSecret, setDeviceSecret] = React.useState<string | undefined>();
const location = useLocation();
const history = useHistory();
const searchParams = new URLSearchParams(location.search);
const shouldAddDevice = searchParams.get('add_device');

// Hide search query from URL to maintain an illusion of security. The device secret
// is still exposed via the 'Referer' header when requesting external content (e.g. Google API fonts)
if (shouldAddDevice && !deviceSecret) {
setDeviceSecret(shouldAddDevice);
history.replace(location.pathname);
}

const [lastEdit, setLastEdit] = React.useState(new Date());
const [isGreen, setIsGreen] = React.useState(false);
const [selectedTab, setSelectedTab] = React.useState(SideContentType.introduction);
const [selectedTab, setSelectedTab] = React.useState(
shouldAddDevice ? SideContentType.remoteExecution : SideContentType.introduction
);
const [hasBreakpoints, setHasBreakpoints] = React.useState(false);
const [sessionId, setSessionId] = React.useState(() =>
initSession('playground', {
Expand All @@ -181,6 +197,23 @@ const Playground: React.FC<PlaygroundProps> = props => {
})
);

const remoteExecutionTab: SideContentTab = React.useMemo(
() => ({
label: 'Remote Execution',
iconName: IconNames.SATELLITE,
body: (
<SideContentRemoteExecution
workspace="playground"
secretParams={deviceSecret || undefined}
callbackFunction={setDeviceSecret}
/>
),
id: SideContentType.remoteExecution,
toSpawn: () => true
}),
[deviceSecret]
);

const usingRemoteExecution =
useSelector((state: OverallState) => !!state.session.remoteExecutionSession) && !isSicpEditor;
// this is still used by remote execution (EV3)
Expand Down Expand Up @@ -216,17 +249,9 @@ const Playground: React.FC<PlaygroundProps> = props => {
* Handles toggling of relevant SideContentTabs when mobile breakpoint it hit
*/
React.useEffect(() => {
if (
isMobileBreakpoint &&
(selectedTab === SideContentType.introduction ||
selectedTab === SideContentType.remoteExecution)
) {
if (isMobileBreakpoint && desktopOnlyTabIds.includes(selectedTab)) {
setSelectedTab(SideContentType.mobileEditor);
} else if (
!isMobileBreakpoint &&
(selectedTab === SideContentType.mobileEditor ||
selectedTab === SideContentType.mobileEditorRun)
) {
} else if (!isMobileBreakpoint && mobileOnlyTabIds.includes(selectedTab)) {
setSelectedTab(SideContentType.introduction);
}
}, [isMobileBreakpoint, selectedTab]);
Expand Down Expand Up @@ -588,13 +613,12 @@ const Playground: React.FC<PlaygroundProps> = props => {
props.output,
props.playgroundSourceChapter,
props.playgroundSourceVariant,
usingRemoteExecution
usingRemoteExecution,
remoteExecutionTab
]);

// Remove Intro and Remote Execution tabs for mobile
const mobileTabs = [...tabs].filter(
x => x !== playgroundIntroductionTab && x !== remoteExecutionTab
);
const mobileTabs = [...tabs].filter(({ id }) => !(id && desktopOnlyTabIds.includes(id)));

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

return isMobileBreakpoint ? (
<MobileWorkspace {...mobileWorkspaceProps} />
<div className={classNames('Playground', Classes.DARK, isGreen ? 'GreenScreen' : undefined)}>
<MobileWorkspace {...mobileWorkspaceProps} />
</div>
) : (
<HotKeys
className={classNames('Playground', Classes.DARK, isGreen ? 'GreenScreen' : undefined)}
Expand All @@ -793,6 +819,12 @@ const Playground: React.FC<PlaygroundProps> = props => {
);
};

const mobileOnlyTabIds: readonly SideContentType[] = [
SideContentType.mobileEditor,
SideContentType.mobileEditorRun
];
const desktopOnlyTabIds: readonly SideContentType[] = [SideContentType.introduction];

const dataVisualizerTab: SideContentTab = {
label: 'Data Visualizer',
iconName: IconNames.EYE_OPEN,
Expand All @@ -809,12 +841,4 @@ const envVisualizerTab: SideContentTab = {
toSpawn: () => true
};

const remoteExecutionTab: SideContentTab = {
label: 'Remote Execution',
iconName: IconNames.SATELLITE,
body: <SideContentRemoteExecution workspace="playground" />,
id: SideContentType.remoteExecution,
toSpawn: () => true
};

export default Playground;
42 changes: 42 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2756,6 +2756,27 @@
resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==

"@zxing/browser@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@zxing/browser/-/browser-0.0.7.tgz#5fa7680a867b660f48d3288fdf63e0174ad531c7"
integrity sha512-AepzMgDnD6EjxewqmXpHJsi4S3Gw9ilZJLIbTf6fWuWySEcHBodnGu3p7FWlgq1Sd5QyfPhTum5z3CBkkhMVng==
optionalDependencies:
"@zxing/text-encoding" "^0.9.0"

"@zxing/library@^0.18.3":
version "0.18.6"
resolved "https://registry.yarnpkg.com/@zxing/library/-/library-0.18.6.tgz#717af8c6c1fd982865e21051afdd7b470ae6674c"
integrity sha512-bulZ9JHoLFd9W36pi+7e7DnEYNJhljYjZ1UTsKPOoLMU3qtC+REHITeCRNx40zTRJZx18W5TBRXt5pq2Uopjsw==
dependencies:
ts-custom-error "^3.0.0"
optionalDependencies:
"@zxing/text-encoding" "~0.9.0"

"@zxing/text-encoding@^0.9.0", "@zxing/text-encoding@~0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@zxing/text-encoding/-/text-encoding-0.9.0.tgz#fb50ffabc6c7c66a0c96b4c03e3d9be74864b70b"
integrity sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==

abab@^2.0.3, abab@^2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz"
Expand Down Expand Up @@ -11299,6 +11320,15 @@ react-popper@^2.2.4:
react-fast-compare "^3.0.1"
warning "^4.0.2"

react-qr-reader@^3.0.0-beta-1:
version "3.0.0-beta-1"
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-3.0.0-beta-1.tgz#e04a20876409313439959d8e0ea6df3ba6e36d68"
integrity sha512-5HeFH9x/BlziRYQYGK2AeWS9WiKYZtGGMs9DXy3bcySTX3C9UJL9EwcPnWw8vlf7JP4FcrAlr1SnZ5nsWLQGyw==
dependencies:
"@zxing/browser" "0.0.7"
"@zxing/library" "^0.18.3"
rollup "^2.67.2"

react-reconciler@~0.26.2:
version "0.26.2"
resolved "https://registry.npmjs.org/react-reconciler/-/react-reconciler-0.26.2.tgz"
Expand Down Expand Up @@ -12000,6 +12030,13 @@ rollup@^1.31.1:
"@types/node" "*"
acorn "^7.1.0"

rollup@^2.67.2:
version "2.78.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e"
integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg==
optionalDependencies:
fsevents "~2.3.2"

rst-selector-parser@^2.2.3:
version "2.2.3"
resolved "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz"
Expand Down Expand Up @@ -13276,6 +13313,11 @@ tryer@^1.0.1:
resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz"
integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==

ts-custom-error@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ts-custom-error/-/ts-custom-error-3.2.0.tgz#ff8f80a3812bab9dc448536312da52dce1b720fb"
integrity sha512-cBvC2QjtvJ9JfWLvstVnI45Y46Y5dMxIaG1TDMGAD/R87hpvqFL+7LhvUDhnRCfOnx/xitollFWWvUKKKhbN0A==

ts-node@^10.4.0:
version "10.4.0"
resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.4.0.tgz"
Expand Down