Skip to content

Commit

Permalink
Updaate the playground to handle multiple files
Browse files Browse the repository at this point in the history
The tab control to switch the main script and the requirements has been removed
so both are now handled as files.
  • Loading branch information
whitphx committed Aug 29, 2022
1 parent 6149902 commit fe67c4f
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 104 deletions.
90 changes: 59 additions & 31 deletions packages/playground/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import { useState, useEffect } from "react";
import { useState, useEffect, useCallback } from "react";
import "./App.css";

import EditorModal from "./EditorModal";
import EditorModal, { EditorFiles } from "./EditorModal";

import { StliteKernel, StliteKernelProvider } from "@stlite/stlite-kernel";
import {
StliteKernel,
StliteKernelOptions,
StliteKernelProvider,
} from "@stlite/stlite-kernel";

import ThemedApp from "streamlit-browser/src/ThemedApp";
import { Client as Styletron } from "styletron-engine-atomic";
import { Provider as StyletronProvider } from "styletron-react";
const engine = new Styletron({ prefix: "st-" });

const DEFAULT_VALUE = `### Sample code copied from https://docs.streamlit.io/library/api-reference/charts/st.pyplot ###
const ENTRYPOINT = "streamlit_app.py";

const REQUIREMENTS_PATH = "requirements";

const DEFAULT_REQUIREMENTS = ["matplotlib", "hiplot"];

const DEFAULT_FILES: EditorFiles = {
[ENTRYPOINT]: {
language: "python",
value: `### Sample code copied from https://docs.streamlit.io/library/api-reference/charts/st.pyplot ###
import streamlit as st
import matplotlib.pyplot as plt
import numpy as np
Expand Down Expand Up @@ -41,26 +54,39 @@ xp = hip.Experiment.from_iterable(data)
ret_val = xp.to_streamlit(ret="selected_uids", key="hip").display()
st.markdown("hiplot returned " + json.dumps(ret_val))
`;

const DEFAULT_REQUIREMENTS = ["matplotlib", "hiplot"];

const ENTRYPOINT = "streamlit_app.py";
`,
},
"pages/page_1.py": {
language: "python",
value: `import streamlit as st
st.write("Page1")`,
},
[REQUIREMENTS_PATH]: {
language: "text",
value: DEFAULT_REQUIREMENTS.join("\n"),
},
};

const DEFAULT_KERNEL_FILES: StliteKernelOptions["files"] = {};
Object.keys(DEFAULT_FILES).forEach((key) => {
if (key === REQUIREMENTS_PATH) {
return;
}

DEFAULT_KERNEL_FILES[key] = {
data: DEFAULT_FILES[key].value,
};
});

