Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/course/configuration/terminal-command.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6List as AntdList,7Button,8Card,9Form,10Input,11InputNumber,12Space,13} from "antd";14import { List, Map, fromJS } from "immutable";15import { useState } from "react";16import { FormattedMessage, useIntl } from "react-intl";1718import {19CSS,20redux,21useActions,22useRedux,23} from "@cocalc/frontend/app-framework";2425import { Gap, Icon, Paragraph } from "@cocalc/frontend/components";26import { course, labels } from "@cocalc/frontend/i18n";27import { COLORS } from "@cocalc/util/theme";28import { CourseActions } from "../actions";29import { CourseStore, TerminalCommand, TerminalCommandOutput } from "../store";30import { MAX_PARALLEL_TASKS } from "../student-projects/actions";31import { Result } from "../student-projects/run-in-all-projects";3233interface Props {34name: string;35}3637export function TerminalCommandPanel({ name }: Props) {38const intl = useIntl();39const actions = useActions<CourseActions>({ name });40const terminal_command: TerminalCommand | undefined = useRedux(41name,42"terminal_command",43);44const [timeout, setTimeout] = useState<number | null>(1);4546function render_button(running: boolean) {47return (48<Button49style={{ width: "6em" }}50onClick={() => run_terminal_command()}51disabled={running}52>53<Icon name={running ? "cocalc-ring" : "play"} spin={running} /> <Gap />{" "}54Run55</Button>56);57}5859function render_input() {60const c = terminal_command;61let running = false;62if (c != null) {63running = c.get("running", false);64}65return (66<Form67style={{ marginBottom: "10px" }}68onFinish={() => {69run_terminal_command();70}}71>72<Space.Compact73style={{74display: "flex",75whiteSpace: "nowrap",76marginBottom: "5px",77}}78>79<Input80style={{ fontFamily: "monospace" }}81placeholder={`${intl.formatMessage(labels.terminal_command)}...`}82onChange={(e) => {83set_field("input", e.target.value);84}}85onPressEnter={() => run_terminal_command()}86/>87{render_button(running)}88</Space.Compact>89<InputNumber90value={timeout}91onChange={(t) => setTimeout(t ?? null)}92min={0}93max={30}94addonAfter={"minute timeout"}95/>96</Form>97);98}99100function render_running() {101const c = terminal_command;102if (c != null && c.get("running")) {103return (104<div105style={{106color: "#888",107padding: "5px",108fontSize: "16px",109fontWeight: "bold",110}}111>112<Icon name={"cocalc-ring"} spin /> Running...113</div>114);115}116}117118function render_output() {119const c = terminal_command;120if (c == null) return;121const output = c.get("output");122if (!output) return;123return (124<AntdList125size="small"126style={{ maxHeight: "400px", overflowY: "auto" }}127bordered128dataSource={output.toArray()}129renderItem={(item) => (130<AntdList.Item style={{ padding: "5px" }}>131<Output result={item} />132</AntdList.Item>133)}134/>135);136}137138function get_store(): CourseStore {139return actions.get_store();140}141142function set_field(field: "input" | "running" | "output", value: any): void {143const store: CourseStore = get_store();144let terminal_command: TerminalCommand = store.get(145"terminal_command",146Map() as TerminalCommand,147);148if (value == null) {149terminal_command = terminal_command.delete(field);150} else {151terminal_command = terminal_command.set(field, value);152}153actions.setState({ terminal_command });154}155156function run_log(result: Result): void {157// Important to get from store, not from props, since on second158// run old output isn't pushed down to props by the time this159// gets called.160const store = redux.getStore(name);161if (!store) {162return;163}164const c = (store as any).get("terminal_command");165let output;166if (c == null) {167output = List();168} else {169output = c.get("output", List());170}171set_field("output", output.push(fromJS(result)));172}173174async function run_terminal_command(): Promise<void> {175const c = terminal_command;176if (c == null) return;177const input = c.get("input");178set_field("output", undefined);179if (!input) return;180try {181set_field("running", true);182await actions.student_projects.run_in_all_student_projects({183command: input,184timeout: (timeout ? timeout : 1) * 60,185log: run_log,186});187} finally {188set_field("running", false);189}190}191192function render_terminal() {193return (194<div>195{render_input()}196{render_output()}197{render_running()}198</div>199);200}201202function render_header() {203return (204<>205<Icon name="terminal" />{" "}206{intl.formatMessage(course.run_terminal_command_title)}207</>208);209}210211return (212<Card title={render_header()}>213{render_terminal()}214<hr />215<Paragraph type="secondary">216<FormattedMessage217id="course.terminal-command.info"218defaultMessage={`Run a BASH terminal command in the home directory of all student projects.219Up to {MAX_PARALLEL_TASKS} commands run in parallel,220with a timeout of {timeout} minutes.`}221values={{ MAX_PARALLEL_TASKS, timeout }}222/>223</Paragraph>224</Card>225);226}227228const PROJECT_LINK_STYLE: CSS = {229maxWidth: "80%",230overflow: "hidden",231textOverflow: "ellipsis",232cursor: "pointer",233display: "block",234whiteSpace: "nowrap",235} as const;236237const CODE_STYLE: CSS = {238maxHeight: "200px",239overflow: "auto",240fontSize: "90%",241padding: "2px",242} as const;243244const ERR_STYLE: CSS = {245...CODE_STYLE,246color: "white",247background: COLORS.ANTD_RED,248} as const;249250function Output({ result }: { result: TerminalCommandOutput }) {251function open_project(): void {252const project_id = result.get("project_id");253redux.getActions("projects").open_project({ project_id });254}255256const project_id: string = result.get("project_id");257const title: string = redux.getStore("projects").get_title(project_id);258259const stdout = result.get("stdout");260const stderr = result.get("stderr");261const noresult = !stdout && !stderr;262const timeout = result.get("timeout");263const total_time = result.get("total_time");264265return (266<div style={{ padding: 0, width: "100%" }}>267<a style={PROJECT_LINK_STYLE} onClick={open_project}>268{title}269</a>270{stdout && <pre style={CODE_STYLE}>{stdout}</pre>}271{stderr && <pre style={ERR_STYLE}>{stderr}</pre>}272{noresult && (273<div>274No output{" "}275{total_time != null && timeout != null && total_time >= timeout - 5276? "(possible timeout)"277: ""}278</div>279)}280{total_time != null && <>(Time: {total_time} seconds)</>}281</div>282);283}284285286