From 9208330038470997328a4c6b3363eb487bf496d5 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sun, 26 Jun 2022 21:15:09 +0300 Subject: [PATCH 1/5] playground: manual play mode --- .../src/components/SquiggleChart.tsx | 104 +++++++++-------- .../src/components/SquigglePlayground.tsx | 109 +++++++++++++++--- .../components/src/components/ui/Toggle.tsx | 34 ++++++ 3 files changed, 180 insertions(+), 67 deletions(-) create mode 100644 packages/components/src/components/ui/Toggle.tsx diff --git a/packages/components/src/components/SquiggleChart.tsx b/packages/components/src/components/SquiggleChart.tsx index 9e352a62..bfb69a4e 100644 --- a/packages/components/src/components/SquiggleChart.tsx +++ b/packages/components/src/components/SquiggleChart.tsx @@ -48,57 +48,59 @@ export interface SquiggleChartProps { const defaultOnChange = () => {}; -export const SquiggleChart: React.FC = ({ - code = "", - environment, - onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here - height = 200, - bindings = defaultBindings, - jsImports = defaultImports, - showSummary = false, - width, - showTypes = false, - showControls = false, - logX = false, - expY = false, - diagramStart = 0, - diagramStop = 10, - diagramCount = 100, -}) => { - const result = useSquiggle({ - code, - bindings, +export const SquiggleChart: React.FC = React.memo( + ({ + code = "", environment, - jsImports, - onChange, - }); + onChange = defaultOnChange, // defaultOnChange must be constant, don't move its definition here + height = 200, + bindings = defaultBindings, + jsImports = defaultImports, + showSummary = false, + width, + showTypes = false, + showControls = false, + logX = false, + expY = false, + diagramStart = 0, + diagramStop = 10, + diagramCount = 100, + }) => { + const result = useSquiggle({ + code, + bindings, + environment, + jsImports, + onChange, + }); - if (result.tag !== "Ok") { - return ; + if (result.tag !== "Ok") { + return ; + } + + let distributionPlotSettings = { + showControls, + showSummary, + logX, + expY, + }; + + let chartSettings = { + start: diagramStart, + stop: diagramStop, + count: diagramCount, + }; + + return ( + + ); } - - let distributionPlotSettings = { - showControls, - showSummary, - logX, - expY, - }; - - let chartSettings = { - start: diagramStart, - stop: diagramStop, - count: diagramCount, - }; - - return ( - - ); -}; +); diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 4b01a344..74f48455 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -1,4 +1,4 @@ -import React, { FC, Fragment, useState, useEffect } from "react"; +import React, { FC, Fragment, useState, useEffect, useMemo } from "react"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import * as yup from "yup"; import { useMaybeControlledValue } from "../lib/hooks"; @@ -6,10 +6,14 @@ import { yupResolver } from "@hookform/resolvers/yup"; import { Tab } from "@headlessui/react"; import { ChartSquareBarIcon, + CheckCircleIcon, CodeIcon, CogIcon, CurrencyDollarIcon, EyeIcon, + PauseIcon, + PlayIcon, + RefreshIcon, } from "@heroicons/react/solid"; import clsx from "clsx"; @@ -20,6 +24,7 @@ import { CodeEditor } from "./CodeEditor"; import { JsonEditor } from "./JsonEditor"; import { ErrorAlert, SuccessAlert } from "./Alert"; import { SquiggleContainer } from "./SquiggleContainer"; +import { Toggle } from "./ui/Toggle"; interface PlaygroundProps { /** The initial squiggle string to put in the playground */ @@ -110,7 +115,7 @@ type StyledTabProps = { const StyledTab: React.FC = ({ name, icon: Icon }) => { return ( - + {({ selected }) => ( + )} + + + ); +}; + export const SquigglePlayground: FC = ({ defaultCode = "", height = 500, @@ -225,7 +277,14 @@ export const SquigglePlayground: FC = ({ const [importString, setImportString] = useState("{}"); const [imports, setImports] = useState({}); const [importsAreValid, setImportsAreValid] = useState(true); - const { register, control } = useForm({ + + const [renderedCode, setRenderedCode] = useState(""); // used only if autoplay is false + + const { + register, + control, + setValue: setFormValue, + } = useForm({ resolver: yupResolver(schema), defaultValues: { sampleCount: 1000, @@ -242,6 +301,7 @@ export const SquigglePlayground: FC = ({ diagramStart: 0, diagramStop: 10, diagramCount: 20, + autoplay: true, }, }); const vars = useWatch({ @@ -252,10 +312,14 @@ export const SquigglePlayground: FC = ({ onSettingsChange?.(vars); }, [vars, onSettingsChange]); - const env: environment = { - sampleCount: Number(vars.sampleCount), - xyPointLength: Number(vars.xyPointLength), - }; + const env: environment = useMemo( + () => ({ + sampleCount: Number(vars.sampleCount), + xyPointLength: Number(vars.xyPointLength), + }), + [vars.sampleCount, vars.xyPointLength] + ); + const getChangeJson = (r: string) => { setImportString(r); try { @@ -418,7 +482,7 @@ export const SquigglePlayground: FC = ({ const squiggleChart = ( = ({
- - + + + + + + + { + setRenderedCode(code); + }} + onAutoplayChange={(newValue) => { + if (!newValue) setRenderedCode(code); + setFormValue("autoplay", newValue); + }} /> - - - - +
{vars.showEditor ? withEditor : withoutEditor}
diff --git a/packages/components/src/components/ui/Toggle.tsx b/packages/components/src/components/ui/Toggle.tsx new file mode 100644 index 00000000..1b96db77 --- /dev/null +++ b/packages/components/src/components/ui/Toggle.tsx @@ -0,0 +1,34 @@ +import clsx from "clsx"; +import React from "react"; + +type IconType = (props: React.ComponentProps<"svg">) => JSX.Element; + +type Props = { + status: boolean; + onChange: (status: boolean) => void; + texts: [string, string]; + icons: [IconType, IconType]; +}; + +export const Toggle: React.FC = ({ + texts: [onText, offText], + icons: [OnIcon, OffIcon], + status, + onChange, +}) => { + const CurrentIcon = status ? OnIcon : OffIcon; + return ( + + ); +}; From 4fe30107a4f953f3be4b8d8f8f266af8b3996365 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sun, 26 Jun 2022 21:30:24 +0300 Subject: [PATCH 2/5] submit editor on cmd+enter --- packages/components/src/components/CodeEditor.tsx | 15 ++++++++++++++- .../src/components/SquigglePlayground.tsx | 10 +++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packages/components/src/components/CodeEditor.tsx b/packages/components/src/components/CodeEditor.tsx index c869b2ea..15802131 100644 --- a/packages/components/src/components/CodeEditor.tsx +++ b/packages/components/src/components/CodeEditor.tsx @@ -1,5 +1,5 @@ import _ from "lodash"; -import React, { FC, useMemo } from "react"; +import React, { FC, useMemo, useRef } from "react"; import AceEditor from "react-ace"; import "ace-builds/src-noconflict/mode-golang"; @@ -8,6 +8,7 @@ import "ace-builds/src-noconflict/theme-github"; interface CodeEditorProps { value: string; onChange: (value: string) => void; + onSubmit?: () => void; oneLine?: boolean; width?: number; height: number; @@ -17,6 +18,7 @@ interface CodeEditorProps { export const CodeEditor: FC = ({ value, onChange, + onSubmit, oneLine = false, showGutter = false, height, @@ -24,6 +26,10 @@ export const CodeEditor: FC = ({ const lineCount = value.split("\n").length; const id = useMemo(() => _.uniqueId(), []); + // this is necessary because AceEditor binds commands on mount, see https://github.com/securingsincity/react-ace/issues/684 + const onSubmitRef = useRef(null); + onSubmitRef.current = onSubmit; + return ( = ({ enableBasicAutocompletion: false, enableLiveAutocompletion: false, }} + commands={[ + { + name: "submit", + bindKey: { mac: "Cmd-Enter", win: "Ctrl-Enter" }, + exec: () => onSubmitRef.current?.(), + }, + ]} /> ); }; diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 74f48455..06b3fd3d 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -330,6 +330,11 @@ export const SquigglePlayground: FC = ({ } }; + const manualPlay = () => { + if (vars.autoplay) return; // should we allow reruns even in autoplay mode? + setRenderedCode(code); // TODO - force play even if code hasn't changed + }; + const samplingSettings = (
@@ -503,6 +508,7 @@ export const SquigglePlayground: FC = ({ = ({ { - setRenderedCode(code); - }} + onPlay={manualPlay} onAutoplayChange={(newValue) => { if (!newValue) setRenderedCode(code); setFormValue("autoplay", newValue); From 87bb752c925fd00367251ae1096897e4cd1ea682 Mon Sep 17 00:00:00 2001 From: Vyacheslav Matyukhin Date: Sun, 26 Jun 2022 23:30:32 +0300 Subject: [PATCH 3/5] extract ui components from playground; show spinner on keyboard-initiated plays --- .../src/components/SquigglePlayground.tsx | 576 ++++++++---------- .../components/src/components/ui/Checkbox.tsx | 24 + .../src/components/ui/StyledTab.tsx | 60 ++ 3 files changed, 349 insertions(+), 311 deletions(-) create mode 100644 packages/components/src/components/ui/Checkbox.tsx create mode 100644 packages/components/src/components/ui/StyledTab.tsx diff --git a/packages/components/src/components/SquigglePlayground.tsx b/packages/components/src/components/SquigglePlayground.tsx index 06b3fd3d..c5104508 100644 --- a/packages/components/src/components/SquigglePlayground.tsx +++ b/packages/components/src/components/SquigglePlayground.tsx @@ -1,9 +1,8 @@ -import React, { FC, Fragment, useState, useEffect, useMemo } from "react"; +import React, { FC, useState, useEffect, useMemo } from "react"; import { Path, useForm, UseFormRegister, useWatch } from "react-hook-form"; import * as yup from "yup"; import { useMaybeControlledValue } from "../lib/hooks"; import { yupResolver } from "@hookform/resolvers/yup"; -import { Tab } from "@headlessui/react"; import { ChartSquareBarIcon, CheckCircleIcon, @@ -25,6 +24,8 @@ import { JsonEditor } from "./JsonEditor"; import { ErrorAlert, SuccessAlert } from "./Alert"; import { SquiggleContainer } from "./SquiggleContainer"; import { Toggle } from "./ui/Toggle"; +import { Checkbox } from "./ui/Checkbox"; +import { StyledTab } from "./ui/StyledTab"; interface PlaygroundProps { /** The initial squiggle string to put in the playground */ @@ -49,104 +50,46 @@ interface PlaygroundProps { showEditor?: boolean; } -const schema = yup - .object() - .shape({ - sampleCount: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(1000000), - xyPointLength: yup - .number() - .required() - .positive() - .integer() - .default(1000) - .min(10) - .max(10000), - chartHeight: yup.number().required().positive().integer().default(350), - leftSizePercent: yup - .number() - .required() - .positive() - .integer() - .min(10) - .max(100) - .default(50), - showTypes: yup.boolean(), - showControls: yup.boolean(), - showSummary: yup.boolean(), - showEditor: yup.boolean(), - logX: yup.boolean(), - expY: yup.boolean(), - showSettingsPage: yup.boolean().default(false), - diagramStart: yup - .number() - .required() - .positive() - .integer() - .default(0) - .min(0), - diagramStop: yup - .number() - .required() - .positive() - .integer() - .default(10) - .min(0), - diagramCount: yup - .number() - .required() - .positive() - .integer() - .default(20) - .min(2), - }) - .required(); +const schema = yup.object({}).shape({ + sampleCount: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(1000000), + xyPointLength: yup + .number() + .required() + .positive() + .integer() + .default(1000) + .min(10) + .max(10000), + chartHeight: yup.number().required().positive().integer().default(350), + leftSizePercent: yup + .number() + .required() + .positive() + .integer() + .min(10) + .max(100) + .default(50), + showTypes: yup.boolean().required(), + showControls: yup.boolean().required(), + showSummary: yup.boolean().required(), + showEditor: yup.boolean().required(), + logX: yup.boolean().required(), + expY: yup.boolean().required(), + showSettingsPage: yup.boolean().default(false), + diagramStart: yup.number().required().positive().integer().default(0).min(0), + diagramStop: yup.number().required().positive().integer().default(10).min(0), + diagramCount: yup.number().required().positive().integer().default(20).min(2), + autoplay: yup.boolean().required(), +}); -type StyledTabProps = { - name: string; - icon: (props: React.ComponentProps<"svg">) => JSX.Element; -}; - -const StyledTab: React.FC = ({ name, icon: Icon }) => { - return ( - - {({ selected }) => ( - - )} - - ); -}; +type FormFields = yup.InferType; const HeadedSection: FC<{ title: string; children: React.ReactNode }> = ({ title, @@ -187,50 +130,189 @@ function InputItem({ ); } -function Checkbox({ - name, - label, +const SamplingSettings: React.FC<{ register: UseFormRegister }> = ({ register, -}: { - name: Path; - label: string; - register: UseFormRegister; -}) { - return ( -