function App() {
const [mainScriptData, setMainScriptData] = useState(DEFAULT_VALUE);

const [kernel, setKernel] = useState<StliteKernel>();
useEffect(() => {
const kernel = new StliteKernel({
command: "run",
entrypoint: ENTRYPOINT,
requirements: DEFAULT_REQUIREMENTS,
files: {
[ENTRYPOINT]: {
data: mainScriptData,
},
},
files: DEFAULT_KERNEL_FILES,
});
setKernel(kernel);

Expand All @@ -70,25 +96,27 @@ function App() {

// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (kernel == null) {
return;
}

kernel.writeFile(ENTRYPOINT, mainScriptData);
}, [kernel, mainScriptData]);

return (
<>
<EditorModal
defaultValue={mainScriptData}
onChange={setMainScriptData}
defaultRequirementsValue={DEFAULT_REQUIREMENTS.join("\n")}
onInstallRequired={(requirements) => {
kernel?.install(requirements).then(() => {
console.log("Installed");
});
}}
defaultFiles={DEFAULT_FILES}
onFileChange={useCallback(
(path: string, value: string) => {
if (path === REQUIREMENTS_PATH) {
const requirements = value
.split("\n")
.map((r) => r.trim())
.filter((r) => r !== "");
kernel?.install(requirements).then(() => {
console.log("Installed");
});
return;
}
kernel?.writeFile(path, value);
},
[kernel]
)}
/>
{kernel && (
<StliteKernelProvider kernel={kernel}>
Expand Down
99 changes: 26 additions & 73 deletions packages/playground/src/EditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,59 +6,24 @@ const DEFAULT_WIDTH = Math.min(600, window.innerWidth);
const DEFAULT_X = Math.min(50, (window.innerWidth - DEFAULT_WIDTH) / 2);
const DEFAULT_HEIGHT = Math.min(400, window.innerHeight - 400);

const TABS = ["code", "requirements"] as const;

interface RadioGroupProps<T extends string = string> {
name: string;
options: readonly T[];
value: T;
onChange: (value: T) => void;
}
function RadioGroup<T extends string = string>({
name,
onChange,
value: currentValue,
options,
}: RadioGroupProps<T>) {
const onClick = useCallback<React.ChangeEventHandler<HTMLInputElement>>(
(e) => {
onChange(e.target.value as T);
},
[onChange]
);
return (
<>
{options.map((value) => (
<label key={value} className="radio-label">
<input
type="radio"
className="radio-radio"
name={name}
value={value}
checked={value === currentValue}
onChange={onClick}
/>
{value}
</label>
))}
</>
);
export interface FileModel {
value: string;
language: string;
}
export type EditorFiles = { [path: string]: FileModel };

export interface EditorModalProps {
defaultValue: string;
onChange: (value: string) => void;
defaultRequirementsValue: string;
onInstallRequired: (requirements: string[]) => void;
defaultFiles: EditorFiles;
onFileChange: (path: string, value: string) => void;
}
function EditorModal(props: EditorModalProps) {
const [tab, setTab] = useState<typeof TABS[number]>("code");

const editorRef = useRef<Parameters<OnMount>[0]>(null);

const { defaultValue, onChange, onInstallRequired } = props;
const { defaultFiles, onFileChange } = props;

const [currentPath, setCurrentPath] = useState(Object.keys(defaultFiles)[0]);

const requirementsTextRef = useRef<HTMLTextAreaElement>(null);
const currentFile = defaultFiles[currentPath];

return (
<Rnd
Expand All @@ -77,52 +42,40 @@ function EditorModal(props: EditorModalProps) {
>
<div className="editor-modal">
<div className="editor-modal-header">
<RadioGroup name="tab" value={tab} options={TABS} onChange={setTab} />
{Object.keys(defaultFiles).map((path) => (
<button
key={path}
disabled={currentPath === path}
onClick={() => setCurrentPath(path)}
>
{path}
</button>
))}
</div>
<div className="editor-container" hidden={tab !== "code"}>
<div className="editor-container">
<Editor
defaultValue={defaultValue}
defaultLanguage="python"
// Multi-model API. See https://github.com/suren-atoyan/monaco-react#multi-model-editor
path={currentPath}
defaultValue={currentFile.value}
defaultLanguage={currentFile.language}
onMount={useCallback<OnMount>((editor) => {
(editorRef as React.MutableRefObject<unknown>).current = editor;
}, [])}
/>
</div>

<div className="editor-container" hidden={tab !== "requirements"}>
<textarea
className="requirements-textarea"
defaultValue={props.defaultRequirementsValue}
ref={requirementsTextRef}
/>
</div>

<div className="editor-modal-footer">
<button
hidden={tab !== "code"}
onClick={useCallback(() => {
const editor = editorRef.current;
if (editor == null) {
return;
}
onChange(editor.getValue());
}, [onChange])}
onFileChange(currentPath, editor.getValue());
}, [currentPath, onFileChange])}
>
Save
</button>
<button
hidden={tab !== "requirements"}
onClick={useCallback(() => {
const value = requirementsTextRef.current?.value || "";
const requirements = value
.split("\n")
.map((r) => r.trim())
.filter((r) => r !== "");
onInstallRequired(requirements);
}, [onInstallRequired])}
>
Install
</button>

<span className="footnote">
See{" "}
Expand Down

0 comments on commit fe67c4f

Please sign in to comment.